├── .github ├── FUNDING.yml ├── SECURITY.md ├── dependabot.yml ├── release.yml └── workflows │ ├── check.yaml │ └── release.yaml ├── .gitignore ├── .markdownlint.yaml ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── ignore-words.txt ├── pyproject.toml ├── src └── sphinx_autodoc_typehints │ ├── __init__.py │ ├── _parser.py │ ├── attributes_patch.py │ ├── patches.py │ └── py.typed ├── tests ├── conftest.py ├── roots │ ├── test-dummy │ │ ├── conf.py │ │ ├── dummy_module.py │ │ ├── dummy_module_future_annotations.py │ │ ├── dummy_module_simple.py │ │ ├── dummy_module_simple_default_role.py │ │ ├── dummy_module_simple_no_use_rtype.py │ │ ├── dummy_module_without_complete_typehints.py │ │ ├── export_module.py │ │ ├── future_annotations.rst │ │ ├── simple.rst │ │ ├── simple_default_role.rst │ │ ├── simple_no_use_rtype.rst │ │ ├── without_complete_typehints.rst │ │ ├── wrong_module_path.py │ │ └── wrong_module_path.rst │ ├── test-integration │ │ └── conf.py │ ├── test-resolve-typing-guard-tmp │ │ ├── conf.py │ │ ├── demo_typing_guard.py │ │ └── index.rst │ └── test-resolve-typing-guard │ │ ├── conf.py │ │ ├── demo_typing_guard.py │ │ ├── demo_typing_guard_dummy.py │ │ └── index.rst ├── test_integration.py ├── test_integration_autodoc_type_aliases.py ├── test_integration_issue_384.py ├── test_sphinx_autodoc_typehints.py └── test_version.py ├── tox.ini └── whitelist.txt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | tidelift: "pypi/sphinx-autodoc-typehints" 2 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.18 + | :white_check_mark: | 8 | | < 1.18 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift 13 | will coordinate the fix and disclosure. 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - dependabot 5 | - pre-commit-ci 6 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: check 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: ["main"] 6 | tags-ignore: ["**"] 7 | pull_request: 8 | schedule: 9 | - cron: "0 8 * * *" 10 | 11 | concurrency: 12 | group: check-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | env: 22 | - "3.13" 23 | - "3.12" 24 | - "3.11" 25 | - type 26 | - dev 27 | - pkg_meta 28 | steps: 29 | - name: Install OS dependencies 30 | run: sudo apt-get install graphviz -y 31 | - uses: actions/checkout@v4 32 | with: 33 | fetch-depth: 0 34 | - name: Install the latest version of uv 35 | uses: astral-sh/setup-uv@v6 36 | with: 37 | enable-cache: true 38 | cache-dependency-glob: "pyproject.toml" 39 | github-token: ${{ secrets.GITHUB_TOKEN }} 40 | - name: Install tox 41 | run: uv tool install --python-preference only-managed --python 3.13 tox --with tox-uv 42 | - name: Install Python 43 | if: startsWith(matrix.env, '3.') && matrix.env != '3.13' 44 | run: uv python install --python-preference only-managed ${{ matrix.env }} 45 | - name: Setup test suite 46 | run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.env }} 47 | - name: Run test suite 48 | run: tox run --skip-pkg-install -e ${{ matrix.env }} 49 | env: 50 | PYTEST_ADDOPTS: "-vv --durations=20" 51 | DIFF_AGAINST: HEAD 52 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release to PyPI 2 | on: 3 | push: 4 | tags: ["*"] 5 | 6 | env: 7 | dists-artifact-name: python-package-distributions 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - name: Install the latest version of uv 17 | uses: astral-sh/setup-uv@v6 18 | with: 19 | enable-cache: true 20 | cache-dependency-glob: "pyproject.toml" 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | - name: Build package 23 | run: uv build --python 3.13 --python-preference only-managed --sdist --wheel . --out-dir dist 24 | - name: Store the distribution packages 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: ${{ env.dists-artifact-name }} 28 | path: dist/* 29 | 30 | release: 31 | needs: 32 | - build 33 | runs-on: ubuntu-latest 34 | environment: 35 | name: release 36 | url: https://pypi.org/project/sphinx-autodoc-typehints/${{ github.ref_name }} 37 | permissions: 38 | id-token: write 39 | steps: 40 | - name: Download all the dists 41 | uses: actions/download-artifact@v4 42 | with: 43 | name: ${{ env.dists-artifact-name }} 44 | path: dist/ 45 | - name: Publish to PyPI 46 | uses: pypa/gh-action-pypi-publish@v1.12.4 47 | with: 48 | attestations: true 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.egg 3 | *.py[codz] 4 | *$py.class 5 | .tox 6 | .*_cache 7 | /src/sphinx_autodoc_typehints/version.py 8 | venv* 9 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | MD013: 2 | code_blocks: false 3 | headers: false 4 | line_length: 120 5 | tables: false 6 | 7 | MD046: 8 | style: fenced 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - id: trailing-whitespace 7 | - repo: https://github.com/python-jsonschema/check-jsonschema 8 | rev: "0.33.0" 9 | hooks: 10 | - id: check-github-workflows 11 | args: ["--verbose"] 12 | - repo: https://github.com/codespell-project/codespell 13 | rev: v2.4.1 14 | hooks: 15 | - id: codespell 16 | additional_dependencies: ["tomli>=2.2.1"] 17 | - repo: https://github.com/tox-dev/tox-ini-fmt 18 | rev: "1.5.0" 19 | hooks: 20 | - id: tox-ini-fmt 21 | args: ["-p", "fix"] 22 | - repo: https://github.com/tox-dev/pyproject-fmt 23 | rev: "v2.6.0" 24 | hooks: 25 | - id: pyproject-fmt 26 | - repo: https://github.com/astral-sh/ruff-pre-commit 27 | rev: "v0.11.11" 28 | hooks: 29 | - id: ruff-format 30 | - id: ruff 31 | args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] 32 | - repo: https://github.com/rbubley/mirrors-prettier 33 | rev: "v3.5.3" # Use the sha / tag you want to point at 34 | hooks: 35 | - id: prettier 36 | additional_dependencies: 37 | - prettier@3.5.1 38 | - "@prettier/plugin-xml@3.4.1" 39 | - repo: meta 40 | hooks: 41 | - id: check-hooks-apply 42 | - id: check-useless-excludes 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-202x The sphinx-autodoc-typehints developers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sphinx-autodoc-typehints 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/sphinx-autodoc-typehints?style=flat-square)](https://pypi.org/project/sphinx-autodoc-typehints/) 4 | [![Supported Python 5 | versions](https://img.shields.io/pypi/pyversions/sphinx-autodoc-typehints.svg)](https://pypi.org/project/sphinx-autodoc-typehints/) 6 | [![Downloads](https://pepy.tech/badge/sphinx-autodoc-typehints/month)](https://pepy.tech/project/sphinx-autodoc-typehints) 7 | [![check](https://github.com/tox-dev/sphinx-autodoc-typehints/actions/workflows/check.yaml/badge.svg)](https://github.com/tox-dev/sphinx-autodoc-typehints/actions/workflows/check.yaml) 8 | 9 | This extension allows you to use Python 3 annotations for documenting acceptable argument types and return value types 10 | of functions. See an example of the Sphinx render at the 11 | [pyproject-api docs](https://pyproject-api.readthedocs.io/latest/api.html). 12 | 13 | This allows you to use type hints in a very natural fashion, allowing you to migrate from this: 14 | 15 | ```python 16 | def format_unit(value, unit): 17 | """ 18 | Formats the given value as a human readable string using the given units. 19 | 20 | :param float|int value: a numeric value 21 | :param str unit: the unit for the value (kg, m, etc.) 22 | :rtype: str 23 | """ 24 | return f"{value} {unit}" 25 | ``` 26 | 27 | to this: 28 | 29 | ```python 30 | from typing import Union 31 | 32 | 33 | def format_unit(value: Union[float, int], unit: str) -> str: 34 | """ 35 | Formats the given value as a human readable string using the given units. 36 | 37 | :param value: a numeric value 38 | :param unit: the unit for the value (kg, m, etc.) 39 | """ 40 | return f"{value} {unit}" 41 | ``` 42 | 43 | ## Installation and setup 44 | 45 | First, use pip to download and install the extension: 46 | 47 | ```bash 48 | pip install sphinx-autodoc-typehints 49 | ``` 50 | 51 | Then, add the extension to your `conf.py`: 52 | 53 | ```python 54 | extensions = ["sphinx.ext.autodoc", "sphinx_autodoc_typehints"] 55 | ``` 56 | 57 | ## Options 58 | 59 | The following configuration options are accepted: 60 | 61 | - `typehints_fully_qualified` (default: `False`): if `True`, class names are always fully qualified (e.g. 62 | `module.for.Class`). If `False`, just the class name displays (e.g. `Class`) 63 | - `always_document_param_types` (default: `False`): If `False`, do not add type info for undocumented parameters. If 64 | `True`, add stub documentation for undocumented parameters to be able to add type info. 65 | - `always_use_bars_union ` (default: `False`): If `True`, display Union's using the | operator described in PEP 604. 66 | (e.g `X` | `Y` or `int` | `None`). If `False`, Unions will display with the typing in brackets. (e.g. `Union[X, Y]` 67 | or `Optional[int]`) 68 | - `typehints_document_rtype` (default: `True`): If `False`, never add an `:rtype:` directive. If `True`, add the 69 | `:rtype:` directive if no existing `:rtype:` is found. 70 | - `typehints_document_rtype_none` (default: `True`): If `False`, never add an `:rtype: None` directive. If `True`, add the `:rtype: None`. 71 | - `typehints_use_rtype` (default: `True`): Controls behavior when `typehints_document_rtype` is set to `True`. If 72 | `True`, document return type in the `:rtype:` directive. If `False`, document return type as part of the `:return:` 73 | directive, if present, otherwise fall back to using `:rtype:`. Use in conjunction with 74 | [napoleon_use_rtype](https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#confval-napoleon_use_rtype) 75 | to avoid generation of duplicate or redundant return type information. 76 | - `typehints_defaults` (default: `None`): If `None`, defaults are not added. Otherwise, adds a default annotation: 77 | 78 | - `'comma'` adds it after the type, changing Sphinx’ default look to “**param** (_int_, default: `1`) -- text”. 79 | - `'braces'` adds `(default: ...)` after the type (useful for numpydoc like styles). 80 | - `'braces-after'` adds `(default: ...)` at the end of the parameter documentation text instead. 81 | 82 | - `simplify_optional_unions` (default: `True`): If `True`, optional parameters of type \"Union\[\...\]\" are simplified 83 | as being of type Union\[\..., None\] in the resulting documentation (e.g. Optional\[Union\[A, B\]\] -\> Union\[A, B, 84 | None\]). If `False`, the \"Optional\"-type is kept. Note: If `False`, **any** Union containing `None` will be 85 | displayed as Optional! Note: If an optional parameter has only a single type (e.g Optional\[A\] or Union\[A, None\]), 86 | it will **always** be displayed as Optional! 87 | - `typehints_formatter` (default: `None`): If set to a function, this function will be called with `annotation` as first 88 | argument and `sphinx.config.Config` argument second. The function is expected to return a string with reStructuredText 89 | code or `None` to fall back to the default formatter. 90 | - `typehints_use_signature` (default: `False`): If `True`, typehints for parameters in the signature are shown. 91 | - `typehints_use_signature_return` (default: `False`): If `True`, return annotations in the signature are shown. 92 | 93 | ## How it works 94 | 95 | The extension listens to the `autodoc-process-signature` and `autodoc-process-docstring` Sphinx events. In the former, 96 | it strips the annotations from the function signature. In the latter, it injects the appropriate `:type argname:` and 97 | `:rtype:` directives into the docstring. 98 | 99 | Only arguments that have an existing `:param:` directive in the docstring get their respective `:type:` directives 100 | added. The `:rtype:` directive is added if and only if no existing `:rtype:` is found. 101 | 102 | ## Compatibility with sphinx.ext.napoleon 103 | 104 | To use [sphinx.ext.napoleon](http://www.sphinx-doc.org/en/stable/ext/napoleon.html) with sphinx-autodoc-typehints, make 105 | sure you load [sphinx.ext.napoleon](http://www.sphinx-doc.org/en/stable/ext/napoleon.html) first, **before** 106 | sphinx-autodoc-typehints. See [Issue 15](https://github.com/tox-dev/sphinx-autodoc-typehints/issues/15) on the issue 107 | tracker for more information. 108 | 109 | ## Dealing with circular imports 110 | 111 | Sometimes functions or classes from two different modules need to reference each other in their type annotations. This 112 | creates a circular import problem. The solution to this is the following: 113 | 114 | 1. Import only the module, not the classes/functions from it 115 | 2. Use forward references in the type annotations (e.g. `def methodname(self, param1: 'othermodule.OtherClass'):`) 116 | 117 | On Python 3.7, you can even use `from __future__ import annotations` and remove the quotes. 118 | -------------------------------------------------------------------------------- /ignore-words.txt: -------------------------------------------------------------------------------- 1 | master 2 | manuel 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = [ 4 | "hatch-vcs>=0.4", 5 | "hatchling>=1.27", 6 | ] 7 | 8 | [project] 9 | name = "sphinx-autodoc-typehints" 10 | description = "Type hints (PEP 484) support for the Sphinx autodoc extension" 11 | readme.content-type = "text/markdown" 12 | readme.file = "README.md" 13 | keywords = [ 14 | "environments", 15 | "isolated", 16 | "testing", 17 | "virtual", 18 | ] 19 | license = "MIT" 20 | maintainers = [ 21 | { name = "Bernát Gábor", email = "gaborjbernat@gmail.com" }, 22 | ] 23 | authors = [ 24 | { name = "Bernát Gábor", email = "gaborjbernat@gmail.com" }, 25 | ] 26 | requires-python = ">=3.11" 27 | classifiers = [ 28 | "Development Status :: 5 - Production/Stable", 29 | "Framework :: Sphinx :: Extension", 30 | "Intended Audience :: Developers", 31 | "License :: OSI Approved :: MIT License", 32 | "Programming Language :: Python", 33 | "Programming Language :: Python :: 3 :: Only", 34 | "Programming Language :: Python :: 3.11", 35 | "Programming Language :: Python :: 3.12", 36 | "Programming Language :: Python :: 3.13", 37 | "Topic :: Documentation :: Sphinx", 38 | ] 39 | dynamic = [ 40 | "version", 41 | ] 42 | dependencies = [ 43 | "sphinx>=8.2", 44 | ] 45 | optional-dependencies.docs = [ 46 | "furo>=2024.8.6", 47 | ] 48 | optional-dependencies.testing = [ 49 | "covdefaults>=2.3", 50 | "coverage>=7.6.12", 51 | "defusedxml>=0.7.1", # required by sphinx.testing 52 | "diff-cover>=9.2.3", 53 | "pytest>=8.3.4", 54 | "pytest-cov>=6", 55 | "sphobjinv>=2.3.1.2", 56 | "typing-extensions>=4.12.2", 57 | ] 58 | urls.Changelog = "https://github.com/tox-dev/sphinx-autodoc-typehints/releases" 59 | urls.Homepage = "https://github.com/tox-dev/sphinx-autodoc-typehints" 60 | urls.Source = "https://github.com/tox-dev/sphinx-autodoc-typehints" 61 | urls.Tracker = "https://github.com/tox-dev/sphinx-autodoc-typehints/issues" 62 | 63 | [tool.hatch] 64 | build.hooks.vcs.version-file = "src/sphinx_autodoc_typehints/version.py" 65 | version.source = "vcs" 66 | 67 | [tool.ruff] 68 | line-length = 120 69 | format.preview = true 70 | format.docstring-code-line-length = 100 71 | format.docstring-code-format = true 72 | lint.select = [ 73 | "ALL", 74 | ] 75 | lint.ignore = [ 76 | "ANN401", # allow Any as type annotation 77 | "COM812", # Conflict with formatter 78 | "CPY", # No copyright statements 79 | "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible 80 | "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible 81 | "DOC", # no sphinx support 82 | "ISC001", # Conflict with formatter 83 | "S104", # Possible binding to all interface 84 | ] 85 | lint.per-file-ignores."tests/**/*.py" = [ 86 | "D", # don't care about documentation in tests 87 | "FBT", # don't care about booleans as positional arguments in tests 88 | "INP001", # no implicit namespace 89 | "PLC2701", # private imports 90 | "PLR0913", # any number of arguments in tests 91 | "PLR0917", # any number of arguments in tests 92 | "PLR2004", # Magic value used in comparison, consider replacing with a constant variable 93 | "S101", # asserts allowed in tests 94 | "S603", # `subprocess` call: check for execution of untrusted input 95 | "UP006", # we test for old List/Tuple syntax 96 | "UP007", # we test for old Union syntax 97 | "UP045", # we test for old Optional syntax 98 | ] 99 | lint.isort = { known-first-party = [ 100 | "sphinx_autodoc_typehints", 101 | "tests", 102 | ], required-imports = [ 103 | "from __future__ import annotations", 104 | ] } 105 | lint.preview = true 106 | 107 | [tool.codespell] 108 | builtin = "clear,usage,en-GB_to_en-US" 109 | ignore-words = "ignore-words.txt" 110 | write-changes = true 111 | count = true 112 | 113 | [tool.pyproject-fmt] 114 | max_supported_python = "3.13" 115 | 116 | [tool.pytest.ini_options] 117 | testpaths = [ 118 | "tests", 119 | ] 120 | 121 | [tool.coverage] 122 | html.show_contexts = true 123 | html.skip_covered = false 124 | paths.source = [ 125 | "src", 126 | ".tox/*/lib/python*/site-packages", 127 | ".tox/pypy*/site-packages", 128 | ".tox\\*\\Lib\\site-packages", 129 | ".tox/*/.venv/lib/python*/site-packages", 130 | ".tox/pypy*/.venv/site-packages", 131 | ".tox\\*\\.venv\\Lib\\site-packages", 132 | "*/src", 133 | "*\\src", 134 | ] 135 | report.fail_under = 88 136 | report.omit = [ 137 | ] 138 | run.parallel = true 139 | run.plugins = [ 140 | "covdefaults", 141 | ] 142 | 143 | [tool.mypy] 144 | python_version = "3.11" 145 | strict = true 146 | exclude = "^(.*/roots/.*)|(tests/test_integration.*.py)$" 147 | overrides = [ 148 | { module = [ 149 | "sphobjinv.*", 150 | ], ignore_missing_imports = true }, 151 | ] 152 | -------------------------------------------------------------------------------- /src/sphinx_autodoc_typehints/__init__.py: -------------------------------------------------------------------------------- 1 | """Sphinx autodoc type hints.""" 2 | 3 | from __future__ import annotations 4 | 5 | import ast 6 | import importlib 7 | import inspect 8 | import re 9 | import sys 10 | import textwrap 11 | import types 12 | from dataclasses import dataclass 13 | from typing import TYPE_CHECKING, Any, AnyStr, ForwardRef, NewType, TypeVar, Union, get_type_hints 14 | 15 | from docutils import nodes 16 | from docutils.frontend import get_default_settings 17 | from sphinx.ext.autodoc.mock import mock 18 | from sphinx.parsers import RSTParser 19 | from sphinx.util import logging, rst 20 | from sphinx.util.inspect import TypeAliasForwardRef, stringify_signature 21 | from sphinx.util.inspect import signature as sphinx_signature 22 | 23 | from ._parser import parse 24 | from .patches import install_patches 25 | from .version import __version__ 26 | 27 | if TYPE_CHECKING: 28 | from ast import FunctionDef, Module, stmt 29 | from collections.abc import Callable 30 | 31 | from docutils.nodes import Node 32 | from docutils.parsers.rst import states 33 | from sphinx.application import Sphinx 34 | from sphinx.config import Config 35 | from sphinx.environment import BuildEnvironment 36 | from sphinx.ext.autodoc import Options 37 | 38 | _LOGGER = logging.getLogger(__name__) 39 | _PYDATA_ANNOTS_TYPING = {"Any", "AnyStr", "Callable", "ClassVar", "Literal", "NoReturn", "Optional", "Tuple", "Union"} 40 | _PYDATA_ANNOTS_TYPES = { 41 | *("AsyncGeneratorType", "BuiltinFunctionType", "BuiltinMethodType"), 42 | *("CellType", "ClassMethodDescriptorType", "CoroutineType"), 43 | "EllipsisType", 44 | *("FrameType", "FunctionType"), 45 | *("GeneratorType", "GetSetDescriptorType"), 46 | "LambdaType", 47 | *("MemberDescriptorType", "MethodDescriptorType", "MethodType", "MethodWrapperType"), 48 | # NoneType is special, but included here for completeness' sake 49 | *("NoneType", "NotImplementedType"), 50 | "WrapperDescriptorType", 51 | } 52 | _PYDATA_ANNOTATIONS = { 53 | *(("typing", n) for n in _PYDATA_ANNOTS_TYPING), 54 | *(("types", n) for n in _PYDATA_ANNOTS_TYPES), 55 | } 56 | 57 | # types has a bunch of things like ModuleType where ModuleType.__module__ is 58 | # "builtins" and ModuleType.__name__ is "module", so we have to check for this. 59 | _TYPES_DICT = {getattr(types, name): name for name in types.__all__} 60 | # Prefer FunctionType to LambdaType (they are synonymous) 61 | _TYPES_DICT[types.FunctionType] = "FunctionType" 62 | 63 | 64 | class MyTypeAliasForwardRef(TypeAliasForwardRef): 65 | def __or__(self, value: Any) -> Any: 66 | return Union[self, value] # noqa: UP007 67 | 68 | 69 | def _get_types_type(obj: Any) -> str | None: 70 | try: 71 | return _TYPES_DICT.get(obj) 72 | except Exception: # noqa: BLE001 73 | # e.g. exception: unhashable type 74 | return None 75 | 76 | 77 | def get_annotation_module(annotation: Any) -> str: 78 | """ 79 | Get module for an annotation. 80 | 81 | :param annotation: 82 | :return: 83 | """ 84 | if annotation is None: 85 | return "builtins" 86 | if _get_types_type(annotation) is not None: 87 | return "types" 88 | is_new_type = isinstance(annotation, NewType) 89 | if ( 90 | is_new_type 91 | or isinstance(annotation, TypeVar) 92 | or type(annotation).__name__ in {"ParamSpec", "ParamSpecArgs", "ParamSpecKwargs"} 93 | ): 94 | return "typing" 95 | if hasattr(annotation, "__module__"): 96 | return annotation.__module__ # type: ignore[no-any-return] 97 | if hasattr(annotation, "__origin__"): 98 | return annotation.__origin__.__module__ # type: ignore[no-any-return] 99 | msg = f"Cannot determine the module of {annotation}" 100 | raise ValueError(msg) 101 | 102 | 103 | def _is_newtype(annotation: Any) -> bool: 104 | return isinstance(annotation, NewType) 105 | 106 | 107 | def get_annotation_class_name(annotation: Any, module: str) -> str: # noqa: C901, PLR0911 108 | """ 109 | Get class name for annotation. 110 | 111 | :param annotation: 112 | :param module: 113 | :return: 114 | """ 115 | # Special cases 116 | if annotation is None: 117 | return "None" 118 | if annotation is AnyStr: 119 | return "AnyStr" 120 | val = _get_types_type(annotation) 121 | if val is not None: 122 | return val 123 | if _is_newtype(annotation): 124 | return "NewType" 125 | 126 | if getattr(annotation, "__qualname__", None): 127 | return annotation.__qualname__ # type: ignore[no-any-return] 128 | if getattr(annotation, "_name", None): # Required for generic aliases on Python 3.7+ 129 | return annotation._name # type: ignore[no-any-return] # noqa: SLF001 130 | if module in {"typing", "typing_extensions"} and isinstance(getattr(annotation, "name", None), str): 131 | # Required for at least Pattern and Match 132 | return annotation.name # type: ignore[no-any-return] 133 | 134 | origin = getattr(annotation, "__origin__", None) 135 | if origin: 136 | if getattr(origin, "__qualname__", None): # Required for Protocol subclasses 137 | return origin.__qualname__ # type: ignore[no-any-return] 138 | if getattr(origin, "_name", None): # Required for Union on Python 3.7+ 139 | return origin._name # type: ignore[no-any-return] # noqa: SLF001 140 | 141 | annotation_cls = annotation if inspect.isclass(annotation) else type(annotation) 142 | return annotation_cls.__qualname__.lstrip("_") 143 | 144 | 145 | def get_annotation_args(annotation: Any, module: str, class_name: str) -> tuple[Any, ...]: # noqa: PLR0911 146 | """ 147 | Get annotation arguments. 148 | 149 | :param annotation: 150 | :param module: 151 | :param class_name: 152 | :return: 153 | """ 154 | try: 155 | original = getattr(sys.modules[module], class_name) 156 | except (KeyError, AttributeError): 157 | pass 158 | else: 159 | if annotation is original: 160 | return () # This is the original, not parametrized type 161 | 162 | # Special cases 163 | if class_name in {"Pattern", "Match"} and hasattr(annotation, "type_var"): # Python < 3.7 164 | return (annotation.type_var,) 165 | if class_name == "ClassVar" and hasattr(annotation, "__type__"): # ClassVar on Python < 3.7 166 | return (annotation.__type__,) 167 | if class_name == "TypeVar" and hasattr(annotation, "__constraints__"): 168 | return annotation.__constraints__ # type: ignore[no-any-return] 169 | if class_name == "NewType" and hasattr(annotation, "__supertype__"): 170 | return (annotation.__supertype__,) 171 | if class_name == "Literal" and hasattr(annotation, "__values__"): 172 | return annotation.__values__ # type: ignore[no-any-return] 173 | if class_name == "Generic": 174 | return annotation.__parameters__ # type: ignore[no-any-return] 175 | result = getattr(annotation, "__args__", ()) 176 | # 3.10 and earlier Tuple[()] returns ((), ) instead of () the tuple does 177 | return () if len(result) == 1 and result[0] == () else result # type: ignore[misc] 178 | 179 | 180 | def format_internal_tuple(t: tuple[Any, ...], config: Config, *, short_literals: bool = False) -> str: 181 | # An annotation can be a tuple, e.g., for numpy.typing: 182 | # In this case, format_annotation receives: 183 | # This solution should hopefully be general for *any* type that allows tuples in annotations 184 | fmt = [format_annotation(a, config, short_literals=short_literals) for a in t] 185 | if len(fmt) == 0: 186 | return "()" 187 | if len(fmt) == 1: 188 | return f"({fmt[0]}, )" 189 | return f"({', '.join(fmt)})" 190 | 191 | 192 | def fixup_module_name(config: Config, module: str) -> str: 193 | if getattr(config, "typehints_fixup_module_name", None): 194 | module = config.typehints_fixup_module_name(module) 195 | 196 | if module == "typing_extensions": 197 | module = "typing" 198 | 199 | if module == "_io": 200 | module = "io" 201 | return module 202 | 203 | 204 | def format_annotation(annotation: Any, config: Config, *, short_literals: bool = False) -> str: # noqa: C901, PLR0911, PLR0912, PLR0915, PLR0914 205 | """ 206 | Format the annotation. 207 | 208 | :param annotation: 209 | :param config: 210 | :param short_literals: Render :py:class:`Literals` in PEP 604 style (``|``). 211 | :return: 212 | """ 213 | typehints_formatter: Callable[..., str] | None = getattr(config, "typehints_formatter", None) 214 | if typehints_formatter is not None: 215 | formatted = typehints_formatter(annotation, config) 216 | if formatted is not None: 217 | return formatted 218 | 219 | # Special cases 220 | if isinstance(annotation, ForwardRef): 221 | return annotation.__forward_arg__ 222 | if annotation is None or annotation is type(None): 223 | return ":py:obj:`None`" 224 | if annotation is Ellipsis: 225 | return ":py:data:`...`" 226 | 227 | if isinstance(annotation, tuple): 228 | return format_internal_tuple(annotation, config) 229 | 230 | if isinstance(annotation, TypeAliasForwardRef): 231 | return annotation.name 232 | 233 | try: 234 | module = get_annotation_module(annotation) 235 | class_name = get_annotation_class_name(annotation, module) 236 | args = get_annotation_args(annotation, module, class_name) 237 | except ValueError: 238 | return str(annotation).strip("'") 239 | 240 | module = fixup_module_name(config, module) 241 | full_name = f"{module}.{class_name}" if module != "builtins" else class_name 242 | fully_qualified: bool = getattr(config, "typehints_fully_qualified", False) 243 | prefix = "" if fully_qualified or full_name == class_name else "~" 244 | role = "data" if (module, class_name) in _PYDATA_ANNOTATIONS else "class" 245 | args_format = "\\[{}]" 246 | formatted_args: str | None = "" 247 | 248 | always_use_bars_union: bool = getattr(config, "always_use_bars_union", True) 249 | is_bars_union = full_name == "types.UnionType" or ( 250 | always_use_bars_union and type(annotation).__qualname__ == "_UnionGenericAlias" 251 | ) 252 | if is_bars_union: 253 | full_name = "" 254 | 255 | # Some types require special handling 256 | if full_name == "typing.NewType": 257 | args_format = f"\\(``{annotation.__name__}``, {{}})" 258 | role = "class" 259 | elif full_name in {"typing.TypeVar", "typing.ParamSpec"}: 260 | params = {k: getattr(annotation, f"__{k}__") for k in ("bound", "covariant", "contravariant")} 261 | params = {k: v for k, v in params.items() if v} 262 | if "bound" in params: 263 | params["bound"] = f" {format_annotation(params['bound'], config, short_literals=short_literals)}" 264 | args_format = f"\\(``{annotation.__name__}``{', {}' if args else ''}" 265 | if params: 266 | args_format += "".join(f", {k}={v}" for k, v in params.items()) 267 | args_format += ")" 268 | formatted_args = None if args else args_format 269 | elif full_name == "typing.Optional": 270 | args = tuple(x for x in args if x is not type(None)) 271 | elif full_name in {"typing.Union", "types.UnionType"} and type(None) in args: 272 | if len(args) == 2: # noqa: PLR2004 273 | full_name = "typing.Optional" 274 | role = "data" 275 | args = tuple(x for x in args if x is not type(None)) 276 | else: 277 | simplify_optional_unions: bool = getattr(config, "simplify_optional_unions", True) 278 | if not simplify_optional_unions: 279 | full_name = "typing.Optional" 280 | role = "data" 281 | args_format = f"\\[:py:data:`{prefix}typing.Union`\\[{{}}]]" 282 | args = tuple(x for x in args if x is not type(None)) 283 | elif full_name in {"typing.Callable", "collections.abc.Callable"} and args and args[0] is not ...: 284 | fmt = [format_annotation(arg, config, short_literals=short_literals) for arg in args] 285 | formatted_args = f"\\[\\[{', '.join(fmt[:-1])}], {fmt[-1]}]" 286 | elif full_name == "typing.Literal": 287 | if short_literals: 288 | return f"\\{' | '.join(f'``{arg!r}``' for arg in args)}" 289 | formatted_args = f"\\[{', '.join(f'``{arg!r}``' for arg in args)}]" 290 | elif is_bars_union: 291 | return " | ".join([format_annotation(arg, config, short_literals=short_literals) for arg in args]) 292 | 293 | if args and not formatted_args: 294 | try: 295 | iter(args) 296 | except TypeError: 297 | fmt = [format_annotation(args, config, short_literals=short_literals)] 298 | else: 299 | fmt = [format_annotation(arg, config, short_literals=short_literals) for arg in args] 300 | formatted_args = args_format.format(", ".join(fmt)) 301 | 302 | escape = "\\ " if formatted_args else "" 303 | return f":py:{role}:`{prefix}{full_name}`{escape}{formatted_args}" 304 | 305 | 306 | # reference: https://github.com/pytorch/pytorch/pull/46548/files 307 | def normalize_source_lines(source_lines: str) -> str: 308 | """ 309 | Normalize the source lines. 310 | 311 | It finds the indentation level of the function definition (`def`), then it indents all lines in the function body to 312 | a point at or greater than that level. This allows for comments and continued string literals that are at a lower 313 | indentation than the rest of the code. 314 | 315 | :param source_lines: source code 316 | :return: source lines that have been correctly aligned 317 | """ 318 | lines = source_lines.split("\n") 319 | 320 | def remove_prefix(text: str, prefix: str) -> str: 321 | return text[text.startswith(prefix) and len(prefix) :] 322 | 323 | # Find the line and line number containing the function definition 324 | for pos, line in enumerate(lines): 325 | if line.lstrip().startswith("def "): 326 | idx = pos 327 | whitespace_separator = "def" 328 | break 329 | if line.lstrip().startswith("async def"): 330 | idx = pos 331 | whitespace_separator = "async def" 332 | break 333 | 334 | else: 335 | return "\n".join(lines) 336 | fn_def = lines[idx] 337 | 338 | # Get a string representing the amount of leading whitespace 339 | whitespace = fn_def.split(whitespace_separator)[0] 340 | 341 | # Add this leading whitespace to all lines before and after the `def` 342 | aligned_prefix = [whitespace + remove_prefix(s, whitespace) for s in lines[:idx]] 343 | aligned_suffix = [whitespace + remove_prefix(s, whitespace) for s in lines[idx + 1 :]] 344 | 345 | # Put it together again 346 | aligned_prefix.append(fn_def) 347 | return "\n".join(aligned_prefix + aligned_suffix) 348 | 349 | 350 | def process_signature( # noqa: C901, PLR0913, PLR0917 351 | app: Sphinx, 352 | what: str, 353 | name: str, 354 | obj: Any, 355 | options: Options, # noqa: ARG001 356 | signature: str, # noqa: ARG001 357 | return_annotation: str, # noqa: ARG001 358 | ) -> tuple[str, None] | None: 359 | """ 360 | Process the signature. 361 | 362 | :param app: 363 | :param what: 364 | :param name: 365 | :param obj: 366 | :param options: 367 | :param signature: 368 | :param return_annotation: 369 | :return: 370 | """ 371 | if not callable(obj): 372 | return None 373 | 374 | original_obj = obj 375 | obj = getattr(obj, "__init__", getattr(obj, "__new__", None)) if inspect.isclass(obj) else obj 376 | if not getattr(obj, "__annotations__", None): # when has no annotation we cannot autodoc typehints so bail 377 | return None 378 | 379 | obj = inspect.unwrap(obj) 380 | sph_signature = sphinx_signature(obj, type_aliases=app.config["autodoc_type_aliases"]) 381 | typehints_formatter: Callable[..., str | None] | None = getattr(app.config, "typehints_formatter", None) 382 | 383 | def _get_formatted_annotation(annotation: TypeVar) -> TypeVar: 384 | if typehints_formatter is None: 385 | return annotation 386 | formatted_name = typehints_formatter(annotation) 387 | return annotation if not isinstance(formatted_name, str) else TypeVar(formatted_name) 388 | 389 | if app.config.typehints_use_signature_return: 390 | sph_signature = sph_signature.replace( 391 | return_annotation=_get_formatted_annotation(sph_signature.return_annotation) 392 | ) 393 | 394 | if app.config.typehints_use_signature: 395 | parameters = [ 396 | param.replace(annotation=_get_formatted_annotation(param.annotation)) 397 | for param in sph_signature.parameters.values() 398 | ] 399 | else: 400 | parameters = [param.replace(annotation=inspect.Parameter.empty) for param in sph_signature.parameters.values()] 401 | 402 | # if we have parameters we may need to delete first argument that's not documented, e.g. self 403 | start = 0 404 | if parameters: 405 | if inspect.isclass(original_obj) or (what == "method" and name.endswith(".__init__")): 406 | start = 1 407 | elif what == "method": 408 | # bail if it is a local method as we cannot determine if first argument needs to be deleted or not 409 | if "" in obj.__qualname__ and not _is_dataclass(name, what, obj.__qualname__): 410 | _LOGGER.warning('Cannot handle as a local function: "%s" (use @functools.wraps)', name) 411 | return None 412 | outer = inspect.getmodule(obj) 413 | for class_name in obj.__qualname__.split(".")[:-1]: 414 | outer = getattr(outer, class_name) 415 | method_name = obj.__name__ 416 | if method_name.startswith("__") and not method_name.endswith("__"): 417 | # when method starts with double underscore Python applies mangling -> prepend the class name 418 | method_name = f"_{obj.__qualname__.split('.')[-2]}{method_name}" 419 | method_object = outer.__dict__[method_name] if outer else obj 420 | if not isinstance(method_object, classmethod | staticmethod): 421 | start = 1 422 | 423 | sph_signature = sph_signature.replace(parameters=parameters[start:]) 424 | show_return_annotation = app.config.typehints_use_signature_return 425 | unqualified_typehints = not getattr(app.config, "typehints_fully_qualified", False) 426 | return ( 427 | stringify_signature( 428 | sph_signature, 429 | show_return_annotation=show_return_annotation, 430 | unqualified_typehints=unqualified_typehints, 431 | ).replace("\\", "\\\\"), 432 | None, 433 | ) 434 | 435 | 436 | def _is_dataclass(name: str, what: str, qualname: str) -> bool: 437 | # generated dataclass __init__() and class need extra checks, as the function operates on the generated class 438 | # and methods (not an instantiated dataclass object) it cannot be replaced by a call to 439 | # `dataclasses.is_dataclass()` => check manually for either generated __init__ or generated class 440 | return (what == "method" and name.endswith(".__init__")) or (what == "class" and qualname.endswith(".__init__")) 441 | 442 | 443 | def _future_annotations_imported(obj: Any) -> bool: 444 | annotations_ = getattr(inspect.getmodule(obj), "annotations", None) 445 | if annotations_ is None: 446 | return False 447 | 448 | # Make sure that annotations is imported from __future__ - defined in cpython/Lib/__future__.py 449 | # annotations become strings at runtime 450 | return bool(annotations_.compiler_flag == 0x1000000) # pragma: no cover # noqa: PLR2004 451 | 452 | 453 | def get_all_type_hints( 454 | autodoc_mock_imports: list[str], obj: Any, name: str, localns: dict[Any, MyTypeAliasForwardRef] 455 | ) -> dict[str, Any]: 456 | result = _get_type_hint(autodoc_mock_imports, name, obj, localns) 457 | if not result: 458 | result = backfill_type_hints(obj, name) 459 | try: 460 | obj.__annotations__ = result 461 | except (AttributeError, TypeError): 462 | pass 463 | else: 464 | result = _get_type_hint(autodoc_mock_imports, name, obj, localns) 465 | return result 466 | 467 | 468 | _TYPE_GUARD_IMPORT_RE = re.compile(r"\nif (typing.)?TYPE_CHECKING:[^\n]*([\s\S]*?)(?=\n\S)") 469 | _TYPE_GUARD_IMPORTS_RESOLVED = set() 470 | _TYPE_GUARD_IMPORTS_RESOLVED_GLOBALS_ID = set() 471 | 472 | 473 | def _should_skip_guarded_import_resolution(obj: Any) -> bool: 474 | if isinstance(obj, types.ModuleType): 475 | return False # Don't skip modules 476 | 477 | if not hasattr(obj, "__globals__"): 478 | return True # Skip objects without __globals__ 479 | 480 | if hasattr(obj, "__module__"): 481 | return obj.__module__ in _TYPE_GUARD_IMPORTS_RESOLVED or obj.__module__ in sys.builtin_module_names 482 | 483 | return id(obj.__globals__) in _TYPE_GUARD_IMPORTS_RESOLVED_GLOBALS_ID 484 | 485 | 486 | def _execute_guarded_code(autodoc_mock_imports: list[str], obj: Any, module_code: str) -> None: 487 | for _, part in _TYPE_GUARD_IMPORT_RE.findall(module_code): 488 | guarded_code = textwrap.dedent(part) 489 | try: 490 | try: 491 | with mock(autodoc_mock_imports): 492 | exec(guarded_code, getattr(obj, "__globals__", obj.__dict__)) # noqa: S102 493 | except ImportError as exc: 494 | # ImportError might have occurred because the module has guarded code as well, 495 | # so we recurse on the module. 496 | if exc.name: 497 | _resolve_type_guarded_imports(autodoc_mock_imports, importlib.import_module(exc.name)) 498 | 499 | # Retry the guarded code and see if it works now after resolving all nested type guards. 500 | with mock(autodoc_mock_imports): 501 | exec(guarded_code, getattr(obj, "__globals__", obj.__dict__)) # noqa: S102 502 | except Exception as exc: # noqa: BLE001 503 | _LOGGER.warning("Failed guarded type import with %r", exc) 504 | 505 | 506 | def _resolve_type_guarded_imports(autodoc_mock_imports: list[str], obj: Any) -> None: 507 | if _should_skip_guarded_import_resolution(obj): 508 | return 509 | 510 | if hasattr(obj, "__globals__"): 511 | _TYPE_GUARD_IMPORTS_RESOLVED_GLOBALS_ID.add(id(obj.__globals__)) 512 | 513 | module = inspect.getmodule(obj) 514 | 515 | if module: 516 | try: 517 | module_code = inspect.getsource(module) 518 | except (TypeError, OSError): 519 | ... # no source code => no type guards 520 | else: 521 | _TYPE_GUARD_IMPORTS_RESOLVED.add(module.__name__) 522 | _execute_guarded_code(autodoc_mock_imports, obj, module_code) 523 | 524 | 525 | def _get_type_hint( 526 | autodoc_mock_imports: list[str], name: str, obj: Any, localns: dict[Any, MyTypeAliasForwardRef] 527 | ) -> dict[str, Any]: 528 | _resolve_type_guarded_imports(autodoc_mock_imports, obj) 529 | try: 530 | result = get_type_hints(obj, None, localns) 531 | except (AttributeError, TypeError, RecursionError) as exc: 532 | # TypeError - slot wrapper, PEP-563 when part of new syntax not supported 533 | # RecursionError - some recursive type definitions https://github.com/python/typing/issues/574 534 | if isinstance(exc, TypeError) and _future_annotations_imported(obj) and "unsupported operand type" in str(exc): 535 | result = obj.__annotations__ 536 | else: 537 | result = {} 538 | except NameError as exc: 539 | _LOGGER.warning('Cannot resolve forward reference in type annotations of "%s": %s', name, exc) 540 | result = obj.__annotations__ 541 | return result 542 | 543 | 544 | def backfill_type_hints(obj: Any, name: str) -> dict[str, Any]: # noqa: C901, PLR0911 545 | """ 546 | Backfill type hints. 547 | 548 | :param obj: the object 549 | :param name: the name 550 | :return: backfilled value 551 | """ 552 | parse_kwargs = {"type_comments": True} 553 | 554 | def _one_child(module: Module) -> stmt | None: 555 | children = module.body # use the body to ignore type comments 556 | if len(children) != 1: 557 | _LOGGER.warning('Did not get exactly one node from AST for "%s", got %s', name, len(children)) 558 | return None 559 | return children[0] 560 | 561 | try: 562 | code = textwrap.dedent(normalize_source_lines(inspect.getsource(obj))) 563 | obj_ast = ast.parse(code, **parse_kwargs) # type: ignore[call-overload] # dynamic kwargs 564 | except (OSError, TypeError, SyntaxError): 565 | return {} 566 | 567 | obj_ast = _one_child(obj_ast) 568 | if obj_ast is None: 569 | return {} 570 | 571 | try: 572 | type_comment = obj_ast.type_comment 573 | except AttributeError: 574 | return {} 575 | 576 | if not type_comment: 577 | return {} 578 | 579 | try: 580 | comment_args_str, comment_returns = type_comment.split(" -> ") 581 | except ValueError: 582 | _LOGGER.warning('Unparseable type hint comment for "%s": Expected to contain ` -> `', name) 583 | return {} 584 | 585 | rv = {} 586 | if comment_returns: 587 | rv["return"] = comment_returns 588 | 589 | args = load_args(obj_ast) 590 | comment_args = split_type_comment_args(comment_args_str) 591 | is_inline = len(comment_args) == 1 and comment_args[0] == "..." 592 | if not is_inline: 593 | if args and args[0].arg in {"self", "cls"} and len(comment_args) != len(args): 594 | comment_args.insert(0, None) # self/cls may be omitted in type comments, insert blank 595 | 596 | if len(args) != len(comment_args): 597 | _LOGGER.warning('Not enough type comments found on "%s"', name) 598 | return rv 599 | 600 | for at, arg in enumerate(args): 601 | arg_key = getattr(arg, "arg", None) 602 | if arg_key is None: 603 | continue 604 | 605 | value = getattr(arg, "type_comment", None) if is_inline else comment_args[at] 606 | 607 | if value is not None: 608 | rv[arg_key] = value 609 | 610 | return rv 611 | 612 | 613 | def load_args(obj_ast: FunctionDef) -> list[Any]: 614 | func_args = obj_ast.args 615 | args = [] 616 | pos_only = getattr(func_args, "posonlyargs", None) 617 | if pos_only: 618 | args.extend(pos_only) 619 | 620 | args.extend(func_args.args) 621 | if func_args.vararg: 622 | args.append(func_args.vararg) 623 | 624 | args.extend(func_args.kwonlyargs) 625 | if func_args.kwarg: 626 | args.append(func_args.kwarg) 627 | 628 | return args 629 | 630 | 631 | def split_type_comment_args(comment: str) -> list[str | None]: 632 | def add(val: str) -> None: 633 | result.append(val.strip().lstrip("*")) # remove spaces, and var/kw arg marker 634 | 635 | comment = comment.strip().lstrip("(").rstrip(")") 636 | result: list[str | None] = [] 637 | if not comment: 638 | return result 639 | 640 | brackets, start_arg_at, at = 0, 0, 0 641 | for at, char in enumerate(comment): 642 | if char in {"[", "("}: 643 | brackets += 1 644 | elif char in {"]", ")"}: 645 | brackets -= 1 646 | elif char == "," and brackets == 0: 647 | add(comment[start_arg_at:at]) 648 | start_arg_at = at + 1 649 | 650 | add(comment[start_arg_at : at + 1]) 651 | return result 652 | 653 | 654 | def format_default(app: Sphinx, default: Any, is_annotated: bool) -> str | None: # noqa: FBT001 655 | if default is inspect.Parameter.empty: 656 | return None 657 | formatted = repr(default).replace("\\", "\\\\") 658 | 659 | if is_annotated: 660 | if app.config.typehints_defaults.startswith("braces"): 661 | return f" (default: ``{formatted}``)" 662 | return f", default: ``{formatted}``" 663 | if app.config.typehints_defaults == "braces-after": 664 | return f" (default: ``{formatted}``)" 665 | return f"default: ``{formatted}``" 666 | 667 | 668 | def process_docstring( # noqa: PLR0913, PLR0917 669 | app: Sphinx, 670 | what: str, 671 | name: str, 672 | obj: Any, 673 | options: Options | None, # noqa: ARG001 674 | lines: list[str], 675 | ) -> None: 676 | """ 677 | Process the docstring for an entry. 678 | 679 | :param app: the Sphinx app 680 | :param what: the target 681 | :param name: the name 682 | :param obj: the object 683 | :param options: the options 684 | :param lines: the lines 685 | :return: 686 | """ 687 | original_obj = obj 688 | obj = obj.fget if isinstance(obj, property) else obj 689 | if not callable(obj): 690 | return 691 | obj = obj.__init__ if inspect.isclass(obj) else obj 692 | obj = inspect.unwrap(obj) 693 | 694 | try: 695 | signature = sphinx_signature(obj, type_aliases=app.config["autodoc_type_aliases"]) 696 | except (ValueError, TypeError): 697 | signature = None 698 | 699 | localns = {key: MyTypeAliasForwardRef(value) for key, value in app.config["autodoc_type_aliases"].items()} 700 | type_hints = get_all_type_hints(app.config.autodoc_mock_imports, obj, name, localns) 701 | app.config._annotation_globals = getattr(obj, "__globals__", {}) # noqa: SLF001 702 | try: 703 | _inject_types_to_docstring(type_hints, signature, original_obj, app, what, name, lines) 704 | finally: 705 | delattr(app.config, "_annotation_globals") 706 | 707 | 708 | def _get_sphinx_line_keyword_and_argument(line: str) -> tuple[str, str | None] | None: 709 | """ 710 | Extract a keyword, and its optional argument out of a sphinx field option line. 711 | 712 | For example 713 | >>> _get_sphinx_line_keyword_and_argument(":param parameter:") 714 | ("param", "parameter") 715 | >>> _get_sphinx_line_keyword_and_argument(":return:") 716 | ("return", None) 717 | >>> _get_sphinx_line_keyword_and_argument("some invalid line") 718 | None 719 | """ 720 | param_line_without_description = line.split(":", maxsplit=2) 721 | if len(param_line_without_description) != 3: # noqa: PLR2004 722 | return None 723 | 724 | split_directive_and_name = param_line_without_description[1].split(maxsplit=1) 725 | if len(split_directive_and_name) != 2: # noqa: PLR2004 726 | if not len(split_directive_and_name): 727 | return None 728 | return split_directive_and_name[0], None 729 | 730 | return tuple(split_directive_and_name) # type: ignore[return-value] 731 | 732 | 733 | def _line_is_param_line_for_arg(line: str, arg_name: str) -> bool: 734 | """Return True if `line` is a valid parameter line for `arg_name`, false otherwise.""" 735 | keyword_and_name = _get_sphinx_line_keyword_and_argument(line) 736 | if keyword_and_name is None: 737 | return False 738 | 739 | keyword, doc_name = keyword_and_name 740 | if doc_name is None: 741 | return False 742 | 743 | if keyword not in {"param", "parameter", "arg", "argument"}: 744 | return False 745 | 746 | return any(doc_name == prefix + arg_name for prefix in ("", "\\*", "\\**", "\\*\\*")) 747 | 748 | 749 | def _inject_types_to_docstring( # noqa: PLR0913, PLR0917 750 | type_hints: dict[str, Any], 751 | signature: inspect.Signature | None, 752 | original_obj: Any, 753 | app: Sphinx, 754 | what: str, 755 | name: str, 756 | lines: list[str], 757 | ) -> None: 758 | if signature is not None: 759 | _inject_signature(type_hints, signature, app, lines) 760 | if "return" in type_hints: 761 | _inject_rtype(type_hints, original_obj, app, what, name, lines) 762 | 763 | 764 | def _inject_signature( 765 | type_hints: dict[str, Any], 766 | signature: inspect.Signature, 767 | app: Sphinx, 768 | lines: list[str], 769 | ) -> None: 770 | for arg_name in signature.parameters: 771 | annotation = type_hints.get(arg_name) 772 | 773 | default = signature.parameters[arg_name].default 774 | 775 | if arg_name.endswith("_"): 776 | arg_name = f"{arg_name[:-1]}\\_" # noqa: PLW2901 777 | 778 | insert_index = None 779 | for at, line in enumerate(lines): 780 | if _line_is_param_line_for_arg(line, arg_name): 781 | # Get the arg_name from the doc to match up for type in case it has a star prefix. 782 | # Line is in the correct format so this is guaranteed to return tuple[str, str]. 783 | func = _get_sphinx_line_keyword_and_argument 784 | _, arg_name = func(line) # type: ignore[assignment, misc] # noqa: PLW2901 785 | insert_index = at 786 | break 787 | 788 | if annotation is not None and insert_index is None and app.config.always_document_param_types: 789 | lines.append(f":param {arg_name}:") 790 | insert_index = len(lines) 791 | 792 | if insert_index is not None: 793 | if annotation is None: 794 | type_annotation = f":type {arg_name}: " 795 | else: 796 | short_literals = app.config.python_display_short_literal_types 797 | formatted_annotation = add_type_css_class( 798 | format_annotation(annotation, app.config, short_literals=short_literals) 799 | ) 800 | type_annotation = f":type {arg_name}: {formatted_annotation}" 801 | 802 | if app.config.typehints_defaults: 803 | formatted_default = format_default(app, default, annotation is not None) 804 | if formatted_default: 805 | type_annotation = _append_default(app, lines, insert_index, type_annotation, formatted_default) 806 | 807 | lines.insert(insert_index, type_annotation) 808 | 809 | 810 | def _append_default( 811 | app: Sphinx, lines: list[str], insert_index: int, type_annotation: str, formatted_default: str 812 | ) -> str: 813 | if app.config.typehints_defaults.endswith("after"): 814 | # advance the index to the end of the :param: paragraphs 815 | # (terminated by a line with no indentation) 816 | # append default to the last nonempty line 817 | nlines = len(lines) 818 | next_index = insert_index + 1 819 | append_index = insert_index # last nonempty line 820 | while next_index < nlines and (not lines[next_index] or lines[next_index].startswith(" ")): 821 | if lines[next_index]: 822 | append_index = next_index 823 | next_index += 1 824 | lines[append_index] += formatted_default 825 | 826 | else: # add to last param doc line 827 | type_annotation += formatted_default 828 | 829 | return type_annotation 830 | 831 | 832 | @dataclass 833 | class InsertIndexInfo: 834 | insert_index: int 835 | found_param: bool = False 836 | found_return: bool = False 837 | found_directive: bool = False 838 | 839 | 840 | # Sphinx allows so many synonyms... 841 | # See sphinx.domains.python.PyObject 842 | PARAM_SYNONYMS = ("param ", "parameter ", "arg ", "argument ", "keyword ", "kwarg ", "kwparam ") 843 | 844 | 845 | def node_line_no(node: Node) -> int | None: 846 | """ 847 | Get the 1-indexed line on which the node starts if possible. If not, return None. 848 | 849 | Descend through the first children until we locate one with a line number or return None if None of them have one. 850 | 851 | I'm not aware of any rst on which this returns None, to find out would require a more detailed analysis of the 852 | docutils rst parser source code. An example where the node doesn't have a line number but the first child does is 853 | all `definition_list` nodes. It seems like bullet_list and option_list get line numbers, but enum_list also doesn't. 854 | """ 855 | if node is None: 856 | return None 857 | 858 | while node.line is None and node.children: 859 | node = node.children[0] 860 | return node.line 861 | 862 | 863 | def tag_name(node: Node) -> str: 864 | return node.tagname # type:ignore[attr-defined,no-any-return] 865 | 866 | 867 | def get_insert_index(app: Sphinx, lines: list[str]) -> InsertIndexInfo | None: 868 | # 1. If there is an existing :rtype: anywhere, don't insert anything. 869 | if any(line.startswith(":rtype:") for line in lines): 870 | return None 871 | 872 | # 2. If there is a :returns: anywhere, either modify that line or insert 873 | # just before it. 874 | for at, line in enumerate(lines): 875 | if line.startswith((":return:", ":returns:")): 876 | return InsertIndexInfo(insert_index=at, found_return=True) 877 | 878 | # 3. Insert after the parameters. 879 | # To find the parameters, parse as a docutils tree. 880 | settings = get_default_settings(RSTParser) 881 | settings.env = app.env 882 | doc = parse("\n".join(lines), settings) 883 | 884 | # Find a top level child which is a field_list that contains a field whose 885 | # name starts with one of the PARAM_SYNONYMS. This is the parameter list. We 886 | # hope there is at most of these. 887 | for child in doc.children: 888 | if tag_name(child) != "field_list": 889 | continue 890 | 891 | if not any(c.children[0].astext().startswith(PARAM_SYNONYMS) for c in child.children): 892 | continue 893 | 894 | # Found it! Try to insert before the next sibling. If there is no next 895 | # sibling, insert at end. 896 | # If there is a next sibling but we can't locate a line number, insert 897 | # at end. (I don't know of any input where this happens.) 898 | next_sibling = child.next_node(descend=False, siblings=True) 899 | line_no = node_line_no(next_sibling) if next_sibling else None 900 | at = max(line_no - 2, 0) if line_no else len(lines) 901 | return InsertIndexInfo(insert_index=at, found_param=True) 902 | 903 | # 4. Insert before examples 904 | for child in doc.children: 905 | if tag_name(child) in {"literal_block", "paragraph", "field_list"}: 906 | continue 907 | line_no = node_line_no(child) 908 | at = max(line_no - 2, 0) if line_no else len(lines) 909 | if lines[at - 1]: # skip if something on this line 910 | break 911 | return InsertIndexInfo(insert_index=at, found_directive=True) 912 | 913 | # 5. Otherwise, insert at end 914 | return InsertIndexInfo(insert_index=len(lines)) 915 | 916 | 917 | def _inject_rtype( # noqa: C901, PLR0913, PLR0917 918 | type_hints: dict[str, Any], 919 | original_obj: Any, 920 | app: Sphinx, 921 | what: str, 922 | name: str, 923 | lines: list[str], 924 | ) -> None: 925 | if inspect.isclass(original_obj) or inspect.isdatadescriptor(original_obj): 926 | return 927 | if what == "method" and name.endswith(".__init__"): # avoid adding a return type for data class __init__ 928 | return 929 | if not app.config.typehints_document_rtype: 930 | return 931 | if not app.config.typehints_document_rtype_none and type_hints["return"] is types.NoneType: 932 | return 933 | 934 | r = get_insert_index(app, lines) 935 | if r is None: 936 | return 937 | 938 | insert_index = r.insert_index 939 | 940 | if not app.config.typehints_use_rtype and r.found_return and " -- " in lines[insert_index]: 941 | return 942 | 943 | short_literals = app.config.python_display_short_literal_types 944 | formatted_annotation = add_type_css_class( 945 | format_annotation(type_hints["return"], app.config, short_literals=short_literals) 946 | ) 947 | 948 | if r.found_param and insert_index < len(lines) and lines[insert_index].strip(): 949 | insert_index -= 1 950 | 951 | if insert_index == len(lines) and not r.found_param: 952 | # ensure that :rtype: doesn't get joined with a paragraph of text 953 | lines.append("") 954 | insert_index += 1 955 | if app.config.typehints_use_rtype or not r.found_return: 956 | line = f":rtype: {formatted_annotation}" 957 | lines.insert(insert_index, line) 958 | if r.found_directive: 959 | lines.insert(insert_index + 1, "") 960 | else: 961 | line = lines[insert_index] 962 | lines[insert_index] = f":return: {formatted_annotation} --{line[line.find(' ') :]}" 963 | 964 | 965 | def validate_config(app: Sphinx, env: BuildEnvironment, docnames: list[str]) -> None: # noqa: ARG001 966 | valid = {None, "comma", "braces", "braces-after"} 967 | if app.config.typehints_defaults not in valid | {False}: 968 | msg = f"typehints_defaults needs to be one of {valid!r}, not {app.config.typehints_defaults!r}" 969 | raise ValueError(msg) 970 | 971 | formatter = app.config.typehints_formatter 972 | if formatter is not None and not callable(formatter): 973 | msg = f"typehints_formatter needs to be callable or `None`, not {formatter}" 974 | raise ValueError(msg) 975 | 976 | 977 | def unescape(escaped: str) -> str: 978 | # For some reason the string we get has a bunch of null bytes in it?? 979 | # Remove them... 980 | escaped = escaped.replace("\x00", "") 981 | # For some reason the extra slash before spaces gets lost between the .rst 982 | # source and when this directive is called. So don't replace "\" => 983 | # "" 984 | return re.sub(r"\\([^ ])", r"\1", escaped) 985 | 986 | 987 | def add_type_css_class(type_rst: str) -> str: 988 | return f":sphinx_autodoc_typehints_type:`{rst.escape(type_rst)}`" 989 | 990 | 991 | def sphinx_autodoc_typehints_type_role( 992 | _role: str, 993 | _rawtext: str, 994 | text: str, 995 | _lineno: int, 996 | inliner: states.Inliner, 997 | _options: dict[str, Any] | None = None, 998 | _content: list[str] | None = None, 999 | ) -> tuple[list[Node], list[Node]]: 1000 | """ 1001 | Add css tag around rendered type. 1002 | 1003 | The body should be escaped rst. This renders its body as rst and wraps the 1004 | result in 1005 | """ 1006 | unescaped = unescape(text) 1007 | doc = parse(unescaped, inliner.document.settings) 1008 | n = nodes.inline(text) 1009 | n["classes"].append("sphinx_autodoc_typehints-type") 1010 | n += doc.children[0].children 1011 | return [n], [] 1012 | 1013 | 1014 | def setup(app: Sphinx) -> dict[str, bool]: 1015 | app.add_config_value("always_document_param_types", False, "html") # noqa: FBT003 1016 | app.add_config_value("typehints_fully_qualified", False, "env") # noqa: FBT003 1017 | app.add_config_value("typehints_document_rtype", True, "env") # noqa: FBT003 1018 | app.add_config_value("typehints_document_rtype_none", True, "env") # noqa: FBT003 1019 | app.add_config_value("typehints_use_rtype", True, "env") # noqa: FBT003 1020 | app.add_config_value("typehints_defaults", None, "env") 1021 | app.add_config_value("simplify_optional_unions", True, "env") # noqa: FBT003 1022 | app.add_config_value("always_use_bars_union", False, "env") # noqa: FBT003 1023 | app.add_config_value("typehints_formatter", None, "env") 1024 | app.add_config_value("typehints_use_signature", False, "env") # noqa: FBT003 1025 | app.add_config_value("typehints_use_signature_return", False, "env") # noqa: FBT003 1026 | app.add_config_value("typehints_fixup_module_name", None, "env") 1027 | app.add_role("sphinx_autodoc_typehints_type", sphinx_autodoc_typehints_type_role) 1028 | app.connect("env-before-read-docs", validate_config) # config may be changed after “config-inited” event 1029 | app.connect("autodoc-process-signature", process_signature) 1030 | app.connect("autodoc-process-docstring", process_docstring) 1031 | install_patches(app) 1032 | return {"parallel_read_safe": True, "parallel_write_safe": True} 1033 | 1034 | 1035 | __all__ = [ 1036 | "__version__", 1037 | "backfill_type_hints", 1038 | "format_annotation", 1039 | "get_annotation_args", 1040 | "get_annotation_class_name", 1041 | "get_annotation_module", 1042 | "normalize_source_lines", 1043 | "process_docstring", 1044 | "process_signature", 1045 | ] 1046 | -------------------------------------------------------------------------------- /src/sphinx_autodoc_typehints/_parser.py: -------------------------------------------------------------------------------- 1 | """Utilities for side-effect-free rST parsing.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import TYPE_CHECKING 6 | 7 | from docutils.utils import new_document 8 | from sphinx.parsers import RSTParser 9 | from sphinx.util.docutils import sphinx_domains 10 | 11 | if TYPE_CHECKING: 12 | import optparse 13 | 14 | from docutils import nodes 15 | from docutils.frontend import Values 16 | from docutils.statemachine import StringList 17 | 18 | 19 | class _RstSnippetParser(RSTParser): 20 | @staticmethod 21 | def decorate(_content: StringList) -> None: 22 | """Override to skip processing rst_epilog/rst_prolog for typing.""" 23 | 24 | 25 | def parse(inputstr: str, settings: Values | optparse.Values) -> nodes.document: 26 | """Parse inputstr and return a docutils document.""" 27 | doc = new_document("", settings=settings) 28 | with sphinx_domains(settings.env): 29 | parser = _RstSnippetParser() 30 | parser.set_application(settings.env.app) 31 | parser.parse(inputstr, doc) 32 | return doc 33 | -------------------------------------------------------------------------------- /src/sphinx_autodoc_typehints/attributes_patch.py: -------------------------------------------------------------------------------- 1 | """Patch for attributes.""" 2 | 3 | from __future__ import annotations 4 | 5 | from functools import partial 6 | from typing import TYPE_CHECKING, Any 7 | from unittest.mock import patch 8 | 9 | import sphinx.domains.python 10 | import sphinx.ext.autodoc 11 | from sphinx.domains.python import PyAttribute 12 | from sphinx.ext.autodoc import AttributeDocumenter 13 | 14 | from ._parser import parse 15 | 16 | if TYPE_CHECKING: 17 | from docutils.frontend import Values 18 | from sphinx.addnodes import desc_signature 19 | from sphinx.application import Sphinx 20 | 21 | # Defensively check for the things we want to patch 22 | _parse_annotation = getattr(sphinx.domains.python, "_parse_annotation", None) 23 | 24 | # We want to patch: 25 | # * sphinx.ext.autodoc.stringify_typehint (in sphinx < 6.1) 26 | # * sphinx.ext.autodoc.stringify_annotation (in sphinx >= 6.1) 27 | STRINGIFY_PATCH_TARGET = "" 28 | for target in ["stringify_typehint", "stringify_annotation"]: 29 | if hasattr(sphinx.ext.autodoc, target): 30 | STRINGIFY_PATCH_TARGET = f"sphinx.ext.autodoc.{target}" 31 | break 32 | 33 | # If we didn't locate both patch targets, we will just do nothing. 34 | OKAY_TO_PATCH = bool(_parse_annotation and STRINGIFY_PATCH_TARGET) 35 | 36 | # A label we inject to the type string so we know not to try to treat it as a 37 | # type annotation 38 | TYPE_IS_RST_LABEL = "--is-rst--" 39 | 40 | 41 | orig_add_directive_header = AttributeDocumenter.add_directive_header 42 | orig_handle_signature = PyAttribute.handle_signature 43 | 44 | 45 | def _stringify_annotation(app: Sphinx, annotation: Any, *args: Any, short_literals: bool = False, **kwargs: Any) -> str: # noqa: ARG001 46 | # Format the annotation with sphinx-autodoc-typehints and inject our magic prefix to tell our patched 47 | # PyAttribute.handle_signature to treat it as rst. 48 | from . import format_annotation # noqa: PLC0415 49 | 50 | return TYPE_IS_RST_LABEL + format_annotation(annotation, app.config, short_literals=short_literals) 51 | 52 | 53 | def patch_attribute_documenter(app: Sphinx) -> None: 54 | """Instead of using stringify_typehint in `AttributeDocumenter.add_directive_header`, use `format_annotation`.""" 55 | 56 | def add_directive_header(*args: Any, **kwargs: Any) -> Any: 57 | with patch(STRINGIFY_PATCH_TARGET, partial(_stringify_annotation, app)): 58 | return orig_add_directive_header(*args, **kwargs) 59 | 60 | AttributeDocumenter.add_directive_header = add_directive_header # type:ignore[method-assign] 61 | 62 | 63 | def rst_to_docutils(settings: Values, rst: str) -> Any: 64 | """Convert rst to a sequence of docutils nodes.""" 65 | doc = parse(rst, settings) 66 | # Remove top level paragraph node so that there is no line break. 67 | return doc.children[0].children 68 | 69 | 70 | def patched_parse_annotation(settings: Values, typ: str, env: Any) -> Any: 71 | # if typ doesn't start with our label, use original function 72 | if not typ.startswith(TYPE_IS_RST_LABEL): 73 | assert _parse_annotation is not None # noqa: S101 74 | return _parse_annotation(typ, env) 75 | # Otherwise handle as rst 76 | typ = typ[len(TYPE_IS_RST_LABEL) :] 77 | return rst_to_docutils(settings, typ) 78 | 79 | 80 | def patched_handle_signature(self: PyAttribute, sig: str, signode: desc_signature) -> tuple[str, str]: 81 | target = "sphinx.domains.python._parse_annotation" 82 | new_func = partial(patched_parse_annotation, self.state.document.settings) 83 | with patch(target, new_func): 84 | return orig_handle_signature(self, sig, signode) 85 | 86 | 87 | def patch_attribute_handling(app: Sphinx) -> None: 88 | """Use format_signature to format class attribute type annotations.""" 89 | if not OKAY_TO_PATCH: 90 | return 91 | PyAttribute.handle_signature = patched_handle_signature # type:ignore[method-assign] 92 | patch_attribute_documenter(app) 93 | 94 | 95 | __all__ = ["patch_attribute_handling"] 96 | -------------------------------------------------------------------------------- /src/sphinx_autodoc_typehints/patches.py: -------------------------------------------------------------------------------- 1 | """Custom patches to make the world work.""" 2 | 3 | from __future__ import annotations 4 | 5 | from functools import lru_cache 6 | from typing import TYPE_CHECKING, Any 7 | 8 | from docutils import nodes 9 | from docutils.parsers.rst.directives.admonitions import BaseAdmonition 10 | from docutils.parsers.rst.states import Body, Text 11 | from sphinx.ext.napoleon.docstring import GoogleDocstring 12 | 13 | from .attributes_patch import patch_attribute_handling 14 | 15 | if TYPE_CHECKING: 16 | from sphinx.application import Sphinx 17 | from sphinx.ext.autodoc import Options 18 | 19 | 20 | @lru_cache # A cute way to make sure the function only runs once. 21 | def fix_autodoc_typehints_for_overloaded_methods() -> None: 22 | """ 23 | sphinx-autodoc-typehints responds to the "autodoc-process-signature" event to remove types from the signature line. 24 | 25 | Normally, `FunctionDocumenter.format_signature` and `MethodDocumenter.format_signature` call 26 | `super().format_signature` which ends up going to `Documenter.format_signature`, and this last method emits the 27 | `autodoc-process-signature` event. However, if there are overloads, `FunctionDocumenter.format_signature` does 28 | something else and the event never occurs. Here we remove this alternative code path by brute force. 29 | 30 | See https://github.com/tox-dev/sphinx-autodoc-typehints/issues/296 31 | """ 32 | from sphinx.ext.autodoc import FunctionDocumenter, MethodDocumenter # noqa: PLC0415 33 | 34 | del FunctionDocumenter.format_signature 35 | del MethodDocumenter.format_signature 36 | 37 | 38 | def napoleon_numpy_docstring_return_type_processor( # noqa: PLR0913, PLR0917 39 | app: Sphinx, 40 | what: str, 41 | name: str, # noqa: ARG001 42 | obj: Any, # noqa: ARG001 43 | options: Options | None, # noqa: ARG001 44 | lines: list[str], 45 | ) -> None: 46 | """Insert a : under Returns: to tell napoleon not to look for a return type.""" 47 | if what not in {"function", "method"}: 48 | return 49 | if not getattr(app.config, "napoleon_numpy_docstring", False): 50 | return 51 | 52 | # Search for the returns header: 53 | # Returns: 54 | # -------- 55 | for pos, line in enumerate(lines[:-2]): 56 | if line.lower().strip(":") not in {"return", "returns"}: 57 | continue 58 | # Underline detection. 59 | chars = set(lines[pos + 1].strip()) 60 | # Napoleon allows the underline to consist of a bunch of weirder things... 61 | if len(chars) != 1 or next(iter(chars)) not in "=-~_*+#": 62 | continue 63 | pos += 2 # noqa: PLW2901 64 | break 65 | else: 66 | return 67 | 68 | lines.insert(pos, ":") 69 | 70 | 71 | def fix_napoleon_numpy_docstring_return_type(app: Sphinx) -> None: 72 | """If no return type is explicitly set, numpy docstrings will use the return type text as return types.""" 73 | # standard priority is 500. Setting priority to 499 ensures this runs before 74 | # napoleon's docstring processor. 75 | app.connect("autodoc-process-docstring", napoleon_numpy_docstring_return_type_processor, priority=499) 76 | 77 | 78 | def _patched_lookup_annotation(*_args: Any) -> str: 79 | """ 80 | GoogleDocstring._lookup_annotation sometimes adds incorrect type annotations to constructor parameters. 81 | 82 | Disable it so we can handle this on our own. 83 | """ 84 | return "" 85 | 86 | 87 | def _patch_google_docstring_lookup_annotation() -> None: 88 | """Fix issue https://github.com/tox-dev/sphinx-autodoc-typehints/issues/308.""" 89 | GoogleDocstring._lookup_annotation = _patched_lookup_annotation # type: ignore[assignment] # noqa: SLF001 90 | 91 | 92 | orig_base_admonition_run = BaseAdmonition.run 93 | 94 | 95 | def _patched_base_admonition_run(self: BaseAdmonition) -> Any: 96 | result = orig_base_admonition_run(self) 97 | result[0].line = self.lineno 98 | return result 99 | 100 | 101 | orig_text_indent = Text.indent 102 | 103 | 104 | def _patched_text_indent(self: Text, *args: Any) -> Any: 105 | _, line = self.state_machine.get_source_and_line() 106 | result = orig_text_indent(self, *args) 107 | node = self.parent[-1] 108 | if node.tagname == "system_message": 109 | node = self.parent[-2] 110 | node.line = line 111 | return result 112 | 113 | 114 | def _patched_body_doctest( 115 | self: Body, _match: None, _context: None, next_state: str | None 116 | ) -> tuple[list[Any], str | None, list[Any]]: 117 | line = self.document.current_line + 1 118 | data = "\n".join(self.state_machine.get_text_block()) 119 | n = nodes.doctest_block(data, data) 120 | n.line = line 121 | self.parent += n 122 | return [], next_state, [] 123 | 124 | 125 | def _patch_line_numbers() -> None: 126 | """ 127 | Make the rst parser put line numbers on more nodes. 128 | 129 | When the line numbers are missing, we have a hard time placing the :rtype:. 130 | """ 131 | Text.indent = _patched_text_indent 132 | BaseAdmonition.run = _patched_base_admonition_run # type: ignore[method-assign,assignment] 133 | Body.doctest = _patched_body_doctest 134 | 135 | 136 | def install_patches(app: Sphinx) -> None: 137 | """ 138 | Install the patches. 139 | 140 | :param app: the Sphinx app 141 | """ 142 | fix_autodoc_typehints_for_overloaded_methods() 143 | patch_attribute_handling(app) 144 | _patch_google_docstring_lookup_annotation() 145 | fix_napoleon_numpy_docstring_return_type(app) 146 | _patch_line_numbers() 147 | 148 | 149 | ___all__ = [ 150 | "install_patches", 151 | ] 152 | -------------------------------------------------------------------------------- /src/sphinx_autodoc_typehints/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tox-dev/sphinx-autodoc-typehints/3c82b0daea54f61af483ef8011a69a3b72d74c17/src/sphinx_autodoc_typehints/py.typed -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | import shutil 5 | import sys 6 | from contextlib import suppress 7 | from pathlib import Path 8 | from typing import TYPE_CHECKING 9 | 10 | import pytest 11 | from sphobjinv import Inventory 12 | 13 | if TYPE_CHECKING: 14 | from _pytest.config import Config 15 | 16 | pytest_plugins = "sphinx.testing.fixtures" 17 | collect_ignore = ["roots"] 18 | 19 | 20 | @pytest.fixture(scope="session") 21 | def inv(pytestconfig: Config) -> Inventory: 22 | cache_path = f"python{sys.version_info.major}.{sys.version_info.minor}/objects.inv" 23 | assert pytestconfig.cache is not None 24 | inv_dict = pytestconfig.cache.get(cache_path, None) 25 | if inv_dict is not None: 26 | return Inventory(inv_dict) 27 | 28 | url = f"https://docs.python.org/{sys.version_info.major}.{sys.version_info.minor}/objects.inv" 29 | inv = Inventory(url=url) 30 | pytestconfig.cache.set(cache_path, inv.json_dict()) 31 | return inv 32 | 33 | 34 | @pytest.fixture(autouse=True) 35 | def _remove_sphinx_projects(sphinx_test_tempdir: Path) -> None: 36 | # Remove any directory which appears to be a Sphinx project from 37 | # the temporary directory area. 38 | # See https://github.com/sphinx-doc/sphinx/issues/4040 39 | for entry in sphinx_test_tempdir.iterdir(): 40 | with suppress(PermissionError): 41 | if entry.is_dir() and Path(entry, "_build").exists(): 42 | shutil.rmtree(str(entry)) 43 | 44 | 45 | @pytest.fixture 46 | def rootdir() -> Path: 47 | return Path(str(Path(__file__).parent) or ".").absolute() / "roots" 48 | 49 | 50 | def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None: # noqa: ARG001 51 | version_re = re.compile(r"_py(\d)(\d)\.py$") 52 | match = version_re.search(collection_path.name) 53 | if match: 54 | version = tuple(int(x) for x in match.groups()) 55 | if sys.version_info < version: 56 | return True 57 | return None 58 | -------------------------------------------------------------------------------- /tests/roots/test-dummy/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pathlib 4 | import sys 5 | 6 | # Make dummy_module.py available for autodoc. 7 | sys.path.insert(0, str(pathlib.Path(__file__).parent)) 8 | 9 | 10 | master_doc = "index" 11 | 12 | extensions = [ 13 | "sphinx.ext.autodoc", 14 | "sphinx.ext.napoleon", 15 | "sphinx_autodoc_typehints", 16 | ] 17 | -------------------------------------------------------------------------------- /tests/roots/test-dummy/dummy_module.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | 5 | 6 | def undocumented_function(x: int) -> str: 7 | """Hi""" 8 | 9 | return str(x) 10 | 11 | 12 | @dataclass 13 | class DataClass: 14 | """Class docstring.""" 15 | 16 | x: int 17 | -------------------------------------------------------------------------------- /tests/roots/test-dummy/dummy_module_future_annotations.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | def function_with_py310_annotations( 5 | self, # noqa: ANN001 6 | x: bool | None, 7 | y: int | str | float, # noqa: PYI041 8 | z: str | None = None, 9 | ) -> str: 10 | """ 11 | Method docstring. 12 | 13 | :param x: foo 14 | :param y: bar 15 | :param z: baz 16 | """ 17 | -------------------------------------------------------------------------------- /tests/roots/test-dummy/dummy_module_simple.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | def function(x: bool, y: int = 1) -> str: 5 | """ 6 | Function docstring. 7 | 8 | :param x: foo 9 | :param y: bar 10 | """ 11 | -------------------------------------------------------------------------------- /tests/roots/test-dummy/dummy_module_simple_default_role.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | def function(x: bool, y: int) -> str: 5 | """ 6 | Function docstring. 7 | 8 | :param x: `foo` 9 | :param y: ``bar`` 10 | """ 11 | -------------------------------------------------------------------------------- /tests/roots/test-dummy/dummy_module_simple_no_use_rtype.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | def function_no_returns(x: bool, y: int = 1) -> str: 5 | """ 6 | Function docstring. 7 | 8 | :param x: foo 9 | :param y: bar 10 | """ 11 | 12 | 13 | def function_returns_with_type(x: bool, y: int = 1) -> str: 14 | """ 15 | Function docstring. 16 | 17 | :param x: foo 18 | :param y: bar 19 | :returns: *CustomType* -- A string 20 | """ 21 | 22 | 23 | def function_returns_with_compound_type(x: bool, y: int = 1) -> str: 24 | """ 25 | Function docstring. 26 | 27 | :param x: foo 28 | :param y: bar 29 | :returns: Union[str, int] -- A string or int 30 | """ 31 | 32 | 33 | def function_returns_without_type(x: bool, y: int = 1) -> str: 34 | """ 35 | Function docstring. 36 | 37 | :param x: foo 38 | :param y: bar 39 | :returns: A string 40 | """ 41 | -------------------------------------------------------------------------------- /tests/roots/test-dummy/dummy_module_without_complete_typehints.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | def function_with_some_defaults_and_without_typehints(x, y=None): # noqa: ANN001, ANN201 5 | """ 6 | Function docstring. 7 | 8 | :param x: foo 9 | :param y: bar 10 | """ 11 | 12 | 13 | def function_with_some_defaults_and_some_typehints(x: int, y=None): # noqa: ANN001, ANN201 14 | """ 15 | Function docstring. 16 | 17 | :param x: foo 18 | :param y: bar 19 | """ 20 | 21 | 22 | def function_with_some_defaults_and_more_typehints(x: int, y=None) -> str: # noqa: ANN001 23 | """ 24 | Function docstring. 25 | 26 | :param x: foo 27 | :param y: bar 28 | """ 29 | 30 | 31 | def function_with_defaults_and_some_typehints(x: int = 0, y=None) -> str: # noqa: ANN001 32 | """ 33 | Function docstring. 34 | 35 | :param x: foo 36 | :param y: bar 37 | """ 38 | -------------------------------------------------------------------------------- /tests/roots/test-dummy/export_module.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from wrong_module_path import A, f 4 | 5 | __all__ = ["A", "f"] 6 | -------------------------------------------------------------------------------- /tests/roots/test-dummy/future_annotations.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | Dummy Module 4 | ============ 5 | 6 | .. autofunction:: dummy_module_future_annotations.function_with_py310_annotations 7 | -------------------------------------------------------------------------------- /tests/roots/test-dummy/simple.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | Simple Module 4 | ============= 5 | 6 | .. autofunction:: dummy_module_simple.function 7 | -------------------------------------------------------------------------------- /tests/roots/test-dummy/simple_default_role.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | Simple Module 4 | ============= 5 | 6 | .. autofunction:: dummy_module_simple_default_role.function 7 | -------------------------------------------------------------------------------- /tests/roots/test-dummy/simple_no_use_rtype.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | Simple Module 4 | ============= 5 | 6 | .. autofunction:: dummy_module_simple_no_use_rtype.function_no_returns 7 | .. autofunction:: dummy_module_simple_no_use_rtype.function_returns_with_type 8 | .. autofunction:: dummy_module_simple_no_use_rtype.function_returns_with_compound_type 9 | .. autofunction:: dummy_module_simple_no_use_rtype.function_returns_without_type 10 | -------------------------------------------------------------------------------- /tests/roots/test-dummy/without_complete_typehints.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | Simple Module 4 | ============= 5 | 6 | .. autofunction:: dummy_module_without_complete_typehints.function_with_some_defaults_and_without_typehints 7 | .. autofunction:: dummy_module_without_complete_typehints.function_with_some_defaults_and_some_typehints 8 | .. autofunction:: dummy_module_without_complete_typehints.function_with_some_defaults_and_more_typehints 9 | .. autofunction:: dummy_module_without_complete_typehints.function_with_defaults_and_some_typehints 10 | -------------------------------------------------------------------------------- /tests/roots/test-dummy/wrong_module_path.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | class A: 5 | pass 6 | 7 | 8 | def f() -> A: 9 | pass 10 | -------------------------------------------------------------------------------- /tests/roots/test-dummy/wrong_module_path.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | .. class:: export_module.A 4 | 5 | .. autofunction:: export_module.f 6 | -------------------------------------------------------------------------------- /tests/roots/test-integration/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pathlib 4 | import sys 5 | 6 | # Make dummy_module.py available for autodoc. 7 | sys.path.insert(0, str(pathlib.Path(__file__).parent)) 8 | 9 | 10 | master_doc = "index" 11 | 12 | extensions = [ 13 | "sphinx.ext.autodoc", 14 | "sphinx.ext.napoleon", 15 | "sphinx_autodoc_typehints", 16 | ] 17 | -------------------------------------------------------------------------------- /tests/roots/test-resolve-typing-guard-tmp/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pathlib 4 | import sys 5 | 6 | master_doc = "index" 7 | sys.path.insert(0, str(pathlib.Path(__file__).parent)) 8 | extensions = [ 9 | "sphinx.ext.autodoc", 10 | "sphinx_autodoc_typehints", 11 | ] 12 | -------------------------------------------------------------------------------- /tests/roots/test-resolve-typing-guard-tmp/demo_typing_guard.py: -------------------------------------------------------------------------------- 1 | """Module demonstrating imports that are type guarded""" 2 | 3 | from __future__ import annotations 4 | 5 | import datetime 6 | 7 | from attrs import define 8 | 9 | 10 | @define() 11 | class SomeClass: 12 | """This class does something.""" 13 | 14 | date: datetime.date 15 | """Date to handle""" 16 | 17 | @classmethod 18 | def from_str(cls, input_value: str) -> SomeClass: 19 | """ 20 | Initialize from string 21 | 22 | :param input_value: Input 23 | :return: result 24 | """ 25 | return cls(input_value) 26 | 27 | @classmethod 28 | def from_date(cls, input_value: datetime.date) -> SomeClass: 29 | """ 30 | Initialize from date 31 | 32 | :param input_value: Input 33 | :return: result 34 | """ 35 | return cls(input_value) 36 | 37 | @classmethod 38 | def from_time(cls, input_value: datetime.time) -> SomeClass: 39 | """ 40 | Initialize from time 41 | 42 | :param input_value: Input 43 | :return: result 44 | """ 45 | return cls(input_value) 46 | 47 | def calculate_thing(self, number: float) -> datetime.timedelta: # noqa: PLR6301 48 | """ 49 | Calculate a thing 50 | 51 | :param number: Input 52 | :return: result 53 | """ 54 | return datetime.timedelta(number) 55 | 56 | 57 | __all__ = ["SomeClass"] 58 | -------------------------------------------------------------------------------- /tests/roots/test-resolve-typing-guard-tmp/index.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: demo_typing_guard 2 | :members: 3 | -------------------------------------------------------------------------------- /tests/roots/test-resolve-typing-guard/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pathlib 4 | import sys 5 | 6 | master_doc = "index" 7 | sys.path.insert(0, str(pathlib.Path(__file__).parent)) 8 | extensions = [ 9 | "sphinx.ext.autodoc", 10 | "sphinx_autodoc_typehints", 11 | ] 12 | -------------------------------------------------------------------------------- /tests/roots/test-resolve-typing-guard/demo_typing_guard.py: -------------------------------------------------------------------------------- 1 | """Module demonstrating imports that are type guarded""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing 6 | from builtins import ValueError # handle does not have __module__ # noqa: A004 7 | from functools import cmp_to_key # has __module__ but cannot get module as is builtin 8 | from typing import TYPE_CHECKING 9 | 10 | from demo_typing_guard_dummy import AnotherClass 11 | 12 | if TYPE_CHECKING: 13 | from collections.abc import Sequence 14 | from decimal import Decimal 15 | 16 | from demo_typing_guard_dummy import Literal # guarded by another `if TYPE_CHECKING` in demo_typing_guard_dummy 17 | 18 | 19 | if typing.TYPE_CHECKING: 20 | from typing import AnyStr 21 | 22 | 23 | if TYPE_CHECKING: # bad import 24 | from functools import missing # noqa: F401 25 | 26 | 27 | def a(f: Decimal, s: AnyStr) -> Sequence[AnyStr | Decimal]: 28 | """ 29 | Do. 30 | 31 | :param f: first 32 | :param s: second 33 | :return: result 34 | """ 35 | return [f, s] 36 | 37 | 38 | class SomeClass: 39 | """This class do something.""" 40 | 41 | def create(self, item: Decimal) -> None: 42 | """ 43 | Create something. 44 | 45 | :param item: the item in question 46 | """ 47 | 48 | if TYPE_CHECKING: # Classes doesn't have `__globals__` attribute 49 | 50 | def guarded(self, item: Decimal) -> None: 51 | """ 52 | Guarded method. 53 | 54 | :param item: some item 55 | """ 56 | 57 | 58 | def func(_x: Literal) -> None: ... 59 | 60 | 61 | __all__ = [ 62 | "AnotherClass", 63 | "SomeClass", 64 | "ValueError", 65 | "a", 66 | "cmp_to_key", 67 | ] 68 | -------------------------------------------------------------------------------- /tests/roots/test-resolve-typing-guard/demo_typing_guard_dummy.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from viktor import AI # module part of autodoc_mock_imports # noqa: F401 6 | 7 | if TYPE_CHECKING: 8 | # Nested type guard 9 | from typing import Literal # noqa: F401 10 | 11 | 12 | class AnotherClass: 13 | """Another class is here""" 14 | -------------------------------------------------------------------------------- /tests/roots/test-resolve-typing-guard/index.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: demo_typing_guard 2 | :members: 3 | -------------------------------------------------------------------------------- /tests/test_integration.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | import sys 5 | from dataclasses import dataclass 6 | from inspect import isclass 7 | from pathlib import Path 8 | from textwrap import dedent, indent 9 | from typing import ( # no type comments 10 | TYPE_CHECKING, 11 | Any, 12 | Literal, 13 | NewType, 14 | Optional, 15 | TypeVar, 16 | Union, 17 | overload, 18 | ) 19 | 20 | import pytest 21 | 22 | if TYPE_CHECKING: 23 | from collections.abc import AsyncGenerator, Callable 24 | from io import StringIO 25 | from mailbox import Mailbox 26 | from types import CodeType, ModuleType 27 | 28 | from sphinx.testing.util import SphinxTestApp 29 | 30 | T = TypeVar("T") 31 | W = NewType("W", str) 32 | 33 | 34 | def expected(expected: str, **options: dict[str, Any]) -> Callable[[T], T]: 35 | def dec(val: T) -> T: 36 | val.EXPECTED = expected 37 | val.OPTIONS = options 38 | return val 39 | 40 | return dec 41 | 42 | 43 | def warns(pattern: str) -> Callable[[T], T]: 44 | def dec(val: T) -> T: 45 | val.WARNING = pattern 46 | return val 47 | 48 | return dec 49 | 50 | 51 | @expected("mod.get_local_function()") 52 | def get_local_function(): # noqa: ANN201 53 | def wrapper(self) -> str: # noqa: ANN001 54 | """ 55 | Wrapper 56 | """ 57 | 58 | return wrapper 59 | 60 | 61 | @warns("Cannot handle as a local function") 62 | @expected( 63 | """\ 64 | class mod.Class(x, y, z=None) 65 | 66 | Initializer docstring. 67 | 68 | Parameters: 69 | * **x** ("bool") -- foo 70 | 71 | * **y** ("int") -- bar 72 | 73 | * **z** ("Optional"["str"]) -- baz 74 | 75 | class InnerClass 76 | 77 | Inner class. 78 | 79 | inner_method(x) 80 | 81 | Inner method. 82 | 83 | Parameters: 84 | **x** ("bool") -- foo 85 | 86 | Return type: 87 | "str" 88 | 89 | classmethod a_classmethod(x, y, z=None) 90 | 91 | Classmethod docstring. 92 | 93 | Parameters: 94 | * **x** ("bool") -- foo 95 | 96 | * **y** ("int") -- bar 97 | 98 | * **z** ("Optional"["str"]) -- baz 99 | 100 | Return type: 101 | "str" 102 | 103 | a_method(x, y, z=None) 104 | 105 | Method docstring. 106 | 107 | Parameters: 108 | * **x** ("bool") -- foo 109 | 110 | * **y** ("int") -- bar 111 | 112 | * **z** ("Optional"["str"]) -- baz 113 | 114 | Return type: 115 | "str" 116 | 117 | property a_property: str 118 | 119 | Property docstring 120 | 121 | static a_staticmethod(x, y, z=None) 122 | 123 | Staticmethod docstring. 124 | 125 | Parameters: 126 | * **x** ("bool") -- foo 127 | 128 | * **y** ("int") -- bar 129 | 130 | * **z** ("Optional"["str"]) -- baz 131 | 132 | Return type: 133 | "str" 134 | 135 | locally_defined_callable_field() -> str 136 | 137 | Wrapper 138 | 139 | Return type: 140 | "str" 141 | """, 142 | ) 143 | class Class: 144 | """ 145 | Initializer docstring. 146 | 147 | :param x: foo 148 | :param y: bar 149 | :param z: baz 150 | """ 151 | 152 | def __init__(self, x: bool, y: int, z: Optional[str] = None) -> None: 153 | pass 154 | 155 | def a_method(self, x: bool, y: int, z: Optional[str] = None) -> str: 156 | """ 157 | Method docstring. 158 | 159 | :param x: foo 160 | :param y: bar 161 | :param z: baz 162 | """ 163 | 164 | def _private_method(self, x: str) -> str: 165 | """ 166 | Private method docstring. 167 | 168 | :param x: foo 169 | """ 170 | 171 | def __dunder_method(self, x: str) -> str: 172 | """ 173 | Dunder method docstring. 174 | 175 | :param x: foo 176 | """ 177 | 178 | def __magic_custom_method__(self, x: str) -> str: # noqa: PLW3201 179 | """ 180 | Magic dunder method docstring. 181 | 182 | :param x: foo 183 | """ 184 | 185 | @classmethod 186 | def a_classmethod(cls, x: bool, y: int, z: Optional[str] = None) -> str: 187 | """ 188 | Classmethod docstring. 189 | 190 | :param x: foo 191 | :param y: bar 192 | :param z: baz 193 | """ 194 | 195 | @staticmethod 196 | def a_staticmethod(x: bool, y: int, z: Optional[str] = None) -> str: 197 | """ 198 | Staticmethod docstring. 199 | 200 | :param x: foo 201 | :param y: bar 202 | :param z: baz 203 | """ 204 | 205 | @property 206 | def a_property(self) -> str: 207 | """ 208 | Property docstring 209 | """ 210 | 211 | class InnerClass: 212 | """ 213 | Inner class. 214 | """ 215 | 216 | def inner_method(self, x: bool) -> str: 217 | """ 218 | Inner method. 219 | 220 | :param x: foo 221 | """ 222 | 223 | def __dunder_inner_method(self, x: bool) -> str: 224 | """ 225 | Dunder inner method. 226 | 227 | :param x: foo 228 | """ 229 | 230 | locally_defined_callable_field = get_local_function() 231 | 232 | 233 | @expected( 234 | """\ 235 | exception mod.DummyException(message) 236 | 237 | Exception docstring 238 | 239 | Parameters: 240 | **message** ("str") -- blah 241 | """, 242 | ) 243 | class DummyException(Exception): # noqa: N818 244 | """ 245 | Exception docstring 246 | 247 | :param message: blah 248 | """ 249 | 250 | def __init__(self, message: str) -> None: 251 | super().__init__(message) 252 | 253 | 254 | @expected( 255 | """\ 256 | mod.function(x, y, z_=None) 257 | 258 | Function docstring. 259 | 260 | Parameters: 261 | * **x** ("bool") -- foo 262 | 263 | * **y** ("int") -- bar 264 | 265 | * **z_** ("Optional"["str"]) -- baz 266 | 267 | Returns: 268 | something 269 | 270 | Return type: 271 | bytes 272 | """, 273 | ) 274 | def function(x: bool, y: int, z_: Optional[str] = None) -> str: 275 | """ 276 | Function docstring. 277 | 278 | :param x: foo 279 | :param y: bar 280 | :param z\\_: baz 281 | :return: something 282 | :rtype: bytes 283 | """ 284 | 285 | 286 | @expected( 287 | """\ 288 | mod.function_with_starred_documentation_param_names(*args, **kwargs) 289 | 290 | Function docstring. 291 | 292 | Usage: 293 | 294 | print(1) 295 | 296 | Parameters: 297 | * ***args** ("int") -- foo 298 | 299 | * ****kwargs** ("str") -- bar 300 | """, 301 | ) 302 | def function_with_starred_documentation_param_names(*args: int, **kwargs: str): # noqa: ANN201 303 | r""" 304 | Function docstring. 305 | 306 | Usage:: 307 | 308 | print(1) 309 | 310 | :param \*args: foo 311 | :param \**kwargs: bar 312 | """ 313 | 314 | 315 | @expected( 316 | """\ 317 | mod.function_with_escaped_default(x='\\\\x08') 318 | 319 | Function docstring. 320 | 321 | Parameters: 322 | **x** ("str") -- foo 323 | """, 324 | ) 325 | def function_with_escaped_default(x: str = "\b"): # noqa: ANN201 326 | """ 327 | Function docstring. 328 | 329 | :param x: foo 330 | """ 331 | 332 | 333 | @warns("Cannot resolve forward reference in type annotations") 334 | @expected( 335 | """\ 336 | mod.function_with_unresolvable_annotation(x) 337 | 338 | Function docstring. 339 | 340 | Parameters: 341 | **x** (a.b.c) -- foo 342 | """, 343 | ) 344 | def function_with_unresolvable_annotation(x: a.b.c): # noqa: ANN201, F821 345 | """ 346 | Function docstring. 347 | 348 | :arg x: foo 349 | """ 350 | 351 | 352 | @expected( 353 | """\ 354 | mod.function_with_typehint_comment(x, y) 355 | 356 | Function docstring. 357 | 358 | Parameters: 359 | * **x** ("int") -- foo 360 | 361 | * **y** ("str") -- bar 362 | 363 | Return type: 364 | "None" 365 | """, 366 | ) 367 | def function_with_typehint_comment( # noqa: ANN201 368 | x, # type: int # noqa: ANN001 369 | y, # type: str # noqa: ANN001 370 | ): 371 | # type: (...) -> None 372 | """ 373 | Function docstring. 374 | 375 | :parameter x: foo 376 | :parameter y: bar 377 | """ 378 | 379 | 380 | @expected( 381 | """\ 382 | class mod.ClassWithTypehints(x) 383 | 384 | Class docstring. 385 | 386 | Parameters: 387 | **x** ("int") -- foo 388 | 389 | foo(x) 390 | 391 | Method docstring. 392 | 393 | Parameters: 394 | **x** ("str") -- foo 395 | 396 | Return type: 397 | "int" 398 | 399 | method_without_typehint(x) 400 | 401 | Method docstring. 402 | """, 403 | ) 404 | class ClassWithTypehints: 405 | """ 406 | Class docstring. 407 | 408 | :param x: foo 409 | """ 410 | 411 | def __init__( 412 | self, 413 | x, # type: int # noqa: ANN001 414 | ) -> None: 415 | # type: (...) -> None 416 | pass 417 | 418 | def foo( # noqa: ANN201, PLR6301 419 | self, 420 | x, # type: str # noqa: ANN001, ARG002 421 | ): 422 | # type: (...) -> int 423 | """ 424 | Method docstring. 425 | 426 | :arg x: foo 427 | """ 428 | return 42 429 | 430 | def method_without_typehint(self, x): # noqa: ANN001, ANN201, ARG002, PLR6301 431 | """ 432 | Method docstring. 433 | """ 434 | # test that multiline str can be correctly indented 435 | multiline_str = """ 436 | test 437 | """ 438 | return multiline_str # noqa: RET504 439 | 440 | 441 | @expected( 442 | """\ 443 | mod.function_with_typehint_comment_not_inline(x=None, *y, z, **kwargs) 444 | 445 | Function docstring. 446 | 447 | Parameters: 448 | * **x** ("Union"["str", "bytes", "None"]) -- foo 449 | 450 | * **y** ("str") -- bar 451 | 452 | * **z** ("bytes") -- baz 453 | 454 | * **kwargs** ("int") -- some kwargs 455 | 456 | Return type: 457 | "None" 458 | """, 459 | ) 460 | def function_with_typehint_comment_not_inline(x=None, *y, z, **kwargs): # noqa: ANN001, ANN002, ANN003, ANN201 461 | # type: (Union[str, bytes, None], *str, bytes, **int) -> None 462 | """ 463 | Function docstring. 464 | 465 | :arg x: foo 466 | :argument y: bar 467 | :parameter z: baz 468 | :parameter kwargs: some kwargs 469 | """ 470 | 471 | 472 | @expected( 473 | """\ 474 | class mod.ClassWithTypehintsNotInline(x=None) 475 | 476 | Class docstring. 477 | 478 | Parameters: 479 | **x** ("Optional"["Callable"[["int", "bytes"], "int"]]) -- foo 480 | 481 | foo(x=1) 482 | 483 | Method docstring. 484 | 485 | Parameters: 486 | **x** ("Callable"[["int", "bytes"], "int"]) -- foo 487 | 488 | Return type: 489 | "int" 490 | 491 | classmethod mk(x=None) 492 | 493 | Method docstring. 494 | 495 | Parameters: 496 | **x** ("Optional"["Callable"[["int", "bytes"], "int"]]) -- 497 | foo 498 | 499 | Return type: 500 | "ClassWithTypehintsNotInline" 501 | """, 502 | ) 503 | class ClassWithTypehintsNotInline: 504 | """ 505 | Class docstring. 506 | 507 | :param x: foo 508 | """ 509 | 510 | def __init__(self, x=None) -> None: # type: (Optional[Callable[[int, bytes], int]]) -> None # noqa: ANN001 511 | pass 512 | 513 | def foo(self, x=1): # type: (Callable[[int, bytes], int]) -> int # noqa: ANN001, ANN201, PLR6301 514 | """ 515 | Method docstring. 516 | 517 | :param x: foo 518 | """ 519 | return x(1, b"") 520 | 521 | @classmethod 522 | def mk( # noqa: ANN206 523 | cls, 524 | x=None, # noqa: ANN001 525 | ): # type: (Optional[Callable[[int, bytes], int]]) -> ClassWithTypehintsNotInline 526 | """ 527 | Method docstring. 528 | 529 | :param x: foo 530 | """ 531 | return cls(x) 532 | 533 | 534 | @expected( 535 | """\ 536 | mod.undocumented_function(x) 537 | 538 | Hi 539 | 540 | Return type: 541 | "str" 542 | """, 543 | ) 544 | def undocumented_function(x: int) -> str: 545 | """Hi""" 546 | 547 | return str(x) 548 | 549 | 550 | @expected( 551 | """\ 552 | class mod.DataClass(x) 553 | 554 | Class docstring. 555 | """, 556 | ) 557 | @dataclass 558 | class DataClass: 559 | """Class docstring.""" 560 | 561 | x: int 562 | 563 | 564 | @expected( 565 | """\ 566 | class mod.Decorator(func) 567 | 568 | Initializer docstring. 569 | 570 | Parameters: 571 | **func** ("Callable"[["int", "str"], "str"]) -- function 572 | """, 573 | ) 574 | class Decorator: 575 | """ 576 | Initializer docstring. 577 | 578 | :param func: function 579 | """ 580 | 581 | def __init__(self, func: Callable[[int, str], str]) -> None: 582 | pass 583 | 584 | 585 | @expected( 586 | """\ 587 | mod.mocked_import(x) 588 | 589 | A docstring. 590 | 591 | Parameters: 592 | **x** ("Mailbox") -- function 593 | """, 594 | ) 595 | def mocked_import(x: Mailbox): # noqa: ANN201 596 | """ 597 | A docstring. 598 | 599 | :param x: function 600 | """ 601 | 602 | 603 | @expected( 604 | """\ 605 | mod.func_with_examples() 606 | 607 | A docstring. 608 | 609 | Return type: 610 | "int" 611 | 612 | -[ Examples ]- 613 | 614 | Here are a couple of examples of how to use this function. 615 | """, 616 | ) 617 | def func_with_examples() -> int: 618 | """ 619 | A docstring. 620 | 621 | .. rubric:: Examples 622 | 623 | Here are a couple of examples of how to use this function. 624 | """ 625 | 626 | 627 | @overload 628 | def func_with_overload(a: int, b: int) -> None: ... 629 | 630 | 631 | @overload 632 | def func_with_overload(a: str, b: str) -> None: ... 633 | 634 | 635 | @expected( 636 | """\ 637 | mod.func_with_overload(a, b) 638 | 639 | f does the thing. The arguments can either be ints or strings but 640 | they must both have the same type. 641 | 642 | Parameters: 643 | * **a** ("Union"["int", "str"]) -- The first thing 644 | 645 | * **b** ("Union"["int", "str"]) -- The second thing 646 | 647 | Return type: 648 | "None" 649 | """, 650 | ) 651 | def func_with_overload(a: Union[int, str], b: Union[int, str]) -> None: 652 | """ 653 | f does the thing. The arguments can either be ints or strings but they must 654 | both have the same type. 655 | 656 | Parameters 657 | ---------- 658 | a: 659 | The first thing 660 | b: 661 | The second thing 662 | """ 663 | 664 | 665 | @expected( 666 | """\ 667 | mod.func_literals_long_format(a, b) 668 | 669 | A docstring. 670 | 671 | Parameters: 672 | * **a** ("Literal"["'arg1'", "'arg2'"]) -- Argument that can 673 | take either of two literal values. 674 | 675 | * **b** ("Literal"["'arg1'", "'arg2'"]) -- Argument that can 676 | take either of two literal values. 677 | 678 | Return type: 679 | "None" 680 | """, 681 | ) 682 | def func_literals_long_format(a: Literal["arg1", "arg2"], b: Literal["arg1", "arg2"]) -> None: 683 | """ 684 | A docstring. 685 | 686 | :param a: Argument that can take either of two literal values. 687 | :param b: Argument that can take either of two literal values. 688 | """ 689 | 690 | 691 | @expected( 692 | """\ 693 | mod.func_literals_short_format(a, b) 694 | 695 | A docstring. 696 | 697 | Parameters: 698 | * **a** ("'arg1'" | "'arg2'") -- Argument that can take either 699 | of two literal values. 700 | 701 | * **b** ("'arg1'" | "'arg2'") -- Argument that can take either 702 | of two literal values. 703 | 704 | Return type: 705 | "None" 706 | """, 707 | python_display_short_literal_types=True, 708 | ) 709 | def func_literals_short_format(a: Literal["arg1", "arg2"], b: Literal["arg1", "arg2"]) -> None: 710 | """ 711 | A docstring. 712 | 713 | :param a: Argument that can take either of two literal values. 714 | :param b: Argument that can take either of two literal values. 715 | """ 716 | 717 | 718 | @expected( 719 | """\ 720 | class mod.TestClassAttributeDocs 721 | 722 | A class 723 | 724 | code: "Optional"["CodeType"] 725 | 726 | An attribute 727 | """, 728 | ) 729 | class TestClassAttributeDocs: 730 | """A class""" 731 | 732 | code: Optional[CodeType] 733 | """An attribute""" 734 | 735 | 736 | @expected( 737 | """\ 738 | mod.func_with_examples_and_returns_after() 739 | 740 | f does the thing. 741 | 742 | -[ Examples ]- 743 | 744 | Here is an example 745 | 746 | Return type: 747 | "int" 748 | 749 | Returns: 750 | The index of the widget 751 | """, 752 | ) 753 | def func_with_examples_and_returns_after() -> int: 754 | """ 755 | f does the thing. 756 | 757 | Examples 758 | -------- 759 | 760 | Here is an example 761 | 762 | :returns: The index of the widget 763 | """ 764 | 765 | 766 | @expected( 767 | """\ 768 | mod.func_with_parameters_and_stuff_after(a, b) 769 | 770 | A func 771 | 772 | Parameters: 773 | * **a** ("int") -- a tells us something 774 | 775 | * **b** ("int") -- b tells us something 776 | 777 | Return type: 778 | "int" 779 | 780 | More info about the function here. 781 | """, 782 | ) 783 | def func_with_parameters_and_stuff_after(a: int, b: int) -> int: 784 | """A func 785 | 786 | :param a: a tells us something 787 | :param b: b tells us something 788 | 789 | More info about the function here. 790 | """ 791 | 792 | 793 | @expected( 794 | """\ 795 | mod.func_with_rtype_in_weird_spot(a, b) 796 | 797 | A func 798 | 799 | Parameters: 800 | * **a** ("int") -- a tells us something 801 | 802 | * **b** ("int") -- b tells us something 803 | 804 | -[ Examples ]- 805 | 806 | Here is an example 807 | 808 | Returns: 809 | The index of the widget 810 | 811 | More info about the function here. 812 | 813 | Return type: 814 | int 815 | """, 816 | ) 817 | def func_with_rtype_in_weird_spot(a: int, b: int) -> int: 818 | """A func 819 | 820 | :param a: a tells us something 821 | :param b: b tells us something 822 | 823 | Examples 824 | -------- 825 | 826 | Here is an example 827 | 828 | :returns: The index of the widget 829 | 830 | More info about the function here. 831 | 832 | :rtype: int 833 | """ 834 | 835 | 836 | @expected( 837 | """\ 838 | mod.empty_line_between_parameters(a, b) 839 | 840 | A func 841 | 842 | Parameters: 843 | * **a** ("int") -- 844 | 845 | One of the following possibilities: 846 | 847 | * a 848 | 849 | * b 850 | 851 | * c 852 | 853 | * **b** ("int") -- 854 | 855 | Whatever else we have to say. 856 | 857 | There is more of it And here too 858 | 859 | Return type: 860 | "int" 861 | 862 | More stuff here. 863 | """, 864 | ) 865 | def empty_line_between_parameters(a: int, b: int) -> int: 866 | """A func 867 | 868 | :param a: One of the following possibilities: 869 | 870 | - a 871 | 872 | - b 873 | 874 | - c 875 | 876 | :param b: Whatever else we have to say. 877 | 878 | There is more of it And here too 879 | 880 | More stuff here. 881 | """ 882 | 883 | 884 | @expected( 885 | """\ 886 | mod.func_with_code_block() 887 | 888 | A docstring. 889 | 890 | You would say: 891 | 892 | print("some python code here") 893 | 894 | Return type: 895 | "int" 896 | 897 | -[ Examples ]- 898 | 899 | Here are a couple of examples of how to use this function. 900 | """, 901 | ) 902 | def func_with_code_block() -> int: 903 | """ 904 | A docstring. 905 | 906 | You would say: 907 | 908 | .. code-block:: 909 | 910 | print("some python code here") 911 | 912 | 913 | .. rubric:: Examples 914 | 915 | Here are a couple of examples of how to use this function. 916 | """ 917 | 918 | 919 | @expected( 920 | """ 921 | mod.func_with_definition_list() 922 | 923 | Some text and then a definition list. 924 | 925 | Return type: 926 | "int" 927 | 928 | abc 929 | x 930 | 931 | xyz 932 | something 933 | """, 934 | ) 935 | def func_with_definition_list() -> int: 936 | """Some text and then a definition list. 937 | 938 | abc 939 | x 940 | 941 | xyz 942 | something 943 | """ 944 | # See https://github.com/tox-dev/sphinx-autodoc-typehints/issues/302 945 | 946 | 947 | @expected( 948 | """\ 949 | mod.decorator_2(f) 950 | 951 | Run the decorated function with *asyncio.run*. 952 | 953 | Parameters: 954 | **f** ("Any") -- The function to wrap. 955 | 956 | Return type: 957 | "Any" 958 | 959 | -[ Examples ]- 960 | 961 | A 962 | """, 963 | ) 964 | def decorator_2(f: Any) -> Any: 965 | """Run the decorated function with `asyncio.run`. 966 | 967 | Parameters 968 | ---------- 969 | f 970 | The function to wrap. 971 | 972 | Examples 973 | -------- 974 | 975 | .. code-block:: python 976 | 977 | A 978 | """ 979 | assert f is not None 980 | 981 | 982 | @expected( 983 | """ 984 | class mod.ParamAndAttributeHaveSameName(blah) 985 | 986 | A Class 987 | 988 | Parameters: 989 | **blah** ("CodeType") -- Description of parameter blah 990 | 991 | blah: "ModuleType" 992 | 993 | Description of attribute blah 994 | 995 | """, 996 | ) 997 | class ParamAndAttributeHaveSameName: 998 | """ 999 | A Class 1000 | 1001 | Parameters 1002 | ---------- 1003 | blah: 1004 | Description of parameter blah 1005 | """ 1006 | 1007 | def __init__(self, blah: CodeType) -> None: 1008 | pass 1009 | 1010 | blah: ModuleType 1011 | """Description of attribute blah""" 1012 | 1013 | 1014 | @expected( 1015 | """ 1016 | mod.napoleon_returns() 1017 | 1018 | A function. 1019 | 1020 | Return type: 1021 | "CodeType" 1022 | 1023 | Returns: 1024 | The info about the whatever. 1025 | """, 1026 | ) 1027 | def napoleon_returns() -> CodeType: 1028 | """ 1029 | A function. 1030 | 1031 | Returns 1032 | ------- 1033 | The info about the whatever. 1034 | """ 1035 | 1036 | 1037 | @expected( 1038 | """ 1039 | mod.google_docstrings(arg1, arg2) 1040 | 1041 | Summary line. 1042 | 1043 | Extended description of function. 1044 | 1045 | Parameters: 1046 | * **arg1** ("CodeType") -- Description of arg1 1047 | 1048 | * **arg2** ("ModuleType") -- Description of arg2 1049 | 1050 | Return type: 1051 | "CodeType" 1052 | 1053 | Returns: 1054 | Description of return value 1055 | 1056 | """, 1057 | ) 1058 | def google_docstrings(arg1: CodeType, arg2: ModuleType) -> CodeType: 1059 | """Summary line. 1060 | 1061 | Extended description of function. 1062 | 1063 | Args: 1064 | arg1: Description of arg1 1065 | arg2: Description of arg2 1066 | 1067 | Returns: 1068 | Description of return value 1069 | """ 1070 | 1071 | 1072 | @expected( 1073 | """ 1074 | mod.docstring_with_multiline_note_after_params(param) 1075 | 1076 | Do something. 1077 | 1078 | Parameters: 1079 | **param** ("int") -- A parameter. 1080 | 1081 | Return type: 1082 | "None" 1083 | 1084 | Note: 1085 | 1086 | Some notes. More notes 1087 | 1088 | """, 1089 | ) 1090 | def docstring_with_multiline_note_after_params(param: int) -> None: 1091 | """Do something. 1092 | 1093 | Args: 1094 | param: A parameter. 1095 | 1096 | Note: 1097 | 1098 | Some notes. 1099 | More notes 1100 | """ 1101 | 1102 | 1103 | @expected( 1104 | """ 1105 | mod.docstring_with_bullet_list_after_params(param) 1106 | 1107 | Do something. 1108 | 1109 | Parameters: 1110 | **param** ("int") -- A parameter. 1111 | 1112 | Return type: 1113 | "None" 1114 | 1115 | * A: B 1116 | 1117 | * C: D 1118 | 1119 | """, 1120 | ) 1121 | def docstring_with_bullet_list_after_params(param: int) -> None: 1122 | """Do something. 1123 | 1124 | Args: 1125 | param: A parameter. 1126 | 1127 | * A: B 1128 | * C: D 1129 | """ 1130 | 1131 | 1132 | @expected( 1133 | """ 1134 | mod.docstring_with_definition_list_after_params(param) 1135 | 1136 | Do something. 1137 | 1138 | Parameters: 1139 | **param** ("int") -- A parameter. 1140 | 1141 | Return type: 1142 | "None" 1143 | 1144 | Term 1145 | A description 1146 | 1147 | maybe multiple lines 1148 | 1149 | Next Term 1150 | Something about it 1151 | 1152 | """, 1153 | ) 1154 | def docstring_with_definition_list_after_params(param: int) -> None: 1155 | """Do something. 1156 | 1157 | Args: 1158 | param: A parameter. 1159 | 1160 | Term 1161 | A description 1162 | 1163 | maybe multiple lines 1164 | 1165 | Next Term 1166 | Something about it 1167 | """ 1168 | 1169 | 1170 | @expected( 1171 | """ 1172 | mod.docstring_with_enum_list_after_params(param) 1173 | 1174 | Do something. 1175 | 1176 | Parameters: 1177 | **param** ("int") -- A parameter. 1178 | 1179 | Return type: 1180 | "None" 1181 | 1182 | 1. A: B 1183 | 1184 | 2. C: D 1185 | 1186 | """, 1187 | ) 1188 | def docstring_with_enum_list_after_params(param: int) -> None: 1189 | """Do something. 1190 | 1191 | Args: 1192 | param: A parameter. 1193 | 1194 | 1. A: B 1195 | 2. C: D 1196 | """ 1197 | 1198 | 1199 | @warns("Definition list ends without a blank line") 1200 | @expected( 1201 | """ 1202 | mod.docstring_with_definition_list_after_params_no_blank_line(param) 1203 | 1204 | Do something. 1205 | 1206 | Parameters: 1207 | **param** ("int") -- A parameter. 1208 | 1209 | Return type: 1210 | "None" 1211 | 1212 | Term 1213 | A description 1214 | 1215 | maybe multiple lines 1216 | 1217 | Next Term 1218 | Something about it 1219 | 1220 | -[ Example ]- 1221 | """, 1222 | ) 1223 | def docstring_with_definition_list_after_params_no_blank_line(param: int) -> None: 1224 | """Do something. 1225 | 1226 | Args: 1227 | param: A parameter. 1228 | 1229 | Term 1230 | A description 1231 | 1232 | maybe multiple lines 1233 | 1234 | Next Term 1235 | Something about it 1236 | .. rubric:: Example 1237 | """ 1238 | 1239 | 1240 | @expected( 1241 | """ 1242 | mod.has_typevar(param) 1243 | 1244 | Do something. 1245 | 1246 | Parameters: 1247 | **param** ("TypeVar"("T")) -- A parameter. 1248 | 1249 | Return type: 1250 | "TypeVar"("T") 1251 | 1252 | """, 1253 | ) 1254 | def has_typevar(param: T) -> T: 1255 | """Do something. 1256 | 1257 | Args: 1258 | param: A parameter. 1259 | """ 1260 | return param 1261 | 1262 | 1263 | @expected( 1264 | """ 1265 | mod.has_newtype(param) 1266 | 1267 | Do something. 1268 | 1269 | Parameters: 1270 | **param** ("NewType"("W", "str")) -- A parameter. 1271 | 1272 | Return type: 1273 | "NewType"("W", "str") 1274 | 1275 | """, 1276 | ) 1277 | def has_newtype(param: W) -> W: 1278 | """Do something. 1279 | 1280 | Args: 1281 | param: A parameter. 1282 | """ 1283 | return param 1284 | 1285 | 1286 | AUTO_FUNCTION = ".. autofunction:: mod.{}" 1287 | AUTO_CLASS = """\ 1288 | .. autoclass:: mod.{} 1289 | :members: 1290 | """ 1291 | AUTO_EXCEPTION = """\ 1292 | .. autoexception:: mod.{} 1293 | :members: 1294 | """ 1295 | 1296 | 1297 | @expected( 1298 | """ 1299 | mod.typehints_use_signature(a: AsyncGenerator) -> AsyncGenerator 1300 | 1301 | Do something. 1302 | 1303 | Parameters: 1304 | **a** ("AsyncGenerator") -- blah 1305 | 1306 | Return type: 1307 | "AsyncGenerator" 1308 | 1309 | """, 1310 | typehints_use_signature=True, 1311 | typehints_use_signature_return=True, 1312 | ) 1313 | def typehints_use_signature(a: AsyncGenerator) -> AsyncGenerator: 1314 | """Do something. 1315 | 1316 | Args: 1317 | a: blah 1318 | """ 1319 | return a 1320 | 1321 | 1322 | @expected( 1323 | """ 1324 | mod.typehints_no_rtype_none() 1325 | 1326 | Do something. 1327 | 1328 | """, 1329 | typehints_document_rtype_none=False, 1330 | ) 1331 | def typehints_no_rtype_none() -> None: 1332 | """Do something.""" 1333 | 1334 | 1335 | prolog = """ 1336 | .. |test_node_start| replace:: {test_node_start} 1337 | """.format(test_node_start="test_start") 1338 | 1339 | 1340 | @expected( 1341 | """ 1342 | mod.docstring_with_multiline_note_after_params_prolog_replace(param) 1343 | 1344 | Do something. 1345 | 1346 | Parameters: 1347 | **param** ("int") -- A parameter. 1348 | 1349 | Return type: 1350 | "None" 1351 | 1352 | Note: 1353 | 1354 | Some notes. test_start More notes 1355 | 1356 | """, 1357 | rst_prolog=prolog, 1358 | ) 1359 | def docstring_with_multiline_note_after_params_prolog_replace(param: int) -> None: 1360 | """Do something. 1361 | 1362 | Args: 1363 | param: A parameter. 1364 | 1365 | Note: 1366 | 1367 | Some notes. |test_node_start| 1368 | More notes 1369 | """ 1370 | 1371 | 1372 | epilog = """ 1373 | .. |test_node_end| replace:: {test_node_end} 1374 | """.format(test_node_end="test_end") 1375 | 1376 | 1377 | @expected( 1378 | """ 1379 | mod.docstring_with_multiline_note_after_params_epilog_replace(param) 1380 | 1381 | Do something. 1382 | 1383 | Parameters: 1384 | **param** ("int") -- A parameter. 1385 | 1386 | Return type: 1387 | "None" 1388 | 1389 | Note: 1390 | 1391 | Some notes. test_end More notes 1392 | 1393 | """, 1394 | rst_epilog=epilog, 1395 | ) 1396 | def docstring_with_multiline_note_after_params_epilog_replace(param: int) -> None: 1397 | """Do something. 1398 | 1399 | Args: 1400 | param: A parameter. 1401 | 1402 | Note: 1403 | 1404 | Some notes. |test_node_end| 1405 | More notes 1406 | """ 1407 | 1408 | 1409 | @expected( 1410 | """ 1411 | mod.docstring_with_see_also() 1412 | 1413 | Test 1414 | 1415 | See also: more info at `_. 1416 | 1417 | Return type: 1418 | "str" 1419 | 1420 | """ 1421 | ) 1422 | def docstring_with_see_also() -> str: 1423 | """ 1424 | Test 1425 | 1426 | .. seealso:: more info at `_. 1427 | """ 1428 | return "" 1429 | 1430 | 1431 | @expected( 1432 | """ 1433 | mod.has_doctest1() 1434 | 1435 | Test that we place the return type correctly when the function has 1436 | a doctest. 1437 | 1438 | Return type: 1439 | "None" 1440 | 1441 | >>> this is a fake doctest 1442 | a 1443 | >>> more doctest 1444 | b 1445 | """ 1446 | ) 1447 | def has_doctest1() -> None: 1448 | r"""Test that we place the return type correctly when the function has a doctest. 1449 | 1450 | >>> this is a fake doctest 1451 | a 1452 | >>> more doctest 1453 | b 1454 | """ 1455 | 1456 | 1457 | Unformatted = TypeVar("Unformatted") 1458 | 1459 | 1460 | @warns("cannot cache unpickleable configuration value: 'typehints_formatter'") 1461 | @expected( 1462 | """ 1463 | mod.typehints_formatter_applied_to_signature(param: Formatted) -> Formatted 1464 | 1465 | Do nothing 1466 | 1467 | Parameters: 1468 | **param** (Formatted) -- A parameter 1469 | 1470 | Return type: 1471 | Formatted 1472 | 1473 | Returns: 1474 | The return value 1475 | """, 1476 | typehints_use_signature=True, 1477 | typehints_use_signature_return=True, 1478 | typehints_formatter=lambda _, __=None: "Formatted", 1479 | ) 1480 | def typehints_formatter_applied_to_signature(param: Unformatted) -> Unformatted: 1481 | """ 1482 | Do nothing 1483 | 1484 | Args: 1485 | param: A parameter 1486 | 1487 | Returns: 1488 | The return value 1489 | """ 1490 | return param 1491 | 1492 | 1493 | # Config settings for each test run. 1494 | # Config Name: Sphinx Options as Dict. 1495 | configs = { 1496 | "default_conf": {}, 1497 | "prolog_conf": {"rst_prolog": prolog}, 1498 | "epilog_conf": { 1499 | "rst_epilog": epilog, 1500 | }, 1501 | "bothlog_conf": { 1502 | "rst_prolog": prolog, 1503 | "rst_epilog": epilog, 1504 | }, 1505 | } 1506 | 1507 | 1508 | @pytest.mark.parametrize("val", [x for x in globals().values() if hasattr(x, "EXPECTED")]) 1509 | @pytest.mark.parametrize("conf_run", ["default_conf", "prolog_conf", "epilog_conf", "bothlog_conf"]) 1510 | @pytest.mark.sphinx("text", testroot="integration") 1511 | def test_integration( 1512 | app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, val: Any, conf_run: str 1513 | ) -> None: 1514 | if isclass(val) and issubclass(val, BaseException): 1515 | template = AUTO_EXCEPTION 1516 | elif isclass(val): 1517 | template = AUTO_CLASS 1518 | else: 1519 | template = AUTO_FUNCTION 1520 | 1521 | (Path(app.srcdir) / "index.rst").write_text(template.format(val.__name__)) 1522 | app.config.__dict__.update(configs[conf_run]) 1523 | app.config.__dict__.update(val.OPTIONS) 1524 | monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__]) 1525 | app.build() 1526 | assert "build succeeded" in status.getvalue() # Build succeeded 1527 | 1528 | regexp = getattr(val, "WARNING", None) 1529 | value = warning.getvalue().strip() 1530 | if regexp: 1531 | msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}" 1532 | assert re.search(regexp, value), msg 1533 | else: 1534 | assert not value 1535 | 1536 | result = (Path(app.srcdir) / "_build/text/index.txt").read_text() 1537 | 1538 | expected = val.EXPECTED 1539 | try: 1540 | assert result.strip() == dedent(expected).strip() 1541 | except Exception: 1542 | indented = indent(f'"""\n{result}\n"""', " " * 4) 1543 | print(f"@expected(\n{indented}\n)\n") # noqa: T201 1544 | raise 1545 | -------------------------------------------------------------------------------- /tests/test_integration_autodoc_type_aliases.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | import sys 5 | from pathlib import Path 6 | from textwrap import dedent, indent 7 | from typing import TYPE_CHECKING, Any, Literal, NewType, TypeVar 8 | 9 | import pytest 10 | 11 | if TYPE_CHECKING: 12 | from collections.abc import Callable 13 | from io import StringIO 14 | 15 | from sphinx.testing.util import SphinxTestApp 16 | 17 | T = TypeVar("T") 18 | W = NewType("W", str) 19 | 20 | 21 | def expected(expected: str, **options: dict[str, Any]) -> Callable[[T], T]: 22 | def dec(val: T) -> T: 23 | val.EXPECTED = expected 24 | val.OPTIONS = options 25 | return val 26 | 27 | return dec 28 | 29 | 30 | def warns(pattern: str) -> Callable[[T], T]: 31 | def dec(val: T) -> T: 32 | val.WARNING = pattern 33 | return val 34 | 35 | return dec 36 | 37 | 38 | ArrayLike = Literal["test"] 39 | 40 | 41 | class _SchemaMeta(type): # noqa: PLW1641 42 | def __eq__(cls, other: object) -> bool: 43 | return True 44 | 45 | 46 | class Schema(metaclass=_SchemaMeta): 47 | pass 48 | 49 | 50 | @expected( 51 | """ 52 | mod.f(s) 53 | 54 | Do something. 55 | 56 | Parameters: 57 | **s** ("Schema") -- Some schema. 58 | 59 | Return type: 60 | "Schema" 61 | """ 62 | ) 63 | def f(s: Schema) -> Schema: 64 | """ 65 | Do something. 66 | 67 | Args: 68 | s: Some schema. 69 | """ 70 | return s 71 | 72 | 73 | class AliasedClass: ... 74 | 75 | 76 | @expected( 77 | """ 78 | mod.g(s) 79 | 80 | Do something. 81 | 82 | Parameters: 83 | **s** ("Class Alias") -- Some schema. 84 | 85 | Return type: 86 | "Class Alias" 87 | """ 88 | ) 89 | def g(s: AliasedClass) -> AliasedClass: 90 | """ 91 | Do something. 92 | 93 | Args: 94 | s: Some schema. 95 | """ 96 | return s 97 | 98 | 99 | @expected( 100 | """\ 101 | mod.function(x, y) 102 | 103 | Function docstring. 104 | 105 | Parameters: 106 | * **x** ("Optional"[Array]) -- foo 107 | 108 | * **y** ("Schema") -- boo 109 | 110 | Returns: 111 | something 112 | 113 | Return type: 114 | bytes 115 | 116 | """, 117 | ) 118 | def function(x: ArrayLike | None, y: Schema) -> str: 119 | """ 120 | Function docstring. 121 | 122 | :param x: foo 123 | :param y: boo 124 | :return: something 125 | :rtype: bytes 126 | """ 127 | 128 | 129 | # Config settings for each test run. 130 | # Config Name: Sphinx Options as Dict. 131 | configs = {"default_conf": {"autodoc_type_aliases": {"ArrayLike": "Array", "AliasedClass": '"Class Alias"'}}} 132 | 133 | 134 | @pytest.mark.parametrize("val", [x for x in globals().values() if hasattr(x, "EXPECTED")]) 135 | @pytest.mark.parametrize("conf_run", list(configs.keys())) 136 | @pytest.mark.sphinx("text", testroot="integration") 137 | def test_integration( 138 | app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, val: Any, conf_run: str 139 | ) -> None: 140 | template = ".. autofunction:: mod.{}" 141 | 142 | (Path(app.srcdir) / "index.rst").write_text(template.format(val.__name__)) 143 | app.config.__dict__.update(configs[conf_run]) 144 | app.config.__dict__.update(val.OPTIONS) 145 | monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__]) 146 | app.build() 147 | assert "build succeeded" in status.getvalue() # Build succeeded 148 | 149 | regexp = getattr(val, "WARNING", None) 150 | value = warning.getvalue().strip() 151 | if regexp: 152 | msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}" 153 | assert re.search(regexp, value), msg 154 | elif not re.search(r"WARNING: Inline strong start-string without end-string.", value): 155 | assert not value 156 | 157 | result = (Path(app.srcdir) / "_build/text/index.txt").read_text() 158 | 159 | expected = val.EXPECTED 160 | try: 161 | assert result.strip() == dedent(expected).strip() 162 | except Exception: 163 | indented = indent(f'"""\n{result}\n"""', " " * 4) 164 | print(f"@expected(\n{indented}\n)\n") # noqa: T201 165 | raise 166 | -------------------------------------------------------------------------------- /tests/test_integration_issue_384.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | import sys 5 | from pathlib import Path 6 | from textwrap import dedent, indent 7 | from typing import TYPE_CHECKING, Any, NewType, TypeVar 8 | 9 | import pytest 10 | 11 | if TYPE_CHECKING: 12 | from collections.abc import Callable 13 | from io import StringIO 14 | 15 | from sphinx.testing.util import SphinxTestApp 16 | 17 | T = TypeVar("T") 18 | W = NewType("W", str) 19 | 20 | 21 | def expected(expected: str, **options: dict[str, Any]) -> Callable[[T], T]: 22 | def dec(val: T) -> T: 23 | val.EXPECTED = expected 24 | val.OPTIONS = options 25 | return val 26 | 27 | return dec 28 | 29 | 30 | def warns(pattern: str) -> Callable[[T], T]: 31 | def dec(val: T) -> T: 32 | val.WARNING = pattern 33 | return val 34 | 35 | return dec 36 | 37 | 38 | @expected( 39 | """\ 40 | mod.function(x=5, y=10, z=15) 41 | 42 | Function docstring. 43 | 44 | Parameters: 45 | * **x** ("int") -- optional specifier line 2 (default: "5") 46 | 47 | * **y** ("int") -- 48 | 49 | another optional line 4 50 | 51 | second paragraph for y (default: "10") 52 | 53 | * **z** ("int") -- yet another optional s line 6 (default: "15") 54 | 55 | Returns: 56 | something 57 | 58 | Return type: 59 | bytes 60 | 61 | """, 62 | ) 63 | def function(x: int = 5, y: int = 10, z: int = 15) -> str: 64 | """ 65 | Function docstring. 66 | 67 | :param x: optional specifier 68 | line 2 69 | :param y: another optional 70 | line 4 71 | 72 | second paragraph for y 73 | 74 | :param z: yet another optional s 75 | line 6 76 | 77 | :return: something 78 | :rtype: bytes 79 | """ 80 | 81 | 82 | # Config settings for each test run. 83 | # Config Name: Sphinx Options as Dict. 84 | configs = {"default_conf": {"typehints_defaults": "braces-after"}} 85 | 86 | 87 | @pytest.mark.parametrize("val", [x for x in globals().values() if hasattr(x, "EXPECTED")]) 88 | @pytest.mark.parametrize("conf_run", list(configs.keys())) 89 | @pytest.mark.sphinx("text", testroot="integration") 90 | def test_integration( 91 | app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, val: Any, conf_run: str 92 | ) -> None: 93 | template = ".. autofunction:: mod.{}" 94 | 95 | (Path(app.srcdir) / "index.rst").write_text(template.format(val.__name__)) 96 | app.config.__dict__.update(configs[conf_run]) 97 | app.config.__dict__.update(val.OPTIONS) 98 | monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__]) 99 | app.build() 100 | assert "build succeeded" in status.getvalue() # Build succeeded 101 | 102 | regexp = getattr(val, "WARNING", None) 103 | value = warning.getvalue().strip() 104 | if regexp: 105 | msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}" 106 | assert re.search(regexp, value), msg 107 | else: 108 | assert not value 109 | 110 | result = (Path(app.srcdir) / "_build/text/index.txt").read_text() 111 | 112 | expected = val.EXPECTED 113 | try: 114 | assert result.strip() == dedent(expected).strip() 115 | except Exception: 116 | indented = indent(f'"""\n{result}\n"""', " " * 4) 117 | print(f"@expected(\n{indented}\n)\n") # noqa: T201 118 | raise 119 | -------------------------------------------------------------------------------- /tests/test_sphinx_autodoc_typehints.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | import sys 5 | import types 6 | import typing 7 | from collections.abc import Callable, Mapping 8 | from functools import cmp_to_key 9 | from io import StringIO 10 | from pathlib import Path 11 | from textwrap import dedent, indent 12 | from types import EllipsisType, FrameType, FunctionType, ModuleType, NotImplementedType, TracebackType 13 | from typing import ( # noqa: UP035 14 | IO, 15 | Any, 16 | AnyStr, 17 | Dict, 18 | Generic, 19 | List, 20 | NewType, 21 | Optional, 22 | Tuple, 23 | Type, 24 | TypeVar, 25 | Union, 26 | ) 27 | from unittest.mock import create_autospec, patch 28 | 29 | import pytest 30 | import typing_extensions 31 | from sphinx.application import Sphinx 32 | from sphinx.config import Config 33 | 34 | from sphinx_autodoc_typehints import ( 35 | _resolve_type_guarded_imports, 36 | backfill_type_hints, 37 | format_annotation, 38 | get_annotation_args, 39 | get_annotation_class_name, 40 | get_annotation_module, 41 | normalize_source_lines, 42 | process_docstring, 43 | ) 44 | 45 | if typing.TYPE_CHECKING: 46 | from sphinx.testing.util import SphinxTestApp 47 | from sphobjinv import Inventory 48 | 49 | T = TypeVar("T") 50 | U_co = TypeVar("U_co", covariant=True) 51 | V_contra = TypeVar("V_contra", contravariant=True) 52 | X = TypeVar("X", str, int) 53 | Y = TypeVar("Y", bound=str) 54 | Z = TypeVar("Z", bound="A") 55 | S = TypeVar("S", bound="miss") # type: ignore[name-defined] # miss not defined on purpose # noqa: F821 56 | W = NewType("W", str) 57 | P = typing_extensions.ParamSpec("P") 58 | P_args = P.args # type:ignore[attr-defined] 59 | P_kwargs = P.kwargs # type:ignore[attr-defined] 60 | P_co = typing_extensions.ParamSpec("P_co", covariant=True) # type: ignore[misc] 61 | P_contra = typing_extensions.ParamSpec("P_contra", contravariant=True) # type: ignore[misc] 62 | P_bound = typing_extensions.ParamSpec("P_bound", bound=str) # type: ignore[misc] 63 | 64 | # Mypy does not support recursive type aliases, but 65 | # other type checkers do. 66 | RecList = Union[int, List["RecList"]] 67 | MutualRecA = Union[bool, List["MutualRecB"]] 68 | MutualRecB = Union[str, List["MutualRecA"]] 69 | 70 | 71 | class A: 72 | def get_type(self) -> type: 73 | return type(self) 74 | 75 | class Inner: ... 76 | 77 | 78 | class B(Generic[T]): 79 | name = "Foo" # This is set to make sure the correct class name ("B") is picked up 80 | 81 | 82 | class C(B[str]): ... 83 | 84 | 85 | class D(typing_extensions.Protocol): ... 86 | 87 | 88 | class E(typing_extensions.Protocol[T]): # type: ignore[misc] 89 | ... 90 | 91 | 92 | class Slotted: 93 | __slots__ = () 94 | 95 | 96 | class Metaclass(type): ... 97 | 98 | 99 | class HintedMethods: 100 | @classmethod 101 | def from_magic(cls) -> typing_extensions.Self: # type: ignore[empty-body] 102 | ... 103 | 104 | def method(self) -> typing_extensions.Self: # type: ignore[empty-body] 105 | ... 106 | 107 | 108 | PY312_PLUS = sys.version_info >= (3, 12) 109 | 110 | 111 | @pytest.mark.parametrize( 112 | ("annotation", "module", "class_name", "args"), 113 | [ 114 | pytest.param(str, "builtins", "str", (), id="str"), 115 | pytest.param(None, "builtins", "None", (), id="None"), 116 | pytest.param(ModuleType, "types", "ModuleType", (), id="ModuleType"), 117 | pytest.param(FunctionType, "types", "FunctionType", (), id="FunctionType"), 118 | pytest.param(types.CodeType, "types", "CodeType", (), id="CodeType"), 119 | pytest.param(types.CoroutineType, "types", "CoroutineType", (), id="CoroutineType"), 120 | pytest.param(Any, "typing", "Any", (), id="Any"), 121 | pytest.param(AnyStr, "typing", "AnyStr", (), id="AnyStr"), 122 | pytest.param(Dict, "typing", "Dict", (), id="Dict"), 123 | pytest.param(Dict[str, int], "typing", "Dict", (str, int), id="Dict_parametrized"), 124 | pytest.param(Dict[T, int], "typing", "Dict", (T, int), id="Dict_typevar"), # type: ignore[valid-type] 125 | pytest.param(Tuple, "typing", "Tuple", (), id="Tuple"), 126 | pytest.param(Tuple[str, int], "typing", "Tuple", (str, int), id="Tuple_parametrized"), 127 | pytest.param(Union[str, int], "typing", "Union", (str, int), id="Union"), 128 | pytest.param(Callable, "collections.abc", "Callable", (), id="Callable"), 129 | pytest.param(Callable[..., str], "collections.abc", "Callable", (..., str), id="Callable_returntype"), 130 | pytest.param( 131 | Callable[[int, str], str], "collections.abc", "Callable", (int, str, str), id="Callable_all_types" 132 | ), 133 | pytest.param( 134 | Callable[[int, str], str], 135 | "collections.abc", 136 | "Callable", 137 | (int, str, str), 138 | id="collections.abc.Callable_all_types", 139 | ), 140 | pytest.param(re.Pattern, "re", "Pattern", (), id="Pattern"), 141 | pytest.param(re.Pattern[str], "re", "Pattern", (str,), id="Pattern_parametrized"), 142 | pytest.param(re.Match, "re", "Match", (), id="Match"), 143 | pytest.param(re.Match[str], "re", "Match", (str,), id="Match_parametrized"), 144 | pytest.param(IO, "typing", "IO", (), id="IO"), 145 | pytest.param(W, "typing", "NewType", (str,), id="W"), 146 | pytest.param(P, "typing", "ParamSpec", (), id="P"), 147 | pytest.param(P_args, "typing", "ParamSpecArgs", (), id="P_args"), 148 | pytest.param(P_kwargs, "typing", "ParamSpecKwargs", (), id="P_kwargs"), 149 | pytest.param(Metaclass, __name__, "Metaclass", (), id="Metaclass"), 150 | pytest.param(Slotted, __name__, "Slotted", (), id="Slotted"), 151 | pytest.param(A, __name__, "A", (), id="A"), 152 | pytest.param(B, __name__, "B", (), id="B"), 153 | pytest.param(C, __name__, "C", (), id="C"), 154 | pytest.param(D, __name__, "D", (), id="D"), 155 | pytest.param(E, __name__, "E", (), id="E"), 156 | pytest.param(E[int], __name__, "E", (int,), id="E_parametrized"), 157 | pytest.param(A.Inner, __name__, "A.Inner", (), id="Inner"), 158 | ], 159 | ) 160 | def test_parse_annotation(annotation: Any, module: str, class_name: str, args: tuple[Any, ...]) -> None: 161 | got_mod = get_annotation_module(annotation) 162 | got_cls = get_annotation_class_name(annotation, module) 163 | got_args = get_annotation_args(annotation, module, class_name) 164 | assert (got_mod, got_cls, got_args) == (module, class_name, args) 165 | 166 | 167 | _CASES = [ 168 | pytest.param(str, ":py:class:`str`", id="str"), 169 | pytest.param(int, ":py:class:`int`", id="int"), 170 | pytest.param(StringIO, ":py:class:`~io.StringIO`", id="StringIO"), 171 | pytest.param(EllipsisType, ":py:data:`~types.EllipsisType`", id="EllipsisType"), 172 | pytest.param(FunctionType, ":py:data:`~types.FunctionType`", id="FunctionType"), 173 | pytest.param(FrameType, ":py:data:`~types.FrameType`", id="FrameType"), 174 | pytest.param(ModuleType, ":py:class:`~types.ModuleType`", id="ModuleType"), 175 | pytest.param(NotImplementedType, ":py:data:`~types.NotImplementedType`", id="NotImplementedType"), 176 | pytest.param(TracebackType, ":py:class:`~types.TracebackType`", id="TracebackType"), 177 | pytest.param(type(None), ":py:obj:`None`", id="type None"), 178 | pytest.param(type, ":py:class:`type`", id="type"), 179 | pytest.param(Callable, ":py:class:`~collections.abc.Callable`", id="abc-Callable"), 180 | pytest.param(Type, ":py:class:`~typing.Type`", id="typing-Type"), 181 | pytest.param(Type[A], rf":py:class:`~typing.Type`\ \[:py:class:`~{__name__}.A`]", id="typing-A"), 182 | pytest.param(Any, ":py:data:`~typing.Any`", id="Any"), 183 | pytest.param(AnyStr, ":py:data:`~typing.AnyStr`", id="AnyStr"), 184 | pytest.param(Generic[T], r":py:class:`~typing.Generic`\ \[:py:class:`~typing.TypeVar`\ \(``T``)]", id="Generic"), 185 | pytest.param(Mapping, ":py:class:`~collections.abc.Mapping`", id="Mapping"), 186 | pytest.param( 187 | Mapping[T, int], # type: ignore[valid-type] 188 | r":py:class:`~collections.abc.Mapping`\ \[:py:class:`~typing.TypeVar`\ \(``T``), :py:class:`int`]", 189 | id="Mapping-T-int", 190 | ), 191 | pytest.param( 192 | Mapping[str, V_contra], # type: ignore[valid-type] 193 | r":py:class:`~collections.abc.Mapping`\ \[:py:class:`str`, :py:class:`~typing.TypeVar`\ \(" 194 | "``V_contra``, contravariant=True)]", 195 | id="Mapping-T-int-contra", 196 | ), 197 | pytest.param( 198 | Mapping[T, U_co], # type: ignore[valid-type] 199 | r":py:class:`~collections.abc.Mapping`\ \[:py:class:`~typing.TypeVar`\ \(``T``), " 200 | r":py:class:`~typing.TypeVar`\ \(``U_co``, covariant=True)]", 201 | id="Mapping-T-int-co", 202 | ), 203 | pytest.param( 204 | Mapping[str, bool], 205 | r":py:class:`~collections.abc.Mapping`\ \[:py:class:`str`, :py:class:`bool`]", 206 | id="Mapping-str-bool", 207 | ), 208 | pytest.param(Dict, ":py:class:`~typing.Dict`", id="Dict"), 209 | pytest.param( 210 | Dict[T, int], # type: ignore[valid-type] 211 | r":py:class:`~typing.Dict`\ \[:py:class:`~typing.TypeVar`\ \(``T``), :py:class:`int`]", 212 | id="Dict-T-int", 213 | ), 214 | pytest.param( 215 | Dict[str, V_contra], # type: ignore[valid-type] 216 | r":py:class:`~typing.Dict`\ \[:py:class:`str`, :py:class:`~typing.TypeVar`\ \(``V_contra``, " 217 | r"contravariant=True)]", 218 | id="Dict-T-int-contra", 219 | ), 220 | pytest.param( 221 | Dict[T, U_co], # type: ignore[valid-type] 222 | r":py:class:`~typing.Dict`\ \[:py:class:`~typing.TypeVar`\ \(``T``)," 223 | r" :py:class:`~typing.TypeVar`\ \(``U_co``, covariant=True)]", 224 | id="Dict-T-int-co", 225 | ), 226 | pytest.param( 227 | Dict[str, bool], 228 | r":py:class:`~typing.Dict`\ \[:py:class:`str`, :py:class:`bool`]", 229 | id="Dict-str-bool", 230 | ), 231 | pytest.param(Tuple, ":py:data:`~typing.Tuple`", id="Tuple"), 232 | pytest.param( 233 | Tuple[str, bool], 234 | r":py:data:`~typing.Tuple`\ \[:py:class:`str`, :py:class:`bool`]", 235 | id="Tuple-str-bool", 236 | ), 237 | pytest.param( 238 | Tuple[int, int, int], 239 | r":py:data:`~typing.Tuple`\ \[:py:class:`int`, :py:class:`int`, :py:class:`int`]", 240 | id="Tuple-int-int-int", 241 | ), 242 | pytest.param( 243 | Tuple[str, ...], 244 | r":py:data:`~typing.Tuple`\ \[:py:class:`str`, :py:data:`...`]", 245 | id="Tuple-str-Ellipsis", 246 | ), 247 | pytest.param(Union, ":py:data:`~typing.Union`", id="Union"), 248 | pytest.param( 249 | Union[str, bool], 250 | r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:class:`bool`]", 251 | id="Union-str-bool", 252 | ), 253 | pytest.param( 254 | Union[str, bool, None], 255 | r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:class:`bool`, :py:obj:`None`]", 256 | id="Union-str-bool-None", 257 | ), 258 | pytest.param( 259 | Union[str, Any], 260 | r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:data:`~typing.Any`]", 261 | id="Union-str-Any", 262 | ), 263 | pytest.param( 264 | Optional[str], 265 | r":py:data:`~typing.Optional`\ \[:py:class:`str`]", 266 | id="Optional-str", 267 | ), 268 | pytest.param( 269 | Union[str, None], 270 | r":py:data:`~typing.Optional`\ \[:py:class:`str`]", 271 | id="Optional-str-None", 272 | ), 273 | pytest.param( 274 | Optional[str | bool], 275 | r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:class:`bool`, :py:obj:`None`]", 276 | id="Optional-Union-str-bool", 277 | ), 278 | pytest.param(Callable, ":py:class:`~collections.abc.Callable`", id="Callable"), 279 | pytest.param( 280 | Callable[..., int], 281 | r":py:class:`~collections.abc.Callable`\ \[:py:data:`...`, :py:class:`int`]", 282 | id="Callable-Ellipsis-int", 283 | ), 284 | pytest.param( 285 | Callable[[int], int], 286 | r":py:class:`~collections.abc.Callable`\ \[\[:py:class:`int`], :py:class:`int`]", 287 | id="Callable-int-int", 288 | ), 289 | pytest.param( 290 | Callable[[int, str], bool], 291 | r":py:class:`~collections.abc.Callable`\ \[\[:py:class:`int`, :py:class:`str`], :py:class:`bool`]", 292 | id="Callable-int-str-bool", 293 | ), 294 | pytest.param( 295 | Callable[[int, str], None], 296 | r":py:class:`~collections.abc.Callable`\ \[\[:py:class:`int`, :py:class:`str`], :py:obj:`None`]", 297 | id="Callable-int-str", 298 | ), 299 | pytest.param( 300 | Callable[[T], T], 301 | r":py:class:`~collections.abc.Callable`\ \[\[:py:class:`~typing.TypeVar`\ \(``T``)]," 302 | r" :py:class:`~typing.TypeVar`\ \(``T``)]", 303 | id="Callable-T-T", 304 | ), 305 | pytest.param( 306 | Callable[[int, str], bool], 307 | r":py:class:`~collections.abc.Callable`\ \[\[:py:class:`int`, :py:class:`str`], :py:class:`bool`]", 308 | id="Callable-int-str-bool", 309 | ), 310 | pytest.param(re.Pattern, ":py:class:`~re.Pattern`", id="Pattern"), 311 | pytest.param(re.Pattern[str], r":py:class:`~re.Pattern`\ \[:py:class:`str`]", id="Pattern-str"), 312 | pytest.param(IO, ":py:class:`~typing.IO`", id="IO"), 313 | pytest.param(IO[str], r":py:class:`~typing.IO`\ \[:py:class:`str`]", id="IO-str"), 314 | pytest.param(Metaclass, f":py:class:`~{__name__}.Metaclass`", id="Metaclass"), 315 | pytest.param(A, f":py:class:`~{__name__}.A`", id="A"), 316 | pytest.param(B, f":py:class:`~{__name__}.B`", id="B"), 317 | pytest.param(B[int], rf":py:class:`~{__name__}.B`\ \[:py:class:`int`]", id="B-int"), 318 | pytest.param(C, f":py:class:`~{__name__}.C`", id="C"), 319 | pytest.param(D, f":py:class:`~{__name__}.D`", id="D"), 320 | pytest.param(E, f":py:class:`~{__name__}.E`", id="E"), 321 | pytest.param(E[int], rf":py:class:`~{__name__}.E`\ \[:py:class:`int`]", id="E-int"), 322 | pytest.param(W, r":py:class:`~typing.NewType`\ \(``W``, :py:class:`str`)", id="W"), 323 | pytest.param(T, r":py:class:`~typing.TypeVar`\ \(``T``)", id="T"), 324 | pytest.param(U_co, r":py:class:`~typing.TypeVar`\ \(``U_co``, covariant=True)", id="U-co"), 325 | pytest.param(V_contra, r":py:class:`~typing.TypeVar`\ \(``V_contra``, contravariant=True)", id="V-contra"), 326 | pytest.param(X, r":py:class:`~typing.TypeVar`\ \(``X``, :py:class:`str`, :py:class:`int`)", id="X"), 327 | pytest.param(Y, r":py:class:`~typing.TypeVar`\ \(``Y``, bound= :py:class:`str`)", id="Y"), 328 | pytest.param(Z, r":py:class:`~typing.TypeVar`\ \(``Z``, bound= A)", id="Z"), 329 | pytest.param(S, r":py:class:`~typing.TypeVar`\ \(``S``, bound= miss)", id="S"), 330 | # ParamSpec should behave like TypeVar, except for missing constraints 331 | pytest.param( 332 | P, rf":py:class:`~typing.ParamSpec`\ \(``P``{', bound= :py:obj:`None`' if PY312_PLUS else ''})", id="P" 333 | ), 334 | pytest.param( 335 | P_co, 336 | rf":py:class:`~typing.ParamSpec`\ \(``P_co``{', bound= :py:obj:`None`' if PY312_PLUS else ''}, covariant=True)", 337 | id="P_co", 338 | ), 339 | pytest.param( 340 | P_contra, 341 | rf":py:class:`~typing.ParamSpec`\ \(``P_contra``{', bound= :py:obj:`None`' if PY312_PLUS else ''}" 342 | ", contravariant=True)", 343 | id="P-contra", 344 | ), 345 | pytest.param(P_bound, r":py:class:`~typing.ParamSpec`\ \(``P_bound``, bound= :py:class:`str`)", id="P-bound"), 346 | # ## These test for correct internal tuple rendering, even if not all are valid Tuple types 347 | # Zero-length tuple remains 348 | pytest.param(Tuple[()], ":py:data:`~typing.Tuple`", id="Tuple-p"), 349 | # Internal single tuple with simple types is flattened in the output 350 | pytest.param(Tuple[int,], r":py:data:`~typing.Tuple`\ \[:py:class:`int`]", id="Tuple-p-int"), 351 | pytest.param( 352 | Tuple[int, int], 353 | r":py:data:`~typing.Tuple`\ \[:py:class:`int`, :py:class:`int`]", 354 | id="Tuple-p-int-int", 355 | ), 356 | # Ellipsis in single tuple also gets flattened 357 | pytest.param( 358 | Tuple[int, ...], 359 | r":py:data:`~typing.Tuple`\ \[:py:class:`int`, :py:data:`...`]", 360 | id="Tuple-p-Ellipsis", 361 | ), 362 | pytest.param( 363 | RecList, r":py:data:`~typing.Union`\ \[:py:class:`int`, :py:class:`~typing.List`\ \[RecList]]", id="RecList" 364 | ), 365 | pytest.param( 366 | MutualRecA, 367 | r":py:data:`~typing.Union`\ \[:py:class:`bool`, :py:class:`~typing.List`\ \[MutualRecB]]", 368 | id="MutualRecA", 369 | ), 370 | ] 371 | 372 | 373 | @pytest.mark.parametrize(("annotation", "expected_result"), _CASES) 374 | def test_format_annotation(inv: Inventory, annotation: Any, expected_result: str) -> None: 375 | conf = create_autospec(Config, _annotation_globals=globals(), always_use_bars_union=False) 376 | result = format_annotation(annotation, conf) 377 | assert result == expected_result 378 | 379 | # Test with the "simplify_optional_unions" flag turned off: 380 | if re.match(r"^:py:data:`~typing\.Union`\\\[.*``None``.*]", expected_result): 381 | # strip None - argument and copy string to avoid conflicts with 382 | # subsequent tests 383 | expected_result_not_simplified = expected_result.replace(", ``None``", "") 384 | # encapsulate Union in typing.Optional 385 | expected_result_not_simplified += ":py:data:`~typing.Optional`\\ \\[" 386 | expected_result_not_simplified += "]" 387 | conf = create_autospec( 388 | Config, 389 | simplify_optional_unions=False, 390 | _annotation_globals=globals(), 391 | always_use_bars_union=False, 392 | ) 393 | assert format_annotation(annotation, conf) == expected_result_not_simplified 394 | 395 | # Test with the "fully_qualified" flag turned on 396 | if "typing" in expected_result_not_simplified: 397 | expected_result_not_simplified = expected_result_not_simplified.replace("~typing", "typing") 398 | conf = create_autospec( 399 | Config, 400 | typehints_fully_qualified=True, 401 | simplify_optional_unions=False, 402 | _annotation_globals=globals(), 403 | ) 404 | assert format_annotation(annotation, conf) == expected_result_not_simplified 405 | 406 | # Test with the "fully_qualified" flag turned on 407 | if "typing" in expected_result or __name__ in expected_result: 408 | expected_result = expected_result.replace("~typing", "typing") 409 | expected_result = expected_result.replace("~collections.abc", "collections.abc") 410 | expected_result = expected_result.replace("~numpy", "numpy") 411 | expected_result = expected_result.replace("~" + __name__, __name__) 412 | conf = create_autospec( 413 | Config, 414 | typehints_fully_qualified=True, 415 | _annotation_globals=globals(), 416 | always_use_bars_union=False, 417 | ) 418 | assert format_annotation(annotation, conf) == expected_result 419 | 420 | # Test for the correct role (class vs data) using the official Sphinx inventory 421 | if any(modname in expected_result for modname in ("typing", "types")): 422 | m = re.match(r"^:py:(?Pclass|data|func):`~(?P[^`]+)`", result) 423 | assert m, "No match" 424 | name = m.group("name") 425 | expected_role = next((o.role for o in inv.objects if o.name == name), None) 426 | if expected_role: 427 | if expected_role == "function": 428 | expected_role = "func" 429 | 430 | assert m.group("role") == expected_role 431 | 432 | 433 | @pytest.mark.parametrize( 434 | ("annotation", "expected_result"), 435 | [ 436 | ("int | float", ":py:class:`int` | :py:class:`float`"), 437 | ("int | float | None", ":py:class:`int` | :py:class:`float` | :py:obj:`None`"), 438 | ("Union[int, float]", ":py:class:`int` | :py:class:`float`"), 439 | ("Union[int, float, None]", ":py:class:`int` | :py:class:`float` | :py:obj:`None`"), 440 | ("Optional[int | float]", ":py:class:`int` | :py:class:`float` | :py:obj:`None`"), 441 | ("Optional[Union[int, float]]", ":py:class:`int` | :py:class:`float` | :py:obj:`None`"), 442 | ("Union[int | float, str]", ":py:class:`int` | :py:class:`float` | :py:class:`str`"), 443 | ("Union[int, float] | str", ":py:class:`int` | :py:class:`float` | :py:class:`str`"), 444 | ], 445 | ) 446 | def test_always_use_bars_union(annotation: str, expected_result: str) -> None: 447 | conf = create_autospec(Config, always_use_bars_union=True) 448 | result = format_annotation(eval(annotation), conf) # noqa: S307 449 | assert result == expected_result 450 | 451 | 452 | @pytest.mark.parametrize("library", [typing, typing_extensions], ids=["typing", "typing_extensions"]) 453 | @pytest.mark.parametrize( 454 | ("annotation", "params", "expected_result"), 455 | [ 456 | pytest.param("ClassVar", int, ":py:data:`~typing.ClassVar`\\ \\[:py:class:`int`]", id="ClassVar"), 457 | pytest.param("NoReturn", None, ":py:data:`~typing.NoReturn`", id="NoReturn"), 458 | pytest.param("Literal", ("a", 1), ":py:data:`~typing.Literal`\\ \\[``'a'``, ``1``]", id="Literal"), 459 | pytest.param("Type", None, ":py:class:`~typing.Type`", id="Type-none"), 460 | pytest.param("Type", (A,), rf":py:class:`~typing.Type`\ \[:py:class:`~{__name__}.A`]", id="Type-A"), 461 | ], 462 | ) 463 | def test_format_annotation_both_libs(library: ModuleType, annotation: str, params: Any, expected_result: str) -> None: 464 | try: 465 | annotation_cls = getattr(library, annotation) 466 | except AttributeError: 467 | pytest.skip(f"{annotation} not available in the {library.__name__} module") 468 | return # pragma: no cover 469 | 470 | ann = annotation_cls if params is None else annotation_cls[params] 471 | result = format_annotation(ann, create_autospec(Config)) 472 | assert result == expected_result 473 | 474 | 475 | def test_process_docstring_slot_wrapper() -> None: 476 | lines: list[str] = [] 477 | config = create_autospec( 478 | Config, 479 | typehints_fully_qualified=False, 480 | simplify_optional_unions=False, 481 | typehints_formatter=None, 482 | autodoc_mock_imports=[], 483 | ) 484 | app: Sphinx = create_autospec(Sphinx, config=config) 485 | process_docstring(app, "class", "SlotWrapper", Slotted, None, lines) 486 | assert not lines 487 | 488 | 489 | def set_python_path() -> None: 490 | test_path = Path(__file__).parent 491 | # Add test directory to sys.path to allow imports of dummy module. 492 | if str(test_path) not in sys.path: 493 | sys.path.insert(0, str(test_path)) 494 | 495 | 496 | @pytest.mark.parametrize("always_document_param_types", [True, False], ids=["doc_param_type", "no_doc_param_type"]) 497 | @pytest.mark.sphinx("text", testroot="dummy") 498 | @patch("sphinx.writers.text.MAXWIDTH", 2000) 499 | def test_always_document_param_types( 500 | app: SphinxTestApp, 501 | status: StringIO, 502 | warning: StringIO, 503 | always_document_param_types: bool, 504 | ) -> None: 505 | set_python_path() 506 | 507 | app.config.always_document_param_types = always_document_param_types # create flag 508 | app.config.autodoc_mock_imports = ["mailbox"] # create flag 509 | 510 | # Prevent "document isn't included in any toctree" warnings 511 | for f in Path(app.srcdir).glob("*.rst"): 512 | f.unlink() 513 | (Path(app.srcdir) / "index.rst").write_text( 514 | dedent( 515 | """ 516 | .. autofunction:: dummy_module.undocumented_function 517 | 518 | .. autoclass:: dummy_module.DataClass 519 | :undoc-members: 520 | :special-members: __init__ 521 | """, 522 | ), 523 | ) 524 | 525 | app.build() 526 | 527 | assert "build succeeded" in status.getvalue() # Build succeeded 528 | assert not warning.getvalue().strip() 529 | 530 | format_args = {} 531 | for indentation_level in range(2): 532 | key = f"undoc_params_{indentation_level}" 533 | if always_document_param_types: 534 | format_args[key] = indent('\n\n Parameters:\n **x** ("int")', " " * indentation_level) 535 | else: 536 | format_args[key] = "" 537 | 538 | contents = (Path(app.srcdir) / "_build/text/index.txt").read_text() 539 | expected_contents = """\ 540 | dummy_module.undocumented_function(x) 541 | 542 | Hi{undoc_params_0} 543 | 544 | Return type: 545 | "str" 546 | 547 | class dummy_module.DataClass(x) 548 | 549 | Class docstring.{undoc_params_0} 550 | 551 | __init__(x){undoc_params_1} 552 | """ 553 | expected_contents = dedent(expected_contents).format(**format_args) 554 | assert contents == expected_contents 555 | 556 | 557 | @pytest.mark.sphinx("text", testroot="dummy") 558 | @patch("sphinx.writers.text.MAXWIDTH", 2000) 559 | def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) -> None: 560 | set_python_path() 561 | 562 | app.config.master_doc = "future_annotations" # create flag 563 | app.build() 564 | 565 | assert "build succeeded" in status.getvalue() # Build succeeded 566 | 567 | contents = (Path(app.srcdir) / "_build/text/future_annotations.txt").read_text() 568 | expected_contents = """\ 569 | Dummy Module 570 | ************ 571 | 572 | dummy_module_future_annotations.function_with_py310_annotations(self, x, y, z=None) 573 | 574 | Method docstring. 575 | 576 | Parameters: 577 | * **x** ("bool" | "None") -- foo 578 | 579 | * **y** ("int" | "str" | "float") -- bar 580 | 581 | * **z** ("str" | "None") -- baz 582 | 583 | Return type: 584 | "str" 585 | """ 586 | expected_contents = dedent(expected_contents) 587 | expected_contents = dedent(expected_contents) 588 | assert contents == expected_contents 589 | 590 | 591 | @pytest.mark.sphinx("pseudoxml", testroot="dummy") 592 | @patch("sphinx.writers.text.MAXWIDTH", 2000) 593 | def test_sphinx_output_default_role(app: SphinxTestApp, status: StringIO) -> None: 594 | set_python_path() 595 | 596 | app.config.master_doc = "simple_default_role" # create flag 597 | app.config.default_role = "literal" 598 | app.build() 599 | 600 | assert "build succeeded" in status.getvalue() # Build succeeded 601 | 602 | contents_lines = ( 603 | (Path(app.srcdir) / "_build/pseudoxml/simple_default_role.pseudoxml").read_text(encoding="utf-8").splitlines() 604 | ) 605 | list_item_idxs = [i for i, line in enumerate(contents_lines) if line.strip() == ""] 606 | foo_param = dedent("\n".join(contents_lines[list_item_idxs[0] : list_item_idxs[1]])) 607 | expected_foo_param = """\ 608 | 609 | 610 | 611 | x 612 | ( 613 | 614 | 615 | bool 616 | ) 617 | \N{EN DASH}\N{SPACE} 618 | 619 | foo 620 | """.rstrip() 621 | expected_foo_param = dedent(expected_foo_param) 622 | assert foo_param == expected_foo_param 623 | 624 | 625 | @pytest.mark.parametrize( 626 | ("defaults_config_val", "expected"), 627 | [ 628 | (None, '("int") -- bar'), 629 | ("comma", '("int", default: "1") -- bar'), 630 | ("braces", '("int" (default: "1")) -- bar'), 631 | ("braces-after", '("int") -- bar (default: "1")'), 632 | ("comma-after", Exception("needs to be one of")), 633 | ], 634 | ) 635 | @pytest.mark.sphinx("text", testroot="dummy") 636 | @patch("sphinx.writers.text.MAXWIDTH", 2000) 637 | def test_sphinx_output_defaults( 638 | app: SphinxTestApp, 639 | status: StringIO, 640 | defaults_config_val: str, 641 | expected: str | Exception, 642 | ) -> None: 643 | set_python_path() 644 | 645 | app.config.master_doc = "simple" # create flag 646 | app.config.typehints_defaults = defaults_config_val # create flag 647 | if isinstance(expected, Exception): 648 | with pytest.raises(Exception, match=re.escape(str(expected))): 649 | app.build() 650 | return 651 | app.build() 652 | assert "build succeeded" in status.getvalue() 653 | 654 | contents = (Path(app.srcdir) / "_build/text/simple.txt").read_text() 655 | expected_contents = f"""\ 656 | Simple Module 657 | ************* 658 | 659 | dummy_module_simple.function(x, y=1) 660 | 661 | Function docstring. 662 | 663 | Parameters: 664 | * **x** ("bool") -- foo 665 | 666 | * **y** {expected} 667 | 668 | Return type: 669 | "str" 670 | """ 671 | assert contents == dedent(expected_contents) 672 | 673 | 674 | @pytest.mark.parametrize( 675 | ("formatter_config_val", "expected"), 676 | [ 677 | (None, ['("bool") -- foo', '("int") -- bar', '"str"']), 678 | (lambda ann, conf: "Test", ["(Test) -- foo", "(Test) -- bar", "Test"]), # noqa: ARG005 679 | ("some string", Exception("needs to be callable or `None`")), 680 | ], 681 | ) 682 | @pytest.mark.sphinx("text", testroot="dummy") 683 | @patch("sphinx.writers.text.MAXWIDTH", 2000) 684 | def test_sphinx_output_formatter( 685 | app: SphinxTestApp, 686 | status: StringIO, 687 | formatter_config_val: str, 688 | expected: tuple[str, ...] | Exception, 689 | ) -> None: 690 | set_python_path() 691 | 692 | app.config.master_doc = "simple" # create flag 693 | app.config.typehints_formatter = formatter_config_val # create flag 694 | if isinstance(expected, Exception): 695 | with pytest.raises(Exception, match=re.escape(str(expected))): 696 | app.build() 697 | return 698 | app.build() 699 | assert "build succeeded" in status.getvalue() 700 | 701 | contents = (Path(app.srcdir) / "_build/text/simple.txt").read_text() 702 | expected_contents = f"""\ 703 | Simple Module 704 | ************* 705 | 706 | dummy_module_simple.function(x, y=1) 707 | 708 | Function docstring. 709 | 710 | Parameters: 711 | * **x** {expected[0]} 712 | 713 | * **y** {expected[1]} 714 | 715 | Return type: 716 | {expected[2]} 717 | """ 718 | assert contents == dedent(expected_contents) 719 | 720 | 721 | def test_normalize_source_lines_async_def() -> None: 722 | source = """ 723 | async def async_function(): 724 | class InnerClass: 725 | def __init__(self): ... 726 | """ 727 | 728 | expected = """ 729 | async def async_function(): 730 | class InnerClass: 731 | def __init__(self): ... 732 | """ 733 | 734 | assert normalize_source_lines(dedent(source)) == dedent(expected) 735 | 736 | 737 | def test_normalize_source_lines_def_starting_decorator_parameter() -> None: 738 | source = """ 739 | @_with_parameters( 740 | _Parameter("self", _Parameter.POSITIONAL_OR_KEYWORD), 741 | *_proxy_instantiation_parameters, 742 | _project_id, 743 | _Parameter( 744 | "node_numbers", 745 | _Parameter.POSITIONAL_OR_KEYWORD, 746 | default=None, 747 | annotation=Optional[Iterable[int]], 748 | ), 749 | ) 750 | def __init__(bound_args): # noqa: N805 751 | ... 752 | """ 753 | 754 | expected = """ 755 | @_with_parameters( 756 | _Parameter("self", _Parameter.POSITIONAL_OR_KEYWORD), 757 | *_proxy_instantiation_parameters, 758 | _project_id, 759 | _Parameter( 760 | "node_numbers", 761 | _Parameter.POSITIONAL_OR_KEYWORD, 762 | default=None, 763 | annotation=Optional[Iterable[int]], 764 | ), 765 | ) 766 | def __init__(bound_args): # noqa: N805 767 | ... 768 | """ 769 | 770 | assert normalize_source_lines(dedent(source)) == dedent(expected) 771 | 772 | 773 | @pytest.mark.parametrize("obj", [cmp_to_key, 1]) 774 | def test_default_no_signature(obj: Any) -> None: 775 | config = create_autospec( 776 | Config, 777 | typehints_fully_qualified=False, 778 | simplify_optional_unions=False, 779 | typehints_formatter=None, 780 | autodoc_mock_imports=[], 781 | ) 782 | app: Sphinx = create_autospec(Sphinx, config=config) 783 | lines: list[str] = [] 784 | process_docstring(app, "what", "name", obj, None, lines) 785 | assert lines == [] 786 | 787 | 788 | @pytest.mark.parametrize("method", [HintedMethods.from_magic, HintedMethods().method]) 789 | def test_bound_class_method(method: FunctionType) -> None: 790 | config = create_autospec( 791 | Config, 792 | typehints_fully_qualified=False, 793 | simplify_optional_unions=False, 794 | typehints_document_rtype=False, 795 | always_document_param_types=True, 796 | typehints_defaults=True, 797 | typehints_formatter=None, 798 | autodoc_mock_imports=[], 799 | ) 800 | app: Sphinx = create_autospec(Sphinx, config=config) 801 | process_docstring(app, "class", method.__qualname__, method, None, []) 802 | 803 | 804 | def test_syntax_error_backfill() -> None: 805 | # Regression test for #188 806 | # fmt: off 807 | def func(x): # type: ignore[no-untyped-def] # noqa: ANN001, ANN202 808 | return x 809 | 810 | # fmt: on 811 | backfill_type_hints(func, "func") 812 | 813 | 814 | @pytest.mark.sphinx("text", testroot="resolve-typing-guard") 815 | def test_resolve_typing_guard_imports(app: SphinxTestApp, status: StringIO, warning: StringIO) -> None: 816 | set_python_path() 817 | app.config.autodoc_mock_imports = ["viktor"] # create flag 818 | app.build() 819 | out = status.getvalue() 820 | assert "build succeeded" in out 821 | err = warning.getvalue() 822 | r = re.compile(r"WARNING: Failed guarded type import") 823 | assert len(r.findall(err)) == 1 824 | pat = r'WARNING: Failed guarded type import with ImportError\("cannot import name \'missing\' from \'functools\'' 825 | assert re.search(pat, err) 826 | 827 | 828 | @pytest.mark.sphinx("text", testroot="resolve-typing-guard-tmp") 829 | def test_resolve_typing_guard_attrs_imports(app: SphinxTestApp, status: StringIO, warning: StringIO) -> None: 830 | set_python_path() 831 | app.build() 832 | assert "build succeeded" in status.getvalue() 833 | assert not warning.getvalue() 834 | 835 | 836 | def test_no_source_code_type_guard() -> None: 837 | from csv import Error # noqa: PLC0415 838 | 839 | _resolve_type_guarded_imports([], Error) 840 | 841 | 842 | @pytest.mark.sphinx("text", testroot="dummy") 843 | @patch("sphinx.writers.text.MAXWIDTH", 2000) 844 | def test_sphinx_output_formatter_no_use_rtype(app: SphinxTestApp, status: StringIO) -> None: 845 | set_python_path() 846 | app.config.master_doc = "simple_no_use_rtype" # create flag 847 | app.config.typehints_use_rtype = False 848 | app.build() 849 | assert "build succeeded" in status.getvalue() 850 | text_path = Path(app.srcdir) / "_build" / "text" / "simple_no_use_rtype.txt" 851 | text_contents = text_path.read_text().replace("–", "--") # noqa: RUF001 # keep ambiguous EN DASH 852 | expected_contents = """\ 853 | Simple Module 854 | ************* 855 | 856 | dummy_module_simple_no_use_rtype.function_no_returns(x, y=1) 857 | 858 | Function docstring. 859 | 860 | Parameters: 861 | * **x** ("bool") -- foo 862 | 863 | * **y** ("int") -- bar 864 | 865 | Return type: 866 | "str" 867 | 868 | dummy_module_simple_no_use_rtype.function_returns_with_type(x, y=1) 869 | 870 | Function docstring. 871 | 872 | Parameters: 873 | * **x** ("bool") -- foo 874 | 875 | * **y** ("int") -- bar 876 | 877 | Returns: 878 | *CustomType* -- A string 879 | 880 | dummy_module_simple_no_use_rtype.function_returns_with_compound_type(x, y=1) 881 | 882 | Function docstring. 883 | 884 | Parameters: 885 | * **x** ("bool") -- foo 886 | 887 | * **y** ("int") -- bar 888 | 889 | Returns: 890 | Union[str, int] -- A string or int 891 | 892 | dummy_module_simple_no_use_rtype.function_returns_without_type(x, y=1) 893 | 894 | Function docstring. 895 | 896 | Parameters: 897 | * **x** ("bool") -- foo 898 | 899 | * **y** ("int") -- bar 900 | 901 | Returns: 902 | "str" -- A string 903 | """ 904 | assert text_contents == dedent(expected_contents) 905 | 906 | 907 | @pytest.mark.sphinx("text", testroot="dummy") 908 | @patch("sphinx.writers.text.MAXWIDTH", 2000) 909 | def test_sphinx_output_with_use_signature(app: SphinxTestApp, status: StringIO) -> None: 910 | set_python_path() 911 | app.config.master_doc = "simple" # create flag 912 | app.config.typehints_use_signature = True 913 | app.build() 914 | assert "build succeeded" in status.getvalue() 915 | text_path = Path(app.srcdir) / "_build" / "text" / "simple.txt" 916 | text_contents = text_path.read_text().replace("–", "--") # noqa: RUF001 # keep ambiguous EN DASH 917 | expected_contents = """\ 918 | Simple Module 919 | ************* 920 | 921 | dummy_module_simple.function(x: bool, y: int = 1) 922 | 923 | Function docstring. 924 | 925 | Parameters: 926 | * **x** ("bool") -- foo 927 | 928 | * **y** ("int") -- bar 929 | 930 | Return type: 931 | "str" 932 | """ 933 | assert text_contents == dedent(expected_contents) 934 | 935 | 936 | @pytest.mark.sphinx("text", testroot="dummy") 937 | @patch("sphinx.writers.text.MAXWIDTH", 2000) 938 | def test_sphinx_output_with_use_signature_return(app: SphinxTestApp, status: StringIO) -> None: 939 | set_python_path() 940 | app.config.master_doc = "simple" # create flag 941 | app.config.typehints_use_signature_return = True 942 | app.build() 943 | assert "build succeeded" in status.getvalue() 944 | text_path = Path(app.srcdir) / "_build" / "text" / "simple.txt" 945 | text_contents = text_path.read_text().replace("–", "--") # noqa: RUF001 # keep ambiguous EN DASH 946 | expected_contents = """\ 947 | Simple Module 948 | ************* 949 | 950 | dummy_module_simple.function(x, y=1) -> str 951 | 952 | Function docstring. 953 | 954 | Parameters: 955 | * **x** ("bool") -- foo 956 | 957 | * **y** ("int") -- bar 958 | 959 | Return type: 960 | "str" 961 | """ 962 | assert text_contents == dedent(expected_contents) 963 | 964 | 965 | @pytest.mark.sphinx("text", testroot="dummy") 966 | @patch("sphinx.writers.text.MAXWIDTH", 2000) 967 | def test_sphinx_output_with_use_signature_and_return(app: SphinxTestApp, status: StringIO) -> None: 968 | set_python_path() 969 | app.config.master_doc = "simple" # create flag 970 | app.config.typehints_use_signature = True 971 | app.config.typehints_use_signature_return = True 972 | app.build() 973 | assert "build succeeded" in status.getvalue() 974 | text_path = Path(app.srcdir) / "_build" / "text" / "simple.txt" 975 | text_contents = text_path.read_text().replace("–", "--") # noqa: RUF001 # keep ambiguous EN DASH 976 | expected_contents = """\ 977 | Simple Module 978 | ************* 979 | 980 | dummy_module_simple.function(x: bool, y: int = 1) -> str 981 | 982 | Function docstring. 983 | 984 | Parameters: 985 | * **x** ("bool") -- foo 986 | 987 | * **y** ("int") -- bar 988 | 989 | Return type: 990 | "str" 991 | """ 992 | assert text_contents == dedent(expected_contents) 993 | 994 | 995 | @pytest.mark.sphinx("text", testroot="dummy") 996 | @patch("sphinx.writers.text.MAXWIDTH", 2000) 997 | def test_default_annotation_without_typehints(app: SphinxTestApp, status: StringIO) -> None: 998 | set_python_path() 999 | app.config.master_doc = "without_complete_typehints" # create flag 1000 | app.config.typehints_defaults = "comma" 1001 | app.build() 1002 | assert "build succeeded" in status.getvalue() 1003 | text_path = Path(app.srcdir) / "_build" / "text" / "without_complete_typehints.txt" 1004 | text_contents = text_path.read_text().replace("–", "--") # noqa: RUF001 # keep ambiguous EN DASH 1005 | expected_contents = """\ 1006 | Simple Module 1007 | ************* 1008 | 1009 | dummy_module_without_complete_typehints.function_with_some_defaults_and_without_typehints(x, y=None) 1010 | 1011 | Function docstring. 1012 | 1013 | Parameters: 1014 | * **x** -- foo 1015 | 1016 | * **y** (default: "None") -- bar 1017 | 1018 | dummy_module_without_complete_typehints.function_with_some_defaults_and_some_typehints(x, y=None) 1019 | 1020 | Function docstring. 1021 | 1022 | Parameters: 1023 | * **x** ("int") -- foo 1024 | 1025 | * **y** (default: "None") -- bar 1026 | 1027 | dummy_module_without_complete_typehints.function_with_some_defaults_and_more_typehints(x, y=None) 1028 | 1029 | Function docstring. 1030 | 1031 | Parameters: 1032 | * **x** ("int") -- foo 1033 | 1034 | * **y** (default: "None") -- bar 1035 | 1036 | Return type: 1037 | "str" 1038 | 1039 | dummy_module_without_complete_typehints.function_with_defaults_and_some_typehints(x=0, y=None) 1040 | 1041 | Function docstring. 1042 | 1043 | Parameters: 1044 | * **x** ("int", default: "0") -- foo 1045 | 1046 | * **y** (default: "None") -- bar 1047 | 1048 | Return type: 1049 | "str" 1050 | """ 1051 | assert text_contents == dedent(expected_contents) 1052 | 1053 | 1054 | @pytest.mark.sphinx("text", testroot="dummy") 1055 | @patch("sphinx.writers.text.MAXWIDTH", 2000) 1056 | def test_wrong_module_path(app: SphinxTestApp, status: StringIO, warning: StringIO) -> None: 1057 | set_python_path() 1058 | 1059 | app.config.master_doc = "wrong_module_path" # create flag 1060 | app.config.default_role = "literal" 1061 | app.config.nitpicky = True 1062 | app.config.nitpick_ignore = {("py:data", "typing.Optional")} 1063 | 1064 | def fixup_module_name(mod: str) -> str: 1065 | if not mod.startswith("wrong_module_path"): 1066 | return mod 1067 | return "export_module" + mod.removeprefix("wrong_module_path") 1068 | 1069 | app.config.suppress_warnings = ["config.cache"] 1070 | app.config.typehints_fixup_module_name = fixup_module_name 1071 | app.build() 1072 | 1073 | assert "build succeeded" in status.getvalue() # Build succeeded 1074 | assert not warning.getvalue().strip() 1075 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | def test_version() -> None: 5 | from sphinx_autodoc_typehints import __version__ # noqa: PLC0415 6 | 7 | assert __version__ 8 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4.24.1 4 | tox-uv>=1.24 5 | env_list = 6 | fix 7 | 3.13 8 | 3.12 9 | 3.11 10 | type 11 | pkg_meta 12 | skip_missing_interpreters = true 13 | 14 | [testenv] 15 | description = run the unit tests with pytest under {base_python} 16 | package = wheel 17 | wheel_build_env = .pkg 18 | extras = 19 | numpy 20 | testing 21 | type-comment 22 | pass_env = 23 | DIFF_AGAINST 24 | PYTEST_* 25 | set_env = 26 | COVERAGE_FILE = {work_dir}/.coverage.{env_name} 27 | commands = 28 | python -m pytest {tty:--color=yes} {posargs: \ 29 | --cov {env_site_packages_dir}{/}sphinx_autodoc_typehints --cov {tox_root}{/}tests \ 30 | --cov-config=pyproject.toml --no-cov-on-fail --cov-report term-missing:skip-covered --cov-context=test \ 31 | --cov-report html:{env_tmp_dir}{/}htmlcov --cov-report xml:{work_dir}{/}coverage.{env_name}.xml \ 32 | --junitxml {work_dir}{/}junit.{env_name}.xml \ 33 | tests} 34 | diff-cover --compare-branch {env:DIFF_AGAINST:origin/main} {work_dir}{/}coverage.{env_name}.xml --fail-under 100 35 | 36 | [testenv:fix] 37 | description = format the code base to adhere to our styles, and complain about what we cannot do automatically 38 | skip_install = true 39 | deps = 40 | pre-commit-uv>=4.1.4 41 | commands = 42 | pre-commit run --all-files --show-diff-on-failure 43 | 44 | [testenv:type] 45 | description = run type check on code base 46 | deps = 47 | mypy==1.15 48 | types-docutils>=0.21.0.20241128 49 | commands = 50 | mypy src 51 | mypy tests 52 | 53 | [testenv:pkg_meta] 54 | description = check that the long description is valid 55 | skip_install = true 56 | deps = 57 | check-wheel-contents>=0.6.1 58 | twine>=6.1 59 | uv>=0.6.1 60 | commands = 61 | uv build --sdist --wheel --out-dir {env_tmp_dir} . 62 | twine check {env_tmp_dir}{/}* 63 | check-wheel-contents --no-config {env_tmp_dir} 64 | 65 | [testenv:dev] 66 | description = generate a DEV environment 67 | package = editable 68 | commands = 69 | uv pip tree 70 | python -c 'import sys; print(sys.executable)' 71 | -------------------------------------------------------------------------------- /whitelist.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tox-dev/sphinx-autodoc-typehints/3c82b0daea54f61af483ef8011a69a3b72d74c17/whitelist.txt --------------------------------------------------------------------------------