sphinx_autodoc_typehints-2.3.0/.markdownlint.yaml0000644000000000000000000000015013615410400017241 0ustar00MD013: code_blocks: false headers: false line_length: 120 tables: false MD046: style: fenced sphinx_autodoc_typehints-2.3.0/.pre-commit-config.yaml0000644000000000000000000000201013615410400020044 0ustar00repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.29.0 hooks: - id: check-github-workflows args: [ "--verbose" ] - repo: https://github.com/codespell-project/codespell rev: v2.3.0 hooks: - id: codespell additional_dependencies: ["tomli>=2.0.1"] - repo: https://github.com/tox-dev/tox-ini-fmt rev: "1.3.1" hooks: - id: tox-ini-fmt args: ["-p", "fix"] - repo: https://github.com/tox-dev/pyproject-fmt rev: "2.1.4" hooks: - id: pyproject-fmt additional_dependencies: ["tox>=4.14.2"] - repo: https://github.com/astral-sh/ruff-pre-commit rev: "v0.5.4" hooks: - id: ruff-format - id: ruff args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] - repo: meta hooks: - id: check-hooks-apply - id: check-useless-excludes sphinx_autodoc_typehints-2.3.0/CHANGELOG.md0000644000000000000000000002276113615410400015413 0ustar00# Changelog ## 1.22 - Allow Sphinx explicitly to write in parallel. - Fixed crash when documenting ParamSpecArgs ## 1.21.7 - Fixed a bug where if a class has an attribute and a constructor argument with the same name, the constructor argument type would be rendered incorrectly (issue 308) - Fixed napoleon handling of numpy docstrings with no specified return type. ## 1.21.6 - Fix a `Field list ends without a blank line` warning (issue 305). ## 1.21.5 - More robust determination of rtype location / fix issue 302 ## 1.21.4 - Improvements to the location of the return type ## 1.21.3 - Use format_annotation to render class attribute type annotations ## 1.21.2 - Fix overloads support ## 1.21.1 - Fix spacing between `:rtype:` and directives ## 1.21 - Handle types from types module - If module is \_io, use io instead - Put rtype before examples or usage section - Remove redundant return type for attributes - Handle collections.abc.Callable as well as typing.Callable - Put Literal args in code blocks ## 1.20.2 - Fix Optional role to be data. ## 1.20.1 - Fixed default options not displaying for parameters without type hints. ## 1.20 - Use hatchling instead of setuptools - Add support for typing.ParamSpec - Allow star prefixes for parameter names in docstring ## 1.19.2 - Fix incorrect domain used for collections.abc.Callable. ## 1.19.1 - Fix bug for recursive type alias. ## 1.19.0 - Support for CPython 3.11, no longer adds `Optional` when the argument is default per [recommendation from PEP-484](https://github.com/tox-dev/sphinx-autodoc-typehints/pull/247). ## 1.18.3 - Support and require `nptyping>=2.1.2` ## 1.18.2 - Support and require `nptyping>=2.1.1` ## 1.18.1 - Fix mocked module import not working when used as guarded import ## 1.18.0 - Support and require `nptyping>=2` - Handle `UnionType` ## 1.17.1 - Mark it as requiring `nptyping<2` ## 1.17.0 - Add `typehints_use_rtype` option - Handles `TypeError` when getting source code via inspect ## 1.16.0 - Add support for type subscriptions with multiple elements, where one or more elements are tuples; e.g., `nptyping.NDArray[(Any, ...), nptyping.Float]` - Fix bug for arbitrary types accepting singleton subscriptions; e.g., `nptyping.Float[64]` - Resolve forward references - Expand and better handle `TypeVar` - Add intershpinx reference link for `...` to `Ellipsis` (as is just an alias) ## 1.15.3 - Prevents reaching inner blocks that contains `if TYPE_CHECKING` ## 1.15.2 - Log a warning instead of crashing when a type guard import fails to resolve - When resolving type guard imports if the target module does not have source code (such is the case for C-extension modules) do nothing instead of crashing ## 1.15.1 - Fix `fully_qualified` should be `typehints_fully_qualified` ## 1.15.0 - Resolve type guard imports before evaluating annotations for objects - Remove `set_type_checking_flag` flag as this is now done by default - Fix crash when the `inspect` module returns an invalid python syntax source - Made formatting function configurable using the option `typehints_formatter` ## 1.14.1 - Fixed `normalize_source_lines()` messing with the indentation of methods with decorators that have parameters starting with `def`. - Handle `ValueError` or `TypeError` being raised when signature of an object cannot be determined - Fix `KeyError` being thrown when argument is not documented (e.g. `cls` argument for class methods, and `self` for methods) ## 1.14.0 - Added `typehints_defaults` config option allowing to automatically annotate parameter defaults. ## 1.13.1 - Fixed `NewType` inserts a reference as first argument instead of a string ## 1.13.0 - Dropped Python 3.6 support - Python 3.10 support - Normalize async functions properly - Allow py310 style annotations (PEP-563) ## 1.12.0 - Dropped Python 3.5 support - Added the simplify_optional_unions config option (PR by tillhainbach) - Fixed indentation of multiline strings (PR by Yuxin Wu) ## 1.11.1 - Changed formatting of `None` to point to the Python stdlib docs (PR by Dominic Davis-Foster) - Updated special dataclass handling (PR by Lihu Ben-Ezri-Ravin) ## 1.11.0 - Dropped support for Sphinx \< 3.0 - Added support for alternative parameter names (`arg`, `argument`, `parameter`) - Fixed import path for Signature (PR by Matthew Treinish) - Fixed `TypeError` when formatting a parametrized `typing.IO` annotation - Fixed data class displaying a return type in its `__init__()` method ## 1.10.3 - Fixed `TypeError` (or wrong rendered class name) when an annotation is a generic class that has a `name` property ## 1.10.2 - Fixed inner classes missing their parent class name(s) when rendered ## 1.10.1 - Fixed `KeyError` when encountering mocked annotations (`autodoc_mock_imports`) ## 1.10.0 - Rewrote the annotation formatting logic (fixes Python 3.5.2 compatibility regressions and an `AttributeError` regression introduced in v1.9.0) - Fixed decorator classes not being processed as classes ## 1.9.0 - Added support for [typing_extensions](https://pypi.org/project/typing-extensions/) - Added the `typehints_document_rtype` option (PR by Simon-Martin Schröder) - Fixed metaclasses as annotations causing `TypeError` - Fixed rendering of `typing.Literal` - Fixed OSError when generating docs for SQLAlchemy mapped classes - Fixed unparametrized generic classes being rendered with their type parameters (e.g. `Dict[~KT, ~VT]`) ## 1.8.0 - Fixed regression which caused `TypeError` or `OSError` when trying to set annotations due to PR #87 - Fixed unintentional mangling of annotation type names - Added proper `:py:data` targets for `NoReturn`, `ClassVar` and `Tuple` - Added support for inline type comments (like `(int, str) -> None`) (PR by Bernát Gábor) - Use the native AST parser for type comment support on Python 3.8+ ## 1.7.0 - Dropped support for Python 3.4 - Fixed unwrapped local functions causing errors (PR by Kimiyuki Onaka) - Fixed `AttributeError` when documenting the `__init__()` method of a data class - Added support for type hint comments (PR by Markus Unterwaditzer) - Added flag for rendering classes with their fully qualified names (PR by Holly Becker) ## 1.6.0 - Fixed `TypeError` when formatting annotations from a class that inherits from a concrete generic type (report and tests by bpeake-illuscio) - Added support for `typing_extensions.Protocol` (PR by Ian Good) - Added support for `typing.NewType` (PR by George Leslie-Waksman) ## 1.5.2 - Emit a warning instead of crashing when an unresolvable forward reference is encountered in type annotations ## 1.5.1 - Fixed escape characters in parameter default values getting lost during signature processing - Replaced use of the `config-inited` event (which inadvertently required Sphinx 1.8) with the `builder-inited` event ## 1.5.0 - The setting of the `typing.TYPECHECKING` flag is now configurable using the `set_type_checking_flag` option ## 1.4.0 - The extension now sets `typing.TYPECHECKING` to `True` during setup to include conditional imports which may be used in type annotations - Fixed parameters with trailing underscores (PR by Daniel Knell) - Fixed KeyError with private methods (PR by Benito Palacios Sánchez) - Fixed deprecation warning about the use of formatargspec (PR by Y. Somda) - The minimum Sphinx version is now v1.7.0 ## 1.3.1 - Fixed rendering of generic types outside the typing module (thanks to Tim Poterba for the PR) ## 1.3.0 - Fixed crash when processing docstrings from nested classes (thanks to dilyanpalauzov for the fix) - Added support for Python 3.7 - Dropped support for Python 3.5.0 and 3.5.1 ## 1.2.5 - Ensured that `:rtype:` doesn\'t get joined with a paragraph of text (thanks to Bruce Merry for the PR) ## 1.2.4 - Removed support for `backports.typing` as it has been removed from the PyPI - Fixed first parameter being cut out from class methods and static methods (thanks to Josiah Wolf Oberholtzer for the PR) ## 1.2.3 - Fixed `process_signature()` clobbering any explicitly overridden signatures from the docstring ## 1.2.2 - Explicitly prefix `:class:`, `:mod:` et al with `:py:`, in case `py` is not the default domain of the project (thanks Monty Taylor) ## 1.2.1 - Fixed ``ValueError` when``getargspec()\`\` encounters a built-in function - Fixed `AttributeError` when `Any` is combined with another type in a `Union` (thanks Davis Kirkendall) ## 1.2.0 - Fixed compatibility with Python 3.6 and 3.5.3 - Fixed `NameError` when processing signatures of wrapped functions with type hints - Fixed handling of slotted classes with no `__init__()` method - Fixed Sphinx warning about parallel reads - Fixed return type being added to class docstring from its `__init__()` method (thanks to Manuel Krebber for the patch) - Fixed return type hints of `@property` methods being omitted (thanks to pknight for the patch) - Added a test suite (thanks Manuel Krebber) ## 1.1.0 - Added proper support for `typing.Tuple` (pull request by Manuel Krebber) ## 1.0.6 - Fixed wrong placement of `:rtype:` if a multi-line `:param:` or a `:returns:` is used ## 1.0.5 - Fixed coroutine functions\' signatures not being processed when using sphinxcontrib-asyncio ## 1.0.4 - Fixed compatibility with Sphinx 1.4 ## 1.0.3 - Fixed \"self\" parameter not being removed from exception class constructor signatures - Fixed process_signature() erroneously removing the first argument of a static method ## 1.0.2 - Fixed exception classes not being processed like normal classes ## 1.0.1 - Fixed errors caused by forward references not being looked up with the right globals ## 1.0.0 - Initial release sphinx_autodoc_typehints-2.3.0/ignore-words.txt0000644000000000000000000000001613615410400016747 0ustar00master manuel sphinx_autodoc_typehints-2.3.0/tox.ini0000644000000000000000000000470213615410400015110 0ustar00[tox] requires = tox>=4.2 env_list = fix py312 py311 py310 py39 type coverage readme skip_missing_interpreters = true [testenv] description = run tests with {basepython} package = wheel wheel_build_env = .pkg extras = numpy testing type-comment pass_env = PIP_* PYTEST_* set_env = COVERAGE_FILE = {toxworkdir}{/}.coverage.{envname} commands = pytest {tty:--color=yes} {posargs: \ --junitxml {toxworkdir}{/}junit.{envname}.xml --cov {envsitepackagesdir}{/}sphinx_autodoc_typehints --cov {toxinidir}{/}tests \ --cov-config=pyproject.toml --no-cov-on-fail --cov-report term-missing:skip-covered --cov-context=test \ --cov-report html:{envtmpdir}{/}htmlcov --cov-report xml:{toxworkdir}{/}coverage.{envname}.xml \ tests} diff-cover --compare-branch {env:DIFF_AGAINST:origin/main} {toxworkdir}{/}coverage.{envname}.xml [testenv:fix] description = format the code base to adhere to our styles, and complain about what we cannot do automatically skip_install = true deps = pre-commit>=3.6.2 commands = pre-commit run --all-files --show-diff-on-failure [testenv:py312] extras = testing type-comment [testenv:type] description = run type check on code base deps = mypy==1.8 types-docutils>=0.20.0.20240304 set_env = {tty:MYPY_FORCE_COLOR = 1} commands = mypy src mypy tests [testenv:coverage] description = combine coverage files and generate diff (against DIFF_AGAINST defaulting to origin/main) skip_install = true deps = covdefaults>=2.3 coverage>=7.4.3 diff-cover>=8.0.3 extras = parallel_show_output = true pass_env = DIFF_AGAINST set_env = COVERAGE_FILE = {toxworkdir}/.coverage commands = coverage combine coverage report --skip-covered --show-missing coverage xml -o {toxworkdir}/coverage.xml coverage html -d {toxworkdir}/htmlcov diff-cover --compare-branch {env:DIFF_AGAINST:origin/main} {toxworkdir}/coverage.xml depends = py312 py311 py310 py39 py38 [testenv:readme] description = check that the long description is valid (need for PyPI) skip_install = true deps = build[virtualenv]>=1.1.1 twine>=5 extras = commands = pyproject-build -o {envtmpdir} --wheel --sdist . twine check {envtmpdir}/* [testenv:dev] description = generate a DEV environment package = editable commands = python -m pip list --format=columns python -c 'import sys; print(sys.executable)' uv_seed = true sphinx_autodoc_typehints-2.3.0/whitelist.txt0000644000000000000000000000126613615410400016354 0ustar00addnodes arg1 arg2 ast3 astext autodoc autouse backfill conf contravariant Coroutine cpython csv dedent delattr desc dirname docnames docstrings Documenter docutils dunder eval exc fget fmt fn formatter func getitem getmodule getsource globals idx inited inv isdatadescriptor isfunction iterdir kwonlyargs libs lineno lru metaclass ModuleType multiline newtype numpy nptyping param parametrized params parsers pathlib pos prepend py310 pydata pytestconfig qualname rootdir rst rtype runtime setitem sig signode skipif sph sphobjinv srcdir stdlib stmt stringify subclasses supertype tagname tempdir testroot textwrap toctree typ typehint typehints unittest unlink unresolvable util utils vararg sphinx_autodoc_typehints-2.3.0/.github/FUNDING.yml0000644000000000000000000000005213615410400016744 0ustar00tidelift: "pypi/sphinx-autodoc-typehints" sphinx_autodoc_typehints-2.3.0/.github/SECURITY.md0000644000000000000000000000055513615410400016730 0ustar00# Security Policy ## Supported Versions | Version | Supported | | ------- | ------------------ | | 1.18 + | :white_check_mark: | | < 1.18 | :x: | ## Reporting a Vulnerability To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. sphinx_autodoc_typehints-2.3.0/.github/dependabot.yml0000644000000000000000000000016513615410400017764 0ustar00version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" sphinx_autodoc_typehints-2.3.0/.github/release.yml0000644000000000000000000000011413615410400017271 0ustar00changelog: exclude: authors: - dependabot - pre-commit-ci sphinx_autodoc_typehints-2.3.0/.github/workflows/check.yml0000644000000000000000000000652713615410400021001 0ustar00name: check on: workflow_dispatch: push: branches: ["main"] tags-ignore: ["**"] pull_request: schedule: - cron: "0 8 * * *" concurrency: group: check-${{ github.ref }} cancel-in-progress: true jobs: test: name: test with CPython ${{ matrix.py }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: py: - "3.12" - "3.11" - "3.10" - "3.9" steps: - name: Setup python for tox uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install tox run: python -m pip install tox-uv - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup python for test ${{ matrix.py }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.py }} - name: Pick environment to run run: | import os import sys from pathlib import Path env = "TOXENV=py{}{}\n".format(*sys.version_info[0:2]) print("Picked:\n{}for{}".format(env, sys.version)) with Path(os.environ["GITHUB_ENV"]).open("a") as file_handler: file_handler.write(env) shell: python - name: Setup test suite run: tox -vv --notest - name: Run test suite run: tox --skip-pkg-install env: PYTEST_ADDOPTS: "-vv --durations=20" CI_RUN: "yes" DIFF_AGAINST: HEAD - name: Rename coverage report file run: import os; import sys; os.rename(f".tox/.coverage.{os.environ['TOXENV']}", f".tox/.coverage.{os.environ['TOXENV']}-{sys.platform}") shell: python - name: Upload coverage data uses: actions/upload-artifact@v3 with: name: coverage-data path: ".tox/.coverage.*" coverage: name: Combine coverage runs-on: ubuntu-latest needs: test steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install tox run: python -m pip install tox-uv - name: Setup coverage tool run: tox -e coverage --notest - name: Install package builder run: python -m pip install build - name: Build package run: pyproject-build --wheel . - name: Download coverage data uses: actions/download-artifact@v3 with: name: coverage-data path: .tox - name: Combine and report coverage run: tox -e coverage - name: Upload HTML report uses: actions/upload-artifact@v3 with: name: html-report path: .tox/htmlcov check: name: tox env ${{ matrix.tox_env }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: tox_env: - dev - readme - type steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Python "3.12" uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install tox run: python -m pip install tox - name: Setup test suite run: tox -vv --notest -e ${{ matrix.tox_env }} - name: Run test suite run: tox --skip-pkg-install -e ${{ matrix.tox_env }} sphinx_autodoc_typehints-2.3.0/.github/workflows/release.yml0000644000000000000000000000122413615410400021331 0ustar00name: Release to PyPI on: push: tags: ["*"] jobs: release: runs-on: ubuntu-latest environment: name: release url: https://pypi.org/p/sphinx-autodoc-typehints permissions: id-token: write steps: - name: Setup python to build package uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install build run: python -m pip install build - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Build package run: pyproject-build -s -w . -o dist - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@v1.9.0 sphinx_autodoc_typehints-2.3.0/src/sphinx_autodoc_typehints/__init__.py0000644000000000000000000011134513615410400023635 0ustar00"""Sphinx autodoc type hints.""" from __future__ import annotations import ast import importlib import inspect import re import sys import textwrap import types from dataclasses import dataclass from typing import TYPE_CHECKING, Any, AnyStr, Callable, ForwardRef, NewType, TypeVar, get_type_hints from docutils import nodes from docutils.frontend import OptionParser from sphinx.ext.autodoc.mock import mock from sphinx.parsers import RSTParser from sphinx.util import logging, rst from sphinx.util.inspect import TypeAliasForwardRef, TypeAliasNamespace, stringify_signature from sphinx.util.inspect import signature as sphinx_signature from .parser import parse from .patches import install_patches from .version import __version__ if TYPE_CHECKING: from ast import FunctionDef, Module, stmt from docutils.nodes import Node from docutils.parsers.rst import states from sphinx.application import Sphinx from sphinx.config import Config from sphinx.environment import BuildEnvironment from sphinx.ext.autodoc import Options _LOGGER = logging.getLogger(__name__) _PYDATA_ANNOTATIONS = {"Any", "AnyStr", "Callable", "ClassVar", "Literal", "NoReturn", "Optional", "Tuple", "Union"} # types has a bunch of things like ModuleType where ModuleType.__module__ is # "builtins" and ModuleType.__name__ is "module", so we have to check for this. _TYPES_DICT = {getattr(types, name): name for name in types.__all__} # Prefer FunctionType to LambdaType (they are synonymous) _TYPES_DICT[types.FunctionType] = "FunctionType" def _get_types_type(obj: Any) -> str | None: try: return _TYPES_DICT.get(obj) except Exception: # noqa: BLE001 # e.g. exception: unhashable type return None def get_annotation_module(annotation: Any) -> str: """ Get module for an annotation. :param annotation: :return: """ if annotation is None: return "builtins" if _get_types_type(annotation) is not None: return "types" is_new_type = sys.version_info >= (3, 10) and isinstance(annotation, NewType) if ( is_new_type or isinstance(annotation, TypeVar) or type(annotation).__name__ in {"ParamSpec", "ParamSpecArgs", "ParamSpecKwargs"} ): return "typing" if hasattr(annotation, "__module__"): return annotation.__module__ # type: ignore[no-any-return] if hasattr(annotation, "__origin__"): return annotation.__origin__.__module__ # type: ignore[no-any-return] msg = f"Cannot determine the module of {annotation}" raise ValueError(msg) def _is_newtype(annotation: Any) -> bool: if sys.version_info < (3, 10): return inspect.isfunction(annotation) and hasattr(annotation, "__supertype__") return isinstance(annotation, NewType) def get_annotation_class_name(annotation: Any, module: str) -> str: # noqa: C901, PLR0911 """ Get class name for annotation. :param annotation: :param module: :return: """ # Special cases if annotation is None: return "None" if annotation is AnyStr: return "AnyStr" val = _get_types_type(annotation) if val is not None: return val if _is_newtype(annotation): return "NewType" if getattr(annotation, "__qualname__", None): return annotation.__qualname__ # type: ignore[no-any-return] if getattr(annotation, "_name", None): # Required for generic aliases on Python 3.7+ return annotation._name # type: ignore[no-any-return] # noqa: SLF001 if module in {"typing", "typing_extensions"} and isinstance(getattr(annotation, "name", None), str): # Required for at least Pattern and Match return annotation.name # type: ignore[no-any-return] origin = getattr(annotation, "__origin__", None) if origin: if getattr(origin, "__qualname__", None): # Required for Protocol subclasses return origin.__qualname__ # type: ignore[no-any-return] if getattr(origin, "_name", None): # Required for Union on Python 3.7+ return origin._name # type: ignore[no-any-return] # noqa: SLF001 annotation_cls = annotation if inspect.isclass(annotation) else type(annotation) return annotation_cls.__qualname__.lstrip("_") def get_annotation_args(annotation: Any, module: str, class_name: str) -> tuple[Any, ...]: # noqa: PLR0911 """ Get annotation arguments. :param annotation: :param module: :param class_name: :return: """ try: original = getattr(sys.modules[module], class_name) except (KeyError, AttributeError): pass else: if annotation is original: return () # This is the original, not parametrized type # Special cases if class_name in {"Pattern", "Match"} and hasattr(annotation, "type_var"): # Python < 3.7 return (annotation.type_var,) if class_name == "ClassVar" and hasattr(annotation, "__type__"): # ClassVar on Python < 3.7 return (annotation.__type__,) if class_name == "TypeVar" and hasattr(annotation, "__constraints__"): return annotation.__constraints__ # type: ignore[no-any-return] if class_name == "NewType" and hasattr(annotation, "__supertype__"): return (annotation.__supertype__,) if class_name == "Literal" and hasattr(annotation, "__values__"): return annotation.__values__ # type: ignore[no-any-return] if class_name == "Generic": return annotation.__parameters__ # type: ignore[no-any-return] result = getattr(annotation, "__args__", ()) # 3.10 and earlier Tuple[()] returns ((), ) instead of () the tuple does return () if len(result) == 1 and result[0] == () else result # type: ignore[misc] def format_internal_tuple(t: tuple[Any, ...], config: Config) -> str: # An annotation can be a tuple, e.g., for nptyping: # In this case, format_annotation receives: # This solution should hopefully be general for *any* type that allows tuples in annotations fmt = [format_annotation(a, config) for a in t] if len(fmt) == 0: return "()" if len(fmt) == 1: return f"({fmt[0]}, )" return f"({', '.join(fmt)})" def fixup_module_name(config: Config, module: str) -> str: if getattr(config, "typehints_fixup_module_name", None): module = config.typehints_fixup_module_name(module) if module == "typing_extensions": module = "typing" if module == "_io": module = "io" return module def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PLR0911, PLR0912, PLR0915, PLR0914 """ Format the annotation. :param annotation: :param config: :return: """ typehints_formatter: Callable[..., str] | None = getattr(config, "typehints_formatter", None) if typehints_formatter is not None: formatted = typehints_formatter(annotation, config) if formatted is not None: return formatted # Special cases if isinstance(annotation, ForwardRef): return annotation.__forward_arg__ if annotation is None or annotation is type(None): return ":py:obj:`None`" if annotation is Ellipsis: return ":py:data:`...`" if isinstance(annotation, tuple): return format_internal_tuple(annotation, config) if isinstance(annotation, TypeAliasForwardRef): return str(annotation) try: module = get_annotation_module(annotation) class_name = get_annotation_class_name(annotation, module) args = get_annotation_args(annotation, module, class_name) except ValueError: return str(annotation).strip("'") module = fixup_module_name(config, module) full_name = f"{module}.{class_name}" if module != "builtins" else class_name fully_qualified: bool = getattr(config, "typehints_fully_qualified", False) prefix = "" if fully_qualified or full_name == class_name else "~" role = "data" if module == "typing" and class_name in _PYDATA_ANNOTATIONS else "class" args_format = "\\[{}]" formatted_args: str | None = "" always_use_bars_union: bool = getattr(config, "always_use_bars_union", True) is_bars_union = full_name == "types.UnionType" or ( always_use_bars_union and type(annotation).__qualname__ == "_UnionGenericAlias" ) if is_bars_union: full_name = "" # Some types require special handling if full_name == "typing.NewType": args_format = f"\\(``{annotation.__name__}``, {{}})" role = "class" if sys.version_info >= (3, 10) else "func" elif full_name in {"typing.TypeVar", "typing.ParamSpec"}: params = {k: getattr(annotation, f"__{k}__") for k in ("bound", "covariant", "contravariant")} params = {k: v for k, v in params.items() if v} if "bound" in params: params["bound"] = f" {format_annotation(params['bound'], config)}" args_format = f"\\(``{annotation.__name__}``{', {}' if args else ''}" if params: args_format += "".join(f", {k}={v}" for k, v in params.items()) args_format += ")" formatted_args = None if args else args_format elif full_name == "typing.Optional": args = tuple(x for x in args if x is not type(None)) elif full_name in {"typing.Union", "types.UnionType"} and type(None) in args: if len(args) == 2: # noqa: PLR2004 full_name = "typing.Optional" role = "data" args = tuple(x for x in args if x is not type(None)) else: simplify_optional_unions: bool = getattr(config, "simplify_optional_unions", True) if not simplify_optional_unions: full_name = "typing.Optional" role = "data" args_format = f"\\[:py:data:`{prefix}typing.Union`\\[{{}}]]" args = tuple(x for x in args if x is not type(None)) elif full_name in {"typing.Callable", "collections.abc.Callable"} and args and args[0] is not ...: fmt = [format_annotation(arg, config) for arg in args] formatted_args = f"\\[\\[{', '.join(fmt[:-1])}], {fmt[-1]}]" elif full_name == "typing.Literal": formatted_args = f"\\[{', '.join(f'``{arg!r}``' for arg in args)}]" elif is_bars_union: return " | ".join([format_annotation(arg, config) for arg in args]) if args and not formatted_args: try: iter(args) except TypeError: fmt = [format_annotation(args, config)] else: fmt = [format_annotation(arg, config) for arg in args] formatted_args = args_format.format(", ".join(fmt)) escape = "\\ " if formatted_args else "" return f":py:{role}:`{prefix}{full_name}`{escape}{formatted_args}" # reference: https://github.com/pytorch/pytorch/pull/46548/files def normalize_source_lines(source_lines: str) -> str: """ Normalize the source lines. It finds the indentation level of the function definition (`def`), then it indents all lines in the function body to a point at or greater than that level. This allows for comments and continued string literals that are at a lower indentation than the rest of the code. :param source_lines: source code :return: source lines that have been correctly aligned """ lines = source_lines.split("\n") def remove_prefix(text: str, prefix: str) -> str: return text[text.startswith(prefix) and len(prefix) :] # Find the line and line number containing the function definition for pos, line in enumerate(lines): if line.lstrip().startswith("def "): idx = pos whitespace_separator = "def" break if line.lstrip().startswith("async def"): idx = pos whitespace_separator = "async def" break else: return "\n".join(lines) fn_def = lines[idx] # Get a string representing the amount of leading whitespace whitespace = fn_def.split(whitespace_separator)[0] # Add this leading whitespace to all lines before and after the `def` aligned_prefix = [whitespace + remove_prefix(s, whitespace) for s in lines[:idx]] aligned_suffix = [whitespace + remove_prefix(s, whitespace) for s in lines[idx + 1 :]] # Put it together again aligned_prefix.append(fn_def) return "\n".join(aligned_prefix + aligned_suffix) def process_signature( # noqa: C901, PLR0913, PLR0917 app: Sphinx, what: str, name: str, obj: Any, options: Options, # noqa: ARG001 signature: str, # noqa: ARG001 return_annotation: str, # noqa: ARG001 ) -> tuple[str, None] | None: """ Process the signature. :param app: :param what: :param name: :param obj: :param options: :param signature: :param return_annotation: :return: """ if not callable(obj): return None original_obj = obj obj = getattr(obj, "__init__", getattr(obj, "__new__", None)) if inspect.isclass(obj) else obj if not getattr(obj, "__annotations__", None): # when has no annotation we cannot autodoc typehints so bail return None obj = inspect.unwrap(obj) sph_signature = sphinx_signature(obj, type_aliases=app.config["autodoc_type_aliases"]) if app.config.typehints_use_signature: parameters = list(sph_signature.parameters.values()) else: parameters = [param.replace(annotation=inspect.Parameter.empty) for param in sph_signature.parameters.values()] # if we have parameters we may need to delete first argument that's not documented, e.g. self start = 0 if parameters: if inspect.isclass(original_obj) or (what == "method" and name.endswith(".__init__")): start = 1 elif what == "method": # bail if it is a local method as we cannot determine if first argument needs to be deleted or not if "" in obj.__qualname__ and not _is_dataclass(name, what, obj.__qualname__): _LOGGER.warning('Cannot handle as a local function: "%s" (use @functools.wraps)', name) return None outer = inspect.getmodule(obj) for class_name in obj.__qualname__.split(".")[:-1]: outer = getattr(outer, class_name) method_name = obj.__name__ if method_name.startswith("__") and not method_name.endswith("__"): # when method starts with double underscore Python applies mangling -> prepend the class name method_name = f"_{obj.__qualname__.split('.')[-2]}{method_name}" method_object = outer.__dict__[method_name] if outer else obj if not isinstance(method_object, (classmethod, staticmethod)): start = 1 sph_signature = sph_signature.replace(parameters=parameters[start:]) show_return_annotation = app.config.typehints_use_signature_return unqualified_typehints = not getattr(app.config, "typehints_fully_qualified", False) return ( stringify_signature( sph_signature, show_return_annotation=show_return_annotation, unqualified_typehints=unqualified_typehints, ).replace("\\", "\\\\"), None, ) def _is_dataclass(name: str, what: str, qualname: str) -> bool: # generated dataclass __init__() and class need extra checks, as the function operates on the generated class # and methods (not an instantiated dataclass object) it cannot be replaced by a call to # `dataclasses.is_dataclass()` => check manually for either generated __init__ or generated class return (what == "method" and name.endswith(".__init__")) or (what == "class" and qualname.endswith(".__init__")) def _future_annotations_imported(obj: Any) -> bool: _annotations = getattr(inspect.getmodule(obj), "annotations", None) if _annotations is None: return False # Make sure that annotations is imported from __future__ - defined in cpython/Lib/__future__.py # annotations become strings at runtime future_annotations = 0x100000 if sys.version_info[0:2] == (3, 7) else 0x1000000 return bool(_annotations.compiler_flag == future_annotations) def get_all_type_hints( autodoc_mock_imports: list[str], obj: Any, name: str, localns: TypeAliasNamespace ) -> dict[str, Any]: result = _get_type_hint(autodoc_mock_imports, name, obj, localns) if not result: result = backfill_type_hints(obj, name) try: obj.__annotations__ = result except (AttributeError, TypeError): pass else: result = _get_type_hint(autodoc_mock_imports, name, obj, localns) return result _TYPE_GUARD_IMPORT_RE = re.compile(r"\nif (typing.)?TYPE_CHECKING:[^\n]*([\s\S]*?)(?=\n\S)") _TYPE_GUARD_IMPORTS_RESOLVED = set() _TYPE_GUARD_IMPORTS_RESOLVED_GLOBALS_ID = set() def _should_skip_guarded_import_resolution(obj: Any) -> bool: if isinstance(obj, types.ModuleType): return False # Don't skip modules if not hasattr(obj, "__globals__"): return True # Skip objects without __globals__ if hasattr(obj, "__module__"): return obj.__module__ in _TYPE_GUARD_IMPORTS_RESOLVED or obj.__module__ in sys.builtin_module_names return id(obj.__globals__) in _TYPE_GUARD_IMPORTS_RESOLVED_GLOBALS_ID def _execute_guarded_code(autodoc_mock_imports: list[str], obj: Any, module_code: str) -> None: for _, part in _TYPE_GUARD_IMPORT_RE.findall(module_code): guarded_code = textwrap.dedent(part) try: try: with mock(autodoc_mock_imports): exec(guarded_code, getattr(obj, "__globals__", obj.__dict__)) # noqa: S102 except ImportError as exc: # ImportError might have occurred because the module has guarded code as well, # so we recurse on the module. if exc.name: _resolve_type_guarded_imports(autodoc_mock_imports, importlib.import_module(exc.name)) # Retry the guarded code and see if it works now after resolving all nested type guards. with mock(autodoc_mock_imports): exec(guarded_code, getattr(obj, "__globals__", obj.__dict__)) # noqa: S102 except Exception as exc: # noqa: BLE001 _LOGGER.warning("Failed guarded type import with %r", exc) def _resolve_type_guarded_imports(autodoc_mock_imports: list[str], obj: Any) -> None: if _should_skip_guarded_import_resolution(obj): return if hasattr(obj, "__globals__"): _TYPE_GUARD_IMPORTS_RESOLVED_GLOBALS_ID.add(id(obj.__globals__)) module = inspect.getmodule(obj) if module: try: module_code = inspect.getsource(module) except (TypeError, OSError): ... # no source code => no type guards else: _TYPE_GUARD_IMPORTS_RESOLVED.add(module.__name__) _execute_guarded_code(autodoc_mock_imports, obj, module_code) def _get_type_hint(autodoc_mock_imports: list[str], name: str, obj: Any, localns: TypeAliasNamespace) -> dict[str, Any]: _resolve_type_guarded_imports(autodoc_mock_imports, obj) try: result = get_type_hints(obj, None, localns) except (AttributeError, TypeError, RecursionError) as exc: # TypeError - slot wrapper, PEP-563 when part of new syntax not supported # RecursionError - some recursive type definitions https://github.com/python/typing/issues/574 if isinstance(exc, TypeError) and _future_annotations_imported(obj) and "unsupported operand type" in str(exc): result = obj.__annotations__ else: result = {} except NameError as exc: _LOGGER.warning('Cannot resolve forward reference in type annotations of "%s": %s', name, exc) result = obj.__annotations__ return result def backfill_type_hints(obj: Any, name: str) -> dict[str, Any]: # noqa: C901, PLR0911 """ Backfill type hints. :param obj: the object :param name: the name :return: backfilled value """ parse_kwargs = {"type_comments": True} def _one_child(module: Module) -> stmt | None: children = module.body # use the body to ignore type comments if len(children) != 1: _LOGGER.warning('Did not get exactly one node from AST for "%s", got %s', name, len(children)) return None return children[0] try: code = textwrap.dedent(normalize_source_lines(inspect.getsource(obj))) obj_ast = ast.parse(code, **parse_kwargs) # type: ignore[call-overload] # dynamic kwargs except (OSError, TypeError, SyntaxError): return {} obj_ast = _one_child(obj_ast) if obj_ast is None: return {} try: type_comment = obj_ast.type_comment except AttributeError: return {} if not type_comment: return {} try: comment_args_str, comment_returns = type_comment.split(" -> ") except ValueError: _LOGGER.warning('Unparseable type hint comment for "%s": Expected to contain ` -> `', name) return {} rv = {} if comment_returns: rv["return"] = comment_returns args = load_args(obj_ast) comment_args = split_type_comment_args(comment_args_str) is_inline = len(comment_args) == 1 and comment_args[0] == "..." if not is_inline: if args and args[0].arg in {"self", "cls"} and len(comment_args) != len(args): comment_args.insert(0, None) # self/cls may be omitted in type comments, insert blank if len(args) != len(comment_args): _LOGGER.warning('Not enough type comments found on "%s"', name) return rv for at, arg in enumerate(args): arg_key = getattr(arg, "arg", None) if arg_key is None: continue value = getattr(arg, "type_comment", None) if is_inline else comment_args[at] if value is not None: rv[arg_key] = value return rv def load_args(obj_ast: FunctionDef) -> list[Any]: func_args = obj_ast.args args = [] pos_only = getattr(func_args, "posonlyargs", None) if pos_only: args.extend(pos_only) args.extend(func_args.args) if func_args.vararg: args.append(func_args.vararg) args.extend(func_args.kwonlyargs) if func_args.kwarg: args.append(func_args.kwarg) return args def split_type_comment_args(comment: str) -> list[str | None]: def add(val: str) -> None: result.append(val.strip().lstrip("*")) # remove spaces, and var/kw arg marker comment = comment.strip().lstrip("(").rstrip(")") result: list[str | None] = [] if not comment: return result brackets, start_arg_at, at = 0, 0, 0 for at, char in enumerate(comment): if char in {"[", "("}: brackets += 1 elif char in {"]", ")"}: brackets -= 1 elif char == "," and brackets == 0: add(comment[start_arg_at:at]) start_arg_at = at + 1 add(comment[start_arg_at : at + 1]) return result def format_default(app: Sphinx, default: Any, is_annotated: bool) -> str | None: # noqa: FBT001 if default is inspect.Parameter.empty: return None formatted = repr(default).replace("\\", "\\\\") if is_annotated: if app.config.typehints_defaults.startswith("braces"): return f" (default: ``{formatted}``)" return f", default: ``{formatted}``" if app.config.typehints_defaults == "braces-after": return f" (default: ``{formatted}``)" return f"default: ``{formatted}``" def process_docstring( # noqa: PLR0913, PLR0917 app: Sphinx, what: str, name: str, obj: Any, options: Options | None, # noqa: ARG001 lines: list[str], ) -> None: """ Process the docstring for an entry. :param app: the Sphinx app :param what: the target :param name: the name :param obj: the object :param options: the options :param lines: the lines :return: """ original_obj = obj obj = obj.fget if isinstance(obj, property) else obj if not callable(obj): return obj = obj.__init__ if inspect.isclass(obj) else obj obj = inspect.unwrap(obj) try: signature = sphinx_signature(obj, type_aliases=app.config["autodoc_type_aliases"]) except (ValueError, TypeError): signature = None localns = TypeAliasNamespace(app.config["autodoc_type_aliases"]) type_hints = get_all_type_hints(app.config.autodoc_mock_imports, obj, name, localns) app.config._annotation_globals = getattr(obj, "__globals__", {}) # noqa: SLF001 try: _inject_types_to_docstring(type_hints, signature, original_obj, app, what, name, lines) finally: delattr(app.config, "_annotation_globals") def _get_sphinx_line_keyword_and_argument(line: str) -> tuple[str, str | None] | None: """ Extract a keyword, and its optional argument out of a sphinx field option line. For example >>> _get_sphinx_line_keyword_and_argument(":param parameter:") ("param", "parameter") >>> _get_sphinx_line_keyword_and_argument(":return:") ("return", None) >>> _get_sphinx_line_keyword_and_argument("some invalid line") None """ param_line_without_description = line.split(":", maxsplit=2) if len(param_line_without_description) != 3: # noqa: PLR2004 return None split_directive_and_name = param_line_without_description[1].split(maxsplit=1) if len(split_directive_and_name) != 2: # noqa: PLR2004 if not len(split_directive_and_name): return None return split_directive_and_name[0], None return tuple(split_directive_and_name) # type: ignore[return-value] def _line_is_param_line_for_arg(line: str, arg_name: str) -> bool: """Return True if `line` is a valid parameter line for `arg_name`, false otherwise.""" keyword_and_name = _get_sphinx_line_keyword_and_argument(line) if keyword_and_name is None: return False keyword, doc_name = keyword_and_name if doc_name is None: return False if keyword not in {"param", "parameter", "arg", "argument"}: return False return any(doc_name == prefix + arg_name for prefix in ("", "\\*", "\\**", "\\*\\*")) def _inject_types_to_docstring( # noqa: PLR0913, PLR0917 type_hints: dict[str, Any], signature: inspect.Signature | None, original_obj: Any, app: Sphinx, what: str, name: str, lines: list[str], ) -> None: if signature is not None: _inject_signature(type_hints, signature, app, lines) if "return" in type_hints: _inject_rtype(type_hints, original_obj, app, what, name, lines) def _inject_signature( type_hints: dict[str, Any], signature: inspect.Signature, app: Sphinx, lines: list[str], ) -> None: for arg_name in signature.parameters: annotation = type_hints.get(arg_name) default = signature.parameters[arg_name].default if arg_name.endswith("_"): arg_name = f"{arg_name[:-1]}\\_" # noqa: PLW2901 insert_index = None for at, line in enumerate(lines): if _line_is_param_line_for_arg(line, arg_name): # Get the arg_name from the doc to match up for type in case it has a star prefix. # Line is in the correct format so this is guaranteed to return tuple[str, str]. func = _get_sphinx_line_keyword_and_argument _, arg_name = func(line) # type: ignore[assignment, misc] # noqa: PLW2901 insert_index = at break if annotation is not None and insert_index is None and app.config.always_document_param_types: lines.append(f":param {arg_name}:") insert_index = len(lines) if insert_index is not None: if annotation is None: type_annotation = f":type {arg_name}: " else: formatted_annotation = add_type_css_class(format_annotation(annotation, app.config)) type_annotation = f":type {arg_name}: {formatted_annotation}" if app.config.typehints_defaults: formatted_default = format_default(app, default, annotation is not None) if formatted_default: type_annotation = _append_default(app, lines, insert_index, type_annotation, formatted_default) lines.insert(insert_index, type_annotation) def _append_default( app: Sphinx, lines: list[str], insert_index: int, type_annotation: str, formatted_default: str ) -> str: if app.config.typehints_defaults.endswith("after"): # advance the index to the end of the :param: paragraphs # (terminated by a line with no indentation) # append default to the last nonempty line nlines = len(lines) next_index = insert_index + 1 append_index = insert_index # last nonempty line while next_index < nlines and (not lines[next_index] or lines[next_index].startswith(" ")): if lines[next_index]: append_index = next_index next_index += 1 lines[append_index] += formatted_default else: # add to last param doc line type_annotation += formatted_default return type_annotation @dataclass class InsertIndexInfo: insert_index: int found_param: bool = False found_return: bool = False found_directive: bool = False # Sphinx allows so many synonyms... # See sphinx.domains.python.PyObject PARAM_SYNONYMS = ("param ", "parameter ", "arg ", "argument ", "keyword ", "kwarg ", "kwparam ") def node_line_no(node: Node) -> int | None: """ Get the 1-indexed line on which the node starts if possible. If not, return None. Descend through the first children until we locate one with a line number or return None if None of them have one. I'm not aware of any rst on which this returns None, to find out would require a more detailed analysis of the docutils rst parser source code. An example where the node doesn't have a line number but the first child does is all `definition_list` nodes. It seems like bullet_list and option_list get line numbers, but enum_list also doesn't. """ if node is None: return None while node.line is None and node.children: node = node.children[0] return node.line def tag_name(node: Node) -> str: return node.tagname # type:ignore[attr-defined,no-any-return] def get_insert_index(app: Sphinx, lines: list[str]) -> InsertIndexInfo | None: # 1. If there is an existing :rtype: anywhere, don't insert anything. if any(line.startswith(":rtype:") for line in lines): return None # 2. If there is a :returns: anywhere, either modify that line or insert # just before it. for at, line in enumerate(lines): if line.startswith((":return:", ":returns:")): return InsertIndexInfo(insert_index=at, found_return=True) # 3. Insert after the parameters. # To find the parameters, parse as a docutils tree. settings = OptionParser(components=(RSTParser,)).get_default_values() settings.env = app.env doc = parse("\n".join(lines), settings) # Find a top level child which is a field_list that contains a field whose # name starts with one of the PARAM_SYNONYMS. This is the parameter list. We # hope there is at most of these. for child in doc.children: if tag_name(child) != "field_list": continue if not any(c.children[0].astext().startswith(PARAM_SYNONYMS) for c in child.children): continue # Found it! Try to insert before the next sibling. If there is no next # sibling, insert at end. # If there is a next sibling but we can't locate a line number, insert # at end. (I don't know of any input where this happens.) next_sibling = child.next_node(descend=False, siblings=True) line_no = node_line_no(next_sibling) if next_sibling else None at = max(line_no - 2, 0) if line_no else len(lines) return InsertIndexInfo(insert_index=at, found_param=True) # 4. Insert before examples for child in doc.children: if tag_name(child) in {"literal_block", "paragraph", "field_list"}: continue line_no = node_line_no(child) at = max(line_no - 2, 0) if line_no else len(lines) return InsertIndexInfo(insert_index=at, found_directive=True) # 5. Otherwise, insert at end return InsertIndexInfo(insert_index=len(lines)) def _inject_rtype( # noqa: PLR0913, PLR0917 type_hints: dict[str, Any], original_obj: Any, app: Sphinx, what: str, name: str, lines: list[str], ) -> None: if inspect.isclass(original_obj) or inspect.isdatadescriptor(original_obj): return if what == "method" and name.endswith(".__init__"): # avoid adding a return type for data class __init__ return if not app.config.typehints_document_rtype: return r = get_insert_index(app, lines) if r is None: return insert_index = r.insert_index if not app.config.typehints_use_rtype and r.found_return and " -- " in lines[insert_index]: return formatted_annotation = add_type_css_class(format_annotation(type_hints["return"], app.config)) if r.found_param and insert_index < len(lines) and lines[insert_index].strip(): insert_index -= 1 if insert_index == len(lines) and not r.found_param: # ensure that :rtype: doesn't get joined with a paragraph of text lines.append("") insert_index += 1 if app.config.typehints_use_rtype or not r.found_return: line = f":rtype: {formatted_annotation}" lines.insert(insert_index, line) if r.found_directive: lines.insert(insert_index + 1, "") else: line = lines[insert_index] lines[insert_index] = f":return: {formatted_annotation} --{line[line.find(' ') :]}" def validate_config(app: Sphinx, env: BuildEnvironment, docnames: list[str]) -> None: # noqa: ARG001 valid = {None, "comma", "braces", "braces-after"} if app.config.typehints_defaults not in valid | {False}: msg = f"typehints_defaults needs to be one of {valid!r}, not {app.config.typehints_defaults!r}" raise ValueError(msg) formatter = app.config.typehints_formatter if formatter is not None and not callable(formatter): msg = f"typehints_formatter needs to be callable or `None`, not {formatter}" raise ValueError(msg) def unescape(escaped: str) -> str: # For some reason the string we get has a bunch of null bytes in it?? # Remove them... escaped = escaped.replace("\x00", "") # For some reason the extra slash before spaces gets lost between the .rst # source and when this directive is called. So don't replace "\" => # "" return re.sub(r"\\([^ ])", r"\1", escaped) def add_type_css_class(type_rst: str) -> str: return f":sphinx_autodoc_typehints_type:`{rst.escape(type_rst)}`" def sphinx_autodoc_typehints_type_role( _role: str, _rawtext: str, text: str, _lineno: int, inliner: states.Inliner, _options: dict[str, Any] | None = None, _content: list[str] | None = None, ) -> tuple[list[Node], list[Node]]: """ Add css tag around rendered type. The body should be escaped rst. This renders its body as rst and wraps the result in """ unescaped = unescape(text) doc = parse(unescaped, inliner.document.settings) n = nodes.inline(text) n["classes"].append("sphinx_autodoc_typehints-type") n += doc.children[0].children return [n], [] def setup(app: Sphinx) -> dict[str, bool]: app.add_config_value("always_document_param_types", False, "html") # noqa: FBT003 app.add_config_value("typehints_fully_qualified", False, "env") # noqa: FBT003 app.add_config_value("typehints_document_rtype", True, "env") # noqa: FBT003 app.add_config_value("typehints_use_rtype", True, "env") # noqa: FBT003 app.add_config_value("typehints_defaults", None, "env") app.add_config_value("simplify_optional_unions", True, "env") # noqa: FBT003 app.add_config_value("always_use_bars_union", False, "env") # noqa: FBT003 app.add_config_value("typehints_formatter", None, "env") app.add_config_value("typehints_use_signature", False, "env") # noqa: FBT003 app.add_config_value("typehints_use_signature_return", False, "env") # noqa: FBT003 app.add_config_value("typehints_fixup_module_name", None, "env") app.add_role("sphinx_autodoc_typehints_type", sphinx_autodoc_typehints_type_role) app.connect("env-before-read-docs", validate_config) # config may be changed after “config-inited” event app.connect("autodoc-process-signature", process_signature) app.connect("autodoc-process-docstring", process_docstring) install_patches(app) return {"parallel_read_safe": True, "parallel_write_safe": True} __all__ = [ "__version__", "backfill_type_hints", "format_annotation", "get_annotation_args", "get_annotation_class_name", "get_annotation_module", "normalize_source_lines", "process_docstring", "process_signature", ] sphinx_autodoc_typehints-2.3.0/src/sphinx_autodoc_typehints/attributes_patch.py0000644000000000000000000000674213615410400025447 0ustar00"""Patch for attributes.""" from __future__ import annotations from functools import partial from typing import TYPE_CHECKING, Any from unittest.mock import patch import sphinx.domains.python import sphinx.ext.autodoc from sphinx.domains.python import PyAttribute from sphinx.ext.autodoc import AttributeDocumenter from .parser import parse if TYPE_CHECKING: from docutils.frontend import Values from sphinx.addnodes import desc_signature from sphinx.application import Sphinx # Defensively check for the things we want to patch _parse_annotation = getattr(sphinx.domains.python, "_parse_annotation", None) # We want to patch: # * sphinx.ext.autodoc.stringify_typehint (in sphinx < 6.1) # * sphinx.ext.autodoc.stringify_annotation (in sphinx >= 6.1) STRINGIFY_PATCH_TARGET = "" for target in ["stringify_typehint", "stringify_annotation"]: if hasattr(sphinx.ext.autodoc, target): STRINGIFY_PATCH_TARGET = f"sphinx.ext.autodoc.{target}" break # If we didn't locate both patch targets, we will just do nothing. OKAY_TO_PATCH = bool(_parse_annotation and STRINGIFY_PATCH_TARGET) # A label we inject to the type string so we know not to try to treat it as a # type annotation TYPE_IS_RST_LABEL = "--is-rst--" orig_add_directive_header = AttributeDocumenter.add_directive_header orig_handle_signature = PyAttribute.handle_signature def _stringify_annotation(app: Sphinx, annotation: Any, mode: str = "") -> str: # noqa: ARG001 # Format the annotation with sphinx-autodoc-typehints and inject our magic prefix to tell our patched # PyAttribute.handle_signature to treat it as rst. from . import format_annotation # noqa: PLC0415 return TYPE_IS_RST_LABEL + format_annotation(annotation, app.config) def patch_attribute_documenter(app: Sphinx) -> None: """Instead of using stringify_typehint in `AttributeDocumenter.add_directive_header`, use `format_annotation`.""" def add_directive_header(*args: Any, **kwargs: Any) -> Any: with patch(STRINGIFY_PATCH_TARGET, partial(_stringify_annotation, app)): return orig_add_directive_header(*args, **kwargs) AttributeDocumenter.add_directive_header = add_directive_header # type:ignore[method-assign] def rst_to_docutils(settings: Values, rst: str) -> Any: """Convert rst to a sequence of docutils nodes.""" doc = parse(rst, settings) # Remove top level paragraph node so that there is no line break. return doc.children[0].children def patched_parse_annotation(settings: Values, typ: str, env: Any) -> Any: # if typ doesn't start with our label, use original function if not typ.startswith(TYPE_IS_RST_LABEL): assert _parse_annotation is not None # noqa: S101 return _parse_annotation(typ, env) # Otherwise handle as rst typ = typ[len(TYPE_IS_RST_LABEL) :] return rst_to_docutils(settings, typ) def patched_handle_signature(self: PyAttribute, sig: str, signode: desc_signature) -> tuple[str, str]: target = "sphinx.domains.python._parse_annotation" new_func = partial(patched_parse_annotation, self.state.document.settings) with patch(target, new_func): return orig_handle_signature(self, sig, signode) def patch_attribute_handling(app: Sphinx) -> None: """Use format_signature to format class attribute type annotations.""" if not OKAY_TO_PATCH: return PyAttribute.handle_signature = patched_handle_signature # type:ignore[method-assign] patch_attribute_documenter(app) __all__ = ["patch_attribute_handling"] sphinx_autodoc_typehints-2.3.0/src/sphinx_autodoc_typehints/parser.py0000644000000000000000000000166413615410400023374 0ustar00"""Utilities for side-effect-free rST parsing.""" from __future__ import annotations from typing import TYPE_CHECKING from docutils.utils import new_document from sphinx.parsers import RSTParser from sphinx.util.docutils import sphinx_domains if TYPE_CHECKING: import optparse from docutils import nodes from docutils.frontend import Values from docutils.statemachine import StringList class _RstSnippetParser(RSTParser): @staticmethod def decorate(_content: StringList) -> None: """Override to skip processing rst_epilog/rst_prolog for typing.""" def parse(inputstr: str, settings: Values | optparse.Values) -> nodes.document: """Parse inputstr and return a docutils document.""" doc = new_document("", settings=settings) with sphinx_domains(settings.env): parser = _RstSnippetParser() parser.set_application(settings.env.app) parser.parse(inputstr, doc) return doc sphinx_autodoc_typehints-2.3.0/src/sphinx_autodoc_typehints/patches.py0000644000000000000000000001067413615410400023530 0ustar00"""Custom patches to make the world work.""" from __future__ import annotations from functools import lru_cache from typing import TYPE_CHECKING, Any from docutils.parsers.rst.directives.admonitions import BaseAdmonition from docutils.parsers.rst.states import Text from sphinx.ext.napoleon.docstring import GoogleDocstring from .attributes_patch import patch_attribute_handling if TYPE_CHECKING: from sphinx.application import Sphinx from sphinx.ext.autodoc import Options @lru_cache # A cute way to make sure the function only runs once. def fix_autodoc_typehints_for_overloaded_methods() -> None: """ sphinx-autodoc-typehints responds to the "autodoc-process-signature" event to remove types from the signature line. Normally, `FunctionDocumenter.format_signature` and `MethodDocumenter.format_signature` call `super().format_signature` which ends up going to `Documenter.format_signature`, and this last method emits the `autodoc-process-signature` event. However, if there are overloads, `FunctionDocumenter.format_signature` does something else and the event never occurs. Here we remove this alternative code path by brute force. See https://github.com/tox-dev/sphinx-autodoc-typehints/issues/296 """ from sphinx.ext.autodoc import FunctionDocumenter, MethodDocumenter # noqa: PLC0415 del FunctionDocumenter.format_signature del MethodDocumenter.format_signature def napoleon_numpy_docstring_return_type_processor( # noqa: PLR0913, PLR0917 app: Sphinx, what: str, name: str, # noqa: ARG001 obj: Any, # noqa: ARG001 options: Options | None, # noqa: ARG001 lines: list[str], ) -> None: """Insert a : under Returns: to tell napoleon not to look for a return type.""" if what not in {"function", "method"}: return if not getattr(app.config, "napoleon_numpy_docstring", False): return # Search for the returns header: # Returns: # -------- for pos, line in enumerate(lines[:-2]): if line.lower().strip(":") not in {"return", "returns"}: continue # Underline detection. chars = set(lines[pos + 1].strip()) # Napoleon allows the underline to consist of a bunch of weirder things... if len(chars) != 1 or next(iter(chars)) not in "=-~_*+#": continue pos += 2 # noqa: PLW2901 break else: return lines.insert(pos, ":") def fix_napoleon_numpy_docstring_return_type(app: Sphinx) -> None: """If no return type is explicitly set, numpy docstrings will use the return type text as return types.""" # standard priority is 500. Setting priority to 499 ensures this runs before # napoleon's docstring processor. app.connect("autodoc-process-docstring", napoleon_numpy_docstring_return_type_processor, priority=499) def _patched_lookup_annotation(*_args: Any) -> str: """ GoogleDocstring._lookup_annotation sometimes adds incorrect type annotations to constructor parameters. Disable it so we can handle this on our own. """ return "" def _patch_google_docstring_lookup_annotation() -> None: """Fix issue https://github.com/tox-dev/sphinx-autodoc-typehints/issues/308.""" GoogleDocstring._lookup_annotation = _patched_lookup_annotation # type: ignore[assignment] # noqa: SLF001 orig_base_admonition_run = BaseAdmonition.run def _patched_base_admonition_run(self: BaseAdmonition) -> Any: result = orig_base_admonition_run(self) result[0].line = self.lineno return result orig_text_indent = Text.indent def _patched_text_indent(self: Text, *args: Any) -> Any: _, line = self.state_machine.get_source_and_line() result = orig_text_indent(self, *args) node = self.parent[-1] if node.tagname == "system_message": node = self.parent[-2] node.line = line return result def _patch_line_numbers() -> None: """ Make the rst parser put line numbers on more nodes. When the line numbers are missing, we have a hard time placing the :rtype:. """ Text.indent = _patched_text_indent BaseAdmonition.run = _patched_base_admonition_run def install_patches(app: Sphinx) -> None: """ Install the patches. :param app: the Sphinx app """ fix_autodoc_typehints_for_overloaded_methods() patch_attribute_handling(app) _patch_google_docstring_lookup_annotation() fix_napoleon_numpy_docstring_return_type(app) _patch_line_numbers() ___all__ = [ "install_patches", ] sphinx_autodoc_typehints-2.3.0/src/sphinx_autodoc_typehints/py.typed0000644000000000000000000000000013615410400023204 0ustar00sphinx_autodoc_typehints-2.3.0/src/sphinx_autodoc_typehints/version.py0000644000000000000000000000063313615410400023560 0ustar00# file generated by setuptools_scm # don't change, don't track in version control TYPE_CHECKING = False if TYPE_CHECKING: from typing import Tuple, Union VERSION_TUPLE = Tuple[Union[int, str], ...] else: VERSION_TUPLE = object version: str __version__: str __version_tuple__: VERSION_TUPLE version_tuple: VERSION_TUPLE __version__ = version = '2.3.0' __version_tuple__ = version_tuple = (2, 3, 0) sphinx_autodoc_typehints-2.3.0/tests/conftest.py0000644000000000000000000000346613615410400017144 0ustar00from __future__ import annotations import re import shutil import sys from pathlib import Path from typing import TYPE_CHECKING, Any import pytest from sphinx.testing.path import path from sphobjinv import Inventory if TYPE_CHECKING: from _pytest.config import Config pytest_plugins = "sphinx.testing.fixtures" collect_ignore = ["roots"] @pytest.fixture(scope="session") def inv(pytestconfig: Config) -> Inventory: cache_path = f"python{sys.version_info.major}.{sys.version_info.minor}/objects.inv" assert pytestconfig.cache is not None inv_dict = pytestconfig.cache.get(cache_path, None) if inv_dict is not None: return Inventory(inv_dict) url = f"https://docs.python.org/{sys.version_info.major}.{sys.version_info.minor}/objects.inv" inv = Inventory(url=url) pytestconfig.cache.set(cache_path, inv.json_dict()) return inv @pytest.fixture(autouse=True) def _remove_sphinx_projects(sphinx_test_tempdir: path) -> None: # Remove any directory which appears to be a Sphinx project from # the temporary directory area. # See https://github.com/sphinx-doc/sphinx/issues/4040 roots_path = Path(sphinx_test_tempdir) for entry in roots_path.iterdir(): try: if entry.is_dir() and Path(entry, "_build").exists(): shutil.rmtree(str(entry)) except PermissionError: # noqa: PERF203 pass @pytest.fixture def rootdir() -> path: return path(str(Path(__file__).parent) or ".").abspath() / "roots" def pytest_ignore_collect(path: Any, config: Config) -> bool | None: # noqa: ARG001 version_re = re.compile(r"_py(\d)(\d)\.py$") match = version_re.search(path.basename) if match: version = tuple(int(x) for x in match.groups()) if sys.version_info < version: return True return None sphinx_autodoc_typehints-2.3.0/tests/test_integration.py0000644000000000000000000005703113615410400020676 0ustar00from __future__ import annotations import re import sys from dataclasses import dataclass from inspect import isclass from pathlib import Path from textwrap import dedent, indent from typing import ( # no type comments TYPE_CHECKING, Any, Callable, NewType, Optional, TypeVar, Union, overload, ) import pytest if TYPE_CHECKING: from collections.abc import AsyncGenerator from io import StringIO from mailbox import Mailbox from types import CodeType, ModuleType from sphinx.testing.util import SphinxTestApp T = TypeVar("T") W = NewType("W", str) def expected(expected: str, **options: dict[str, Any]) -> Callable[[T], T]: def dec(val: T) -> T: val.EXPECTED = expected val.OPTIONS = options return val return dec def warns(pattern: str) -> Callable[[T], T]: def dec(val: T) -> T: val.WARNING = pattern return val return dec @expected("mod.get_local_function()") def get_local_function(): # noqa: ANN201 def wrapper(self) -> str: # noqa: ANN001, ARG001 """ Wrapper """ return wrapper @warns("Cannot handle as a local function") @expected( """\ class mod.Class(x, y, z=None) Initializer docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar * **z** ("Optional"["str"]) -- baz class InnerClass Inner class. inner_method(x) Inner method. Parameters: **x** ("bool") -- foo Return type: "str" classmethod a_classmethod(x, y, z=None) Classmethod docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar * **z** ("Optional"["str"]) -- baz Return type: "str" a_method(x, y, z=None) Method docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar * **z** ("Optional"["str"]) -- baz Return type: "str" property a_property: str Property docstring static a_staticmethod(x, y, z=None) Staticmethod docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar * **z** ("Optional"["str"]) -- baz Return type: "str" locally_defined_callable_field() -> str Wrapper Return type: "str" """, ) class Class: """ Initializer docstring. :param x: foo :param y: bar :param z: baz """ def __init__(self, x: bool, y: int, z: Optional[str] = None) -> None: # noqa: UP007 pass def a_method(self, x: bool, y: int, z: Optional[str] = None) -> str: # noqa: UP007 """ Method docstring. :param x: foo :param y: bar :param z: baz """ def _private_method(self, x: str) -> str: """ Private method docstring. :param x: foo """ def __dunder_method(self, x: str) -> str: """ Dunder method docstring. :param x: foo """ def __magic_custom_method__(self, x: str) -> str: # noqa: PLW3201 """ Magic dunder method docstring. :param x: foo """ @classmethod def a_classmethod(cls, x: bool, y: int, z: Optional[str] = None) -> str: # noqa: UP007 """ Classmethod docstring. :param x: foo :param y: bar :param z: baz """ @staticmethod def a_staticmethod(x: bool, y: int, z: Optional[str] = None) -> str: # noqa: UP007 """ Staticmethod docstring. :param x: foo :param y: bar :param z: baz """ @property def a_property(self) -> str: """ Property docstring """ class InnerClass: """ Inner class. """ def inner_method(self, x: bool) -> str: """ Inner method. :param x: foo """ def __dunder_inner_method(self, x: bool) -> str: """ Dunder inner method. :param x: foo """ locally_defined_callable_field = get_local_function() @expected( """\ exception mod.DummyException(message) Exception docstring Parameters: **message** ("str") -- blah """, ) class DummyException(Exception): # noqa: N818 """ Exception docstring :param message: blah """ def __init__(self, message: str) -> None: super().__init__(message) @expected( """\ mod.function(x, y, z_=None) Function docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar * **z_** ("Optional"["str"]) -- baz Returns: something Return type: bytes """, ) def function(x: bool, y: int, z_: Optional[str] = None) -> str: # noqa: ARG001, UP007 """ Function docstring. :param x: foo :param y: bar :param z\\_: baz :return: something :rtype: bytes """ @expected( """\ mod.function_with_starred_documentation_param_names(*args, **kwargs) Function docstring. Usage: print(1) Parameters: * ***args** ("int") -- foo * ****kwargs** ("str") -- bar """, ) def function_with_starred_documentation_param_names(*args: int, **kwargs: str): # noqa: ANN201, ARG001 r""" Function docstring. Usage:: print(1) :param \*args: foo :param \**kwargs: bar """ @expected( """\ mod.function_with_escaped_default(x='\\\\x08') Function docstring. Parameters: **x** ("str") -- foo """, ) def function_with_escaped_default(x: str = "\b"): # noqa: ANN201, ARG001 """ Function docstring. :param x: foo """ @warns("Cannot resolve forward reference in type annotations") @expected( """\ mod.function_with_unresolvable_annotation(x) Function docstring. Parameters: **x** (a.b.c) -- foo """, ) def function_with_unresolvable_annotation(x: a.b.c): # noqa: ANN201, ARG001, F821 """ Function docstring. :arg x: foo """ @expected( """\ mod.function_with_typehint_comment(x, y) Function docstring. Parameters: * **x** ("int") -- foo * **y** ("str") -- bar Return type: "None" """, ) def function_with_typehint_comment( # noqa: ANN201 x, # type: int # noqa: ANN001, ARG001 y, # type: str # noqa: ANN001, ARG001 ): # type: (...) -> None """ Function docstring. :parameter x: foo :parameter y: bar """ @expected( """\ class mod.ClassWithTypehints(x) Class docstring. Parameters: **x** ("int") -- foo foo(x) Method docstring. Parameters: **x** ("str") -- foo Return type: "int" method_without_typehint(x) Method docstring. """, ) class ClassWithTypehints: """ Class docstring. :param x: foo """ def __init__( self, x, # type: int # noqa: ANN001 ) -> None: # type: (...) -> None pass def foo( # noqa: ANN201, PLR6301 self, x, # type: str # noqa: ANN001, ARG002 ): # type: (...) -> int """ Method docstring. :arg x: foo """ return 42 def method_without_typehint(self, x): # noqa: ANN001, ANN201, ARG002, PLR6301 """ Method docstring. """ # test that multiline str can be correctly indented multiline_str = """ test """ return multiline_str # noqa: RET504 @expected( """\ mod.function_with_typehint_comment_not_inline(x=None, *y, z, **kwargs) Function docstring. Parameters: * **x** ("Union"["str", "bytes", "None"]) -- foo * **y** ("str") -- bar * **z** ("bytes") -- baz * **kwargs** ("int") -- some kwargs Return type: "None" """, ) def function_with_typehint_comment_not_inline(x=None, *y, z, **kwargs): # noqa: ANN001, ANN002, ANN003, ANN201, ARG001 # type: (Union[str, bytes, None], *str, bytes, **int) -> None """ Function docstring. :arg x: foo :argument y: bar :parameter z: baz :parameter kwargs: some kwargs """ @expected( """\ class mod.ClassWithTypehintsNotInline(x=None) Class docstring. Parameters: **x** ("Optional"["Callable"[["int", "bytes"], "int"]]) -- foo foo(x=1) Method docstring. Parameters: **x** ("Callable"[["int", "bytes"], "int"]) -- foo Return type: "int" classmethod mk(x=None) Method docstring. Parameters: **x** ("Optional"["Callable"[["int", "bytes"], "int"]]) -- foo Return type: "ClassWithTypehintsNotInline" """, ) class ClassWithTypehintsNotInline: """ Class docstring. :param x: foo """ def __init__(self, x=None) -> None: # type: (Optional[Callable[[int, bytes], int]]) -> None # noqa: ANN001 pass def foo(self, x=1): # type: (Callable[[int, bytes], int]) -> int # noqa: ANN001, ANN201, PLR6301 """ Method docstring. :param x: foo """ return x(1, b"") @classmethod def mk( # noqa: ANN206 cls, x=None, # noqa: ANN001 ): # type: (Optional[Callable[[int, bytes], int]]) -> ClassWithTypehintsNotInline """ Method docstring. :param x: foo """ return cls(x) @expected( """\ mod.undocumented_function(x) Hi Return type: "str" """, ) def undocumented_function(x: int) -> str: """Hi""" return str(x) @expected( """\ class mod.DataClass(x) Class docstring. """, ) @dataclass class DataClass: """Class docstring.""" x: int @expected( """\ class mod.Decorator(func) Initializer docstring. Parameters: **func** ("Callable"[["int", "str"], "str"]) -- function """, ) class Decorator: """ Initializer docstring. :param func: function """ def __init__(self, func: Callable[[int, str], str]) -> None: pass @expected( """\ mod.mocked_import(x) A docstring. Parameters: **x** ("Mailbox") -- function """, ) def mocked_import(x: Mailbox): # noqa: ANN201, ARG001 """ A docstring. :param x: function """ @expected( """\ mod.func_with_examples() A docstring. Return type: "int" -[ Examples ]- Here are a couple of examples of how to use this function. """, ) def func_with_examples() -> int: """ A docstring. .. rubric:: Examples Here are a couple of examples of how to use this function. """ @overload def func_with_overload(a: int, b: int) -> None: ... @overload def func_with_overload(a: str, b: str) -> None: ... @expected( """\ mod.func_with_overload(a, b) f does the thing. The arguments can either be ints or strings but they must both have the same type. Parameters: * **a** ("Union"["int", "str"]) -- The first thing * **b** ("Union"["int", "str"]) -- The second thing Return type: "None" """, ) def func_with_overload(a: Union[int, str], b: Union[int, str]) -> None: # noqa: ARG001, UP007 """ f does the thing. The arguments can either be ints or strings but they must both have the same type. Parameters ---------- a: The first thing b: The second thing """ @expected( """\ class mod.TestClassAttributeDocs A class code: "Optional"["CodeType"] An attribute """, ) class TestClassAttributeDocs: """A class""" code: Optional[CodeType] # noqa: UP007 """An attribute""" @expected( """\ mod.func_with_examples_and_returns_after() f does the thing. -[ Examples ]- Here is an example Return type: "int" Returns: The index of the widget """, ) def func_with_examples_and_returns_after() -> int: """ f does the thing. Examples -------- Here is an example :returns: The index of the widget """ @expected( """\ mod.func_with_parameters_and_stuff_after(a, b) A func Parameters: * **a** ("int") -- a tells us something * **b** ("int") -- b tells us something Return type: "int" More info about the function here. """, ) def func_with_parameters_and_stuff_after(a: int, b: int) -> int: # noqa: ARG001 """A func :param a: a tells us something :param b: b tells us something More info about the function here. """ @expected( """\ mod.func_with_rtype_in_weird_spot(a, b) A func Parameters: * **a** ("int") -- a tells us something * **b** ("int") -- b tells us something -[ Examples ]- Here is an example Returns: The index of the widget More info about the function here. Return type: int """, ) def func_with_rtype_in_weird_spot(a: int, b: int) -> int: # noqa: ARG001 """A func :param a: a tells us something :param b: b tells us something Examples -------- Here is an example :returns: The index of the widget More info about the function here. :rtype: int """ @expected( """\ mod.empty_line_between_parameters(a, b) A func Parameters: * **a** ("int") -- One of the following possibilities: * a * b * c * **b** ("int") -- Whatever else we have to say. There is more of it And here too Return type: "int" More stuff here. """, ) def empty_line_between_parameters(a: int, b: int) -> int: # noqa: ARG001 """A func :param a: One of the following possibilities: - a - b - c :param b: Whatever else we have to say. There is more of it And here too More stuff here. """ @expected( """\ mod.func_with_code_block() A docstring. You would say: print("some python code here") Return type: "int" -[ Examples ]- Here are a couple of examples of how to use this function. """, ) def func_with_code_block() -> int: """ A docstring. You would say: .. code-block:: print("some python code here") .. rubric:: Examples Here are a couple of examples of how to use this function. """ @expected( """ mod.func_with_definition_list() Some text and then a definition list. Return type: "int" abc x xyz something """, ) def func_with_definition_list() -> int: """Some text and then a definition list. abc x xyz something """ # See https://github.com/tox-dev/sphinx-autodoc-typehints/issues/302 @expected( """\ mod.decorator_2(f) Run the decorated function with *asyncio.run*. Parameters: **f** ("Any") -- The function to wrap. Return type: "Any" -[ Examples ]- A """, ) def decorator_2(f: Any) -> Any: """Run the decorated function with `asyncio.run`. Parameters ---------- f The function to wrap. Examples -------- .. code-block:: python A """ assert f is not None @expected( """ class mod.ParamAndAttributeHaveSameName(blah) A Class Parameters: **blah** ("CodeType") -- Description of parameter blah blah: "ModuleType" Description of attribute blah """, ) class ParamAndAttributeHaveSameName: """ A Class Parameters ---------- blah: Description of parameter blah """ def __init__(self, blah: CodeType) -> None: pass blah: ModuleType """Description of attribute blah""" @expected( """ mod.napoleon_returns() A function. Return type: "CodeType" Returns: The info about the whatever. """, ) def napoleon_returns() -> CodeType: """ A function. Returns ------- The info about the whatever. """ @expected( """ mod.google_docstrings(arg1, arg2) Summary line. Extended description of function. Parameters: * **arg1** ("CodeType") -- Description of arg1 * **arg2** ("ModuleType") -- Description of arg2 Return type: "CodeType" Returns: Description of return value """, ) def google_docstrings(arg1: CodeType, arg2: ModuleType) -> CodeType: # noqa: ARG001 """Summary line. Extended description of function. Args: arg1: Description of arg1 arg2: Description of arg2 Returns: Description of return value """ @expected( """ mod.docstring_with_multiline_note_after_params(param) Do something. Parameters: **param** ("int") -- A parameter. Return type: "None" Note: Some notes. More notes """, ) def docstring_with_multiline_note_after_params(param: int) -> None: # noqa: ARG001 """Do something. Args: param: A parameter. Note: Some notes. More notes """ @expected( """ mod.docstring_with_bullet_list_after_params(param) Do something. Parameters: **param** ("int") -- A parameter. Return type: "None" * A: B * C: D """, ) def docstring_with_bullet_list_after_params(param: int) -> None: # noqa: ARG001 """Do something. Args: param: A parameter. * A: B * C: D """ @expected( """ mod.docstring_with_definition_list_after_params(param) Do something. Parameters: **param** ("int") -- A parameter. Return type: "None" Term A description maybe multiple lines Next Term Something about it """, ) def docstring_with_definition_list_after_params(param: int) -> None: # noqa: ARG001 """Do something. Args: param: A parameter. Term A description maybe multiple lines Next Term Something about it """ @expected( """ mod.docstring_with_enum_list_after_params(param) Do something. Parameters: **param** ("int") -- A parameter. Return type: "None" 1. A: B 2. C: D """, ) def docstring_with_enum_list_after_params(param: int) -> None: # noqa: ARG001 """Do something. Args: param: A parameter. 1. A: B 2. C: D """ @warns("Definition list ends without a blank line") @expected( """ mod.docstring_with_definition_list_after_params_no_blank_line(param) Do something. Parameters: **param** ("int") -- A parameter. Return type: "None" Term A description maybe multiple lines Next Term Something about it -[ Example ]- """, ) def docstring_with_definition_list_after_params_no_blank_line(param: int) -> None: # noqa: ARG001 """Do something. Args: param: A parameter. Term A description maybe multiple lines Next Term Something about it .. rubric:: Example """ @expected( """ mod.has_typevar(param) Do something. Parameters: **param** ("TypeVar"("T")) -- A parameter. Return type: "TypeVar"("T") """, ) def has_typevar(param: T) -> T: """Do something. Args: param: A parameter. """ return param @expected( """ mod.has_newtype(param) Do something. Parameters: **param** ("NewType"("W", "str")) -- A parameter. Return type: "NewType"("W", "str") """, ) def has_newtype(param: W) -> W: """Do something. Args: param: A parameter. """ return param AUTO_FUNCTION = ".. autofunction:: mod.{}" AUTO_CLASS = """\ .. autoclass:: mod.{} :members: """ AUTO_EXCEPTION = """\ .. autoexception:: mod.{} :members: """ LT_PY310 = sys.version_info < (3, 10) @expected( """ mod.typehints_use_signature(a: AsyncGenerator) -> AsyncGenerator Do something. Parameters: **a** ("AsyncGenerator") -- blah Return type: "AsyncGenerator" """, typehints_use_signature=True, typehints_use_signature_return=True, ) def typehints_use_signature(a: AsyncGenerator) -> AsyncGenerator: """Do something. Args: a: blah """ return a prolog = """ .. |test_node_start| replace:: {test_node_start} """.format(test_node_start="test_start") @expected( """ mod.docstring_with_multiline_note_after_params_prolog_replace(param) Do something. Parameters: **param** ("int") -- A parameter. Return type: "None" Note: Some notes. test_start More notes """, rst_prolog=prolog, ) def docstring_with_multiline_note_after_params_prolog_replace(param: int) -> None: # noqa: ARG001 """Do something. Args: param: A parameter. Note: Some notes. |test_node_start| More notes """ epilog = """ .. |test_node_end| replace:: {test_node_end} """.format(test_node_end="test_end") @expected( """ mod.docstring_with_multiline_note_after_params_epilog_replace(param) Do something. Parameters: **param** ("int") -- A parameter. Return type: "None" Note: Some notes. test_end More notes """, rst_epilog=epilog, ) def docstring_with_multiline_note_after_params_epilog_replace(param: int) -> None: # noqa: ARG001 """Do something. Args: param: A parameter. Note: Some notes. |test_node_end| More notes """ @expected( """ mod.docstring_with_see_also() Return type: "str" See also: more info at `_. """ ) def docstring_with_see_also() -> str: """ .. seealso:: more info at `_. """ return "" # Config settings for each test run. # Config Name: Sphinx Options as Dict. configs = { "default_conf": {}, "prolog_conf": {"rst_prolog": prolog}, "epilog_conf": { "rst_epilog": epilog, }, "bothlog_conf": { "rst_prolog": prolog, "rst_epilog": epilog, }, } @pytest.mark.parametrize("val", [x for x in globals().values() if hasattr(x, "EXPECTED")]) @pytest.mark.parametrize("conf_run", ["default_conf", "prolog_conf", "epilog_conf", "bothlog_conf"]) @pytest.mark.sphinx("text", testroot="integration") def test_integration( app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, val: Any, conf_run: str ) -> None: if isclass(val) and issubclass(val, BaseException): template = AUTO_EXCEPTION elif isclass(val): template = AUTO_CLASS else: template = AUTO_FUNCTION (Path(app.srcdir) / "index.rst").write_text(template.format(val.__name__)) app.config.__dict__.update(configs[conf_run]) app.config.__dict__.update(val.OPTIONS) monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__]) app.build() assert "build succeeded" in status.getvalue() # Build succeeded regexp = getattr(val, "WARNING", None) value = warning.getvalue().strip() if regexp: msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}" assert re.search(regexp, value), msg else: assert not value result = (Path(app.srcdir) / "_build/text/index.txt").read_text() expected = val.EXPECTED if LT_PY310: expected = expected.replace("NewType", "NewType()") try: assert result.strip() == dedent(expected).strip() except Exception: indented = indent(f'"""\n{result}\n"""', " " * 4) print(f"@expected(\n{indented}\n)\n") # noqa: T201 raise sphinx_autodoc_typehints-2.3.0/tests/test_integration_autodoc_type_aliases.py0000644000000000000000000000665413615410400025163 0ustar00from __future__ import annotations import re import sys from pathlib import Path from textwrap import dedent, indent from typing import TYPE_CHECKING, Any, Callable, Literal, NewType, TypeVar # no type comments import pytest if TYPE_CHECKING: from io import StringIO from sphinx.testing.util import SphinxTestApp T = TypeVar("T") W = NewType("W", str) def expected(expected: str, **options: dict[str, Any]) -> Callable[[T], T]: def dec(val: T) -> T: val.EXPECTED = expected val.OPTIONS = options return val return dec def warns(pattern: str) -> Callable[[T], T]: def dec(val: T) -> T: val.WARNING = pattern return val return dec ArrayLike = Literal["test"] class _SchemaMeta(type): # noqa: PLW1641 def __eq__(cls, other: object) -> bool: return True class Schema(metaclass=_SchemaMeta): pass @expected( """ mod.f(s) Do something. Parameters: **s** ("Schema") -- Some schema. Return type: "Schema" """ ) def f(s: Schema) -> Schema: """ Do something. Args: s: Some schema. """ return s class AliasedClass: ... @expected( """ mod.g(s) Do something. Parameters: **s** ("Class Alias") -- Some schema. Return type: "Class Alias" """ ) def g(s: AliasedClass) -> AliasedClass: """ Do something. Args: s: Some schema. """ return s @expected( """\ mod.function(x, y) Function docstring. Parameters: * **x** (Array) -- foo * **y** ("Schema") -- boo Returns: something Return type: bytes """, ) def function(x: ArrayLike, y: Schema) -> str: # noqa: ARG001 """ Function docstring. :param x: foo :param y: boo :return: something :rtype: bytes """ # Config settings for each test run. # Config Name: Sphinx Options as Dict. configs = {"default_conf": {"autodoc_type_aliases": {"ArrayLike": "Array", "AliasedClass": '"Class Alias"'}}} @pytest.mark.parametrize("val", [x for x in globals().values() if hasattr(x, "EXPECTED")]) @pytest.mark.parametrize("conf_run", list(configs.keys())) @pytest.mark.sphinx("text", testroot="integration") def test_integration( app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, val: Any, conf_run: str ) -> None: template = ".. autofunction:: mod.{}" (Path(app.srcdir) / "index.rst").write_text(template.format(val.__name__)) app.config.__dict__.update(configs[conf_run]) app.config.__dict__.update(val.OPTIONS) monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__]) app.build() assert "build succeeded" in status.getvalue() # Build succeeded regexp = getattr(val, "WARNING", None) value = warning.getvalue().strip() if regexp: msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}" assert re.search(regexp, value), msg elif not re.search("WARNING: Inline strong start-string without end-string.", value): assert not value result = (Path(app.srcdir) / "_build/text/index.txt").read_text() expected = val.EXPECTED if sys.version_info < (3, 10): expected = expected.replace("NewType", "NewType()") try: assert result.strip() == dedent(expected).strip() except Exception: indented = indent(f'"""\n{result}\n"""', " " * 4) print(f"@expected(\n{indented}\n)\n") # noqa: T201 raise sphinx_autodoc_typehints-2.3.0/tests/test_integration_issue_384.py0000644000000000000000000000567713615410400022515 0ustar00from __future__ import annotations import re import sys from pathlib import Path from textwrap import dedent, indent from typing import TYPE_CHECKING, Any, Callable, NewType, TypeVar # no type comments import pytest if TYPE_CHECKING: from io import StringIO from sphinx.testing.util import SphinxTestApp T = TypeVar("T") W = NewType("W", str) def expected(expected: str, **options: dict[str, Any]) -> Callable[[T], T]: def dec(val: T) -> T: val.EXPECTED = expected val.OPTIONS = options return val return dec def warns(pattern: str) -> Callable[[T], T]: def dec(val: T) -> T: val.WARNING = pattern return val return dec @expected( """\ mod.function(x=5, y=10, z=15) Function docstring. Parameters: * **x** ("int") -- optional specifier line 2 (default: "5") * **y** ("int") -- another optional line 4 second paragraph for y (default: "10") * **z** ("int") -- yet another optional s line 6 (default: "15") Returns: something Return type: bytes """, ) def function(x: int = 5, y: int = 10, z: int = 15) -> str: # noqa: ARG001 """ Function docstring. :param x: optional specifier line 2 :param y: another optional line 4 second paragraph for y :param z: yet another optional s line 6 :return: something :rtype: bytes """ # Config settings for each test run. # Config Name: Sphinx Options as Dict. configs = {"default_conf": {"typehints_defaults": "braces-after"}} @pytest.mark.parametrize("val", [x for x in globals().values() if hasattr(x, "EXPECTED")]) @pytest.mark.parametrize("conf_run", list(configs.keys())) @pytest.mark.sphinx("text", testroot="integration") def test_integration( app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, val: Any, conf_run: str ) -> None: template = ".. autofunction:: mod.{}" (Path(app.srcdir) / "index.rst").write_text(template.format(val.__name__)) app.config.__dict__.update(configs[conf_run]) app.config.__dict__.update(val.OPTIONS) monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__]) app.build() assert "build succeeded" in status.getvalue() # Build succeeded regexp = getattr(val, "WARNING", None) value = warning.getvalue().strip() if regexp: msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}" assert re.search(regexp, value), msg else: assert not value result = (Path(app.srcdir) / "_build/text/index.txt").read_text() expected = val.EXPECTED if sys.version_info < (3, 10): expected = expected.replace("NewType", "NewType()") try: assert result.strip() == dedent(expected).strip() except Exception: indented = indent(f'"""\n{result}\n"""', " " * 4) print(f"@expected(\n{indented}\n)\n") # noqa: T201 raise sphinx_autodoc_typehints-2.3.0/tests/test_sphinx_autodoc_typehints.py0000644000000000000000000012204513615410400023507 0ustar00from __future__ import annotations import collections.abc import re import sys import types import typing from functools import cmp_to_key from io import StringIO from pathlib import Path from textwrap import dedent, indent from types import FunctionType, ModuleType from typing import ( # noqa: UP035 IO, Any, AnyStr, Callable, Dict, Generic, List, Mapping, Match, NewType, Optional, Pattern, Tuple, Type, TypeVar, Union, ) from unittest.mock import create_autospec, patch import pytest import typing_extensions from sphinx.application import Sphinx from sphinx.config import Config from sphinx_autodoc_typehints import ( _resolve_type_guarded_imports, backfill_type_hints, format_annotation, get_annotation_args, get_annotation_class_name, get_annotation_module, normalize_source_lines, process_docstring, ) if typing.TYPE_CHECKING: from sphinx.testing.util import SphinxTestApp from sphobjinv import Inventory try: import nptyping except ImportError: nptyping = None # type: ignore[assignment] T = TypeVar("T") U_co = TypeVar("U_co", covariant=True) V_contra = TypeVar("V_contra", contravariant=True) X = TypeVar("X", str, int) Y = TypeVar("Y", bound=str) Z = TypeVar("Z", bound="A") S = TypeVar("S", bound="miss") # type: ignore[name-defined] # miss not defined on purpose # noqa: F821 W = NewType("W", str) P = typing_extensions.ParamSpec("P") P_args = P.args # type:ignore[attr-defined] P_kwargs = P.kwargs # type:ignore[attr-defined] P_co = typing_extensions.ParamSpec("P_co", covariant=True) # type: ignore[misc] P_contra = typing_extensions.ParamSpec("P_contra", contravariant=True) # type: ignore[misc] P_bound = typing_extensions.ParamSpec("P_bound", bound=str) # type: ignore[misc] # Mypy does not support recursive type aliases, but # other type checkers do. RecList = Union[int, List["RecList"]] # noqa: UP006 MutualRecA = Union[bool, List["MutualRecB"]] # noqa: UP006 MutualRecB = Union[str, List["MutualRecA"]] # noqa: UP006 class A: def get_type(self) -> type: return type(self) class Inner: ... class B(Generic[T]): name = "Foo" # This is set to make sure the correct class name ("B") is picked up class C(B[str]): ... class D(typing_extensions.Protocol): ... class E(typing_extensions.Protocol[T]): # type: ignore[misc] ... class Slotted: __slots__ = () class Metaclass(type): ... class HintedMethods: @classmethod def from_magic(cls: type[T]) -> T: # type: ignore[empty-body] ... def method(self: T) -> T: # type: ignore[empty-body] ... PY310_PLUS = sys.version_info >= (3, 10) PY312_PLUS = sys.version_info >= (3, 12) if sys.version_info >= (3, 9): # noqa: UP036 AbcCallable = collections.abc.Callable # type: ignore[type-arg] else: # We could also set AbcCallable = typing.Callable and x fail the tests that # use AbcCallable when in versions less than 3.9. class MyGenericAlias(typing._VariadicGenericAlias, _root=True): # noqa: SLF001 def __getitem__(self, params): # noqa: ANN001, ANN204 result = super().__getitem__(params) # Make a copy so we don't change the name of a cached annotation result = result.copy_with(result.__args__) result.__module__ = "collections.abc" return result AbcCallable = MyGenericAlias(collections.abc.Callable, (), special=True) AbcCallable.__module__ = "collections.abc" @pytest.mark.parametrize( ("annotation", "module", "class_name", "args"), [ pytest.param(str, "builtins", "str", (), id="str"), pytest.param(None, "builtins", "None", (), id="None"), pytest.param(ModuleType, "types", "ModuleType", (), id="ModuleType"), pytest.param(FunctionType, "types", "FunctionType", (), id="FunctionType"), pytest.param(types.CodeType, "types", "CodeType", (), id="CodeType"), pytest.param(types.CoroutineType, "types", "CoroutineType", (), id="CoroutineType"), pytest.param(Any, "typing", "Any", (), id="Any"), pytest.param(AnyStr, "typing", "AnyStr", (), id="AnyStr"), pytest.param(Dict, "typing", "Dict", (), id="Dict"), # noqa: UP006 pytest.param(Dict[str, int], "typing", "Dict", (str, int), id="Dict_parametrized"), # noqa: UP006 pytest.param(Dict[T, int], "typing", "Dict", (T, int), id="Dict_typevar"), # type: ignore[valid-type] # noqa: UP006 pytest.param(Tuple, "typing", "Tuple", (), id="Tuple"), # noqa: UP006 pytest.param(Tuple[str, int], "typing", "Tuple", (str, int), id="Tuple_parametrized"), # noqa: UP006 pytest.param(Union[str, int], "typing", "Union", (str, int), id="Union"), pytest.param(Callable, "typing", "Callable", (), id="Callable"), pytest.param(Callable[..., str], "typing", "Callable", (..., str), id="Callable_returntype"), pytest.param(Callable[[int, str], str], "typing", "Callable", (int, str, str), id="Callable_all_types"), pytest.param( AbcCallable[[int, str], str], # type: ignore[type-arg,misc,valid-type] "collections.abc", "Callable", (int, str, str), id="collections.abc.Callable_all_types", ), pytest.param(Pattern, "typing", "Pattern", (), id="Pattern"), pytest.param(Pattern[str], "typing", "Pattern", (str,), id="Pattern_parametrized"), pytest.param(Match, "typing", "Match", (), id="Match"), pytest.param(Match[str], "typing", "Match", (str,), id="Match_parametrized"), pytest.param(IO, "typing", "IO", (), id="IO"), pytest.param(W, "typing", "NewType", (str,), id="W"), pytest.param(P, "typing", "ParamSpec", (), id="P"), pytest.param(P_args, "typing", "ParamSpecArgs", (), id="P_args"), pytest.param(P_kwargs, "typing", "ParamSpecKwargs", (), id="P_kwargs"), pytest.param(Metaclass, __name__, "Metaclass", (), id="Metaclass"), pytest.param(Slotted, __name__, "Slotted", (), id="Slotted"), pytest.param(A, __name__, "A", (), id="A"), pytest.param(B, __name__, "B", (), id="B"), pytest.param(C, __name__, "C", (), id="C"), pytest.param(D, __name__, "D", (), id="D"), pytest.param(E, __name__, "E", (), id="E"), pytest.param(E[int], __name__, "E", (int,), id="E_parametrized"), pytest.param(A.Inner, __name__, "A.Inner", (), id="Inner"), ], ) def test_parse_annotation(annotation: Any, module: str, class_name: str, args: tuple[Any, ...]) -> None: got_mod = get_annotation_module(annotation) got_cls = get_annotation_class_name(annotation, module) got_args = get_annotation_args(annotation, module, class_name) assert (got_mod, got_cls, got_args) == (module, class_name, args) _CASES = [ pytest.param(str, ":py:class:`str`", id="str"), pytest.param(int, ":py:class:`int`", id="int"), pytest.param(StringIO, ":py:class:`~io.StringIO`", id="StringIO"), pytest.param(FunctionType, ":py:class:`~types.FunctionType`", id="FunctionType"), pytest.param(ModuleType, ":py:class:`~types.ModuleType`", id="ModuleType"), pytest.param(type(None), ":py:obj:`None`", id="type None"), pytest.param(type, ":py:class:`type`", id="type"), pytest.param(collections.abc.Callable, ":py:class:`~collections.abc.Callable`", id="abc-Callable"), pytest.param(Type, ":py:class:`~typing.Type`", id="typing-Type"), # noqa: UP006 pytest.param(Type[A], rf":py:class:`~typing.Type`\ \[:py:class:`~{__name__}.A`]", id="typing-A"), # noqa: UP006 pytest.param(Any, ":py:data:`~typing.Any`", id="Any"), pytest.param(AnyStr, ":py:data:`~typing.AnyStr`", id="AnyStr"), pytest.param(Generic[T], r":py:class:`~typing.Generic`\ \[:py:class:`~typing.TypeVar`\ \(``T``)]", id="Generic"), pytest.param(Mapping, ":py:class:`~typing.Mapping`", id="Mapping"), pytest.param( Mapping[T, int], # type: ignore[valid-type] r":py:class:`~typing.Mapping`\ \[:py:class:`~typing.TypeVar`\ \(``T``), :py:class:`int`]", id="Mapping-T-int", ), pytest.param( Mapping[str, V_contra], # type: ignore[valid-type] r":py:class:`~typing.Mapping`\ \[:py:class:`str`, :py:class:`~typing.TypeVar`\ \(" "``V_contra``, contravariant=True)]", id="Mapping-T-int-contra", ), pytest.param( Mapping[T, U_co], # type: ignore[valid-type] r":py:class:`~typing.Mapping`\ \[:py:class:`~typing.TypeVar`\ \(``T``), " r":py:class:`~typing.TypeVar`\ \(``U_co``, covariant=True)]", id="Mapping-T-int-co", ), pytest.param( Mapping[str, bool], r":py:class:`~typing.Mapping`\ \[:py:class:`str`, :py:class:`bool`]", id="Mapping-str-bool", ), pytest.param(Dict, ":py:class:`~typing.Dict`", id="Dict"), # noqa: UP006 pytest.param( Dict[T, int], # type: ignore[valid-type] # noqa: UP006 r":py:class:`~typing.Dict`\ \[:py:class:`~typing.TypeVar`\ \(``T``), :py:class:`int`]", id="Dict-T-int", ), pytest.param( Dict[str, V_contra], # type: ignore[valid-type] # noqa: UP006 r":py:class:`~typing.Dict`\ \[:py:class:`str`, :py:class:`~typing.TypeVar`\ \(``V_contra``, " r"contravariant=True)]", id="Dict-T-int-contra", ), pytest.param( Dict[T, U_co], # type: ignore[valid-type] # noqa: UP006 r":py:class:`~typing.Dict`\ \[:py:class:`~typing.TypeVar`\ \(``T``)," r" :py:class:`~typing.TypeVar`\ \(``U_co``, covariant=True)]", id="Dict-T-int-co", ), pytest.param( Dict[str, bool], # noqa: UP006 r":py:class:`~typing.Dict`\ \[:py:class:`str`, :py:class:`bool`]", id="Dict-str-bool", # noqa: RUF100, UP006 ), pytest.param(Tuple, ":py:data:`~typing.Tuple`", id="Tuple"), # noqa: UP006 pytest.param( Tuple[str, bool], # noqa: UP006 r":py:data:`~typing.Tuple`\ \[:py:class:`str`, :py:class:`bool`]", id="Tuple-str-bool", # noqa: RUF100, UP006 ), pytest.param( Tuple[int, int, int], # noqa: UP006 r":py:data:`~typing.Tuple`\ \[:py:class:`int`, :py:class:`int`, :py:class:`int`]", id="Tuple-int-int-int", ), pytest.param( Tuple[str, ...], # noqa: UP006 r":py:data:`~typing.Tuple`\ \[:py:class:`str`, :py:data:`...`]", id="Tuple-str-Ellipsis", ), pytest.param(Union, ":py:data:`~typing.Union`", id="Union"), pytest.param( Union[str, bool], r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:class:`bool`]", id="Union-str-bool" ), pytest.param( Union[str, bool, None], r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:class:`bool`, :py:obj:`None`]", id="Union-str-bool-None", ), pytest.param( Union[str, Any], r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:data:`~typing.Any`]", id="Union-str-Any" ), pytest.param(Optional[str], r":py:data:`~typing.Optional`\ \[:py:class:`str`]", id="Optional-str"), pytest.param(Union[str, None], r":py:data:`~typing.Optional`\ \[:py:class:`str`]", id="Optional-str-None"), pytest.param( Optional[Union[str, bool]], r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:class:`bool`, :py:obj:`None`]", id="Optional-Union-str-bool", ), pytest.param(Callable, ":py:data:`~typing.Callable`", id="Callable"), pytest.param( Callable[..., int], r":py:data:`~typing.Callable`\ \[:py:data:`...`, :py:class:`int`]", id="Callable-Ellipsis-int", ), pytest.param( Callable[[int], int], r":py:data:`~typing.Callable`\ \[\[:py:class:`int`], :py:class:`int`]", id="Callable-int-int", ), pytest.param( Callable[[int, str], bool], r":py:data:`~typing.Callable`\ \[\[:py:class:`int`, :py:class:`str`], :py:class:`bool`]", id="Callable-int-str-bool", ), pytest.param( Callable[[int, str], None], r":py:data:`~typing.Callable`\ \[\[:py:class:`int`, :py:class:`str`], :py:obj:`None`]", id="Callable-int-str", ), pytest.param( Callable[[T], T], r":py:data:`~typing.Callable`\ \[\[:py:class:`~typing.TypeVar`\ \(``T``)]," r" :py:class:`~typing.TypeVar`\ \(``T``)]", id="Callable-T-T", ), pytest.param( AbcCallable[[int, str], bool], # type: ignore[valid-type,misc,type-arg] r":py:class:`~collections.abc.Callable`\ \[\[:py:class:`int`, :py:class:`str`], :py:class:`bool`]", id="AbcCallable-int-str-bool", ), pytest.param(Pattern, ":py:class:`~typing.Pattern`", id="Pattern"), pytest.param(Pattern[str], r":py:class:`~typing.Pattern`\ \[:py:class:`str`]", id="Pattern-str"), pytest.param(IO, ":py:class:`~typing.IO`", id="IO"), pytest.param(IO[str], r":py:class:`~typing.IO`\ \[:py:class:`str`]", id="IO-str"), pytest.param(Metaclass, f":py:class:`~{__name__}.Metaclass`", id="Metaclass"), pytest.param(A, f":py:class:`~{__name__}.A`", id="A"), pytest.param(B, f":py:class:`~{__name__}.B`", id="B"), pytest.param(B[int], rf":py:class:`~{__name__}.B`\ \[:py:class:`int`]", id="B-int"), pytest.param(C, f":py:class:`~{__name__}.C`", id="C"), pytest.param(D, f":py:class:`~{__name__}.D`", id="D"), pytest.param(E, f":py:class:`~{__name__}.E`", id="E"), pytest.param(E[int], rf":py:class:`~{__name__}.E`\ \[:py:class:`int`]", id="E-int"), pytest.param(W, rf":py:{'class' if PY310_PLUS else 'func'}:`~typing.NewType`\ \(``W``, :py:class:`str`)", id="W"), pytest.param(T, r":py:class:`~typing.TypeVar`\ \(``T``)", id="T"), pytest.param(U_co, r":py:class:`~typing.TypeVar`\ \(``U_co``, covariant=True)", id="U-co"), pytest.param(V_contra, r":py:class:`~typing.TypeVar`\ \(``V_contra``, contravariant=True)", id="V-contra"), pytest.param(X, r":py:class:`~typing.TypeVar`\ \(``X``, :py:class:`str`, :py:class:`int`)", id="X"), pytest.param(Y, r":py:class:`~typing.TypeVar`\ \(``Y``, bound= :py:class:`str`)", id="Y"), pytest.param(Z, r":py:class:`~typing.TypeVar`\ \(``Z``, bound= A)", id="Z"), pytest.param(S, r":py:class:`~typing.TypeVar`\ \(``S``, bound= miss)", id="S"), # ParamSpec should behave like TypeVar, except for missing constraints pytest.param( P, rf":py:class:`~typing.ParamSpec`\ \(``P``{', bound= :py:obj:`None`' if PY312_PLUS else ''})", id="P" ), pytest.param( P_co, rf":py:class:`~typing.ParamSpec`\ \(``P_co``{', bound= :py:obj:`None`' if PY312_PLUS else ''}, covariant=True)", id="P_co", ), pytest.param( P_contra, rf":py:class:`~typing.ParamSpec`\ \(``P_contra``{', bound= :py:obj:`None`' if PY312_PLUS else ''}" ", contravariant=True)", id="P-contra", ), pytest.param(P_bound, r":py:class:`~typing.ParamSpec`\ \(``P_bound``, bound= :py:class:`str`)", id="P-bound"), # ## These test for correct internal tuple rendering, even if not all are valid Tuple types # Zero-length tuple remains pytest.param(Tuple[()], ":py:data:`~typing.Tuple`", id="Tuple-p"), # noqa: UP006 # Internal single tuple with simple types is flattened in the output pytest.param(Tuple[(int,)], r":py:data:`~typing.Tuple`\ \[:py:class:`int`]", id="Tuple-p-int"), # noqa: UP006 pytest.param( Tuple[(int, int)], # noqa: UP006 r":py:data:`~typing.Tuple`\ \[:py:class:`int`, :py:class:`int`]", id="Tuple-p-int-int", # noqa: RUF100, UP006 ), # Ellipsis in single tuple also gets flattened pytest.param( Tuple[(int, ...)], # noqa: UP006 r":py:data:`~typing.Tuple`\ \[:py:class:`int`, :py:data:`...`]", id="Tuple-p-Ellipsis", ), pytest.param( RecList, r":py:data:`~typing.Union`\ \[:py:class:`int`, :py:class:`~typing.List`\ \[RecList]]", id="RecList" ), pytest.param( MutualRecA, r":py:data:`~typing.Union`\ \[:py:class:`bool`, :py:class:`~typing.List`\ \[MutualRecB]]", id="MutualRecA", ), ] if nptyping is not None: _CASES.extend( [ # Internal tuple with following additional type cannot be flattened (specific to nptyping?) # These cases will fail if nptyping restructures its internal module hierarchy pytest.param( nptyping.NDArray[nptyping.Shape["*"], nptyping.Float], ( ":py:class:`~nptyping.ndarray.NDArray`\\ \\[:py:class:`~nptyping.base_meta_classes.Shape`\\ \\[*], " ":py:class:`~numpy.float64`]" ), id="NDArray-star-float", ), pytest.param( nptyping.NDArray[nptyping.Shape["64"], nptyping.Float], ( ":py:class:`~nptyping.ndarray.NDArray`\\ \\[:py:class:`~nptyping.base_meta_classes.Shape`\\ \\[64]," " :py:class:`~numpy.float64`]" ), id="NDArray-64-float", ), pytest.param( nptyping.NDArray[nptyping.Shape["*, *"], nptyping.Float], ( ":py:class:`~nptyping.ndarray.NDArray`\\ \\[:py:class:`~nptyping.base_meta_classes.Shape`\\ \\[*, " "*], :py:class:`~numpy.float64`]" ), id="NDArray-star-star-float", ), pytest.param( nptyping.NDArray[nptyping.Shape["*, ..."], nptyping.Float], ":py:class:`~nptyping.ndarray.NDArray`\\ \\[:py:data:`~typing.Any`, :py:class:`~numpy.float64`]", id="NDArray-star-Ellipsis-float", ), pytest.param( nptyping.NDArray[nptyping.Shape["*, 3"], nptyping.Float], ( ":py:class:`~nptyping.ndarray.NDArray`\\ \\[:py:class:`~nptyping.base_meta_classes.Shape`\\ \\[*, 3" "], :py:class:`~numpy.float64`]" ), id="NDArray-star-3-float", ), pytest.param( nptyping.NDArray[nptyping.Shape["3, ..."], nptyping.Float], ( ":py:class:`~nptyping.ndarray.NDArray`\\ \\[:py:class:`~nptyping.base_meta_classes.Shape`\\ \\[3, " "...], :py:class:`~numpy.float64`]" ), id="NDArray-3-Ellipsis-float", ), ], ) @pytest.mark.parametrize(("annotation", "expected_result"), _CASES) def test_format_annotation(inv: Inventory, annotation: Any, expected_result: str) -> None: conf = create_autospec(Config, _annotation_globals=globals(), always_use_bars_union=False) result = format_annotation(annotation, conf) assert result == expected_result # Test with the "simplify_optional_unions" flag turned off: if re.match(r"^:py:data:`~typing\.Union`\\\[.*``None``.*]", expected_result): # strip None - argument and copy string to avoid conflicts with # subsequent tests expected_result_not_simplified = expected_result.replace(", ``None``", "") # encapsulate Union in typing.Optional expected_result_not_simplified += ":py:data:`~typing.Optional`\\ \\[" expected_result_not_simplified += "]" conf = create_autospec( Config, simplify_optional_unions=False, _annotation_globals=globals(), always_use_bars_union=False, ) assert format_annotation(annotation, conf) == expected_result_not_simplified # Test with the "fully_qualified" flag turned on if "typing" in expected_result_not_simplified: expected_result_not_simplified = expected_result_not_simplified.replace("~typing", "typing") conf = create_autospec( Config, typehints_fully_qualified=True, simplify_optional_unions=False, _annotation_globals=globals(), ) assert format_annotation(annotation, conf) == expected_result_not_simplified # Test with the "fully_qualified" flag turned on if "typing" in expected_result or "nptyping" in expected_result or __name__ in expected_result: expected_result = expected_result.replace("~typing", "typing") expected_result = expected_result.replace("~nptyping", "nptyping") expected_result = expected_result.replace("~numpy", "numpy") expected_result = expected_result.replace("~" + __name__, __name__) conf = create_autospec( Config, typehints_fully_qualified=True, _annotation_globals=globals(), always_use_bars_union=False, ) assert format_annotation(annotation, conf) == expected_result # Test for the correct role (class vs data) using the official Sphinx inventory if "typing" in expected_result: m = re.match("^:py:(?Pclass|data|func):`~(?P[^`]+)`", result) assert m, "No match" name = m.group("name") expected_role = next((o.role for o in inv.objects if o.name == name), None) if expected_role: if expected_role == "function": expected_role = "func" assert m.group("role") == expected_role @pytest.mark.parametrize( ("annotation", "expected_result"), [ ("int | float", ":py:class:`int` | :py:class:`float`"), ("int | float | None", ":py:class:`int` | :py:class:`float` | :py:obj:`None`"), ("Union[int, float]", ":py:class:`int` | :py:class:`float`"), ("Union[int, float, None]", ":py:class:`int` | :py:class:`float` | :py:obj:`None`"), ("Optional[int | float]", ":py:class:`int` | :py:class:`float` | :py:obj:`None`"), ("Optional[Union[int, float]]", ":py:class:`int` | :py:class:`float` | :py:obj:`None`"), ("Union[int | float, str]", ":py:class:`int` | :py:class:`float` | :py:class:`str`"), ("Union[int, float] | str", ":py:class:`int` | :py:class:`float` | :py:class:`str`"), ], ) @pytest.mark.skipif(not PY310_PLUS, reason="| union doesn't work before py310") def test_always_use_bars_union(annotation: str, expected_result: str) -> None: conf = create_autospec(Config, always_use_bars_union=True) result = format_annotation(eval(annotation), conf) # noqa: S307 assert result == expected_result @pytest.mark.parametrize("library", [typing, typing_extensions], ids=["typing", "typing_extensions"]) @pytest.mark.parametrize( ("annotation", "params", "expected_result"), [ pytest.param("ClassVar", int, ":py:data:`~typing.ClassVar`\\ \\[:py:class:`int`]", id="ClassVar"), pytest.param("NoReturn", None, ":py:data:`~typing.NoReturn`", id="NoReturn"), pytest.param("Literal", ("a", 1), ":py:data:`~typing.Literal`\\ \\[``'a'``, ``1``]", id="Literal"), pytest.param("Type", None, ":py:class:`~typing.Type`", id="Type-none"), pytest.param("Type", (A,), rf":py:class:`~typing.Type`\ \[:py:class:`~{__name__}.A`]", id="Type-A"), ], ) def test_format_annotation_both_libs(library: ModuleType, annotation: str, params: Any, expected_result: str) -> None: try: annotation_cls = getattr(library, annotation) except AttributeError: pytest.skip(f"{annotation} not available in the {library.__name__} module") return # pragma: no cover ann = annotation_cls if params is None else annotation_cls[params] result = format_annotation(ann, create_autospec(Config)) assert result == expected_result def test_process_docstring_slot_wrapper() -> None: lines: list[str] = [] config = create_autospec( Config, typehints_fully_qualified=False, simplify_optional_unions=False, typehints_formatter=None, autodoc_mock_imports=[], ) app: Sphinx = create_autospec(Sphinx, config=config) process_docstring(app, "class", "SlotWrapper", Slotted, None, lines) assert not lines def set_python_path() -> None: test_path = Path(__file__).parent # Add test directory to sys.path to allow imports of dummy module. if str(test_path) not in sys.path: sys.path.insert(0, str(test_path)) @pytest.mark.parametrize("always_document_param_types", [True, False], ids=["doc_param_type", "no_doc_param_type"]) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_always_document_param_types( app: SphinxTestApp, status: StringIO, warning: StringIO, always_document_param_types: bool, ) -> None: set_python_path() app.config.always_document_param_types = always_document_param_types # create flag app.config.autodoc_mock_imports = ["mailbox"] # create flag # Prevent "document isn't included in any toctree" warnings for f in Path(app.srcdir).glob("*.rst"): f.unlink() (Path(app.srcdir) / "index.rst").write_text( dedent( """ .. autofunction:: dummy_module.undocumented_function .. autoclass:: dummy_module.DataClass :undoc-members: :special-members: __init__ """, ), ) app.build() assert "build succeeded" in status.getvalue() # Build succeeded assert not warning.getvalue().strip() format_args = {} for indentation_level in range(2): key = f"undoc_params_{indentation_level}" if always_document_param_types: format_args[key] = indent('\n\n Parameters:\n **x** ("int")', " " * indentation_level) else: format_args[key] = "" contents = (Path(app.srcdir) / "_build/text/index.txt").read_text() expected_contents = """\ dummy_module.undocumented_function(x) Hi{undoc_params_0} Return type: "str" class dummy_module.DataClass(x) Class docstring.{undoc_params_0} __init__(x){undoc_params_1} """ expected_contents = dedent(expected_contents).format(**format_args) assert contents == expected_contents def maybe_fix_py310(expected_contents: str) -> str: if sys.version_info >= (3, 11): return expected_contents if not PY310_PLUS: return expected_contents.replace('"', "") for old, new in [ ('"str" | "None"', '"Optional"["str"]'), ]: expected_contents = expected_contents.replace(old, new) return expected_contents @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) -> None: set_python_path() app.config.master_doc = "future_annotations" # create flag app.build() assert "build succeeded" in status.getvalue() # Build succeeded contents = (Path(app.srcdir) / "_build/text/future_annotations.txt").read_text() expected_contents = """\ Dummy Module ************ dummy_module_future_annotations.function_with_py310_annotations(self, x, y, z=None) Method docstring. Parameters: * **x** ("bool" | "None") -- foo * **y** ("int" | "str" | "float") -- bar * **z** ("str" | "None") -- baz Return type: "str" """ expected_contents = dedent(expected_contents) expected_contents = maybe_fix_py310(dedent(expected_contents)) assert contents == expected_contents @pytest.mark.sphinx("pseudoxml", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_default_role(app: SphinxTestApp, status: StringIO) -> None: set_python_path() app.config.master_doc = "simple_default_role" # create flag app.config.default_role = "literal" app.build() assert "build succeeded" in status.getvalue() # Build succeeded contents_lines = ( (Path(app.srcdir) / "_build/pseudoxml/simple_default_role.pseudoxml").read_text(encoding="utf-8").splitlines() ) list_item_idxs = [i for i, line in enumerate(contents_lines) if line.strip() == ""] foo_param = dedent("\n".join(contents_lines[list_item_idxs[0] : list_item_idxs[1]])) expected_foo_param = """\ x ( bool ) \N{EN DASH}\N{SPACE} foo """.rstrip() expected_foo_param = dedent(expected_foo_param) assert foo_param == expected_foo_param @pytest.mark.parametrize( ("defaults_config_val", "expected"), [ (None, '("int") -- bar'), ("comma", '("int", default: "1") -- bar'), ("braces", '("int" (default: "1")) -- bar'), ("braces-after", '("int") -- bar (default: "1")'), ("comma-after", Exception("needs to be one of")), ], ) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_defaults( app: SphinxTestApp, status: StringIO, defaults_config_val: str, expected: str | Exception, ) -> None: set_python_path() app.config.master_doc = "simple" # create flag app.config.typehints_defaults = defaults_config_val # create flag if isinstance(expected, Exception): with pytest.raises(Exception, match=re.escape(str(expected))): app.build() return app.build() assert "build succeeded" in status.getvalue() contents = (Path(app.srcdir) / "_build/text/simple.txt").read_text() expected_contents = f"""\ Simple Module ************* dummy_module_simple.function(x, y=1) Function docstring. Parameters: * **x** ("bool") -- foo * **y** {expected} Return type: "str" """ assert contents == dedent(expected_contents) @pytest.mark.parametrize( ("formatter_config_val", "expected"), [ (None, ['("bool") -- foo', '("int") -- bar', '"str"']), (lambda ann, conf: "Test", ["(Test) -- foo", "(Test) -- bar", "Test"]), # noqa: ARG005 ("some string", Exception("needs to be callable or `None`")), ], ) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_formatter( app: SphinxTestApp, status: StringIO, formatter_config_val: str, expected: tuple[str, ...] | Exception, ) -> None: set_python_path() app.config.master_doc = "simple" # create flag app.config.typehints_formatter = formatter_config_val # create flag if isinstance(expected, Exception): with pytest.raises(Exception, match=re.escape(str(expected))): app.build() return app.build() assert "build succeeded" in status.getvalue() contents = (Path(app.srcdir) / "_build/text/simple.txt").read_text() expected_contents = f"""\ Simple Module ************* dummy_module_simple.function(x, y=1) Function docstring. Parameters: * **x** {expected[0]} * **y** {expected[1]} Return type: {expected[2]} """ assert contents == dedent(expected_contents) def test_normalize_source_lines_async_def() -> None: source = """ async def async_function(): class InnerClass: def __init__(self): ... """ expected = """ async def async_function(): class InnerClass: def __init__(self): ... """ assert normalize_source_lines(dedent(source)) == dedent(expected) def test_normalize_source_lines_def_starting_decorator_parameter() -> None: source = """ @_with_parameters( _Parameter("self", _Parameter.POSITIONAL_OR_KEYWORD), *_proxy_instantiation_parameters, _project_id, _Parameter( "node_numbers", _Parameter.POSITIONAL_OR_KEYWORD, default=None, annotation=Optional[Iterable[int]], ), ) def __init__(bound_args): # noqa: N805 ... """ expected = """ @_with_parameters( _Parameter("self", _Parameter.POSITIONAL_OR_KEYWORD), *_proxy_instantiation_parameters, _project_id, _Parameter( "node_numbers", _Parameter.POSITIONAL_OR_KEYWORD, default=None, annotation=Optional[Iterable[int]], ), ) def __init__(bound_args): # noqa: N805 ... """ assert normalize_source_lines(dedent(source)) == dedent(expected) @pytest.mark.parametrize("obj", [cmp_to_key, 1]) def test_default_no_signature(obj: Any) -> None: config = create_autospec( Config, typehints_fully_qualified=False, simplify_optional_unions=False, typehints_formatter=None, autodoc_mock_imports=[], ) app: Sphinx = create_autospec(Sphinx, config=config) lines: list[str] = [] process_docstring(app, "what", "name", obj, None, lines) assert lines == [] @pytest.mark.parametrize("method", [HintedMethods.from_magic, HintedMethods().method]) def test_bound_class_method(method: FunctionType) -> None: config = create_autospec( Config, typehints_fully_qualified=False, simplify_optional_unions=False, typehints_document_rtype=False, always_document_param_types=True, typehints_defaults=True, typehints_formatter=None, autodoc_mock_imports=[], ) app: Sphinx = create_autospec(Sphinx, config=config) process_docstring(app, "class", method.__qualname__, method, None, []) def test_syntax_error_backfill() -> None: # Regression test for #188 # fmt: off def func(x): # type: ignore[no-untyped-def] # noqa: ANN001, ANN202 return x # fmt: on backfill_type_hints(func, "func") @pytest.mark.sphinx("text", testroot="resolve-typing-guard") def test_resolve_typing_guard_imports(app: SphinxTestApp, status: StringIO, warning: StringIO) -> None: set_python_path() app.config.autodoc_mock_imports = ["viktor"] # create flag app.build() out = status.getvalue() assert "build succeeded" in out err = warning.getvalue() r = re.compile("WARNING: Failed guarded type import") assert len(r.findall(err)) == 1 pat = r'WARNING: Failed guarded type import with ImportError\("cannot import name \'missing\' from \'functools\'' assert re.search(pat, err) @pytest.mark.sphinx("text", testroot="resolve-typing-guard-tmp") def test_resolve_typing_guard_attrs_imports(app: SphinxTestApp, status: StringIO, warning: StringIO) -> None: set_python_path() app.build() assert "build succeeded" in status.getvalue() assert not warning.getvalue() def test_no_source_code_type_guard() -> None: from csv import Error # noqa: PLC0415 _resolve_type_guarded_imports([], Error) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_formatter_no_use_rtype(app: SphinxTestApp, status: StringIO) -> None: set_python_path() app.config.master_doc = "simple_no_use_rtype" # create flag app.config.typehints_use_rtype = False app.build() assert "build succeeded" in status.getvalue() text_path = Path(app.srcdir) / "_build" / "text" / "simple_no_use_rtype.txt" text_contents = text_path.read_text().replace("–", "--") # noqa: RUF001 # keep ambiguous EN DASH expected_contents = """\ Simple Module ************* dummy_module_simple_no_use_rtype.function_no_returns(x, y=1) Function docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar Return type: "str" dummy_module_simple_no_use_rtype.function_returns_with_type(x, y=1) Function docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar Returns: *CustomType* -- A string dummy_module_simple_no_use_rtype.function_returns_with_compound_type(x, y=1) Function docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar Returns: Union[str, int] -- A string or int dummy_module_simple_no_use_rtype.function_returns_without_type(x, y=1) Function docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar Returns: "str" -- A string """ assert text_contents == dedent(expected_contents) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_with_use_signature(app: SphinxTestApp, status: StringIO) -> None: set_python_path() app.config.master_doc = "simple" # create flag app.config.typehints_use_signature = True app.build() assert "build succeeded" in status.getvalue() text_path = Path(app.srcdir) / "_build" / "text" / "simple.txt" text_contents = text_path.read_text().replace("–", "--") # noqa: RUF001 # keep ambiguous EN DASH expected_contents = """\ Simple Module ************* dummy_module_simple.function(x: bool, y: int = 1) Function docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar Return type: "str" """ assert text_contents == dedent(expected_contents) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_with_use_signature_return(app: SphinxTestApp, status: StringIO) -> None: set_python_path() app.config.master_doc = "simple" # create flag app.config.typehints_use_signature_return = True app.build() assert "build succeeded" in status.getvalue() text_path = Path(app.srcdir) / "_build" / "text" / "simple.txt" text_contents = text_path.read_text().replace("–", "--") # noqa: RUF001 # keep ambiguous EN DASH expected_contents = """\ Simple Module ************* dummy_module_simple.function(x, y=1) -> str Function docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar Return type: "str" """ assert text_contents == dedent(expected_contents) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_with_use_signature_and_return(app: SphinxTestApp, status: StringIO) -> None: set_python_path() app.config.master_doc = "simple" # create flag app.config.typehints_use_signature = True app.config.typehints_use_signature_return = True app.build() assert "build succeeded" in status.getvalue() text_path = Path(app.srcdir) / "_build" / "text" / "simple.txt" text_contents = text_path.read_text().replace("–", "--") # noqa: RUF001 # keep ambiguous EN DASH expected_contents = """\ Simple Module ************* dummy_module_simple.function(x: bool, y: int = 1) -> str Function docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar Return type: "str" """ assert text_contents == dedent(expected_contents) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_default_annotation_without_typehints(app: SphinxTestApp, status: StringIO) -> None: set_python_path() app.config.master_doc = "without_complete_typehints" # create flag app.config.typehints_defaults = "comma" app.build() assert "build succeeded" in status.getvalue() text_path = Path(app.srcdir) / "_build" / "text" / "without_complete_typehints.txt" text_contents = text_path.read_text().replace("–", "--") # noqa: RUF001 # keep ambiguous EN DASH expected_contents = """\ Simple Module ************* dummy_module_without_complete_typehints.function_with_some_defaults_and_without_typehints(x, y=None) Function docstring. Parameters: * **x** -- foo * **y** (default: "None") -- bar dummy_module_without_complete_typehints.function_with_some_defaults_and_some_typehints(x, y=None) Function docstring. Parameters: * **x** ("int") -- foo * **y** (default: "None") -- bar dummy_module_without_complete_typehints.function_with_some_defaults_and_more_typehints(x, y=None) Function docstring. Parameters: * **x** ("int") -- foo * **y** (default: "None") -- bar Return type: "str" dummy_module_without_complete_typehints.function_with_defaults_and_some_typehints(x=0, y=None) Function docstring. Parameters: * **x** ("int", default: "0") -- foo * **y** (default: "None") -- bar Return type: "str" """ assert text_contents == dedent(expected_contents) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_wrong_module_path(app: SphinxTestApp, status: StringIO, warning: StringIO) -> None: set_python_path() app.config.master_doc = "wrong_module_path" # create flag app.config.default_role = "literal" app.config.nitpicky = True app.config.nitpick_ignore = {("py:data", "typing.Optional")} def fixup_module_name(mod: str) -> str: if not mod.startswith("wrong_module_path"): return mod return "export_module" + mod.removeprefix("wrong_module_path") app.config.suppress_warnings = ["config.cache"] app.config.typehints_fixup_module_name = fixup_module_name app.build() assert "build succeeded" in status.getvalue() # Build succeeded assert not warning.getvalue().strip() sphinx_autodoc_typehints-2.3.0/tests/test_version.py0000644000000000000000000000023713615410400020034 0ustar00from __future__ import annotations def test_version() -> None: from sphinx_autodoc_typehints import __version__ # noqa: PLC0415 assert __version__ sphinx_autodoc_typehints-2.3.0/tests/roots/test-dummy/conf.py0000644000000000000000000000044213615410400021511 0ustar00from __future__ import annotations import pathlib import sys # Make dummy_module.py available for autodoc. sys.path.insert(0, str(pathlib.Path(__file__).parent)) master_doc = "index" extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_autodoc_typehints", ] sphinx_autodoc_typehints-2.3.0/tests/roots/test-dummy/dummy_module.py0000644000000000000000000000032713615410400023266 0ustar00from __future__ import annotations from dataclasses import dataclass def undocumented_function(x: int) -> str: """Hi""" return str(x) @dataclass class DataClass: """Class docstring.""" x: int sphinx_autodoc_typehints-2.3.0/tests/roots/test-dummy/dummy_module_future_annotations.py0000644000000000000000000000052213615410400027272 0ustar00from __future__ import annotations def function_with_py310_annotations( self, # noqa: ANN001, ARG001 x: bool | None, # noqa: ARG001 y: int | str | float, # noqa: ARG001,PYI041 z: str | None = None, # noqa: ARG001 ) -> str: """ Method docstring. :param x: foo :param y: bar :param z: baz """ sphinx_autodoc_typehints-2.3.0/tests/roots/test-dummy/dummy_module_simple.py0000644000000000000000000000025413615410400024636 0ustar00from __future__ import annotations def function(x: bool, y: int = 1) -> str: # noqa: ARG001 """ Function docstring. :param x: foo :param y: bar """ sphinx_autodoc_typehints-2.3.0/tests/roots/test-dummy/dummy_module_simple_default_role.py0000644000000000000000000000025613615410400027365 0ustar00from __future__ import annotations def function(x: bool, y: int) -> str: # noqa: ARG001 """ Function docstring. :param x: `foo` :param y: ``bar`` """ sphinx_autodoc_typehints-2.3.0/tests/roots/test-dummy/dummy_module_simple_no_use_rtype.py0000644000000000000000000000140313615410400027426 0ustar00from __future__ import annotations def function_no_returns(x: bool, y: int = 1) -> str: # noqa: ARG001 """ Function docstring. :param x: foo :param y: bar """ def function_returns_with_type(x: bool, y: int = 1) -> str: # noqa: ARG001 """ Function docstring. :param x: foo :param y: bar :returns: *CustomType* -- A string """ def function_returns_with_compound_type(x: bool, y: int = 1) -> str: # noqa: ARG001 """ Function docstring. :param x: foo :param y: bar :returns: Union[str, int] -- A string or int """ def function_returns_without_type(x: bool, y: int = 1) -> str: # noqa: ARG001 """ Function docstring. :param x: foo :param y: bar :returns: A string """ sphinx_autodoc_typehints-2.3.0/tests/roots/test-dummy/dummy_module_without_complete_typehints.py0000644000000000000000000000135213615410400031047 0ustar00from __future__ import annotations def function_with_some_defaults_and_without_typehints(x, y=None): # noqa: ANN001, ANN201, ARG001 """ Function docstring. :param x: foo :param y: bar """ def function_with_some_defaults_and_some_typehints(x: int, y=None): # noqa: ANN001, ANN201, ARG001 """ Function docstring. :param x: foo :param y: bar """ def function_with_some_defaults_and_more_typehints(x: int, y=None) -> str: # noqa: ANN001, ARG001 """ Function docstring. :param x: foo :param y: bar """ def function_with_defaults_and_some_typehints(x: int = 0, y=None) -> str: # noqa: ANN001, ARG001 """ Function docstring. :param x: foo :param y: bar """ sphinx_autodoc_typehints-2.3.0/tests/roots/test-dummy/export_module.py0000644000000000000000000000013513615410400023451 0ustar00from __future__ import annotations from wrong_module_path import A, f __all__ = ["A", "f"] sphinx_autodoc_typehints-2.3.0/tests/roots/test-dummy/future_annotations.rst0000644000000000000000000000016713615410400024677 0ustar00:orphan: Dummy Module ============ .. autofunction:: dummy_module_future_annotations.function_with_py310_annotations sphinx_autodoc_typehints-2.3.0/tests/roots/test-dummy/simple.rst0000644000000000000000000000012613615410400022234 0ustar00:orphan: Simple Module ============= .. autofunction:: dummy_module_simple.function sphinx_autodoc_typehints-2.3.0/tests/roots/test-dummy/simple_default_role.rst0000644000000000000000000000014313615410400024760 0ustar00:orphan: Simple Module ============= .. autofunction:: dummy_module_simple_default_role.function sphinx_autodoc_typehints-2.3.0/tests/roots/test-dummy/simple_no_use_rtype.rst0000644000000000000000000000054413615410400025033 0ustar00:orphan: Simple Module ============= .. autofunction:: dummy_module_simple_no_use_rtype.function_no_returns .. autofunction:: dummy_module_simple_no_use_rtype.function_returns_with_type .. autofunction:: dummy_module_simple_no_use_rtype.function_returns_with_compound_type .. autofunction:: dummy_module_simple_no_use_rtype.function_returns_without_type sphinx_autodoc_typehints-2.3.0/tests/roots/test-dummy/without_complete_typehints.rst0000644000000000000000000000071113615410400026445 0ustar00:orphan: Simple Module ============= .. autofunction:: dummy_module_without_complete_typehints.function_with_some_defaults_and_without_typehints .. autofunction:: dummy_module_without_complete_typehints.function_with_some_defaults_and_some_typehints .. autofunction:: dummy_module_without_complete_typehints.function_with_some_defaults_and_more_typehints .. autofunction:: dummy_module_without_complete_typehints.function_with_defaults_and_some_typehints sphinx_autodoc_typehints-2.3.0/tests/roots/test-dummy/wrong_module_path.py0000644000000000000000000000012013615410400024272 0ustar00from __future__ import annotations class A: pass def f() -> A: pass sphinx_autodoc_typehints-2.3.0/tests/roots/test-dummy/wrong_module_path.rst0000644000000000000000000000011013615410400024451 0ustar00:orphan: .. class:: export_module.A .. autofunction:: export_module.f sphinx_autodoc_typehints-2.3.0/tests/roots/test-integration/conf.py0000644000000000000000000000044213615410400022701 0ustar00from __future__ import annotations import pathlib import sys # Make dummy_module.py available for autodoc. sys.path.insert(0, str(pathlib.Path(__file__).parent)) master_doc = "index" extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_autodoc_typehints", ] sphinx_autodoc_typehints-2.3.0/tests/roots/test-resolve-typing-guard/conf.py0000644000000000000000000000032613615410400024446 0ustar00from __future__ import annotations import pathlib import sys master_doc = "index" sys.path.insert(0, str(pathlib.Path(__file__).parent)) extensions = [ "sphinx.ext.autodoc", "sphinx_autodoc_typehints", ] sphinx_autodoc_typehints-2.3.0/tests/roots/test-resolve-typing-guard/demo_typing_guard.py0000644000000000000000000000260313615410400027221 0ustar00"""Module demonstrating imports that are type guarded""" from __future__ import annotations import typing from builtins import ValueError # handle does not have __module__ from functools import cmp_to_key # has __module__ but cannot get module as is builtin from typing import TYPE_CHECKING from demo_typing_guard_dummy import AnotherClass if TYPE_CHECKING: from decimal import Decimal from typing import Sequence # noqa: UP035 from demo_typing_guard_dummy import Literal # guarded by another `if TYPE_CHECKING` in demo_typing_guard_dummy if typing.TYPE_CHECKING: from typing import AnyStr if TYPE_CHECKING: # bad import from functools import missing # noqa: F401 def a(f: Decimal, s: AnyStr) -> Sequence[AnyStr | Decimal]: """ Do. :param f: first :param s: second :return: result """ return [f, s] class SomeClass: """This class do something.""" def create(self, item: Decimal) -> None: """ Create something. :param item: the item in question """ if TYPE_CHECKING: # Classes doesn't have `__globals__` attribute def guarded(self, item: Decimal) -> None: """ Guarded method. :param item: some item """ def func(_x: Literal) -> None: ... __all__ = [ "AnotherClass", "SomeClass", "ValueError", "a", "cmp_to_key", ] sphinx_autodoc_typehints-2.3.0/tests/roots/test-resolve-typing-guard/demo_typing_guard_dummy.py0000644000000000000000000000043613615410400030436 0ustar00from __future__ import annotations from typing import TYPE_CHECKING from viktor import AI # module part of autodoc_mock_imports # noqa: F401 if TYPE_CHECKING: # Nested type guard from typing import Literal # noqa: F401 class AnotherClass: """Another class is here""" sphinx_autodoc_typehints-2.3.0/tests/roots/test-resolve-typing-guard/index.rst0000644000000000000000000000005713615410400025011 0ustar00.. automodule:: demo_typing_guard :members: sphinx_autodoc_typehints-2.3.0/tests/roots/test-resolve-typing-guard-tmp/conf.py0000644000000000000000000000032613615410400025244 0ustar00from __future__ import annotations import pathlib import sys master_doc = "index" sys.path.insert(0, str(pathlib.Path(__file__).parent)) extensions = [ "sphinx.ext.autodoc", "sphinx_autodoc_typehints", ] sphinx_autodoc_typehints-2.3.0/tests/roots/test-resolve-typing-guard-tmp/demo_typing_guard.py0000644000000000000000000000223613615410400030021 0ustar00"""Module demonstrating imports that are type guarded""" from __future__ import annotations import datetime from attrs import define @define() class SomeClass: """This class does something.""" date: datetime.date """Date to handle""" @classmethod def from_str(cls, input_value: str) -> SomeClass: """ Initialize from string :param input_value: Input :return: result """ return cls(input_value) @classmethod def from_date(cls, input_value: datetime.date) -> SomeClass: """ Initialize from date :param input_value: Input :return: result """ return cls(input_value) @classmethod def from_time(cls, input_value: datetime.time) -> SomeClass: """ Initialize from time :param input_value: Input :return: result """ return cls(input_value) def calculate_thing(self, number: float) -> datetime.timedelta: # noqa: PLR6301 """ Calculate a thing :param number: Input :return: result """ return datetime.timedelta(number) __all__ = ["SomeClass"] sphinx_autodoc_typehints-2.3.0/tests/roots/test-resolve-typing-guard-tmp/index.rst0000644000000000000000000000005713615410400025607 0ustar00.. automodule:: demo_typing_guard :members: sphinx_autodoc_typehints-2.3.0/.gitignore0000644000000000000000000000013613615410400015562 0ustar00dist *.egg *.py[codz] *$py.class .tox .*_cache /src/sphinx_autodoc_typehints/version.py venv* sphinx_autodoc_typehints-2.3.0/LICENSE0000644000000000000000000000211513615410400014576 0ustar00MIT License Copyright (c) 2015-202x The sphinx-autodoc-typehints developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sphinx_autodoc_typehints-2.3.0/README.md0000644000000000000000000001374113615410400015057 0ustar00# sphinx-autodoc-typehints [![PyPI](https://img.shields.io/pypi/v/sphinx-autodoc-typehints?style=flat-square)](https://pypi.org/project/sphinx-autodoc-typehints/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/sphinx-autodoc-typehints.svg)](https://pypi.org/project/sphinx-autodoc-typehints/) [![Downloads](https://pepy.tech/badge/sphinx-autodoc-typehints/month)](https://pepy.tech/project/sphinx-autodoc-typehints) [![check](https://github.com/tox-dev/sphinx-autodoc-typehints/actions/workflows/check.yml/badge.svg)](https://github.com/tox-dev/sphinx-autodoc-typehints/actions/workflows/check.yml) This extension allows you to use Python 3 annotations for documenting acceptable argument types and return value types of functions. See an example of the Sphinx render at the [pyproject-api docs](https://pyproject-api.readthedocs.io/en/latest/). This allows you to use type hints in a very natural fashion, allowing you to migrate from this: ```python def format_unit(value, unit): """ Formats the given value as a human readable string using the given units. :param float|int value: a numeric value :param str unit: the unit for the value (kg, m, etc.) :rtype: str """ return f"{value} {unit}" ``` to this: ```python from typing import Union def format_unit(value: Union[float, int], unit: str) -> str: """ Formats the given value as a human readable string using the given units. :param value: a numeric value :param unit: the unit for the value (kg, m, etc.) """ return f"{value} {unit}" ``` ## Installation and setup First, use pip to download and install the extension: ```bash pip install sphinx-autodoc-typehints ``` Then, add the extension to your `conf.py`: ```python extensions = ["sphinx.ext.autodoc", "sphinx_autodoc_typehints"] ``` ## Options The following configuration options are accepted: - `typehints_fully_qualified` (default: `False`): if `True`, class names are always fully qualified (e.g. `module.for.Class`). If `False`, just the class name displays (e.g. `Class`) - `always_document_param_types` (default: `False`): If `False`, do not add type info for undocumented parameters. If `True`, add stub documentation for undocumented parameters to be able to add type info. - `always_use_bars_union ` (default: `False`): If `True`, display Union's using the | operator described in PEP 604. (e.g `X` | `Y` or `int` | `None`). If `False`, Unions will display with the typing in brackets. (e.g. `Union[X, Y]` or `Optional[int]`) - `typehints_document_rtype` (default: `True`): If `False`, never add an `:rtype:` directive. If `True`, add the `:rtype:` directive if no existing `:rtype:` is found. - `typehints_use_rtype` (default: `True`): Controls behavior when `typehints_document_rtype` is set to `True`. If `True`, document return type in the `:rtype:` directive. If `False`, document return type as part of the `:return:` directive, if present, otherwise fall back to using `:rtype:`. Use in conjunction with [napoleon_use_rtype](https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#confval-napoleon_use_rtype) to avoid generation of duplicate or redundant return type information. - `typehints_defaults` (default: `None`): If `None`, defaults are not added. Otherwise, adds a default annotation: - `'comma'` adds it after the type, changing Sphinx’ default look to “**param** (_int_, default: `1`) -- text”. - `'braces'` adds `(default: ...)` after the type (useful for numpydoc like styles). - `'braces-after'` adds `(default: ...)` at the end of the parameter documentation text instead. - `simplify_optional_unions` (default: `True`): If `True`, optional parameters of type \"Union\[\...\]\" are simplified as being of type Union\[\..., None\] in the resulting documentation (e.g. Optional\[Union\[A, B\]\] -\> Union\[A, B, None\]). If `False`, the \"Optional\"-type is kept. Note: If `False`, **any** Union containing `None` will be displayed as Optional! Note: If an optional parameter has only a single type (e.g Optional\[A\] or Union\[A, None\]), it will **always** be displayed as Optional! - `typehints_formatter` (default: `None`): If set to a function, this function will be called with `annotation` as first argument and `sphinx.config.Config` argument second. The function is expected to return a string with reStructuredText code or `None` to fall back to the default formatter. - `typehints_use_signature` (default: `False`): If `True`, typehints for parameters in the signature are shown. - `typehints_use_signature_return` (default: `False`): If `True`, return annotations in the signature are shown. ## How it works The extension listens to the `autodoc-process-signature` and `autodoc-process-docstring` Sphinx events. In the former, it strips the annotations from the function signature. In the latter, it injects the appropriate `:type argname:` and `:rtype:` directives into the docstring. Only arguments that have an existing `:param:` directive in the docstring get their respective `:type:` directives added. The `:rtype:` directive is added if and only if no existing `:rtype:` is found. ## Compatibility with sphinx.ext.napoleon To use [sphinx.ext.napoleon](http://www.sphinx-doc.org/en/stable/ext/napoleon.html) with sphinx-autodoc-typehints, make sure you load [sphinx.ext.napoleon](http://www.sphinx-doc.org/en/stable/ext/napoleon.html) first, **before** sphinx-autodoc-typehints. See [Issue 15](https://github.com/tox-dev/sphinx-autodoc-typehints/issues/15) on the issue tracker for more information. ## Dealing with circular imports Sometimes functions or classes from two different modules need to reference each other in their type annotations. This creates a circular import problem. The solution to this is the following: 1. Import only the module, not the classes/functions from it 2. Use forward references in the type annotations (e.g. `def methodname(self, param1: 'othermodule.OtherClass'):`) On Python 3.7, you can even use `from __future__ import annotations` and remove the quotes. sphinx_autodoc_typehints-2.3.0/pyproject.toml0000644000000000000000000001001613615410400016504 0ustar00[build-system] build-backend = "hatchling.build" requires = [ "hatch-vcs>=0.4", "hatchling>=1.24", ] [project] name = "sphinx-autodoc-typehints" description = "Type hints (PEP 484) support for the Sphinx autodoc extension" readme.content-type = "text/markdown" readme.file = "README.md" keywords = [ "environments", "isolated", "testing", "virtual", ] license = "MIT" maintainers = [ { name = "Bernát Gábor", email = "gaborjbernat@gmail.com" }, ] authors = [ { name = "Bernát Gábor", email = "gaborjbernat@gmail.com" }, ] requires-python = ">=3.9" classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: Sphinx :: Extension", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Documentation :: Sphinx", ] dynamic = [ "version", ] dependencies = [ "sphinx>=7.3.5", ] optional-dependencies.docs = [ "furo>=2024.1.29", ] optional-dependencies.numpy = [ "nptyping>=2.5", ] optional-dependencies.testing = [ "covdefaults>=2.3", "coverage>=7.4.4", "defusedxml>=0.7.1", # required by sphinx.testing "diff-cover>=9", "pytest>=8.1.1", "pytest-cov>=5", "sphobjinv>=2.3.1", "typing-extensions>=4.11", ] urls.Changelog = "https://github.com/tox-dev/sphinx-autodoc-typehints/blob/main/CHANGELOG.md" urls.Homepage = "https://github.com/tox-dev/sphinx-autodoc-typehints" urls.Source = "https://github.com/tox-dev/sphinx-autodoc-typehints" urls.Tracker = "https://github.com/tox-dev/sphinx-autodoc-typehints/issues" [tool.hatch] build.hooks.vcs.version-file = "src/sphinx_autodoc_typehints/version.py" version.source = "vcs" [tool.black] line-length = 120 [tool.ruff] target-version = "py39" line-length = 120 format.preview = true format.docstring-code-line-length = 100 format.docstring-code-format = true lint.select = [ "ALL", ] lint.ignore = [ "ANN101", # no type annotation for self "ANN401", # allow Any as type annotation "COM812", # Conflict with formatter "CPY", # No copyright statements "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible "ISC001", # Conflict with formatter "S104", # Possible binding to all interface ] lint.per-file-ignores."tests/**/*.py" = [ "D", # don't care about documentation in tests "FBT", # don"t care about booleans as positional arguments in tests "INP001", # no implicit namespace "PLC2701", # private imports "PLR0913", # any number of arguments in tests "PLR0917", # any number of arguments in tests "PLR2004", # Magic value used in comparison, consider replacing with a constant variable "S101", # asserts allowed in tests... "S603", # `subprocess` call: check for execution of untrusted input ] lint.isort = { known-first-party = [ "sphinx_autodoc_typehints", "tests", ], required-imports = [ "from __future__ import annotations", ] } lint.preview = true [tool.codespell] builtin = "clear,usage,en-GB_to_en-US" ignore-words = "ignore-words.txt" write-changes = true count = true [tool.pytest.ini_options] testpaths = [ "tests", ] [tool.coverage] html.show_contexts = true html.skip_covered = false paths.source = [ "src", ".tox/*/lib/python*/site-packages", ".tox/pypy*/site-packages", ".tox\\*\\Lib\\site-packages", ".tox/*/.venv/lib/python*/site-packages", ".tox/pypy*/.venv/site-packages", ".tox\\*\\.venv\\Lib\\site-packages", "*/src", "*\\src", ] report.fail_under = 85 report.omit = [ ] run.parallel = true run.plugins = [ "covdefaults", ] [tool.mypy] python_version = "3.10" strict = true exclude = "^(.*/roots/.*)|(tests/test_integration.*.py)$" overrides = [ { module = [ "sphobjinv.*", ], ignore_missing_imports = true }, ] sphinx_autodoc_typehints-2.3.0/PKG-INFO0000644000000000000000000001747613615410400014706 0ustar00Metadata-Version: 2.3 Name: sphinx-autodoc-typehints Version: 2.3.0 Summary: Type hints (PEP 484) support for the Sphinx autodoc extension Project-URL: Changelog, https://github.com/tox-dev/sphinx-autodoc-typehints/blob/main/CHANGELOG.md Project-URL: Homepage, https://github.com/tox-dev/sphinx-autodoc-typehints Project-URL: Source, https://github.com/tox-dev/sphinx-autodoc-typehints Project-URL: Tracker, https://github.com/tox-dev/sphinx-autodoc-typehints/issues Author-email: Bernát Gábor Maintainer-email: Bernát Gábor License-Expression: MIT License-File: LICENSE Keywords: environments,isolated,testing,virtual Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: Sphinx :: Extension Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Documentation :: Sphinx Requires-Python: >=3.9 Requires-Dist: sphinx>=7.3.5 Provides-Extra: docs Requires-Dist: furo>=2024.1.29; extra == 'docs' Provides-Extra: numpy Requires-Dist: nptyping>=2.5; extra == 'numpy' Provides-Extra: testing Requires-Dist: covdefaults>=2.3; extra == 'testing' Requires-Dist: coverage>=7.4.4; extra == 'testing' Requires-Dist: defusedxml>=0.7.1; extra == 'testing' Requires-Dist: diff-cover>=9; extra == 'testing' Requires-Dist: pytest-cov>=5; extra == 'testing' Requires-Dist: pytest>=8.1.1; extra == 'testing' Requires-Dist: sphobjinv>=2.3.1; extra == 'testing' Requires-Dist: typing-extensions>=4.11; extra == 'testing' Description-Content-Type: text/markdown # sphinx-autodoc-typehints [![PyPI](https://img.shields.io/pypi/v/sphinx-autodoc-typehints?style=flat-square)](https://pypi.org/project/sphinx-autodoc-typehints/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/sphinx-autodoc-typehints.svg)](https://pypi.org/project/sphinx-autodoc-typehints/) [![Downloads](https://pepy.tech/badge/sphinx-autodoc-typehints/month)](https://pepy.tech/project/sphinx-autodoc-typehints) [![check](https://github.com/tox-dev/sphinx-autodoc-typehints/actions/workflows/check.yml/badge.svg)](https://github.com/tox-dev/sphinx-autodoc-typehints/actions/workflows/check.yml) This extension allows you to use Python 3 annotations for documenting acceptable argument types and return value types of functions. See an example of the Sphinx render at the [pyproject-api docs](https://pyproject-api.readthedocs.io/en/latest/). This allows you to use type hints in a very natural fashion, allowing you to migrate from this: ```python def format_unit(value, unit): """ Formats the given value as a human readable string using the given units. :param float|int value: a numeric value :param str unit: the unit for the value (kg, m, etc.) :rtype: str """ return f"{value} {unit}" ``` to this: ```python from typing import Union def format_unit(value: Union[float, int], unit: str) -> str: """ Formats the given value as a human readable string using the given units. :param value: a numeric value :param unit: the unit for the value (kg, m, etc.) """ return f"{value} {unit}" ``` ## Installation and setup First, use pip to download and install the extension: ```bash pip install sphinx-autodoc-typehints ``` Then, add the extension to your `conf.py`: ```python extensions = ["sphinx.ext.autodoc", "sphinx_autodoc_typehints"] ``` ## Options The following configuration options are accepted: - `typehints_fully_qualified` (default: `False`): if `True`, class names are always fully qualified (e.g. `module.for.Class`). If `False`, just the class name displays (e.g. `Class`) - `always_document_param_types` (default: `False`): If `False`, do not add type info for undocumented parameters. If `True`, add stub documentation for undocumented parameters to be able to add type info. - `always_use_bars_union ` (default: `False`): If `True`, display Union's using the | operator described in PEP 604. (e.g `X` | `Y` or `int` | `None`). If `False`, Unions will display with the typing in brackets. (e.g. `Union[X, Y]` or `Optional[int]`) - `typehints_document_rtype` (default: `True`): If `False`, never add an `:rtype:` directive. If `True`, add the `:rtype:` directive if no existing `:rtype:` is found. - `typehints_use_rtype` (default: `True`): Controls behavior when `typehints_document_rtype` is set to `True`. If `True`, document return type in the `:rtype:` directive. If `False`, document return type as part of the `:return:` directive, if present, otherwise fall back to using `:rtype:`. Use in conjunction with [napoleon_use_rtype](https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#confval-napoleon_use_rtype) to avoid generation of duplicate or redundant return type information. - `typehints_defaults` (default: `None`): If `None`, defaults are not added. Otherwise, adds a default annotation: - `'comma'` adds it after the type, changing Sphinx’ default look to “**param** (_int_, default: `1`) -- text”. - `'braces'` adds `(default: ...)` after the type (useful for numpydoc like styles). - `'braces-after'` adds `(default: ...)` at the end of the parameter documentation text instead. - `simplify_optional_unions` (default: `True`): If `True`, optional parameters of type \"Union\[\...\]\" are simplified as being of type Union\[\..., None\] in the resulting documentation (e.g. Optional\[Union\[A, B\]\] -\> Union\[A, B, None\]). If `False`, the \"Optional\"-type is kept. Note: If `False`, **any** Union containing `None` will be displayed as Optional! Note: If an optional parameter has only a single type (e.g Optional\[A\] or Union\[A, None\]), it will **always** be displayed as Optional! - `typehints_formatter` (default: `None`): If set to a function, this function will be called with `annotation` as first argument and `sphinx.config.Config` argument second. The function is expected to return a string with reStructuredText code or `None` to fall back to the default formatter. - `typehints_use_signature` (default: `False`): If `True`, typehints for parameters in the signature are shown. - `typehints_use_signature_return` (default: `False`): If `True`, return annotations in the signature are shown. ## How it works The extension listens to the `autodoc-process-signature` and `autodoc-process-docstring` Sphinx events. In the former, it strips the annotations from the function signature. In the latter, it injects the appropriate `:type argname:` and `:rtype:` directives into the docstring. Only arguments that have an existing `:param:` directive in the docstring get their respective `:type:` directives added. The `:rtype:` directive is added if and only if no existing `:rtype:` is found. ## Compatibility with sphinx.ext.napoleon To use [sphinx.ext.napoleon](http://www.sphinx-doc.org/en/stable/ext/napoleon.html) with sphinx-autodoc-typehints, make sure you load [sphinx.ext.napoleon](http://www.sphinx-doc.org/en/stable/ext/napoleon.html) first, **before** sphinx-autodoc-typehints. See [Issue 15](https://github.com/tox-dev/sphinx-autodoc-typehints/issues/15) on the issue tracker for more information. ## Dealing with circular imports Sometimes functions or classes from two different modules need to reference each other in their type annotations. This creates a circular import problem. The solution to this is the following: 1. Import only the module, not the classes/functions from it 2. Use forward references in the type annotations (e.g. `def methodname(self, param1: 'othermodule.OtherClass'):`) On Python 3.7, you can even use `from __future__ import annotations` and remove the quotes.