sphinx_autodoc_typehints-3.8.0/.markdownlint.yaml0000644000000000000000000000015013615410400017247 0ustar00MD013: code_blocks: false headers: false line_length: 120 tables: false MD046: style: fenced sphinx_autodoc_typehints-3.8.0/.pre-commit-config.yaml0000644000000000000000000000227313615410400020065 0ustar00repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema rev: "0.36.2" hooks: - id: check-github-workflows args: ["--verbose"] - repo: https://github.com/codespell-project/codespell rev: v2.4.1 hooks: - id: codespell additional_dependencies: ["tomli>=2.4"] - repo: https://github.com/tox-dev/tox-toml-fmt rev: "v1.7.2" hooks: - id: tox-toml-fmt - repo: https://github.com/tox-dev/pyproject-fmt rev: "v2.16.2" hooks: - id: pyproject-fmt - repo: https://github.com/astral-sh/ruff-pre-commit rev: "v0.15.2" hooks: - id: ruff-format - id: ruff-check args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] - repo: https://github.com/rbubley/mirrors-prettier rev: "v3.8.1" # Use the sha / tag you want to point at hooks: - id: prettier additional_dependencies: - prettier@3.8.1 - "@prettier/plugin-xml@3.4.2" - repo: meta hooks: - id: check-hooks-apply - id: check-useless-excludes sphinx_autodoc_typehints-3.8.0/ignore-words.txt0000644000000000000000000000001613615410400016755 0ustar00master manuel sphinx_autodoc_typehints-3.8.0/tox.toml0000644000000000000000000000421013615410400015304 0ustar00requires = [ "tox>=4.45", "tox-uv>=1.31", ] env_list = [ "3.14", "3.13", "3.12", "3.14t", "fix", "pkg_meta", "type", ] skip_missing_interpreters = true [env_run_base] description = "run the unit tests with pytest under {base_python}" package = "wheel" wheel_build_env = ".pkg" dependency_groups = [ "test" ] pass_env = [ "DIFF_AGAINST", "PYTEST_*" ] set_env = { COVERAGE_FILE = "{work_dir}/.coverage.{env_name}" } commands = [ [ "python", "-m", "pytest", "{tty:--color=yes}", { replace = "posargs", default = [ "--cov", "{env_site_packages_dir}{/}sphinx_autodoc_typehints", "--cov", "{tox_root}{/}tests", "--cov-config=pyproject.toml", "--no-cov-on-fail", "--cov-report", "term-missing:skip-covered", "--cov-context=test", "--cov-report", "html:{env_tmp_dir}{/}htmlcov", "--cov-report", "xml:{work_dir}{/}coverage.{env_name}.xml", "--junitxml", "{work_dir}{/}junit.{env_name}.xml", "tests", ], extend = true }, ], [ "diff-cover", "--compare-branch", "{env:DIFF_AGAINST:origin/main}", "{work_dir}{/}coverage.{env_name}.xml", "--fail-under", "100", ], ] [env.fix] description = "format the code base to adhere to our styles, and complain about what we cannot do automatically" skip_install = true dependency_groups = [ "lint" ] commands = [ [ "pre-commit", "run", "--all-files", "--show-diff-on-failure" ] ] [env.pkg_meta] description = "check that the long description is valid" skip_install = true dependency_groups = [ "pkg-meta" ] commands = [ [ "uv", "build", "--sdist", "--wheel", "--out-dir", "{env_tmp_dir}", "." ], [ "twine", "check", "{env_tmp_dir}{/}*" ], [ "check-wheel-contents", "--no-config", "{env_tmp_dir}" ], ] [env.type] description = "run type check on code base" dependency_groups = [ "type" ] commands = [ [ "ty", "check", "--output-format", "concise", "--error-on-warning", "." ] ] [env.dev] description = "generate a DEV environment" package = "editable" dependency_groups = [ "dev" ] commands = [ [ "uv", "pip", "tree" ], [ "python", "-c", "import sys; print(sys.executable)" ] ] sphinx_autodoc_typehints-3.8.0/whitelist.txt0000644000000000000000000000000013615410400016343 0ustar00sphinx_autodoc_typehints-3.8.0/.github/FUNDING.yml0000644000000000000000000000005213615410400016752 0ustar00tidelift: "pypi/sphinx-autodoc-typehints" sphinx_autodoc_typehints-3.8.0/.github/SECURITY.md0000644000000000000000000000055513615410400016736 0ustar00# Security Policy ## Supported Versions | Version | Supported | | ------- | ------------------ | | 1.18 + | :white_check_mark: | | < 1.18 | :x: | ## Reporting a Vulnerability To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. sphinx_autodoc_typehints-3.8.0/.github/dependabot.yml0000644000000000000000000000016513615410400017772 0ustar00version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" sphinx_autodoc_typehints-3.8.0/.github/release.yml0000644000000000000000000000012613615410400017302 0ustar00changelog: exclude: authors: - dependabot[bot] - pre-commit-ci[bot] sphinx_autodoc_typehints-3.8.0/.github/workflows/check.yaml0000644000000000000000000000264613615410400021146 0ustar00name: check on: workflow_dispatch: push: branches: ["main"] tags-ignore: ["**"] pull_request: schedule: - cron: "0 8 * * *" concurrency: group: check-${{ github.ref }} cancel-in-progress: true jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: env: - "3.14t" - "3.14" - "3.13" - "3.12" - type - dev - pkg_meta steps: - name: Install OS dependencies run: sudo apt-get install graphviz -y - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install the latest version of uv uses: astral-sh/setup-uv@v7 with: enable-cache: true cache-dependency-glob: "pyproject.toml" github-token: ${{ secrets.GITHUB_TOKEN }} - name: Install tox run: uv tool install --python-preference only-managed --python 3.14 tox --with tox-uv - name: Install Python if: startsWith(matrix.env, '3.') && matrix.env != '3.14' run: uv python install --python-preference only-managed ${{ matrix.env }} - name: Setup test suite run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.env }} - name: Run test suite run: tox run --skip-pkg-install -e ${{ matrix.env }} env: PYTEST_ADDOPTS: "-vv --durations=20" DIFF_AGAINST: HEAD sphinx_autodoc_typehints-3.8.0/.github/workflows/release.yaml0000644000000000000000000000240513615410400021502 0ustar00name: Release to PyPI on: push: tags: ["*"] env: dists-artifact-name: python-package-distributions jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install the latest version of uv uses: astral-sh/setup-uv@v7 with: enable-cache: true cache-dependency-glob: "pyproject.toml" github-token: ${{ secrets.GITHUB_TOKEN }} - name: Build package run: uv build --python 3.14 --python-preference only-managed --sdist --wheel . --out-dir dist - name: Store the distribution packages uses: actions/upload-artifact@v6 with: name: ${{ env.dists-artifact-name }} path: dist/* release: needs: - build runs-on: ubuntu-latest environment: name: release url: https://pypi.org/project/sphinx-autodoc-typehints/${{ github.ref_name }} permissions: id-token: write steps: - name: Download all the dists uses: actions/download-artifact@v7 with: name: ${{ env.dists-artifact-name }} path: dist/ - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@v1.13.0 with: attestations: true sphinx_autodoc_typehints-3.8.0/src/sphinx_autodoc_typehints/__init__.py0000644000000000000000000003773013615410400023650 0ustar00"""Sphinx autodoc type hints.""" from __future__ import annotations import inspect import types from typing import TYPE_CHECKING, Any, TypeVar from docutils import nodes from sphinx.util import logging from sphinx.util.inspect import signature as sphinx_signature from sphinx.util.inspect import stringify_signature # re-exports for backward compatibility from ._annotations import ( MyTypeAliasForwardRef, add_type_css_class, format_annotation, get_annotation_args, get_annotation_class_name, get_annotation_module, unescape, ) from ._formats import detect_format from ._formats._numpydoc import _convert_numpydoc_to_sphinx_fields # noqa: F401 from ._formats._sphinx import _has_yields_section, _is_generator_type from ._parser import parse from ._resolver import ( backfill_attrs_annotations, backfill_type_hints, collect_documented_type_aliases, get_all_type_hints, get_obj_location, ) from .patches import _OVERLOADS_CACHE, install_patches from .version import __version__ if TYPE_CHECKING: from collections.abc import Callable from docutils.nodes import Node from docutils.parsers.rst import states from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment from sphinx.ext.autodoc import Options _LOGGER = logging.getLogger(__name__) def process_signature( # noqa: C901, PLR0911, PLR0912, PLR0913, PLR0917 app: Sphinx, what: str, name: str, obj: Any, options: Options, # noqa: ARG001 signature: str, # noqa: ARG001 return_annotation: str, # noqa: ARG001 ) -> tuple[str, None] | None: """Process the signature.""" if not callable(obj): return None original_obj = obj obj = getattr(obj, "__init__", getattr(obj, "__new__", None)) if inspect.isclass(obj) else obj if not getattr(obj, "__annotations__", None): return None try: obj = inspect.unwrap(obj) # ty: ignore[invalid-argument-type] except ValueError: return None sph_signature = sphinx_signature(obj, type_aliases=app.config["autodoc_type_aliases"]) typehints_formatter: Callable[..., str | None] | None = getattr(app.config, "typehints_formatter", None) def _get_formatted_annotation(annotation: TypeVar) -> TypeVar: if typehints_formatter is None: return annotation formatted_name = typehints_formatter(annotation) return annotation if not isinstance(formatted_name, str) else TypeVar(formatted_name) # ty: ignore[invalid-legacy-type-variable] if app.config.typehints_use_signature_return: sph_signature = sph_signature.replace( return_annotation=_get_formatted_annotation(sph_signature.return_annotation) ) if app.config.typehints_use_signature: parameters = [ param.replace(annotation=_get_formatted_annotation(param.annotation)) for param in sph_signature.parameters.values() ] else: parameters = [param.replace(annotation=inspect.Parameter.empty) for param in sph_signature.parameters.values()] start = 0 if parameters: if inspect.isclass(original_obj) or (what == "method" and name.endswith(".__init__")): start = 1 elif what == "method": if "" in obj.__qualname__ and not _is_dataclass(name, what, obj.__qualname__): _LOGGER.warning( 'Cannot handle as a local function: "%s" (use @functools.wraps)', name, type="sphinx_autodoc_typehints", subtype="local_function", location=get_obj_location(obj), ) return None outer = inspect.getmodule(obj) if outer is None: return None for class_name in obj.__qualname__.split(".")[:-1]: if (outer := getattr(outer, class_name, None)) is None: return None method_name = obj.__name__ if method_name.startswith("__") and not method_name.endswith("__"): method_name = f"_{obj.__qualname__.split('.')[-2]}{method_name}" method_object = outer.__dict__.get(method_name, obj) if outer else obj if not isinstance(method_object, classmethod | staticmethod): start = 1 sph_signature = sph_signature.replace(parameters=parameters[start:]) show_return_annotation = app.config.typehints_use_signature_return unqualified_typehints = not getattr(app.config, "typehints_fully_qualified", False) return ( stringify_signature( sph_signature, show_return_annotation=show_return_annotation, unqualified_typehints=unqualified_typehints, ).replace("\\", "\\\\"), None, ) def _is_dataclass(name: str, what: str, qualname: str) -> bool: return (what == "method" and name.endswith(".__init__")) or (what == "class" and qualname.endswith(".__init__")) def process_docstring( # noqa: PLR0913, PLR0917 app: Sphinx, what: str, name: str, obj: Any, options: Options | None, # noqa: ARG001 lines: list[str], ) -> None: """Process the docstring for an entry.""" original_obj = obj obj = obj.fget if isinstance(obj, property) else obj if not callable(obj): return if inspect.isclass(obj): backfill_attrs_annotations(obj) obj = obj.__init__ if inspect.isclass(obj) else obj try: obj = inspect.unwrap(obj) except ValueError: return try: signature = sphinx_signature(obj, type_aliases=app.config["autodoc_type_aliases"]) except (ValueError, TypeError): signature = None localns = {key: MyTypeAliasForwardRef(value) for key, value in app.config["autodoc_type_aliases"].items()} module_prefix = name.rsplit(".", maxsplit=1)[0] if "." in name else "" eager_aliases: dict[int, MyTypeAliasForwardRef] = {} if (env := getattr(app, "env", None)) is not None: deferred, eager_aliases = collect_documented_type_aliases(obj, module_prefix, env) localns.update(deferred) type_hints = get_all_type_hints(app.config.autodoc_mock_imports, obj, name, localns) for param, hint in type_hints.items(): if id(hint) in eager_aliases: type_hints[param] = eager_aliases[id(hint)] app.config._annotation_globals = getattr(obj, "__globals__", {}) # noqa: SLF001 app.config._typehints_env = env # noqa: SLF001 app.config._typehints_module_prefix = module_prefix # noqa: SLF001 try: has_overloads = _inject_overload_signatures(app, what, name, obj, lines) _inject_types_to_docstring(type_hints, signature, original_obj, app, what, name, lines, has_overloads) finally: delattr(app.config, "_annotation_globals") delattr(app.config, "_typehints_env") delattr(app.config, "_typehints_module_prefix") def _inject_overload_signatures( app: Sphinx, what: str, name: str, # noqa: ARG001 obj: Any, lines: list[str], ) -> bool: if what not in {"function", "method"}: return False module_name = getattr(obj, "__module__", None) if not module_name or module_name not in _OVERLOADS_CACHE: return False qualname = getattr(obj, "__qualname__", None) if not qualname: return False overloads = _OVERLOADS_CACHE[module_name].get(qualname) if not overloads: return False short_literals = app.config.python_display_short_literal_types overload_lines = [":Overloads:"] for overload_sig in overloads: params = [] for param_name, param in overload_sig.parameters.items(): if param.annotation != inspect.Parameter.empty: formatted_type = format_annotation(param.annotation, app.config, short_literals=short_literals) formatted_type = add_type_css_class(formatted_type) params.append(f"**{param_name}** ({formatted_type})") else: params.append(f"**{param_name}**") return_annotation = "" if overload_sig.return_annotation != inspect.Signature.empty: formatted_return = format_annotation( overload_sig.return_annotation, app.config, short_literals=short_literals ) formatted_return = add_type_css_class(formatted_return) return_annotation = f" \u2192 {formatted_return}" sig_line = f" * {', '.join(params)}{return_annotation}" overload_lines.append(sig_line) overload_lines.append("") for line in reversed(overload_lines): lines.insert(0, line) return True def format_default(app: Sphinx, default: Any, is_annotated: bool) -> str | None: # noqa: FBT001 if default is inspect.Parameter.empty: return None formatted = repr(default).replace("\\", "\\\\") if is_annotated: if app.config.typehints_defaults.startswith("braces"): return f" (default: ``{formatted}``)" return f", default: ``{formatted}``" if app.config.typehints_defaults == "braces-after": return f" (default: ``{formatted}``)" return f"default: ``{formatted}``" def _inject_types_to_docstring( # noqa: PLR0913, PLR0917 type_hints: dict[str, Any], signature: inspect.Signature | None, original_obj: Any, app: Sphinx, what: str, name: str, lines: list[str], has_overloads: bool = False, # noqa: FBT001, FBT002 ) -> None: fmt = detect_format(lines) if signature is not None: _inject_signature(type_hints, signature, app, lines, fmt) if "return" in type_hints and not has_overloads: _inject_rtype(type_hints, original_obj, app, what, name, lines, fmt) def _inject_signature( type_hints: dict[str, Any], signature: inspect.Signature, app: Sphinx, lines: list[str], fmt: Any, ) -> None: for arg_name in signature.parameters: _inject_arg_signature(type_hints, signature, app, lines, arg_name, fmt) def _inject_arg_signature( # noqa: PLR0913, PLR0917 type_hints: dict[str, Any], signature: inspect.Signature, app: Sphinx, lines: list[str], arg_name: str, fmt: Any, ) -> None: annotation = type_hints.get(arg_name) default = signature.parameters[arg_name].default doc_description = _extract_doc_description(annotation) if annotation is not None else None if arg_name.endswith("_"): arg_name = f"{arg_name[:-1]}\\_" insert_index = fmt.find_param(lines, arg_name) if insert_index is not None and hasattr(fmt, "get_arg_name_from_line"): arg_name = fmt.get_arg_name_from_line(lines[insert_index]) or arg_name if insert_index is None and doc_description: lines.append(f":param {arg_name}: {doc_description}") insert_index = len(lines) - 1 elif annotation is not None and insert_index is None and app.config.always_document_param_types: insert_index = fmt.add_undocumented_param(lines, arg_name) if insert_index is not None: has_preexisting_annotation = False if annotation is None: type_annotation, has_preexisting_annotation = fmt.find_preexisting_type(lines, arg_name) else: short_literals = app.config.python_display_short_literal_types formatted_annotation = add_type_css_class( format_annotation(annotation, app.config, short_literals=short_literals) ) type_annotation = f":type {arg_name}: {formatted_annotation}" if app.config.typehints_defaults: formatted_default = format_default(app, default, annotation is not None or has_preexisting_annotation) if formatted_default: after = app.config.typehints_defaults.endswith("after") type_annotation = fmt.append_default( lines, insert_index, type_annotation, formatted_default, after=after ) lines.insert(insert_index, type_annotation) def _inject_rtype( # noqa: PLR0913, PLR0917 type_hints: dict[str, Any], original_obj: Any, app: Sphinx, what: str, name: str, lines: list[str], fmt: Any, ) -> None: if inspect.isclass(original_obj) or inspect.isdatadescriptor(original_obj): return if what == "method" and name.endswith(".__init__"): return if not app.config.typehints_document_rtype: return if not app.config.typehints_document_rtype_none and type_hints["return"] is types.NoneType: return if _has_yields_section(lines) and _is_generator_type(type_hints["return"]): return if (return_doc := _extract_doc_description(type_hints["return"])) and not any( line.lstrip().startswith((":return:", ":returns:")) for line in lines ): lines.append(f":return: {return_doc}") r = fmt.get_rtype_insert_info(app, lines) if r is None: return short_literals = app.config.python_display_short_literal_types formatted_annotation = add_type_css_class( format_annotation(type_hints["return"], app.config, short_literals=short_literals) ) fmt.inject_rtype(lines, formatted_annotation, r, use_rtype=app.config.typehints_use_rtype) def _extract_doc_description(annotation: Any) -> str | None: if not (hasattr(annotation, "__metadata__") and hasattr(annotation, "__origin__")): return None for meta in annotation.__metadata__: if type(meta).__qualname__ == "Doc" and type(meta).__module__ in {"typing_extensions", "typing"}: return meta.documentation return None def validate_config(app: Sphinx, env: BuildEnvironment, docnames: list[str]) -> None: # noqa: ARG001 valid = {None, "comma", "braces", "braces-after"} if app.config.typehints_defaults not in valid | {False}: msg = f"typehints_defaults needs to be one of {valid!r}, not {app.config.typehints_defaults!r}" raise ValueError(msg) formatter = app.config.typehints_formatter if formatter is not None and not callable(formatter): msg = f"typehints_formatter needs to be callable or `None`, not {formatter}" raise ValueError(msg) def sphinx_autodoc_typehints_type_role( _role: str, _rawtext: str, text: str, _lineno: int, inliner: states.Inliner, _options: dict[str, Any] | None = None, _content: list[str] | None = None, ) -> tuple[list[Node], list[Node]]: unescaped = unescape(text) doc = parse(unescaped, inliner.document.settings) n = nodes.inline(text) n["classes"].append("sphinx_autodoc_typehints-type") n += doc.children[0].children return [n], [] def setup(app: Sphinx) -> dict[str, bool]: app.add_config_value("always_document_param_types", False, "html") # noqa: FBT003 app.add_config_value("typehints_fully_qualified", False, "env") # noqa: FBT003 app.add_config_value("typehints_document_rtype", True, "env") # noqa: FBT003 app.add_config_value("typehints_document_rtype_none", True, "env") # noqa: FBT003 app.add_config_value("typehints_use_rtype", True, "env") # noqa: FBT003 app.add_config_value("typehints_defaults", None, "env") app.add_config_value("simplify_optional_unions", True, "env") # noqa: FBT003 app.add_config_value("always_use_bars_union", False, "env") # noqa: FBT003 app.add_config_value("typehints_formatter", None, "env") app.add_config_value("typehints_use_signature", False, "env") # noqa: FBT003 app.add_config_value("typehints_use_signature_return", False, "env") # noqa: FBT003 app.add_config_value("typehints_fixup_module_name", None, "env") app.add_role("sphinx_autodoc_typehints_type", sphinx_autodoc_typehints_type_role) app.connect("env-before-read-docs", validate_config) app.connect("autodoc-process-signature", process_signature) app.connect("autodoc-process-docstring", process_docstring) install_patches(app) return {"parallel_read_safe": True, "parallel_write_safe": True} __all__ = [ "__version__", "backfill_type_hints", "format_annotation", "get_annotation_args", "get_annotation_class_name", "get_annotation_module", "process_docstring", "process_signature", ] sphinx_autodoc_typehints-3.8.0/src/sphinx_autodoc_typehints/_annotations.py0000644000000000000000000002701113615410400024574 0ustar00""" Annotation inspection and RST formatting. Converts Python type annotations into reStructuredText cross-reference markup for Sphinx output. Handles standard library types, typing constructs (Union, Optional, Literal, NewType, etc.), forward references, type aliases, and pydata-sphinx-theme compatibility annotations. This is a leaf module with no internal package imports to keep the dependency graph acyclic. """ from __future__ import annotations import enum import inspect import re import sys import types from typing import TYPE_CHECKING, Any, AnyStr, ForwardRef, NewType, TypeVar, Union from sphinx.util import rst from sphinx.util.inspect import TypeAliasForwardRef if TYPE_CHECKING: from collections.abc import Callable from sphinx.config import Config _PYDATA_ANNOTS_TYPING = { "Any", "AnyStr", "Callable", "ClassVar", "Literal", "NoReturn", "Optional", "Tuple", *({"Union"} if sys.version_info < (3, 14) else set()), } _PYDATA_ANNOTS_TYPES = { *("AsyncGeneratorType", "BuiltinFunctionType", "BuiltinMethodType"), *("CellType", "ClassMethodDescriptorType", "CoroutineType"), "EllipsisType", *("FrameType", "FunctionType"), *("GeneratorType", "GetSetDescriptorType"), "LambdaType", *("MemberDescriptorType", "MethodDescriptorType", "MethodType", "MethodWrapperType"), *("NoneType", "NotImplementedType"), "WrapperDescriptorType", } _PYDATA_ANNOTATIONS = { *(("typing", n) for n in _PYDATA_ANNOTS_TYPING), *(("types", n) for n in _PYDATA_ANNOTS_TYPES), } _TYPES_DICT = {getattr(types, name): name for name in types.__all__} _TYPES_DICT[types.FunctionType] = "FunctionType" _UNESCAPE_RE = re.compile( r""" \\ # literal backslash ([^ ]) # followed by any non-space character (captured) """, re.VERBOSE, ) class MyTypeAliasForwardRef(TypeAliasForwardRef): def __or__(self, value: Any) -> Any: # ty: ignore[invalid-method-override] return Union[self, value] # noqa: UP007 def format_annotation(annotation: Any, config: Config, *, short_literals: bool = False) -> str: # noqa: C901, PLR0911, PLR0912, PLR0915, PLR0914 """Format the annotation.""" typehints_formatter: Callable[..., str] | None = getattr(config, "typehints_formatter", None) if typehints_formatter is not None: formatted = typehints_formatter(annotation, config) if formatted is not None: return formatted if isinstance(annotation, ForwardRef): return annotation.__forward_arg__ if annotation is None or annotation is type(None): return ":py:obj:`None`" if annotation is Ellipsis: return ":py:data:`...`" if isinstance(annotation, tuple): return _format_internal_tuple(annotation, config) if isinstance(annotation, TypeAliasForwardRef): if (env := getattr(config, "_typehints_env", None)) is not None: py_domain = env.get_domain("py") module_prefix = getattr(config, "_typehints_module_prefix", "") for candidate in (f"{module_prefix}.{annotation.name}", annotation.name): if candidate in py_domain.objects and py_domain.objects[candidate].objtype == "type": fully_qualified: bool = getattr(config, "typehints_fully_qualified", False) prefix = "" if fully_qualified else "~" return f":py:type:`{prefix}{candidate}`" return annotation.name try: module = get_annotation_module(annotation) class_name = get_annotation_class_name(annotation, module) args = get_annotation_args(annotation, module, class_name) except ValueError: return str(annotation).strip("'") module = _fixup_module_name(config, module) full_name = f"{module}.{class_name}" if module != "builtins" else class_name fully_qualified: bool = getattr(config, "typehints_fully_qualified", False) prefix = "" if fully_qualified or full_name == class_name else "~" role = "data" if (module, class_name) in _PYDATA_ANNOTATIONS else "class" args_format = "\\[{}]" formatted_args: str | None = "" always_use_bars_union: bool = getattr(config, "always_use_bars_union", True) is_bars_union = ( (sys.version_info >= (3, 14) and full_name == "typing.Union") or full_name == "types.UnionType" or (always_use_bars_union and type(annotation).__qualname__ == "_UnionGenericAlias") ) if is_bars_union: full_name = "" if full_name == "typing.NewType": newtype_module = _fixup_module_name(config, getattr(annotation, "__module__", "")) newtype_name = annotation.__name__ newtype_qualified = f"{newtype_module}.{newtype_name}" if newtype_module else newtype_name newtype_prefix = "" if fully_qualified or not newtype_module else "~" supertype = format_annotation(annotation.__supertype__, config, short_literals=short_literals) return f":py:class:`{newtype_prefix}{newtype_qualified}` ({supertype})" if full_name == "typing.Annotated": return format_annotation(annotation.__origin__, config, short_literals=short_literals) if full_name in {"typing.TypeVar", "typing.ParamSpec"}: params = {k: getattr(annotation, f"__{k}__") for k in ("bound", "covariant", "contravariant")} params = {k: v for k, v in params.items() if v} if "bound" in params: params["bound"] = f" {format_annotation(params['bound'], config, short_literals=short_literals)}" args_format = f"\\(``{annotation.__name__}``{', {}' if args else ''}" if params: args_format += "".join(f", {k}={v}" for k, v in params.items()) args_format += ")" formatted_args = None if args else args_format elif full_name == "typing.Optional": # pragma: <3.14 cover args = tuple(x for x in args if x is not type(None)) elif full_name in {"typing.Union", "types.UnionType"} and type(None) in args: # pragma: <3.14 cover if len(args) == 2: # noqa: PLR2004 full_name = "typing.Optional" role = "data" args = tuple(x for x in args if x is not type(None)) else: simplify_optional_unions: bool = getattr(config, "simplify_optional_unions", True) if not simplify_optional_unions: full_name = "typing.Optional" role = "data" args_format = f"\\[:py:data:`{prefix}typing.Union`\\[{{}}]]" args = tuple(x for x in args if x is not type(None)) elif full_name in {"typing.Callable", "collections.abc.Callable"} and args and args[0] is not ...: fmt = [format_annotation(arg, config, short_literals=short_literals) for arg in args] formatted_args = f"\\[\\[{', '.join(fmt[:-1])}], {fmt[-1]}]" elif full_name == "typing.Literal": literal_parts = [_format_literal_arg(arg, config) for arg in args] if short_literals: return f"\\{' | '.join(literal_parts)}" formatted_args = f"\\[{', '.join(literal_parts)}]" elif is_bars_union: if not args: return f":py:{'class' if sys.version_info >= (3, 14) else 'data'}:`{prefix}typing.Union`" return " | ".join([format_annotation(arg, config, short_literals=short_literals) for arg in args]) if args and not formatted_args: fmt = [format_annotation(arg, config, short_literals=short_literals) for arg in args] formatted_args = args_format.format(", ".join(fmt)) escape = "\\ " if formatted_args else "" return f":py:{role}:`{prefix}{full_name}`{escape}{formatted_args}" def get_annotation_module(annotation: Any) -> str: if annotation is None: return "builtins" if _get_types_type(annotation) is not None: return "types" is_new_type = isinstance(annotation, NewType) if ( is_new_type or isinstance(annotation, TypeVar) or type(annotation).__name__ in {"ParamSpec", "ParamSpecArgs", "ParamSpecKwargs"} ): return "typing" if hasattr(annotation, "__module__"): return annotation.__module__ msg = f"Cannot determine the module of {annotation}" raise ValueError(msg) def get_annotation_class_name(annotation: Any, module: str) -> str: # noqa: C901, PLR0911 if annotation is None: return "None" if annotation is AnyStr: return "AnyStr" val = _get_types_type(annotation) if val is not None: return val if _is_newtype(annotation): return "NewType" if getattr(annotation, "__qualname__", None): return annotation.__qualname__ if getattr(annotation, "_name", None): # pragma: <3.14 cover return annotation._name # noqa: SLF001 if module in {"typing", "typing_extensions"} and isinstance( getattr(annotation, "name", None), str ): # pragma: <3.14 cover return annotation.name origin = getattr(annotation, "__origin__", None) if origin: if getattr(origin, "__qualname__", None): # pragma: <3.14 cover return origin.__qualname__ if getattr(origin, "_name", None): # pragma: <3.14 cover return origin._name # noqa: SLF001 annotation_cls = annotation if inspect.isclass(annotation) else type(annotation) return annotation_cls.__qualname__.lstrip("_") def get_annotation_args(annotation: Any, module: str, class_name: str) -> tuple[Any, ...]: try: original = getattr(sys.modules[module], class_name) except (KeyError, AttributeError): pass else: if annotation is original: return () if class_name == "TypeVar" and hasattr(annotation, "__constraints__"): return annotation.__constraints__ if class_name == "NewType" and hasattr(annotation, "__supertype__"): return (annotation.__supertype__,) if class_name == "Generic": return annotation.__parameters__ result = getattr(annotation, "__args__", ()) return () if len(result) == 1 and result[0] == () else result # type: ignore[misc] def _format_internal_tuple(t: tuple[Any, ...], config: Config, *, short_literals: bool = False) -> str: fmt = [format_annotation(a, config, short_literals=short_literals) for a in t] if len(fmt) == 0: return "()" if len(fmt) == 1: return f"({fmt[0]}, )" return f"({', '.join(fmt)})" def _fixup_module_name(config: Config, module: str) -> str: if getattr(config, "typehints_fixup_module_name", None): module = config.typehints_fixup_module_name(module) if module == "typing_extensions": # pragma: <3.14 cover module = "typing" if module == "_io": module = "io" return module def _format_literal_arg(arg: Any, config: Config) -> str: if isinstance(arg, enum.Enum): enum_cls = type(arg) module = _fixup_module_name(config, enum_cls.__module__) fully_qualified = getattr(config, "typehints_fully_qualified", False) qualified = f"{module}.{enum_cls.__qualname__}.{arg.name}" if module else f"{enum_cls.__qualname__}.{arg.name}" prefix = "" if fully_qualified or not module else "~" return f":py:attr:`{prefix}{qualified}`" return f"``{arg!r}``" def _get_types_type(obj: Any) -> str | None: try: return _TYPES_DICT.get(obj) except Exception: # noqa: BLE001 return None def _is_newtype(annotation: Any) -> bool: return isinstance(annotation, NewType) def unescape(escaped: str) -> str: escaped = escaped.replace("\x00", "") return _UNESCAPE_RE.sub(r"\1", escaped) def add_type_css_class(type_rst: str) -> str: return f":sphinx_autodoc_typehints_type:`{rst.escape(type_rst)}`" sphinx_autodoc_typehints-3.8.0/src/sphinx_autodoc_typehints/_parser.py0000644000000000000000000000171713615410400023540 0ustar00"""Utilities for side-effect-free rST parsing.""" from __future__ import annotations from typing import TYPE_CHECKING from docutils.utils import new_document from sphinx.parsers import RSTParser from sphinx.util.docutils import sphinx_domains if TYPE_CHECKING: import optparse from docutils import nodes from docutils.frontend import Values from docutils.statemachine import StringList class _RstSnippetParser(RSTParser): @staticmethod def decorate(_content: StringList) -> None: # ty: ignore[invalid-method-override] """Override to skip processing rst_epilog/rst_prolog for typing.""" def parse(inputstr: str, settings: Values | optparse.Values) -> nodes.document: """Parse inputstr and return a docutils document.""" doc = new_document("", settings=settings) # ty: ignore[invalid-argument-type] with sphinx_domains(settings.env): parser = _RstSnippetParser() parser.parse(inputstr, doc) return doc sphinx_autodoc_typehints-3.8.0/src/sphinx_autodoc_typehints/attributes_patch.py0000644000000000000000000000414713615410400025452 0ustar00"""Patch for attributes.""" from __future__ import annotations from functools import partial from typing import TYPE_CHECKING, Any from unittest.mock import patch import sphinx.domains.python from sphinx.domains.python import PyAttribute from ._parser import parse if TYPE_CHECKING: from docutils.frontend import Values from sphinx.addnodes import desc_signature from sphinx.application import Sphinx # Defensively check for the things we want to patch _parse_annotation = getattr(sphinx.domains.python, "_parse_annotation", None) # If we didn't locate the patch target, we will just do nothing. OKAY_TO_PATCH = bool(_parse_annotation) # A label we inject to the type string so we know not to try to treat it as a # type annotation TYPE_IS_RST_LABEL = "--is-rst--" orig_handle_signature = PyAttribute.handle_signature def rst_to_docutils(settings: Values, rst: str) -> Any: """Convert rst to a sequence of docutils nodes.""" doc = parse(rst, settings) # Remove top level paragraph node so that there is no line break. return doc.children[0].children def patched_parse_annotation(settings: Values, typ: str, env: Any) -> Any: # if typ doesn't start with our label, use original function if not typ.startswith(TYPE_IS_RST_LABEL): assert _parse_annotation is not None # noqa: S101 return _parse_annotation(typ, env) # Otherwise handle as rst typ = typ[len(TYPE_IS_RST_LABEL) :] return rst_to_docutils(settings, typ) def patched_handle_signature(self: PyAttribute, sig: str, signode: desc_signature) -> tuple[str, str]: target = "sphinx.domains.python._parse_annotation" new_func = partial(patched_parse_annotation, self.state.document.settings) with patch(target, new_func): return orig_handle_signature(self, sig, signode) def patch_attribute_handling(app: Sphinx) -> None: # noqa: ARG001 """Patch PyAttribute.handle_signature to format class attribute type annotations.""" if not OKAY_TO_PATCH: return PyAttribute.handle_signature = patched_handle_signature # type:ignore[method-assign] __all__ = ["patch_attribute_handling"] sphinx_autodoc_typehints-3.8.0/src/sphinx_autodoc_typehints/patches.py0000644000000000000000000001214013615410400023524 0ustar00"""Custom patches to make the world work.""" from __future__ import annotations from functools import lru_cache from typing import TYPE_CHECKING, Any from docutils import nodes from docutils.parsers.rst.directives.admonitions import BaseAdmonition from docutils.parsers.rst.states import Body, Text from sphinx.ext.napoleon.docstring import GoogleDocstring from .attributes_patch import patch_attribute_handling if TYPE_CHECKING: from sphinx.application import Sphinx from sphinx.ext.autodoc import Options def napoleon_numpy_docstring_return_type_processor( # noqa: PLR0913, PLR0917 app: Sphinx, what: str, name: str, # noqa: ARG001 obj: Any, # noqa: ARG001 options: Options | None, # noqa: ARG001 lines: list[str], ) -> None: """Insert a : under Returns: to tell napoleon not to look for a return type.""" if what not in {"function", "method"}: return if not getattr(app.config, "napoleon_numpy_docstring", False): return # Search for the returns header: # Returns: # -------- for pos, line in enumerate(lines[:-2]): if line.lower().strip(":") not in {"return", "returns"}: continue # Underline detection. chars = set(lines[pos + 1].strip()) # Napoleon allows the underline to consist of a bunch of weirder things... if len(chars) != 1 or next(iter(chars)) not in "=-~_*+#": continue pos += 2 # noqa: PLW2901 break else: return lines.insert(pos, ":") def fix_napoleon_numpy_docstring_return_type(app: Sphinx) -> None: """If no return type is explicitly set, numpy docstrings will use the return type text as return types.""" # standard priority is 500. Setting priority to 499 ensures this runs before # napoleon's docstring processor. app.connect("autodoc-process-docstring", napoleon_numpy_docstring_return_type_processor, priority=499) def _patched_lookup_annotation(*_args: Any) -> str: """ GoogleDocstring._lookup_annotation sometimes adds incorrect type annotations to constructor parameters. Disable it so we can handle this on our own. """ return "" def _patch_google_docstring_lookup_annotation() -> None: """Fix issue https://github.com/tox-dev/sphinx-autodoc-typehints/issues/308.""" GoogleDocstring._lookup_annotation = _patched_lookup_annotation # type: ignore[assignment] # noqa: SLF001 orig_base_admonition_run = BaseAdmonition.run def _patched_base_admonition_run(self: BaseAdmonition) -> Any: result = orig_base_admonition_run(self) result[0].line = self.lineno return result orig_text_indent = Text.indent def _patched_text_indent(self: Text, *args: Any) -> Any: _, line = self.state_machine.get_source_and_line() # ty: ignore[unresolved-attribute] result = orig_text_indent(self, *args) node = self.parent[-1] # ty: ignore[not-subscriptable] if node.tagname == "system_message": node = self.parent[-2] # ty: ignore[not-subscriptable] node.line = line return result def _patched_body_doctest( self: Body, _match: None, _context: None, next_state: str | None ) -> tuple[list[Any], str | None, list[Any]]: assert self.document.current_line is not None # noqa: S101 line = self.document.current_line + 1 data = "\n".join(self.state_machine.get_text_block()) # ty: ignore[unresolved-attribute] n = nodes.doctest_block(data, data) n.line = line self.parent += n # ty: ignore[unsupported-operator] return [], next_state, [] def _patch_line_numbers() -> None: """ Make the rst parser put line numbers on more nodes. When the line numbers are missing, we have a hard time placing the :rtype:. """ Text.indent = _patched_text_indent # type: ignore[method-assign] BaseAdmonition.run = _patched_base_admonition_run # type: ignore[method-assign,assignment] Body.doctest = _patched_body_doctest # type: ignore[method-assign] _OVERLOADS_CACHE: dict[str, dict[Any, Any]] = {} @lru_cache def fix_directive_based_signature_formatting() -> None: """ Patch Sphinx 9's new directive-based autodoc to cache and clear overload detection. Overloads are cached before being cleared so we can render them in docstrings with proper type formatting instead of in signatures where types can't be cross-referenced. """ from sphinx.pycode import ModuleAnalyzer # noqa: PLC0415 original_analyze = ModuleAnalyzer.analyze def patched_analyze(self: Any) -> None: original_analyze(self) if self.overloads: _OVERLOADS_CACHE[self.modname] = dict(self.overloads) self.overloads = {} ModuleAnalyzer.analyze = patched_analyze # type: ignore[method-assign] def install_patches(app: Sphinx) -> None: """ Install the patches. :param app: the Sphinx app """ # For Sphinx 9+ directive-based architecture fix_directive_based_signature_formatting() patch_attribute_handling(app) _patch_google_docstring_lookup_annotation() fix_napoleon_numpy_docstring_return_type(app) _patch_line_numbers() __all__ = [ "_OVERLOADS_CACHE", "install_patches", ] sphinx_autodoc_typehints-3.8.0/src/sphinx_autodoc_typehints/py.typed0000644000000000000000000000000013615410400023212 0ustar00sphinx_autodoc_typehints-3.8.0/src/sphinx_autodoc_typehints/version.py0000644000000000000000000000130013615410400023556 0ustar00# file generated by setuptools-scm # don't change, don't track in version control __all__ = [ "__version__", "__version_tuple__", "version", "version_tuple", "__commit_id__", "commit_id", ] TYPE_CHECKING = False if TYPE_CHECKING: from typing import Tuple from typing import Union VERSION_TUPLE = Tuple[Union[int, str], ...] COMMIT_ID = Union[str, None] else: VERSION_TUPLE = object COMMIT_ID = object version: str __version__: str __version_tuple__: VERSION_TUPLE version_tuple: VERSION_TUPLE commit_id: COMMIT_ID __commit_id__: COMMIT_ID __version__ = version = '3.8.0' __version_tuple__ = version_tuple = (3, 8, 0) __commit_id__ = commit_id = None sphinx_autodoc_typehints-3.8.0/src/sphinx_autodoc_typehints/_formats/__init__.py0000644000000000000000000000161213615410400025450 0ustar00""" Docstring format detection and dispatch to format-specific handlers. Provides the `detect_format` entry point that inspects docstring lines and returns the appropriate `DocstringFormat` handler. Numpydoc-style sections (`:Parameters:`, `:Returns:`) get a `NumpydocFormat` handler; everything else (including Napoleon-processed output) gets `SphinxFieldListFormat`. """ from __future__ import annotations from ._base import DocstringFormat, InsertIndexInfo from ._numpydoc import NumpydocFormat from ._sphinx import SphinxFieldListFormat __all__ = ["DocstringFormat", "InsertIndexInfo", "NumpydocFormat", "SphinxFieldListFormat", "detect_format"] def detect_format(lines: list[str]) -> DocstringFormat: """Detect and return the appropriate docstring format handler for the given lines.""" if NumpydocFormat.detect(lines): return NumpydocFormat() return SphinxFieldListFormat() sphinx_autodoc_typehints-3.8.0/src/sphinx_autodoc_typehints/_formats/_base.py0000644000000000000000000000406613615410400024770 0ustar00"""Abstract base class for docstring format handlers and shared types used across all format implementations.""" from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass from typing import TYPE_CHECKING if TYPE_CHECKING: from sphinx.application import Sphinx @dataclass class InsertIndexInfo: insert_index: int found_param: bool = False found_return: bool = False found_directive: bool = False class DocstringFormat(ABC): @staticmethod @abstractmethod def detect(lines: list[str]) -> bool: """Return True if lines are in this format.""" @abstractmethod def find_param(self, lines: list[str], arg_name: str) -> int | None: """Find the line index where arg_name is documented.""" @abstractmethod def inject_param_type(self, lines: list[str], arg_name: str, formatted_type: str, at: int) -> None: """Insert type annotation for a parameter at the given index.""" @abstractmethod def add_undocumented_param(self, lines: list[str], arg_name: str) -> int: """Add a :param: line for an undocumented parameter, return its index.""" @abstractmethod def find_preexisting_type(self, lines: list[str], arg_name: str) -> tuple[str, bool]: """Check if a type annotation already exists for arg_name.""" @abstractmethod def get_rtype_insert_info(self, app: Sphinx, lines: list[str]) -> InsertIndexInfo | None: """Find where to insert return type information.""" @abstractmethod def inject_rtype( self, lines: list[str], formatted_annotation: str, info: InsertIndexInfo, *, use_rtype: bool, ) -> None: """Insert return type annotation.""" @abstractmethod def append_default( self, lines: list[str], insert_index: int, type_annotation: str, formatted_default: str, *, after: bool, ) -> str: """Append a default value to a parameter annotation, return the (possibly modified) type_annotation.""" sphinx_autodoc_typehints-3.8.0/src/sphinx_autodoc_typehints/_formats/_numpydoc.py0000644000000000000000000001552413615410400025715 0ustar00""" Numpydoc format handler for ``:Parameters:``/``:Returns:`` style docstrings. Handles docstrings produced by the numpydoc extension, which uses bold parameter names and colon-separated types under section headers. Converts these sections to standard Sphinx field list syntax (``:param:``/``:type:``/``:returns:``) in-place, then delegates all injection operations to `SphinxFieldListFormat`. """ from __future__ import annotations import re from dataclasses import dataclass from typing import TYPE_CHECKING from ._base import DocstringFormat, InsertIndexInfo from ._sphinx import SphinxFieldListFormat if TYPE_CHECKING: from sphinx.application import Sphinx _NUMPYDOC_SECTIONS = frozenset({":Parameters:", ":Other Parameters:", ":Returns:", ":Raises:", ":Yields:"}) _NUMPYDOC_BOLD_RE = re.compile( r""" ^ \s{4} # 4-space indent \*\*(.+?)\*\* # **name** (captured) (?: # optional type part \s*:\s* # colon separator (.+) # type (captured) )? $ """, re.VERBOSE, ) _NUMPYDOC_PLAIN_RE = re.compile( r""" ^ \s{4} # 4-space indent (\S.+?) # non-whitespace start, content (captured) $ """, re.VERBOSE, ) @dataclass class _NumpydocEntry: name: str type: str description: list[str] end: int def _parse_numpydoc_entries(lines: list[str], start: int) -> list[_NumpydocEntry]: entries: list[_NumpydocEntry] = [] i = start while i < len(lines): stripped = lines[i].strip() if stripped in _NUMPYDOC_SECTIONS: break if not stripped: i += 1 continue if m := _NUMPYDOC_BOLD_RE.match(lines[i]): name, typ = m.group(1), m.group(2) or "" elif m := _NUMPYDOC_PLAIN_RE.match(lines[i]): name, typ = "", m.group(1).strip() else: break i += 1 desc_lines: list[str] = [] while i < len(lines) and lines[i].startswith(" ") and lines[i].strip(): desc_lines.append(lines[i].strip()) i += 1 entries.append(_NumpydocEntry(name=name, type=typ, description=desc_lines, end=i)) return entries def _convert_numpydoc_to_sphinx_fields(lines: list[str]) -> None: # noqa: C901, PLR0912 """Convert numpydoc-formatted sections in ``lines`` to Sphinx field list syntax in-place.""" if not any(line.strip() in _NUMPYDOC_SECTIONS for line in lines): return result: list[str] = [] i = 0 while i < len(lines): stripped = lines[i].strip() if stripped not in _NUMPYDOC_SECTIONS: result.append(lines[i]) i += 1 continue section = stripped[1:-1] i += 1 if i < len(lines) and not lines[i].strip(): i += 1 entries = _parse_numpydoc_entries(lines, i) i = entries[-1].end if entries else i if section in {"Parameters", "Other Parameters"}: for entry in entries: desc = " ".join(entry.description) if entry.description else "" result.append(f":param {entry.name}: {desc}".rstrip()) if entry.type: result.append(f":type {entry.name}: {entry.type}") elif section == "Returns": if len(entries) == 1: entry = entries[0] desc = " ".join(entry.description) if entry.description else "" if desc: result.append(f":returns: {desc}") else: result.append(":returns:") for entry in entries: desc = " ".join(entry.description) if entry.description else "" label = f"**{entry.name}**" if entry.name else "" type_part = f" ({entry.type})" if entry.type else "" result.append(f" * {label}{type_part} -- {desc}".rstrip()) elif section == "Raises": for entry in entries: desc = " ".join(entry.description) if entry.description else "" exc_name = entry.name or entry.type result.append(f":raises {exc_name}: {desc}".rstrip()) elif len(entries) == 1: entry = entries[0] desc = " ".join(entry.description) if entry.description else "" if desc: result.append(f":Yields: {desc}") else: result.append(":Yields:") for entry in entries: desc = " ".join(entry.description) if entry.description else "" label = f"**{entry.name}**" if entry.name else "" type_part = f" ({entry.type})" if entry.type else "" result.append(f" * {label}{type_part} -- {desc}".rstrip()) lines[:] = result class NumpydocFormat(DocstringFormat): """Converts numpydoc sections to Sphinx field lists, then delegates to SphinxFieldListFormat.""" def __init__(self) -> None: self._converted = False self._sphinx = SphinxFieldListFormat() @staticmethod def detect(lines: list[str]) -> bool: return any(line.strip() in _NUMPYDOC_SECTIONS for line in lines) def _ensure_converted(self, lines: list[str]) -> None: if not self._converted: _convert_numpydoc_to_sphinx_fields(lines) self._converted = True def find_param(self, lines: list[str], arg_name: str) -> int | None: self._ensure_converted(lines) return self._sphinx.find_param(lines, arg_name) def inject_param_type(self, lines: list[str], arg_name: str, formatted_type: str, at: int) -> None: self._sphinx.inject_param_type(lines, arg_name, formatted_type, at) def add_undocumented_param(self, lines: list[str], arg_name: str) -> int: self._ensure_converted(lines) return self._sphinx.add_undocumented_param(lines, arg_name) def find_preexisting_type(self, lines: list[str], arg_name: str) -> tuple[str, bool]: return self._sphinx.find_preexisting_type(lines, arg_name) def get_rtype_insert_info(self, app: Sphinx, lines: list[str]) -> InsertIndexInfo | None: self._ensure_converted(lines) return self._sphinx.get_rtype_insert_info(app, lines) def inject_rtype( self, lines: list[str], formatted_annotation: str, info: InsertIndexInfo, *, use_rtype: bool, ) -> None: self._sphinx.inject_rtype(lines, formatted_annotation, info, use_rtype=use_rtype) def append_default( self, lines: list[str], insert_index: int, type_annotation: str, formatted_default: str, *, after: bool, ) -> str: return self._sphinx.append_default(lines, insert_index, type_annotation, formatted_default, after=after) def get_arg_name_from_line(self, line: str) -> str | None: return self._sphinx.get_arg_name_from_line(line) sphinx_autodoc_typehints-3.8.0/src/sphinx_autodoc_typehints/_formats/_sphinx.py0000644000000000000000000002053113615410400025362 0ustar00""" Sphinx field list format handler for standard ``:param:``/``:type:``/``:rtype:`` docstrings. Implements `DocstringFormat` for docstrings that use Sphinx-native field list syntax, which is also the output format produced by Napoleon when processing Google or NumPy-style docstrings. This is the default fallback format when no other format is detected. """ from __future__ import annotations import collections.abc from typing import TYPE_CHECKING, Any from docutils.frontend import get_default_settings from docutils.parsers.rst import Directive, directives from docutils.utils import new_document from sphinx.parsers import RSTParser from sphinx.util.docutils import sphinx_domains from sphinx_autodoc_typehints._parser import _RstSnippetParser from ._base import DocstringFormat, InsertIndexInfo if TYPE_CHECKING: import optparse from docutils import nodes from docutils.frontend import Values from docutils.nodes import Node from sphinx.application import Sphinx _BUILTIN_DIRECTIVES = frozenset(directives._directive_registry) # noqa: SLF001 PARAM_SYNONYMS = ("param ", "parameter ", "arg ", "argument ", "keyword ", "kwarg ", "kwparam ") _GENERATOR_TYPES = frozenset({ collections.abc.Generator, collections.abc.Iterator, collections.abc.AsyncGenerator, collections.abc.AsyncIterator, }) def _get_sphinx_line_keyword_and_argument(line: str) -> tuple[str, str | None] | None: param_line_without_description = line.split(":", maxsplit=2) if len(param_line_without_description) != 3: # noqa: PLR2004 return None split_directive_and_name = param_line_without_description[1].split(maxsplit=1) if len(split_directive_and_name) != 2: # noqa: PLR2004 if not len(split_directive_and_name): return None return split_directive_and_name[0], None return tuple(split_directive_and_name) # type: ignore[return-value] def _line_is_param_line_for_arg(line: str, arg_name: str) -> bool: keyword_and_name = _get_sphinx_line_keyword_and_argument(line) if keyword_and_name is None: return False keyword, doc_name = keyword_and_name if doc_name is None: return False if keyword not in {"param", "parameter", "arg", "argument"}: return False return any(doc_name == prefix + arg_name for prefix in ("", "\\*", "\\**", "\\*\\*")) def _is_generator_type(annotation: Any) -> bool: origin = getattr(annotation, "__origin__", None) return origin in _GENERATOR_TYPES or annotation in _GENERATOR_TYPES def _has_yields_section(lines: list[str]) -> bool: return any(line.lstrip().startswith((":Yields:", ":yields:", ":yield:")) for line in lines) def _safe_parse(inputstr: str, settings: Values | optparse.Values) -> nodes.document: original_lookup = directives.directive def _safe_directive_lookup( directive_name: str, language_module: Any, document: Any, ) -> tuple[type[Directive] | None, list[Any]]: cls, messages = original_lookup(directive_name, language_module, document) if cls is not None and directive_name not in _BUILTIN_DIRECTIVES: return _NoOpDirective, messages return cls, messages doc = new_document("", settings=settings) # ty: ignore[invalid-argument-type] with sphinx_domains(settings.env): directives.directive = _safe_directive_lookup # type: ignore[assignment] try: parser = _RstSnippetParser() parser.parse(inputstr, doc) finally: directives.directive = original_lookup return doc class _NoOpDirective(Directive): has_content = True optional_arguments = 99 final_argument_whitespace = True def run(self) -> list[nodes.Node]: # noqa: PLR6301 return [] def _node_line_no(node: Node) -> int | None: if node is None: return None while node.line is None and node.children: node = node.children[0] return node.line def _tag_name(node: Node) -> str: return node.tagname class SphinxFieldListFormat(DocstringFormat): @staticmethod def detect(lines: list[str]) -> bool: # noqa: ARG004 return True def find_param(self, lines: list[str], arg_name: str) -> int | None: # noqa: PLR6301 for at, line in enumerate(lines): if _line_is_param_line_for_arg(line, arg_name): return at return None def inject_param_type(self, lines: list[str], arg_name: str, formatted_type: str, at: int) -> None: # noqa: PLR6301 lines.insert(at, f":type {arg_name}: {formatted_type}") def add_undocumented_param(self, lines: list[str], arg_name: str) -> int: # noqa: PLR6301 lines.append(f":param {arg_name}:") return len(lines) - 1 def find_preexisting_type(self, lines: list[str], arg_name: str) -> tuple[str, bool]: # noqa: PLR6301 type_annotation = f":type {arg_name}: " for line in lines: if line.startswith(type_annotation): return line, True return type_annotation, False def get_rtype_insert_info(self, app: Sphinx, lines: list[str]) -> InsertIndexInfo | None: # noqa: PLR6301 if any(line.startswith(":rtype:") for line in lines): return None for at, line in enumerate(lines): if line.startswith((":return:", ":returns:")): return InsertIndexInfo(insert_index=at, found_return=True) settings = get_default_settings(RSTParser) # type: ignore[arg-type] settings.env = app.env doc = _safe_parse("\n".join(lines), settings) for child in doc.children: if _tag_name(child) != "field_list": continue if not any(c.children[0].astext().startswith(PARAM_SYNONYMS) for c in child.children): continue next_sibling = child.next_node(descend=False, siblings=True) line_no = _node_line_no(next_sibling) if next_sibling else None at = max(line_no - 2, 0) if line_no else len(lines) return InsertIndexInfo(insert_index=at, found_param=True) for child in doc.children: if _tag_name(child) in {"literal_block", "paragraph", "field_list"}: continue line_no = _node_line_no(child) at = max(line_no - 2, 0) if line_no else len(lines) if lines[at - 1]: break return InsertIndexInfo(insert_index=at, found_directive=True) return InsertIndexInfo(insert_index=len(lines)) def inject_rtype( # noqa: PLR6301 self, lines: list[str], formatted_annotation: str, info: InsertIndexInfo, *, use_rtype: bool, ) -> None: insert_index = info.insert_index if not use_rtype and info.found_return and " -- " in lines[insert_index]: return if info.found_param and insert_index < len(lines) and lines[insert_index].strip(): insert_index -= 1 if insert_index > 0 and insert_index <= len(lines) and lines[insert_index - 1].strip(): lines.insert(insert_index, "") insert_index += 1 if use_rtype or not info.found_return: lines.insert(insert_index, f":rtype: {formatted_annotation}") if info.found_directive: lines.insert(insert_index + 1, "") else: line = lines[insert_index] lines[insert_index] = f":return: {formatted_annotation} --{line[line.find(' ') :]}" def append_default( # noqa: PLR6301 self, lines: list[str], insert_index: int, type_annotation: str, formatted_default: str, *, after: bool, ) -> str: if after: nlines = len(lines) next_index = insert_index + 1 append_index = insert_index while next_index < nlines and (not lines[next_index] or lines[next_index].startswith(" ")): if lines[next_index]: append_index = next_index next_index += 1 lines[append_index] += formatted_default else: type_annotation += formatted_default return type_annotation @staticmethod def get_arg_name_from_line(line: str) -> str | None: result = _get_sphinx_line_keyword_and_argument(line) if result is None: return None _, arg_name = result return arg_name sphinx_autodoc_typehints-3.8.0/src/sphinx_autodoc_typehints/_resolver/__init__.py0000644000000000000000000000073613615410400025644 0ustar00"""Type hint resolution and backfilling from source annotations.""" from __future__ import annotations from ._attrs import backfill_attrs_annotations from ._type_comments import backfill_type_hints from ._type_hints import get_all_type_hints from ._util import collect_documented_type_aliases, get_obj_location __all__ = [ "backfill_attrs_annotations", "backfill_type_hints", "collect_documented_type_aliases", "get_all_type_hints", "get_obj_location", ] sphinx_autodoc_typehints-3.8.0/src/sphinx_autodoc_typehints/_resolver/_attrs.py0000644000000000000000000000117213615410400025374 0ustar00"""Backfill annotations from attrs field metadata.""" from __future__ import annotations from typing import Any def backfill_attrs_annotations(obj: Any) -> None: try: from attrs import fields, has # noqa: PLC0415 except ImportError: return if not has(obj): return if (annotations := getattr(obj, "__annotations__", None)) is None: annotations = {} obj.__annotations__ = annotations for field in fields(obj): if field.name not in annotations and field.type is not None: annotations[field.name] = field.type __all__ = ["backfill_attrs_annotations"] sphinx_autodoc_typehints-3.8.0/src/sphinx_autodoc_typehints/_resolver/_stubs.py0000644000000000000000000000557113615410400025406 0ustar00"""Stub file (.pyi) annotation backfill.""" from __future__ import annotations import ast import inspect from pathlib import Path from typing import Any from ._type_comments import _load_args _STUB_AST_CACHE: dict[Path, ast.Module | None] = {} def _backfill_from_stub(obj: Any) -> dict[str, str]: if (stub_path := _find_stub_path(obj)) and (tree := _parse_stub_ast(stub_path)): return _extract_annotations_from_stub(tree, obj) return {} def _find_stub_path(obj: Any) -> Path | None: module = inspect.getmodule(obj) if module is None: return None try: source_file = inspect.getfile(module) except TypeError: return None stub = Path(source_file).with_suffix(".pyi") if stub.is_file(): return stub if hasattr(module, "__path__"): for pkg_dir in module.__path__: init_stub = Path(pkg_dir) / "__init__.pyi" if init_stub.is_file(): return init_stub return None def _parse_stub_ast(stub_path: Path) -> ast.Module | None: if stub_path not in _STUB_AST_CACHE: try: _STUB_AST_CACHE[stub_path] = ast.parse(stub_path.read_text(encoding="utf-8")) except (OSError, SyntaxError): _STUB_AST_CACHE[stub_path] = None return _STUB_AST_CACHE[stub_path] def _extract_annotations_from_stub(tree: ast.Module, obj: Any) -> dict[str, str]: qualname = getattr(obj, "__qualname__", None) if not qualname: return {} parts = qualname.split(".") if (node := _find_ast_node(tree.body, parts)) is None: return {} if isinstance(node, ast.ClassDef): return _extract_class_annotations(node) if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef): return _extract_func_annotations(node) return {} # pragma: no cover def _find_ast_node(body: list[ast.stmt], parts: list[str]) -> ast.stmt | None: target, *rest = parts for node in body: if isinstance(node, ast.ClassDef) and node.name == target: return _find_ast_node(node.body, rest) if rest else node if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == target and not rest: return node return None def _extract_func_annotations(node: ast.FunctionDef | ast.AsyncFunctionDef) -> dict[str, str]: result: dict[str, str] = {} for arg in _load_args(node): if arg.annotation is not None: result[arg.arg] = ast.unparse(arg.annotation) if node.returns is not None: result["return"] = ast.unparse(node.returns) return result def _extract_class_annotations(node: ast.ClassDef) -> dict[str, str]: result: dict[str, str] = {} for child in node.body: if isinstance(child, ast.AnnAssign) and isinstance(child.target, ast.Name): result[child.target.id] = ast.unparse(child.annotation) return result __all__: list[str] = [] sphinx_autodoc_typehints-3.8.0/src/sphinx_autodoc_typehints/_resolver/_type_comments.py0000644000000000000000000001133413615410400027126 0ustar00"""Type comment backfill for functions lacking runtime annotations.""" from __future__ import annotations import ast import inspect import textwrap from typing import TYPE_CHECKING, Any from sphinx.util import logging from ._util import get_obj_location if TYPE_CHECKING: from ast import AsyncFunctionDef, FunctionDef, Module, stmt _LOGGER = logging.getLogger(__name__) def backfill_type_hints(obj: Any, name: str) -> dict[str, Any]: # noqa: C901, PLR0911 parse_kwargs = {"type_comments": True} def _one_child(module: Module) -> stmt | None: children = module.body if len(children) != 1: _LOGGER.warning( 'Did not get exactly one node from AST for "%s", got %s', name, len(children), type="sphinx_autodoc_typehints", subtype="multiple_ast_nodes", location=get_obj_location(obj), ) return None return children[0] try: code = textwrap.dedent(_normalize_source_lines(inspect.getsource(obj))) obj_ast = ast.parse(code, **parse_kwargs) # type: ignore[call-overload] # dynamic kwargs except (OSError, TypeError, SyntaxError): return {} obj_ast = _one_child(obj_ast) if obj_ast is None: return {} try: type_comment = obj_ast.type_comment # type: ignore[attr-defined] except AttributeError: return {} if not type_comment: return {} try: comment_args_str, comment_returns = type_comment.split(" -> ") except ValueError: _LOGGER.warning( 'Unparseable type hint comment for "%s": Expected to contain ` -> `', name, type="sphinx_autodoc_typehints", subtype="comment", location=get_obj_location(obj), ) return {} rv = {} if comment_returns: rv["return"] = comment_returns args = _load_args(obj_ast) # type: ignore[arg-type] comment_args = _split_type_comment_args(comment_args_str) is_inline = len(comment_args) == 1 and comment_args[0] == "..." if not is_inline: if args and args[0].arg in {"self", "cls"} and len(comment_args) != len(args): comment_args.insert(0, None) if len(args) != len(comment_args): _LOGGER.warning( 'Not enough type comments found on "%s"', name, type="sphinx_autodoc_typehints", subtype="comment", location=get_obj_location(obj), ) return rv for at, arg in enumerate(args): value = getattr(arg, "type_comment", None) if is_inline else comment_args[at] if value is not None: rv[arg.arg] = value return rv def _normalize_source_lines(source_lines: str) -> str: lines = source_lines.split("\n") def remove_prefix(text: str, prefix: str) -> str: return text[text.startswith(prefix) and len(prefix) :] for pos, line in enumerate(lines): if line.lstrip().startswith("def "): idx = pos whitespace_separator = "def" break if line.lstrip().startswith("async def"): idx = pos whitespace_separator = "async def" break else: return "\n".join(lines) fn_def = lines[idx] whitespace = fn_def.split(whitespace_separator)[0] aligned_prefix = [whitespace + remove_prefix(s, whitespace) for s in lines[:idx]] aligned_suffix = [whitespace + remove_prefix(s, whitespace) for s in lines[idx + 1 :]] aligned_prefix.append(fn_def) return "\n".join(aligned_prefix + aligned_suffix) def _load_args(obj_ast: FunctionDef | AsyncFunctionDef) -> list[Any]: func_args = obj_ast.args args = [] pos_only = getattr(func_args, "posonlyargs", None) if pos_only: args.extend(pos_only) args.extend(func_args.args) if func_args.vararg: args.append(func_args.vararg) args.extend(func_args.kwonlyargs) if func_args.kwarg: args.append(func_args.kwarg) return args def _split_type_comment_args(comment: str) -> list[str | None]: def add(val: str) -> None: result.append(val.strip().lstrip("*")) comment = comment.strip().lstrip("(").rstrip(")") result: list[str | None] = [] if not comment: return result brackets, start_arg_at, at = 0, 0, 0 for at, char in enumerate(comment): if char in {"[", "("}: brackets += 1 elif char in {"]", ")"}: brackets -= 1 elif char == "," and brackets == 0: add(comment[start_arg_at:at]) start_arg_at = at + 1 add(comment[start_arg_at : at + 1]) return result __all__ = ["backfill_type_hints"] sphinx_autodoc_typehints-3.8.0/src/sphinx_autodoc_typehints/_resolver/_type_hints.py0000644000000000000000000001374113615410400026432 0ustar00"""Type hint resolution with TYPE_CHECKING guard handling.""" from __future__ import annotations import importlib import inspect import re import sys import textwrap import types from typing import TYPE_CHECKING, Any, get_type_hints from sphinx.ext.autodoc.mock import mock from sphinx.util import logging from ._stubs import _backfill_from_stub from ._type_comments import backfill_type_hints from ._util import get_obj_location if TYPE_CHECKING: from sphinx_autodoc_typehints._annotations import MyTypeAliasForwardRef if sys.version_info >= (3, 14): # pragma: >=3.14 cover import annotationlib _LOGGER = logging.getLogger(__name__) _TYPE_GUARD_IMPORT_RE = re.compile( r""" \n # leading newline before the guard if[ ](typing\.)? # "if typing." or "if " prefix TYPE_CHECKING: # the TYPE_CHECKING constant [^\n]* # rest of the if-line ([\s\S]*?) # guarded block body (captured, non-greedy) (?=\n\S) # lookahead: next non-indented line """, re.VERBOSE, ) _TYPE_GUARD_IMPORTS_RESOLVED: set[str] = set() def get_all_type_hints( autodoc_mock_imports: list[str], obj: Any, name: str, localns: dict[Any, MyTypeAliasForwardRef] ) -> dict[str, Any]: result = _get_type_hint(autodoc_mock_imports, name, obj, localns) if not result: result = backfill_type_hints(obj, name) if not result: result = _backfill_from_stub(obj) try: obj.__annotations__ = result except (AttributeError, TypeError): pass else: result = _get_type_hint(autodoc_mock_imports, name, obj, localns) return result def _get_type_hint( autodoc_mock_imports: list[str], name: str, obj: Any, localns: dict[Any, MyTypeAliasForwardRef] ) -> dict[str, Any]: _resolve_type_guarded_imports(autodoc_mock_imports, obj) localns = _add_type_params_to_localns(obj, localns) try: result = get_type_hints(obj, None, localns, include_extras=True) except (AttributeError, TypeError, RecursionError) as exc: if ( isinstance(exc, TypeError) and _future_annotations_imported(obj) and "unsupported operand type" in str(exc) ): # pragma: <3.14 cover result = obj.__annotations__ else: result = {} except NameError as exc: _LOGGER.warning( 'Cannot resolve forward reference in type annotations of "%s" (module %s): %s', name, getattr(obj, "__module__", "?"), exc, type="sphinx_autodoc_typehints", subtype="forward_reference", location=get_obj_location(obj), ) if sys.version_info >= (3, 14): result = annotationlib.get_annotations(obj, format=annotationlib.Format.FORWARDREF) else: result = obj.__annotations__ # pragma: <3.14 cover return result def _resolve_type_guarded_imports(autodoc_mock_imports: list[str], obj: Any) -> None: if _should_skip_guarded_import_resolution(obj): return module = inspect.getmodule(obj) if module: try: module_code = inspect.getsource(module) except (TypeError, OSError): ... else: _TYPE_GUARD_IMPORTS_RESOLVED.add(module.__name__) _execute_guarded_code(autodoc_mock_imports, obj, module_code) def _should_skip_guarded_import_resolution(obj: Any) -> bool: if isinstance(obj, types.ModuleType): return False if not hasattr(obj, "__globals__"): return True return obj.__module__ in _TYPE_GUARD_IMPORTS_RESOLVED or obj.__module__ in sys.builtin_module_names def _execute_guarded_code(autodoc_mock_imports: list[str], obj: Any, module_code: str) -> None: for _, part in _TYPE_GUARD_IMPORT_RE.findall(module_code): guarded_code = textwrap.dedent(part) try: _run_guarded_import(autodoc_mock_imports, obj, guarded_code) except Exception as exc: # noqa: BLE001 module_name = getattr(obj, "__module__", None) or getattr(obj, "__name__", "?") _LOGGER.warning( "Failed guarded type import in %r: %r", module_name, exc, type="sphinx_autodoc_typehints", subtype="guarded_import", location=get_obj_location(obj), ) def _run_guarded_import(autodoc_mock_imports: list[str], obj: Any, guarded_code: str) -> None: ns = getattr(obj, "__globals__", obj.__dict__) try: with mock(autodoc_mock_imports): exec(guarded_code, ns) # noqa: S102 except ImportError as exc: if not exc.name: return _resolve_type_guarded_imports(autodoc_mock_imports, importlib.import_module(exc.name)) try: with mock(autodoc_mock_imports): exec(guarded_code, ns) # noqa: S102 except ImportError: pass def _add_type_params_to_localns( obj: Any, localns: dict[Any, MyTypeAliasForwardRef] ) -> dict[Any, MyTypeAliasForwardRef]: if type_params := getattr(obj, "__type_params__", None): localns = {**localns, **{p.__name__: p for p in type_params}} qualname = getattr(obj, "__qualname__", "") or "" parts = qualname.rsplit(".", 1) if len(parts) > 1: parent_name = parts[0] ns = getattr(obj, "__globals__", None) if ns is None: module = inspect.getmodule(obj) ns = vars(module) if module else None if ns and (parent := ns.get(parent_name)) and (parent_params := getattr(parent, "__type_params__", None)): localns = {**localns, **{p.__name__: p for p in parent_params}} return localns def _future_annotations_imported(obj: Any) -> bool: annotations_ = getattr(inspect.getmodule(obj), "annotations", None) if annotations_ is None: return False return bool(annotations_.compiler_flag == 0x1000000) # noqa: PLR2004 __all__ = ["get_all_type_hints"] sphinx_autodoc_typehints-3.8.0/src/sphinx_autodoc_typehints/_resolver/_util.py0000644000000000000000000000405613615410400025220 0ustar00"""Shared utilities for type hint resolution.""" from __future__ import annotations import inspect from typing import TYPE_CHECKING, Any from sphinx_autodoc_typehints._annotations import MyTypeAliasForwardRef if TYPE_CHECKING: from sphinx.environment import BuildEnvironment def get_obj_location(obj: Any) -> str | None: try: filepath = inspect.getsourcefile(obj) or inspect.getfile(obj) except TypeError: return None try: lineno = inspect.getsourcelines(obj)[1] except (OSError, TypeError): return filepath else: return f"{filepath}:{lineno}" def collect_documented_type_aliases( obj: Any, module_prefix: str, env: BuildEnvironment ) -> tuple[dict[str, MyTypeAliasForwardRef], dict[int, MyTypeAliasForwardRef]]: raw_annotations = getattr(obj, "__annotations__", {}) if not raw_annotations: return {}, {} py_objects = env.get_domain("py").objects # ty: ignore[unresolved-attribute] deferred: dict[str, MyTypeAliasForwardRef] = {} eager: dict[int, MyTypeAliasForwardRef] = {} obj_globals = getattr(obj, "__globals__", {}) for annotation in raw_annotations.values(): if isinstance(annotation, str): if _is_documented_type(annotation, module_prefix, py_objects): deferred[annotation] = MyTypeAliasForwardRef(annotation) else: for var_name, var_value in obj_globals.items(): if ( var_value is annotation and not var_name.startswith("_") and _is_documented_type(var_name, module_prefix, py_objects) ): eager[id(annotation)] = MyTypeAliasForwardRef(var_name) return deferred, eager def _is_documented_type(name: str, module_prefix: str, py_objects: dict[str, Any]) -> bool: return any( candidate in py_objects and py_objects[candidate].objtype == "type" for candidate in (f"{module_prefix}.{name}", name) ) __all__ = [ "collect_documented_type_aliases", "get_obj_location", ] sphinx_autodoc_typehints-3.8.0/tests/conftest.py0000644000000000000000000000455513615410400017152 0ustar00from __future__ import annotations import re import shutil import sys from contextlib import suppress from pathlib import Path from typing import TYPE_CHECKING import pytest from sphobjinv import Inventory if TYPE_CHECKING: from _pytest.config import Config pytest_plugins = "sphinx.testing.fixtures" collect_ignore = ["roots"] _SPHINX_EMPHASIS_RE = re.compile( r""" (? str: text = text.replace("\u2013", "--") return _SPHINX_EMPHASIS_RE.sub(r'"\1"', text) @pytest.fixture(scope="session") def inv(pytestconfig: Config) -> Inventory: cache_path = f"python{sys.version_info.major}.{sys.version_info.minor}/objects.inv" assert pytestconfig.cache is not None inv_dict = pytestconfig.cache.get(cache_path, None) if inv_dict is not None: return Inventory(inv_dict) # ty: ignore[too-many-positional-arguments] url = f"https://docs.python.org/{sys.version_info.major}.{sys.version_info.minor}/objects.inv" inv = Inventory(url=url) # ty: ignore[unknown-argument] pytestconfig.cache.set(cache_path, inv.json_dict()) return inv @pytest.fixture(autouse=True) def _remove_sphinx_projects(sphinx_test_tempdir: Path) -> None: # Remove any directory which appears to be a Sphinx project from # the temporary directory area. # See https://github.com/sphinx-doc/sphinx/issues/4040 for entry in sphinx_test_tempdir.iterdir(): with suppress(PermissionError): if entry.is_dir() and Path(entry, "_build").exists(): shutil.rmtree(str(entry)) @pytest.fixture def rootdir() -> Path: return Path(str(Path(__file__).parent) or ".").absolute() / "roots" def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None: # noqa: ARG001 version_re = re.compile(r"_py(\d)(\d)\.py$") match = version_re.search(collection_path.name) if match: version = tuple(int(x) for x in match.groups()) if sys.version_info < version: # ty: ignore[unsupported-operator] return True return None sphinx_autodoc_typehints-3.8.0/tests/test_annotated_doc.py0000644000000000000000000000662513615410400021166 0ustar00from __future__ import annotations import importlib.util import sys from pathlib import Path from typing import TYPE_CHECKING, Annotated import pytest from conftest import normalize_sphinx_text from typing_extensions import Doc from sphinx_autodoc_typehints import _extract_doc_description if TYPE_CHECKING: from io import StringIO from sphinx.testing.util import SphinxTestApp numpydoc = pytest.importorskip("numpydoc") @pytest.mark.parametrize( ("annotation", "expected"), [ pytest.param(Annotated[int, Doc("hello")], "hello", id="annotated-with-doc"), pytest.param(Annotated[int, 42], None, id="annotated-without-doc"), pytest.param(int, None, id="plain-type"), pytest.param(Annotated[int, Doc("first"), Doc("second")], "first", id="picks-first-doc"), ], ) def test_extract_doc_description(annotation: type, expected: str | None) -> None: assert _extract_doc_description(annotation) == expected def _load_and_build( app: SphinxTestApp, status: StringIO, monkeypatch: pytest.MonkeyPatch, testroot: str, func_name: str, ) -> str: mod_name = "mod" mod_path = Path(__file__).parent / "roots" / f"test-{testroot}" / f"{mod_name}.py" spec = importlib.util.spec_from_file_location(mod_name, mod_path) assert spec is not None assert spec.loader is not None module = importlib.util.module_from_spec(spec) sys.modules[mod_name] = module spec.loader.exec_module(module) (Path(app.srcdir) / "index.rst").write_text(f".. autofunction:: {mod_name}.{func_name}\n") monkeypatch.setitem(sys.modules, mod_name, module) app.build() assert "build succeeded" in status.getvalue() return normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) @pytest.mark.parametrize( ("func_name", "expected", "not_expected"), [ pytest.param( "greet", ["The person's name", "The greeting phrase", "The full greeting message"], [], id="all-params-and-return", ), pytest.param("partial_doc", ["The x value", "The y value"], [], id="partial-doc-with-docstring"), pytest.param("no_doc", ["Identity"], [], id="annotated-without-doc-metadata"), pytest.param("docstring_wins", ["Docstring description"], ["Doc description"], id="docstring-takes-precedence"), ], ) @pytest.mark.sphinx("text", testroot="annotated-doc") def test_sphinx_field_list( app: SphinxTestApp, status: StringIO, monkeypatch: pytest.MonkeyPatch, func_name: str, expected: list[str], not_expected: list[str], ) -> None: result = _load_and_build(app, status, monkeypatch, "annotated-doc", func_name) for text in expected: assert text in result for text in not_expected: assert text not in result @pytest.mark.parametrize( ("func_name", "expected"), [ pytest.param("transform", ["The input data", "The transformed result"], id="doc-injected"), pytest.param("compute", ["Placeholder", "The sum"], id="existing-params-preserved"), ], ) @pytest.mark.sphinx("text", testroot="annotated-doc-numpydoc") def test_numpydoc( app: SphinxTestApp, status: StringIO, monkeypatch: pytest.MonkeyPatch, func_name: str, expected: list[str], ) -> None: result = _load_and_build(app, status, monkeypatch, "annotated-doc-numpydoc", func_name) for text in expected: assert text in result sphinx_autodoc_typehints-3.8.0/tests/test_annotations.py0000644000000000000000000005600513615410400020716 0ustar00from __future__ import annotations import enum import re import sys import types import typing from collections.abc import Callable, Mapping from io import StringIO from types import EllipsisType, FrameType, FunctionType, ModuleType, NotImplementedType, TracebackType from typing import ( # noqa: UP035 IO, TYPE_CHECKING, Annotated, Any, AnyStr, Dict, ForwardRef, Generic, List, Literal, NewType, NotRequired, Optional, Required, Tuple, Type, TypeVar, Union, ) from unittest.mock import create_autospec import pytest import typing_extensions from sphinx.config import Config from sphinx_autodoc_typehints import ( format_annotation, get_annotation_args, get_annotation_class_name, get_annotation_module, ) if TYPE_CHECKING: from sphobjinv import Inventory T = TypeVar("T") U_co = TypeVar("U_co", covariant=True) V_contra = TypeVar("V_contra", contravariant=True) X = TypeVar("X", str, int) Y = TypeVar("Y", bound=str) Z = TypeVar("Z", bound="A") S = TypeVar("S", bound="miss") # type: ignore[name-defined] # miss not defined on purpose # noqa: F821 W = NewType("W", str) class SomeEnum(enum.Enum): VALUE = "val" P = typing_extensions.ParamSpec("P") P_args = P.args P_kwargs = P.kwargs P_co = typing_extensions.ParamSpec("P_co", covariant=True) # ty: ignore[invalid-paramspec] P_contra = typing_extensions.ParamSpec("P_contra", contravariant=True) # ty: ignore[invalid-paramspec] P_bound = typing_extensions.ParamSpec("P_bound", bound=str) # ty: ignore[invalid-paramspec] RecList = Union[int, List["RecList"]] MutualRecA = Union[bool, List["MutualRecB"]] MutualRecB = Union[str, List["MutualRecA"]] class A: def get_type(self) -> type: return type(self) # pragma: no cover class Inner: ... class B[T]: name = "Foo" class C(B[str]): ... class D(typing_extensions.Protocol): ... class E(typing_extensions.Protocol[T]): ... class Slotted: __slots__ = () class Metaclass(type): ... PY312_PLUS = sys.version_info >= (3, 12) @pytest.mark.parametrize( ("annotation", "module", "class_name", "args"), [ pytest.param(str, "builtins", "str", (), id="str"), pytest.param(None, "builtins", "None", (), id="None"), pytest.param(ModuleType, "types", "ModuleType", (), id="ModuleType"), pytest.param(FunctionType, "types", "FunctionType", (), id="FunctionType"), pytest.param(types.CodeType, "types", "CodeType", (), id="CodeType"), pytest.param(types.CoroutineType, "types", "CoroutineType", (), id="CoroutineType"), pytest.param(Any, "typing", "Any", (), id="Any"), pytest.param(AnyStr, "typing", "AnyStr", (), id="AnyStr"), pytest.param(Dict, "typing", "Dict", (), id="Dict"), pytest.param(Dict[str, int], "typing", "Dict", (str, int), id="Dict_parametrized"), pytest.param(Dict[T, int], "typing", "Dict", (T, int), id="Dict_typevar"), pytest.param(Tuple, "typing", "Tuple", (), id="Tuple"), pytest.param(Tuple[str, int], "typing", "Tuple", (str, int), id="Tuple_parametrized"), pytest.param(Union[str, int], "typing", "Union", (str, int), id="Union"), pytest.param(Callable, "collections.abc", "Callable", (), id="Callable"), pytest.param(Callable[..., str], "collections.abc", "Callable", (..., str), id="Callable_returntype"), pytest.param( Callable[[int, str], str], "collections.abc", "Callable", (int, str, str), id="Callable_all_types" ), pytest.param( Callable[[int, str], str], "collections.abc", "Callable", (int, str, str), id="collections.abc.Callable_all_types", ), pytest.param(re.Pattern, "re", "Pattern", (), id="Pattern"), pytest.param(re.Pattern[str], "re", "Pattern", (str,), id="Pattern_parametrized"), pytest.param(re.Match, "re", "Match", (), id="Match"), pytest.param(re.Match[str], "re", "Match", (str,), id="Match_parametrized"), pytest.param(IO, "typing", "IO", (), id="IO"), pytest.param(W, "typing", "NewType", (str,), id="W"), pytest.param(P, "typing", "ParamSpec", (), id="P"), pytest.param(P_args, "typing", "ParamSpecArgs", (), id="P_args"), pytest.param(P_kwargs, "typing", "ParamSpecKwargs", (), id="P_kwargs"), pytest.param(Metaclass, __name__, "Metaclass", (), id="Metaclass"), pytest.param(Slotted, __name__, "Slotted", (), id="Slotted"), pytest.param(A, __name__, "A", (), id="A"), pytest.param(B, __name__, "B", (), id="B"), pytest.param(C, __name__, "C", (), id="C"), pytest.param(D, __name__, "D", (), id="D"), pytest.param(E, __name__, "E", (), id="E"), pytest.param(E[int], __name__, "E", (int,), id="E_parametrized"), pytest.param(A.Inner, __name__, "A.Inner", (), id="Inner"), ], ) def test_parse_annotation(annotation: Any, module: str, class_name: str, args: tuple[Any, ...]) -> None: got_mod = get_annotation_module(annotation) got_cls = get_annotation_class_name(annotation, module) got_args = get_annotation_args(annotation, module, class_name) assert (got_mod, got_cls, got_args) == (module, class_name, args) _CASES = [ pytest.param(str, ":py:class:`str`", id="str"), pytest.param(int, ":py:class:`int`", id="int"), pytest.param(StringIO, ":py:class:`~io.StringIO`", id="StringIO"), pytest.param(EllipsisType, ":py:data:`~types.EllipsisType`", id="EllipsisType"), pytest.param(FunctionType, ":py:data:`~types.FunctionType`", id="FunctionType"), pytest.param(FrameType, ":py:data:`~types.FrameType`", id="FrameType"), pytest.param(ModuleType, ":py:class:`~types.ModuleType`", id="ModuleType"), pytest.param(NotImplementedType, ":py:data:`~types.NotImplementedType`", id="NotImplementedType"), pytest.param(TracebackType, ":py:class:`~types.TracebackType`", id="TracebackType"), pytest.param(type(None), ":py:obj:`None`", id="type None"), pytest.param(type, ":py:class:`type`", id="type"), pytest.param(Callable, ":py:class:`~collections.abc.Callable`", id="abc-Callable"), pytest.param(Type, ":py:class:`~typing.Type`", id="typing-Type"), pytest.param(Type[A], rf":py:class:`~typing.Type`\ \[:py:class:`~{__name__}.A`]", id="typing-A"), pytest.param(Any, ":py:data:`~typing.Any`", id="Any"), pytest.param(AnyStr, ":py:data:`~typing.AnyStr`", id="AnyStr"), pytest.param(Generic[T], r":py:class:`~typing.Generic`\ \[:py:class:`~typing.TypeVar`\ \(``T``)]", id="Generic"), pytest.param(Mapping, ":py:class:`~collections.abc.Mapping`", id="Mapping"), pytest.param( Mapping[T, int], r":py:class:`~collections.abc.Mapping`\ \[:py:class:`~typing.TypeVar`\ \(``T``), :py:class:`int`]", id="Mapping-T-int", ), pytest.param( Mapping[str, V_contra], r":py:class:`~collections.abc.Mapping`\ \[:py:class:`str`, :py:class:`~typing.TypeVar`\ \(" "``V_contra``, contravariant=True)]", id="Mapping-T-int-contra", ), pytest.param( Mapping[T, U_co], r":py:class:`~collections.abc.Mapping`\ \[:py:class:`~typing.TypeVar`\ \(``T``), " r":py:class:`~typing.TypeVar`\ \(``U_co``, covariant=True)]", id="Mapping-T-int-co", ), pytest.param( Mapping[str, bool], r":py:class:`~collections.abc.Mapping`\ \[:py:class:`str`, :py:class:`bool`]", id="Mapping-str-bool", ), pytest.param(Dict, ":py:class:`~typing.Dict`", id="Dict"), pytest.param( Dict[T, int], r":py:class:`~typing.Dict`\ \[:py:class:`~typing.TypeVar`\ \(``T``), :py:class:`int`]", id="Dict-T-int", ), pytest.param( Dict[str, V_contra], r":py:class:`~typing.Dict`\ \[:py:class:`str`, :py:class:`~typing.TypeVar`\ \(``V_contra``, " r"contravariant=True)]", id="Dict-T-int-contra", ), pytest.param( Dict[T, U_co], r":py:class:`~typing.Dict`\ \[:py:class:`~typing.TypeVar`\ \(``T``)," r" :py:class:`~typing.TypeVar`\ \(``U_co``, covariant=True)]", id="Dict-T-int-co", ), pytest.param( Dict[str, bool], r":py:class:`~typing.Dict`\ \[:py:class:`str`, :py:class:`bool`]", id="Dict-str-bool", ), pytest.param(Tuple, ":py:data:`~typing.Tuple`", id="Tuple"), pytest.param( Tuple[str, bool], r":py:data:`~typing.Tuple`\ \[:py:class:`str`, :py:class:`bool`]", id="Tuple-str-bool", ), pytest.param( Tuple[int, int, int], r":py:data:`~typing.Tuple`\ \[:py:class:`int`, :py:class:`int`, :py:class:`int`]", id="Tuple-int-int-int", ), pytest.param( Tuple[str, ...], r":py:data:`~typing.Tuple`\ \[:py:class:`str`, :py:data:`...`]", id="Tuple-str-Ellipsis", ), pytest.param(Union, f":py:{'class' if sys.version_info >= (3, 14) else 'data'}:`~typing.Union`", id="Union"), pytest.param( types.UnionType, f":py:{'class' if sys.version_info >= (3, 14) else 'data'}:`~typing.Union`", id="UnionType" ), pytest.param( Union[str, bool], ":py:class:`str` | :py:class:`bool`" if sys.version_info >= (3, 14) else r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:class:`bool`]", id="Union-str-bool", ), pytest.param( Union[str, bool, None], ":py:class:`str` | :py:class:`bool` | :py:obj:`None`" if sys.version_info >= (3, 14) else r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:class:`bool`, :py:obj:`None`]", id="Union-str-bool-None", ), pytest.param( Union[str, Any], ":py:class:`str` | :py:data:`~typing.Any`" if sys.version_info >= (3, 14) else r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:data:`~typing.Any`]", id="Union-str-Any", ), pytest.param( Optional[str], ":py:class:`str` | :py:obj:`None`" if sys.version_info >= (3, 14) else r":py:data:`~typing.Optional`\ \[:py:class:`str`]", id="Optional-str", ), pytest.param( Union[str, None], ":py:class:`str` | :py:obj:`None`" if sys.version_info >= (3, 14) else r":py:data:`~typing.Optional`\ \[:py:class:`str`]", id="Optional-str-None", ), pytest.param( type[T] | types.UnionType, ":py:class:`type`\\ \\[:py:class:`~typing.TypeVar`\\ \\(``T``)] | " f":py:{'class' if sys.version_info >= (3, 14) else 'data'}:`~typing.Union`", id="typevar union bar uniontype", ), pytest.param( Optional[str | bool], ":py:class:`str` | :py:class:`bool` | :py:obj:`None`" if sys.version_info >= (3, 14) else r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:class:`bool`, :py:obj:`None`]", id="Optional-Union-str-bool", ), pytest.param( RecList, ":py:class:`int` | :py:class:`~typing.List`\\ \\[RecList]" if sys.version_info >= (3, 14) else r":py:data:`~typing.Union`\ \[:py:class:`int`, :py:class:`~typing.List`\ \[RecList]]", id="RecList", ), pytest.param( MutualRecA, ":py:class:`bool` | :py:class:`~typing.List`\\ \\[MutualRecB]" if sys.version_info >= (3, 14) else r":py:data:`~typing.Union`\ \[:py:class:`bool`, :py:class:`~typing.List`\ \[MutualRecB]]", id="MutualRecA", ), pytest.param(Callable, ":py:class:`~collections.abc.Callable`", id="Callable"), pytest.param( Callable[..., int], r":py:class:`~collections.abc.Callable`\ \[:py:data:`...`, :py:class:`int`]", id="Callable-Ellipsis-int", ), pytest.param( Callable[[int], int], r":py:class:`~collections.abc.Callable`\ \[\[:py:class:`int`], :py:class:`int`]", id="Callable-int-int", ), pytest.param( Callable[[int, str], bool], r":py:class:`~collections.abc.Callable`\ \[\[:py:class:`int`, :py:class:`str`], :py:class:`bool`]", id="Callable-int-str-bool", ), pytest.param( Callable[[int, str], None], r":py:class:`~collections.abc.Callable`\ \[\[:py:class:`int`, :py:class:`str`], :py:obj:`None`]", id="Callable-int-str", ), pytest.param( Callable[[T], T], r":py:class:`~collections.abc.Callable`\ \[\[:py:class:`~typing.TypeVar`\ \(``T``)]," r" :py:class:`~typing.TypeVar`\ \(``T``)]", id="Callable-T-T", ), pytest.param( Callable[[int, str], bool], r":py:class:`~collections.abc.Callable`\ \[\[:py:class:`int`, :py:class:`str`], :py:class:`bool`]", id="Callable-int-str-bool", ), pytest.param(re.Pattern, ":py:class:`~re.Pattern`", id="Pattern"), pytest.param(re.Pattern[str], r":py:class:`~re.Pattern`\ \[:py:class:`str`]", id="Pattern-str"), pytest.param(IO, ":py:class:`~typing.IO`", id="IO"), pytest.param(IO[str], r":py:class:`~typing.IO`\ \[:py:class:`str`]", id="IO-str"), pytest.param(Metaclass, f":py:class:`~{__name__}.Metaclass`", id="Metaclass"), pytest.param(A, f":py:class:`~{__name__}.A`", id="A"), pytest.param(B, f":py:class:`~{__name__}.B`", id="B"), pytest.param(B[int], rf":py:class:`~{__name__}.B`\ \[:py:class:`int`]", id="B-int"), pytest.param(C, f":py:class:`~{__name__}.C`", id="C"), pytest.param(D, f":py:class:`~{__name__}.D`", id="D"), pytest.param(E, f":py:class:`~{__name__}.E`", id="E"), pytest.param(E[int], rf":py:class:`~{__name__}.E`\ \[:py:class:`int`]", id="E-int"), pytest.param(W, f":py:class:`~{__name__}.W` (:py:class:`str`)", id="W"), pytest.param(T, r":py:class:`~typing.TypeVar`\ \(``T``)", id="T"), pytest.param(U_co, r":py:class:`~typing.TypeVar`\ \(``U_co``, covariant=True)", id="U-co"), pytest.param(V_contra, r":py:class:`~typing.TypeVar`\ \(``V_contra``, contravariant=True)", id="V-contra"), pytest.param(X, r":py:class:`~typing.TypeVar`\ \(``X``, :py:class:`str`, :py:class:`int`)", id="X"), pytest.param(Y, r":py:class:`~typing.TypeVar`\ \(``Y``, bound= :py:class:`str`)", id="Y"), pytest.param(Z, r":py:class:`~typing.TypeVar`\ \(``Z``, bound= A)", id="Z"), pytest.param(S, r":py:class:`~typing.TypeVar`\ \(``S``, bound= miss)", id="S"), pytest.param( P, rf":py:class:`~typing.ParamSpec`\ \(``P``{', bound= :py:obj:`None`' if PY312_PLUS else ''})", id="P" ), pytest.param( P_co, rf":py:class:`~typing.ParamSpec`\ \(``P_co``{', bound= :py:obj:`None`' if PY312_PLUS else ''}, covariant=True)", id="P_co", ), pytest.param( P_contra, rf":py:class:`~typing.ParamSpec`\ \(``P_contra``{', bound= :py:obj:`None`' if PY312_PLUS else ''}" ", contravariant=True)", id="P-contra", ), pytest.param(P_bound, r":py:class:`~typing.ParamSpec`\ \(``P_bound``, bound= :py:class:`str`)", id="P-bound"), pytest.param(Tuple[()], ":py:data:`~typing.Tuple`", id="Tuple-p"), pytest.param(Tuple[int,], r":py:data:`~typing.Tuple`\ \[:py:class:`int`]", id="Tuple-p-int"), pytest.param( Tuple[int, int], r":py:data:`~typing.Tuple`\ \[:py:class:`int`, :py:class:`int`]", id="Tuple-p-int-int", ), pytest.param( Tuple[int, ...], r":py:data:`~typing.Tuple`\ \[:py:class:`int`, :py:data:`...`]", id="Tuple-p-Ellipsis", ), pytest.param(Annotated[int, "metadata"], r":py:class:`int`", id="Annotated-metadata"), pytest.param(Required[int], r":py:class:`~typing.Required`\ \[:py:class:`int`]", id="Required"), pytest.param(NotRequired[int], r":py:class:`~typing.NotRequired`\ \[:py:class:`int`]", id="NotRequired"), ] @pytest.mark.parametrize(("annotation", "expected_result"), _CASES) def test_format_annotation(inv: Inventory, annotation: Any, expected_result: str) -> None: conf = create_autospec(Config, _annotation_globals=globals(), always_use_bars_union=False) result = format_annotation(annotation, conf) assert result == expected_result if re.match(r"^:py:data:`~typing\.Union`\\\[.*``None``.*]", expected_result): # pragma: <3.14 cover expected_result_not_simplified = expected_result.replace(", ``None``", "") expected_result_not_simplified += ":py:data:`~typing.Optional`\\ \\[" expected_result_not_simplified += "]" conf = create_autospec( Config, simplify_optional_unions=False, _annotation_globals=globals(), always_use_bars_union=False, ) assert format_annotation(annotation, conf) == expected_result_not_simplified if "typing" in expected_result_not_simplified: expected_result_not_simplified = expected_result_not_simplified.replace("~typing", "typing") conf = create_autospec( Config, typehints_fully_qualified=True, simplify_optional_unions=False, _annotation_globals=globals(), ) assert format_annotation(annotation, conf) == expected_result_not_simplified if "typing" in expected_result or __name__ in expected_result: expected_result = expected_result.replace("~typing", "typing") expected_result = expected_result.replace("~collections.abc", "collections.abc") expected_result = expected_result.replace("~numpy", "numpy") expected_result = expected_result.replace("~" + __name__, __name__) conf = create_autospec( Config, typehints_fully_qualified=True, _annotation_globals=globals(), always_use_bars_union=False, ) assert format_annotation(annotation, conf) == expected_result if ( result.count(":py:") == 1 and ("typing" in result or "types" in result) and (match := re.match(r"^:py:(?Pclass|data|func):`~(?P[^`]+)`", result)) ): name = match.group("name") expected_role = next((o.role for o in inv.objects if o.name == name), None) if expected_role and expected_role == "function": # pragma: no cover expected_role = "func" assert match.group("role") == expected_role @pytest.mark.parametrize( ("annotation", "expected_result"), [ ("int | float", ":py:class:`int` | :py:class:`float`"), ("int | float | None", ":py:class:`int` | :py:class:`float` | :py:obj:`None`"), ("Union[int, float]", ":py:class:`int` | :py:class:`float`"), ("Union[int, float, None]", ":py:class:`int` | :py:class:`float` | :py:obj:`None`"), ("Optional[int | float]", ":py:class:`int` | :py:class:`float` | :py:obj:`None`"), ("Optional[Union[int, float]]", ":py:class:`int` | :py:class:`float` | :py:obj:`None`"), ("Union[int | float, str]", ":py:class:`int` | :py:class:`float` | :py:class:`str`"), ("Union[int, float] | str", ":py:class:`int` | :py:class:`float` | :py:class:`str`"), ], ) def test_always_use_bars_union(annotation: str, expected_result: str) -> None: conf = create_autospec(Config, always_use_bars_union=True) result = format_annotation(eval(annotation), conf) # noqa: S307 assert result == expected_result @pytest.mark.parametrize("library", [typing, typing_extensions], ids=["typing", "typing_extensions"]) @pytest.mark.parametrize( ("annotation", "params", "expected_result"), [ pytest.param("ClassVar", int, ":py:data:`~typing.ClassVar`\\ \\[:py:class:`int`]", id="ClassVar"), pytest.param("NoReturn", None, ":py:data:`~typing.NoReturn`", id="NoReturn"), pytest.param("Literal", ("a", 1), ":py:data:`~typing.Literal`\\ \\[``'a'``, ``1``]", id="Literal"), pytest.param( "Literal", (SomeEnum.VALUE,), rf":py:data:`~typing.Literal`\ \[:py:attr:`~{__name__}.SomeEnum.VALUE`]", id="Literal-enum", ), pytest.param("Type", None, ":py:class:`~typing.Type`", id="Type-none"), pytest.param("Type", (A,), rf":py:class:`~typing.Type`\ \[:py:class:`~{__name__}.A`]", id="Type-A"), ], ) def test_format_annotation_both_libs(library: ModuleType, annotation: str, params: Any, expected_result: str) -> None: try: annotation_cls = getattr(library, annotation) except AttributeError: # pragma: no cover pytest.skip(f"{annotation} not available in the {library.__name__} module") ann = annotation_cls if params is None else annotation_cls[params] result = format_annotation(ann, create_autospec(Config)) assert result == expected_result def test_format_annotation_tuple() -> None: conf = create_autospec(Config) assert format_annotation((int, str), conf) == "(:py:class:`int`, :py:class:`str`)" def test_format_annotation_empty_tuple() -> None: conf = create_autospec(Config) assert format_annotation((), conf) == "()" def test_format_annotation_single_element_tuple() -> None: conf = create_autospec(Config) assert format_annotation((int,), conf) == "(:py:class:`int`, )" def test_format_annotation_none() -> None: conf = create_autospec(Config) assert format_annotation(None, conf) == ":py:obj:`None`" def test_format_annotation_ellipsis() -> None: conf = create_autospec(Config) assert format_annotation(Ellipsis, conf) == ":py:data:`...`" def test_format_annotation_forward_ref() -> None: conf = create_autospec(Config) assert format_annotation(ForwardRef("SomeClass"), conf) == "SomeClass" def test_format_annotation_typing_extensions_module_fixup() -> None: conf = create_autospec(Config) result = format_annotation(typing_extensions.ClassVar[int], conf) assert "typing.ClassVar" in result def test_format_annotation_io_module_fixup() -> None: conf = create_autospec(Config) result = format_annotation(StringIO, conf) assert "io.StringIO" in result def test_format_annotation_with_formatter_returning_value() -> None: conf = create_autospec(Config, typehints_formatter=lambda ann, _cfg: f"Custom({ann})") result = format_annotation(int, conf) assert result == "Custom()" def test_format_annotation_with_formatter_returning_none() -> None: conf = create_autospec(Config, typehints_formatter=lambda _ann, _cfg: None, always_use_bars_union=False) result = format_annotation(int, conf) assert result == ":py:class:`int`" def test_format_annotation_short_literals() -> None: conf = create_autospec(Config) result = format_annotation(Literal["a", "b"], conf, short_literals=True) assert result == "\\``'a'`` | ``'b'``" def test_get_annotation_module_raises_on_unknown() -> None: with pytest.raises(ValueError, match="Cannot determine the module"): get_annotation_module(42) def test_get_annotation_class_name_name_attr() -> None: result = get_annotation_class_name(IO, "typing") assert result == "IO" def test_get_annotation_args_classvar() -> None: result = get_annotation_args(typing.ClassVar[int], "typing", "ClassVar") assert result == (int,) def test_get_annotation_args_literal_values() -> None: result = get_annotation_args(typing.Literal["a", 1], "typing", "Literal") assert result == ("a", 1) def test_get_annotation_args_generic() -> None: result = get_annotation_args(Generic[T], "typing", "Generic") assert result == (T,) sphinx_autodoc_typehints-3.8.0/tests/test_attributes_patch.py0000644000000000000000000000357713615410400021734 0ustar00from __future__ import annotations from unittest.mock import MagicMock, patch from sphinx_autodoc_typehints.attributes_patch import ( OKAY_TO_PATCH, TYPE_IS_RST_LABEL, patch_attribute_handling, patched_parse_annotation, ) def test_okay_to_patch_is_true() -> None: assert OKAY_TO_PATCH def test_patched_parse_annotation_without_label() -> None: settings = MagicMock() env = MagicMock() with patch("sphinx_autodoc_typehints.attributes_patch._parse_annotation") as mock_parse: mock_parse.return_value = "parsed" result = patched_parse_annotation(settings, "int", env) assert result == "parsed" mock_parse.assert_called_once_with("int", env) def test_patched_parse_annotation_dispatches_on_label() -> None: settings = MagicMock() env = MagicMock() with patch("sphinx_autodoc_typehints.attributes_patch.rst_to_docutils", return_value=["nodes"]) as mock_rst: result = patched_parse_annotation(settings, f"{TYPE_IS_RST_LABEL}**bold**", env) mock_rst.assert_called_once_with(settings, "**bold**") assert result == ["nodes"] def test_patch_attribute_handling_when_not_okay() -> None: app = MagicMock() with patch("sphinx_autodoc_typehints.attributes_patch.OKAY_TO_PATCH", False): patch_attribute_handling(app) def test_rst_to_docutils_returns_parsed_nodes() -> None: """Lines 34-36: rst_to_docutils parses RST and returns inner nodes.""" from docutils.frontend import get_default_settings # noqa: PLC0415 from sphinx.parsers import RSTParser # noqa: PLC0415 from sphinx_autodoc_typehints.attributes_patch import rst_to_docutils # noqa: PLC0415 settings = get_default_settings(RSTParser) # type: ignore[arg-type] settings.env = MagicMock() result = rst_to_docutils(settings, "**bold text**") assert len(result) > 0 assert any("bold text" in node.astext() for node in result) sphinx_autodoc_typehints-3.8.0/tests/test_formats_numpydoc.py0000644000000000000000000002424213615410400021750 0ustar00from __future__ import annotations from unittest.mock import MagicMock from sphinx.application import Sphinx from sphinx_autodoc_typehints._formats import detect_format from sphinx_autodoc_typehints._formats._base import InsertIndexInfo from sphinx_autodoc_typehints._formats._numpydoc import NumpydocFormat, _convert_numpydoc_to_sphinx_fields from sphinx_autodoc_typehints._formats._sphinx import SphinxFieldListFormat def test_convert_parameters_section() -> None: lines = [ "", "Summary.", "", ":Parameters:", "", " **x** : int", " The x value.", "", " **y** : str", " The y value.", "", ] _convert_numpydoc_to_sphinx_fields(lines) assert ":param x: The x value." in lines assert ":type x: int" in lines assert ":param y: The y value." in lines assert ":type y: str" in lines assert ":Parameters:" not in lines def test_convert_returns_single() -> None: lines = [ ":Returns:", "", " **result** : str", " The combined result.", "", ] _convert_numpydoc_to_sphinx_fields(lines) assert ":returns: The combined result." in lines assert ":Returns:" not in lines def test_convert_returns_no_name() -> None: lines = [ ":Returns:", "", " str", " A greeting.", "", ] _convert_numpydoc_to_sphinx_fields(lines) assert ":returns: A greeting." in lines def test_convert_returns_multiple() -> None: lines = [ ":Returns:", "", " **name** : str", " The name.", "", " **value** : int", " The value.", "", ] _convert_numpydoc_to_sphinx_fields(lines) assert ":returns:" in lines assert any("**name**" in line and "str" in line for line in lines) assert any("**value**" in line and "int" in line for line in lines) def test_convert_raises() -> None: lines = [ ":Raises:", "", " ValueError", " If x is negative.", "", " TypeError", " If x is not an integer.", "", ] _convert_numpydoc_to_sphinx_fields(lines) assert ":raises ValueError: If x is negative." in lines assert ":raises TypeError: If x is not an integer." in lines def test_convert_yields_single() -> None: lines = [ ":Yields:", "", " int", " A number.", "", ] _convert_numpydoc_to_sphinx_fields(lines) assert ":Yields: A number." in lines def test_convert_yields_multiple() -> None: lines = [ ":Yields:", "", " **name** : str", " The name.", "", " **value** : int", " The value.", "", ] _convert_numpydoc_to_sphinx_fields(lines) assert ":Yields:" in lines assert any("**name**" in line and "str" in line for line in lines) assert any("**value**" in line and "int" in line for line in lines) def test_convert_other_parameters() -> None: lines = [ ":Other Parameters:", "", " **flag** : bool", " A flag.", "", ] _convert_numpydoc_to_sphinx_fields(lines) assert ":param flag: A flag." in lines assert ":type flag: bool" in lines def test_no_numpydoc_content_is_noop() -> None: original = [ "", "A normal docstring.", "", ":param x: The x value.", ":type x: int", ] lines = list(original) _convert_numpydoc_to_sphinx_fields(lines) assert lines == original def test_multiline_description() -> None: lines = [ ":Parameters:", "", " **data** : dict", " The data dictionary.", " It can span multiple lines.", "", ] _convert_numpydoc_to_sphinx_fields(lines) assert ":param data: The data dictionary. It can span multiple lines." in lines assert ":type data: dict" in lines def test_param_without_type() -> None: lines = [ ":Parameters:", "", " **x**", " The x value.", "", ] _convert_numpydoc_to_sphinx_fields(lines) assert ":param x: The x value." in lines assert not any(line.startswith(":type x:") for line in lines) def test_param_without_description() -> None: lines = [ ":Parameters:", "", " **x** : int", "", ] _convert_numpydoc_to_sphinx_fields(lines) assert ":param x:" in lines assert ":type x: int" in lines def test_raises_with_named_exception() -> None: lines = [ ":Raises:", "", " **err** : ValueError", " If something is wrong.", "", ] _convert_numpydoc_to_sphinx_fields(lines) assert ":raises err: If something is wrong." in lines def test_returns_single_no_description() -> None: lines = [ ":Returns:", "", " **result** : str", "", ] _convert_numpydoc_to_sphinx_fields(lines) assert not any(line.startswith(":returns:") for line in lines) def test_multiple_sections() -> None: lines = [ "Summary.", "", ":Parameters:", "", " **x** : int", " The x value.", "", ":Returns:", "", " **result** : str", " The result.", "", ":Raises:", "", " ValueError", " If bad.", "", ] _convert_numpydoc_to_sphinx_fields(lines) assert ":param x: The x value." in lines assert ":type x: int" in lines assert ":returns: The result." in lines assert ":raises ValueError: If bad." in lines def test_numpydoc_format_detect() -> None: assert NumpydocFormat.detect([":Parameters:", " **x** : int", " desc"]) assert NumpydocFormat.detect([":Returns:", " str", " result"]) assert not NumpydocFormat.detect([":param x: foo", ":type x: int"]) def test_numpydoc_format_find_param() -> None: fmt = NumpydocFormat() lines = [ ":Parameters:", "", " **x** : int", " The x value.", "", ] idx = fmt.find_param(lines, "x") assert idx is not None assert lines[idx].startswith(":param x:") def test_numpydoc_format_find_preexisting_type() -> None: fmt = NumpydocFormat() lines = [ ":Parameters:", "", " **x** : int", " The x value.", "", ] fmt._ensure_converted(lines) # noqa: SLF001 annotation, found = fmt.find_preexisting_type(lines, "x") assert found assert "int" in annotation def test_numpydoc_format_add_undocumented_param() -> None: fmt = NumpydocFormat() lines = [ ":Parameters:", "", " **x** : int", " The x value.", "", ] idx = fmt.add_undocumented_param(lines, "y") assert idx is not None assert ":param y:" in lines[idx] def test_numpydoc_format_inject_param_type() -> None: fmt = NumpydocFormat() lines = [ ":Parameters:", "", " **x** : int", " The x value.", "", ] fmt._ensure_converted(lines) # noqa: SLF001 idx = fmt.find_param(lines, "x") assert idx is not None fmt.inject_param_type(lines, "x", "int", idx + 1) assert ":type x: int" in lines def test_numpydoc_format_get_arg_name_from_line() -> None: fmt = NumpydocFormat() assert fmt.get_arg_name_from_line(":param x: foo") == "x" assert fmt.get_arg_name_from_line("not a param line") is None def test_numpydoc_format_append_default() -> None: """Line 199: append_default delegates to SphinxFieldListFormat.""" fmt = NumpydocFormat() lines = [":param x: the x"] result = fmt.append_default(lines, 0, ":type x: int", " (default: ``0``)", after=False) assert result == ":type x: int (default: ``0``)" def test_convert_section_without_blank_line_after_header() -> None: """Branch 99->102: no empty line between section header and first entry.""" lines = [ ":Parameters:", " **x** : int", " The x value.", "", ] _convert_numpydoc_to_sphinx_fields(lines) assert ":param x: The x value." in lines assert ":type x: int" in lines def test_convert_yields_single_no_description_then_params() -> None: """Branch 133->90: single Yields entry without description followed by another section.""" lines = [ ":Yields:", "", " **result** : int", "", ":Parameters:", "", " **x** : int", " The x.", "", ] _convert_numpydoc_to_sphinx_fields(lines) assert not any(line.startswith(":Yields:") for line in lines) assert ":param x: The x." in lines assert ":type x: int" in lines def test_detect_format_returns_numpydoc_for_numpydoc_lines() -> None: lines = [":Parameters:", "", " **x** : int", " desc"] fmt = detect_format(lines) assert isinstance(fmt, NumpydocFormat) def test_detect_format_returns_sphinx_for_plain_lines() -> None: lines = [":param x: The x value."] fmt = detect_format(lines) assert isinstance(fmt, SphinxFieldListFormat) def test_parse_numpydoc_entries_breaks_on_unrecognized_format() -> None: lines = [ ":Parameters:", "", " **x** : int", " The x value.", "not an entry", ] _convert_numpydoc_to_sphinx_fields(lines) assert ":param x: The x value." in lines def test_numpydoc_format_get_rtype_insert_info() -> None: fmt = NumpydocFormat() lines = [ ":Parameters:", "", " **x** : int", " The x value.", "", ] app: Sphinx = MagicMock(spec=Sphinx) app.env = MagicMock() result = fmt.get_rtype_insert_info(app, lines) assert result is not None def test_numpydoc_format_inject_rtype() -> None: fmt = NumpydocFormat() lines = [":param x: value"] info = InsertIndexInfo(insert_index=len(lines)) fmt.inject_rtype(lines, ":py:class:`str`", info, use_rtype=True) assert any("rtype" in line for line in lines) sphinx_autodoc_typehints-3.8.0/tests/test_formats_sphinx.py0000644000000000000000000000737713615410400021435 0ustar00from __future__ import annotations from typing import TYPE_CHECKING from unittest.mock import MagicMock, create_autospec from docutils import nodes from sphinx.application import Sphinx from sphinx.config import Config import sphinx_autodoc_typehints as sat from sphinx_autodoc_typehints._formats import InsertIndexInfo from sphinx_autodoc_typehints._formats._sphinx import SphinxFieldListFormat, _node_line_no if TYPE_CHECKING: import pytest def test_inject_rtype_inserts_blank_line_before_rtype(monkeypatch: pytest.MonkeyPatch) -> None: def sample() -> str: ... config = create_autospec( Config, typehints_document_rtype=True, typehints_document_rtype_none=True, typehints_use_rtype=True, python_display_short_literal_types=False, ) app: Sphinx = create_autospec(Sphinx, config=config) fmt = SphinxFieldListFormat() monkeypatch.setattr( fmt, "get_rtype_insert_info", lambda _app, _lines: InsertIndexInfo(insert_index=1, found_directive=True) ) monkeypatch.setattr(sat, "format_annotation", lambda *_args, **_kwargs: "str") monkeypatch.setattr(sat, "add_type_css_class", lambda value: value) lines = ["A paragraph.", ".. note:: hi"] sat._inject_rtype({"return": str}, sample, app, "function", "sample", lines, fmt) # noqa: SLF001 assert lines == ["A paragraph.", "", ":rtype: str", "", ".. note:: hi"] def test_inject_rtype_does_not_add_extra_blank_line(monkeypatch: pytest.MonkeyPatch) -> None: def sample() -> str: ... config = create_autospec( Config, typehints_document_rtype=True, typehints_document_rtype_none=True, typehints_use_rtype=True, python_display_short_literal_types=False, ) app: Sphinx = create_autospec(Sphinx, config=config) fmt = SphinxFieldListFormat() monkeypatch.setattr(fmt, "get_rtype_insert_info", lambda _app, _lines: InsertIndexInfo(insert_index=1)) monkeypatch.setattr(sat, "format_annotation", lambda *_args, **_kwargs: "str") monkeypatch.setattr(sat, "add_type_css_class", lambda value: value) lines = ["", ""] sat._inject_rtype({"return": str}, sample, app, "function", "sample", lines, fmt) # noqa: SLF001 assert lines == ["", ":rtype: str", ""] def test_node_line_no_none_input() -> None: assert _node_line_no(None) is None # type: ignore[arg-type] def test_node_line_no_descends_to_find_line() -> None: child = MagicMock(spec=nodes.Node) child.line = 5 child.children = [] parent = MagicMock(spec=nodes.Node) parent.line = None parent.children = [child] assert _node_line_no(parent) == 5 def test_sphinx_format_detect_always_true() -> None: assert SphinxFieldListFormat.detect([]) is True assert SphinxFieldListFormat.detect([":param x: foo"]) is True def test_get_rtype_insert_info_field_list_without_param_synonyms() -> None: """Line 169: field list exists but has no param-like fields, so it's skipped.""" config = create_autospec(Config) app: Sphinx = create_autospec(Sphinx, config=config) app.env = MagicMock() fmt = SphinxFieldListFormat() lines = [":var x: some variable"] result = fmt.get_rtype_insert_info(app, lines) assert result is not None assert result.insert_index == len(lines) def test_get_rtype_insert_info_directive_with_nonempty_preceding_line() -> None: """Line 182: directive found but preceding line is non-empty triggers break.""" config = create_autospec(Config) app: Sphinx = create_autospec(Sphinx, config=config) app.env = MagicMock() fmt = SphinxFieldListFormat() lines = ["Some text.", "", ".. note::", "", " Important note"] result = fmt.get_rtype_insert_info(app, lines) assert result is not None assert result.insert_index == len(lines) sphinx_autodoc_typehints-3.8.0/tests/test_generator_yields.py0000644000000000000000000000605213615410400021715 0ustar00from __future__ import annotations import re import sys from pathlib import Path from textwrap import dedent from typing import TYPE_CHECKING import pytest from conftest import normalize_sphinx_text if TYPE_CHECKING: from collections.abc import AsyncGenerator, AsyncIterator, Generator, Iterator from io import StringIO from sphinx.testing.util import SphinxTestApp class GenClass: """A class with generators.""" def gen_method( # noqa: PLR6301 self, arg1: int | None = None, # noqa: ARG002 ) -> Generator[tuple[str, ...], None, None]: """Summary. Args: arg1: argument 1 Yields: strings of things """ yield ("test",) def iter_method(self) -> Iterator[int]: # noqa: PLR6301 """Summary. Yields: integers """ yield 1 async def async_gen_method(self) -> AsyncGenerator[str, None]: # noqa: PLR6301 """Summary. Yields: strings """ yield "test" async def async_iter_method(self) -> AsyncIterator[int]: # noqa: PLR6301 """Summary. Yields: integers """ yield 1 def no_yields_method(self) -> Generator[int, None, None]: # noqa: PLR6301 """Summary without yields section.""" yield 1 def _extract_section(text: str, method_name: str) -> str: pattern = rf"({method_name}\(\).*?)(?=(?:async\s+)?\w+\(\)|$)" if match := re.search(pattern, text, re.DOTALL): return match.group(1) msg = f"Method {method_name} not found in output" raise ValueError(msg) def _build_genclass(app: SphinxTestApp, monkeypatch: pytest.MonkeyPatch) -> str: (Path(app.srcdir) / "index.rst").write_text( dedent("""\ Test ==== .. autoclass:: mod.GenClass :members: """) ) monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__]) app.build() return normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) @pytest.mark.sphinx("text", testroot="integration") def test_generator_no_duplicate_return_type( app: SphinxTestApp, status: StringIO, warning: StringIO, # noqa: ARG001 monkeypatch: pytest.MonkeyPatch, ) -> None: text = _build_genclass(app, monkeypatch) assert "build succeeded" in status.getvalue() for method in ("gen_method", "iter_method", "async_gen_method", "async_iter_method"): section = _extract_section(text, method) assert "Yields" in section, f"{method} missing Yields" assert "Return type" not in section, f"{method} has unexpected Return type" @pytest.mark.sphinx("text", testroot="integration") def test_generator_without_yields_keeps_rtype( app: SphinxTestApp, status: StringIO, warning: StringIO, # noqa: ARG001 monkeypatch: pytest.MonkeyPatch, ) -> None: text = _build_genclass(app, monkeypatch) assert "build succeeded" in status.getvalue() section = _extract_section(text, "no_yields_method") assert "Return type" in section sphinx_autodoc_typehints-3.8.0/tests/test_guarded_import.py0000644000000000000000000000266713615410400021373 0ustar00from __future__ import annotations import sys import types from pathlib import Path from textwrap import dedent from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from io import StringIO from sphinx.testing.util import SphinxTestApp @pytest.mark.sphinx("text", testroot="integration") def test_guarded_import_missing_name_no_warning( app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, ) -> None: target_mod = types.ModuleType("target_mod") target_mod.__file__ = "/fake/target_mod.py" source = dedent("""\ from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from target_mod import nonexistent_name def func(x: int) -> int: '''Do something. Args: x: a number ''' return x """) user_mod = types.ModuleType("user_mod") user_mod.__file__ = "/fake/user_mod.py" exec(compile(source, "/fake/user_mod.py", "exec"), user_mod.__dict__) # noqa: S102 monkeypatch.setitem(sys.modules, "target_mod", target_mod) monkeypatch.setitem(sys.modules, "user_mod", user_mod) (Path(app.srcdir) / "index.rst").write_text( dedent("""\ Test ==== .. autofunction:: user_mod.func """) ) app.build() assert "build succeeded" in status.getvalue() assert "Failed guarded type import" not in warning.getvalue() sphinx_autodoc_typehints-3.8.0/tests/test_init.py0000644000000000000000000001451213615410400017321 0ustar00from __future__ import annotations import inspect from typing import Any from unittest.mock import MagicMock, create_autospec, patch from sphinx.application import Sphinx from sphinx.config import Config import sphinx_autodoc_typehints as sat from sphinx_autodoc_typehints import _inject_overload_signatures, process_docstring, process_signature from sphinx_autodoc_typehints._resolver._util import get_obj_location from sphinx_autodoc_typehints.patches import _OVERLOADS_CACHE class _ClassWithPrivate: def __secret(self, x: int) -> str: ... def _make_sig_app(**overrides: Any) -> Sphinx: defaults = { "typehints_fully_qualified": False, "simplify_optional_unions": False, "typehints_formatter": None, "typehints_use_signature": False, "typehints_use_signature_return": False, "autodoc_type_aliases": {}, } defaults.update(overrides) config = create_autospec(Config, **defaults) # ty: ignore[invalid-argument-type] return create_autospec(Sphinx, config=config) def _make_docstring_app(**overrides: Any) -> Sphinx: config = MagicMock() config.__getitem__ = getattr config.autodoc_type_aliases = {} config.autodoc_mock_imports = [] config.typehints_fully_qualified = False config.simplify_optional_unions = False config.typehints_formatter = None config.typehints_document_rtype = True config.typehints_document_rtype_none = True config.typehints_use_rtype = True config.typehints_defaults = None config.always_document_param_types = False config.python_display_short_literal_types = False for key, value in overrides.items(): setattr(config, key, value) app = MagicMock(spec=Sphinx) app.config = config app.env = None return app def test_process_signature_class_attr_lookup_fails() -> None: """Line 111: class name not found on module returns None.""" def fake_method(self: Any, x: int) -> str: ... fake_method.__annotations__ = {"x": int, "return": str} fake_method.__qualname__ = "NonExistentClass.method" fake_method.__module__ = __name__ app = _make_sig_app() result = process_signature(app, "method", "test.NonExistentClass.method", fake_method, MagicMock(), "", "") assert result is None def test_process_signature_private_name_mangling() -> None: """Line 114: dunder-prefixed method names get mangled.""" method = _ClassWithPrivate.__dict__["_ClassWithPrivate__secret"] app = _make_sig_app() result = process_signature(app, "method", "test_init._ClassWithPrivate.__secret", method, MagicMock(), "", "") assert result is not None sig_str, _ = result assert "self" not in sig_str def test_process_docstring_sphinx_signature_raises_value_error() -> None: """Lines 157-158: sphinx_signature raising ValueError sets signature to None.""" def func(x: int) -> str: ... app = _make_docstring_app(typehints_document_rtype=False) lines: list[str] = [":param x: the x"] with patch("sphinx_autodoc_typehints.sphinx_signature", side_effect=ValueError("bad")): process_docstring(app, "function", "func", func, None, lines) assert ":param x: the x" in lines def test_process_docstring_sphinx_signature_raises_type_error() -> None: """Lines 157-158: sphinx_signature raising TypeError sets signature to None.""" def func(x: int) -> str: ... app = _make_docstring_app(typehints_document_rtype=False) lines: list[str] = [":param x: the x"] with patch("sphinx_autodoc_typehints.sphinx_signature", side_effect=TypeError("bad")): process_docstring(app, "function", "func", func, None, lines) assert ":param x: the x" in lines def test_inject_overload_no_qualname() -> None: """Line 198: obj without __qualname__ returns False.""" obj = MagicMock(spec=[]) obj.__module__ = "some_module" obj.__qualname__ = "" _OVERLOADS_CACHE["some_module"] = {} try: app = _make_docstring_app() assert _inject_overload_signatures(app, "function", "name", obj, []) is False finally: _OVERLOADS_CACHE.pop("some_module", None) def test_inject_overload_unannotated_param_and_no_return() -> None: """Lines 214, 217->224: overload with unannotated param and no return annotation.""" obj = MagicMock() obj.__module__ = "test_mod" obj.__qualname__ = "func" sig = inspect.Signature( parameters=[inspect.Parameter("x", inspect.Parameter.POSITIONAL_OR_KEYWORD)], ) _OVERLOADS_CACHE["test_mod"] = {"func": [sig]} try: app = _make_docstring_app() lines: list[str] = [] result = _inject_overload_signatures(app, "function", "name", obj, lines) assert result is True joined = "\n".join(lines) assert "**x**" in joined assert "\u2192" not in joined finally: _OVERLOADS_CACHE.pop("test_mod", None) def test_inject_overload_empty_overloads_returns_false() -> None: """Line 233: no matching overloads returns False.""" obj = MagicMock() obj.__module__ = "test_mod2" obj.__qualname__ = "func" _OVERLOADS_CACHE["test_mod2"] = {"other_func": []} try: app = _make_docstring_app() assert _inject_overload_signatures(app, "function", "name", obj, []) is False finally: _OVERLOADS_CACHE.pop("test_mod2", None) def test_local_function_warning_includes_location() -> None: def fake_method(self: Any, x: int) -> str: ... fake_method.__annotations__ = {"x": int, "return": str} fake_method.__qualname__ = "outer..inner" fake_method.__module__ = __name__ app = _make_sig_app() mock_logger = MagicMock() with patch("sphinx_autodoc_typehints._LOGGER", mock_logger): process_signature(app, "method", "test.outer..inner", fake_method, MagicMock(), "", "") mock_logger.warning.assert_called_once() kwargs = mock_logger.warning.call_args.kwargs assert "location" in kwargs assert kwargs["location"] == get_obj_location(fake_method) def test_inject_types_no_signature() -> None: """Branch 261->263: signature is None skips _inject_signature.""" def sample() -> str: ... app = _make_docstring_app(typehints_document_rtype=False) lines: list[str] = [] sat._inject_types_to_docstring({"return": str}, None, sample, app, "function", "sample", lines) # noqa: SLF001 assert not any(line.startswith(":type") for line in lines) sphinx_autodoc_typehints-3.8.0/tests/test_integration.py0000644000000000000000000006704213615410400020707 0ustar00from __future__ import annotations import re import sys from dataclasses import dataclass from inspect import isclass from pathlib import Path from textwrap import dedent, indent from typing import ( # no type comments TYPE_CHECKING, Any, Literal, NewType, Optional, TypeVar, Union, overload, ) import pytest from conftest import normalize_sphinx_text if TYPE_CHECKING: from collections.abc import AsyncGenerator, Callable from io import StringIO from mailbox import Mailbox from types import CodeType, ModuleType from sphinx.testing.util import SphinxTestApp T = TypeVar("T") W = NewType("W", str) @dataclass class WarningInfo: """Properties and assertion methods for warnings.""" regexp: str type: str def assert_regexp(self, message: str) -> None: regexp = self.regexp msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {message!r}" assert re.search(regexp, message), msg def assert_type(self, message: str) -> None: expected = f"[{self.type}]" msg = f"Warning did not contain type and subtype.\n Expected: {expected}\n Input: {message}" assert expected in message, msg def assert_warning(self, message: str) -> None: self.assert_regexp(message) self.assert_type(message) def expected(expected: str, **options: Any) -> Callable[[T], T]: def dec(val: T) -> T: val.EXPECTED = expected # ty: ignore[unresolved-attribute] val.OPTIONS = options # ty: ignore[unresolved-attribute] return val return dec def warns(info: WarningInfo) -> Callable[[T], T]: def dec(val: T) -> T: val.WARNING = info # ty: ignore[unresolved-attribute] return val return dec @expected("mod.get_local_function()") def get_local_function(): # noqa: ANN201 def wrapper(self) -> str: # noqa: ANN001 """ Wrapper """ return wrapper @warns(WarningInfo(regexp="Cannot handle as a local function", type="sphinx_autodoc_typehints.local_function")) @expected( """\ class mod.Class(x, y, z=None) Initializer docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar * **z** ("str" | "None") -- baz class InnerClass Inner class. inner_method(x) Inner method. Parameters: **x** ("bool") -- foo Return type: "str" classmethod a_classmethod(x, y, z=None) Classmethod docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar * **z** ("str" | "None") -- baz Return type: "str" a_method(x, y, z=None) Method docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar * **z** ("str" | "None") -- baz Return type: "str" property a_property: str Property docstring static a_staticmethod(x, y, z=None) Staticmethod docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar * **z** ("str" | "None") -- baz Return type: "str" locally_defined_callable_field() -> str Wrapper Return type: "str" """, ) class Class: """ Initializer docstring. :param x: foo :param y: bar :param z: baz """ def __init__(self, x: bool, y: int, z: Optional[str] = None) -> None: pass def a_method(self, x: bool, y: int, z: Optional[str] = None) -> str: """ Method docstring. :param x: foo :param y: bar :param z: baz """ def _private_method(self, x: str) -> str: """ Private method docstring. :param x: foo """ def __dunder_method(self, x: str) -> str: """ Dunder method docstring. :param x: foo """ def __magic_custom_method__(self, x: str) -> str: # noqa: PLW3201 """ Magic dunder method docstring. :param x: foo """ @classmethod def a_classmethod(cls, x: bool, y: int, z: Optional[str] = None) -> str: """ Classmethod docstring. :param x: foo :param y: bar :param z: baz """ @staticmethod def a_staticmethod(x: bool, y: int, z: Optional[str] = None) -> str: """ Staticmethod docstring. :param x: foo :param y: bar :param z: baz """ @property def a_property(self) -> str: """ Property docstring """ class InnerClass: """ Inner class. """ def inner_method(self, x: bool) -> str: """ Inner method. :param x: foo """ def __dunder_inner_method(self, x: bool) -> str: """ Dunder inner method. :param x: foo """ locally_defined_callable_field = get_local_function() @expected( """\ exception mod.DummyException(message) Exception docstring Parameters: **message** ("str") -- blah """, ) class DummyException(Exception): # noqa: N818 """ Exception docstring :param message: blah """ def __init__(self, message: str) -> None: super().__init__(message) @expected( """\ mod.function(x, y, z_=None) Function docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar * **z_** ("str" | "None") -- baz Returns: something Return type: bytes """, ) def function(x: bool, y: int, z_: Optional[str] = None) -> str: """ Function docstring. :param x: foo :param y: bar :param z\\_: baz :return: something :rtype: bytes """ @expected( """\ mod.function_with_starred_documentation_param_names(*args, **kwargs) Function docstring. Usage: print(1) Parameters: * ***args** ("int") -- foo * ****kwargs** ("str") -- bar """, ) def function_with_starred_documentation_param_names(*args: int, **kwargs: str): # noqa: ANN201 r""" Function docstring. Usage:: print(1) :param \*args: foo :param \**kwargs: bar """ @expected( """\ mod.function_with_escaped_default(x='\\\\x08') Function docstring. Parameters: **x** ("str") -- foo """, ) def function_with_escaped_default(x: str = "\b"): # noqa: ANN201 """ Function docstring. :param x: foo """ @warns( WarningInfo( regexp="Cannot resolve forward reference in type annotations", type="sphinx_autodoc_typehints.forward_reference" ) ) @expected( """\ mod.function_with_unresolvable_annotation(x) Function docstring. Parameters: **x** (a.b.c) -- foo """, ) def function_with_unresolvable_annotation(x: a.b.c): # noqa: ANN201, F821 # ty: ignore[unresolved-reference] """ Function docstring. :arg x: foo """ @expected( """\ mod.function_with_typehint_comment(x, y) Function docstring. Parameters: * **x** ("int") -- foo * **y** ("str") -- bar Return type: "None" """, ) def function_with_typehint_comment( # noqa: ANN201 x, # type: int # noqa: ANN001 y, # type: str # noqa: ANN001 ): # type: (...) -> None """ Function docstring. :parameter x: foo :parameter y: bar """ @expected( """\ class mod.ClassWithTypehints(x) Class docstring. Parameters: **x** ("int") -- foo foo(x) Method docstring. Parameters: **x** ("str") -- foo Return type: "int" method_without_typehint(x) Method docstring. """, ) class ClassWithTypehints: """ Class docstring. :param x: foo """ def __init__( self, x, # type: int # noqa: ANN001 ) -> None: # type: (...) -> None pass def foo( # noqa: ANN201, PLR6301 self, x, # type: str # noqa: ANN001, ARG002 ): # type: (...) -> int """ Method docstring. :arg x: foo """ return 42 def method_without_typehint(self, x): # noqa: ANN001, ANN201, ARG002, PLR6301 """ Method docstring. """ # test that multiline str can be correctly indented multiline_str = """ test """ return multiline_str # noqa: RET504 @expected( """\ mod.function_with_typehint_comment_not_inline(x=None, *y, z, **kwargs) Function docstring. Parameters: * **x** ("str" | "bytes" | "None") -- foo * **y** ("str") -- bar * **z** ("bytes") -- baz * **kwargs** ("int") -- some kwargs Return type: "None" """, ) def function_with_typehint_comment_not_inline(x=None, *y, z, **kwargs): # noqa: ANN001, ANN002, ANN003, ANN201 # type: (Union[str, bytes, None], *str, bytes, **int) -> None """ Function docstring. :arg x: foo :argument y: bar :parameter z: baz :parameter kwargs: some kwargs """ @expected( """\ class mod.ClassWithTypehintsNotInline(x=None) Class docstring. Parameters: **x** ("Callable"[["int", "bytes"], "int"] | "None") -- foo foo(x=1) Method docstring. Parameters: **x** ("Callable"[["int", "bytes"], "int"]) -- foo Return type: "int" classmethod mk(x=None) Method docstring. Parameters: **x** ("Callable"[["int", "bytes"], "int"] | "None") -- foo Return type: "ClassWithTypehintsNotInline" """, ) class ClassWithTypehintsNotInline: """ Class docstring. :param x: foo """ def __init__(self, x=None) -> None: # type: (Optional[Callable[[int, bytes], int]]) -> None # noqa: ANN001 pass def foo(self, x=1): # type: (Callable[[int, bytes], int]) -> int # noqa: ANN001, ANN201, PLR6301 """ Method docstring. :param x: foo """ return x(1, b"") # ty: ignore[call-non-callable] @classmethod def mk( # noqa: ANN206 cls, x=None, # noqa: ANN001 ): # type: (Optional[Callable[[int, bytes], int]]) -> ClassWithTypehintsNotInline """ Method docstring. :param x: foo """ return cls(x) @expected( """\ mod.undocumented_function(x) Hi Return type: "str" """, ) def undocumented_function(x: int) -> str: """Hi""" return str(x) @expected( """\ class mod.DataClass(x) Class docstring. """, ) @dataclass class DataClass: """Class docstring.""" x: int @expected( """\ class mod.Decorator(func) Initializer docstring. Parameters: **func** ("Callable"[["int", "str"], "str"]) -- function """, ) class Decorator: """ Initializer docstring. :param func: function """ def __init__(self, func: Callable[[int, str], str]) -> None: pass @expected( """\ mod.mocked_import(x) A docstring. Parameters: **x** ("Mailbox") -- function """, ) def mocked_import(x: Mailbox): # noqa: ANN201 """ A docstring. :param x: function """ @expected( """\ mod.func_with_examples() A docstring. Return type: "int" -[ Examples ]- Here are a couple of examples of how to use this function. """, ) def func_with_examples() -> int: """ A docstring. .. rubric:: Examples Here are a couple of examples of how to use this function. """ @overload def func_with_overload(a: int, b: int) -> None: ... @overload def func_with_overload(a: str, b: str) -> None: ... @expected( """\ mod.func_with_overload(a, b) Overloads: * **a** (int), **b** (int) → None * **a** (str), **b** (str) → None f does the thing. The arguments can either be ints or strings but they must both have the same type. Parameters: * **a** ("int" | "str") -- The first thing * **b** ("int" | "str") -- The second thing """, ) def func_with_overload(a: Union[int, str], b: Union[int, str]) -> None: """ f does the thing. The arguments can either be ints or strings but they must both have the same type. Parameters ---------- a: The first thing b: The second thing """ @overload def overload_with_complex_types(x: list[int]) -> dict[str, int]: ... @overload def overload_with_complex_types(x: dict[str, int]) -> list[int]: ... @expected( """\ mod.overload_with_complex_types(x) Overloads: * **x** (list[int]) → dict[str, int] * **x** (dict[str, int]) → list[int] Function with overloaded complex types. Parameters: **x** ("list"["int"] | "dict"["str", "int"]) -- Input value """, ) def overload_with_complex_types(x: list[int] | dict[str, int]) -> dict[str, int] | list[int]: """Function with overloaded complex types. Parameters: x: Input value """ if isinstance(x, list): return {str(i): i for i in x} return list(x.values()) @expected( """\ mod.func_literals_long_format(a, b) A docstring. Parameters: * **a** ("Literal"["'arg1'", "'arg2'"]) -- Argument that can take either of two literal values. * **b** ("Literal"["'arg1'", "'arg2'"]) -- Argument that can take either of two literal values. Return type: "None" """, ) def func_literals_long_format(a: Literal["arg1", "arg2"], b: Literal["arg1", "arg2"]) -> None: """ A docstring. :param a: Argument that can take either of two literal values. :param b: Argument that can take either of two literal values. """ @expected( """\ mod.func_literals_short_format(a, b) A docstring. Parameters: * **a** ("'arg1'" | "'arg2'") -- Argument that can take either of two literal values. * **b** ("'arg1'" | "'arg2'") -- Argument that can take either of two literal values. Return type: "None" """, python_display_short_literal_types=True, ) def func_literals_short_format(a: Literal["arg1", "arg2"], b: Literal["arg1", "arg2"]) -> None: """ A docstring. :param a: Argument that can take either of two literal values. :param b: Argument that can take either of two literal values. """ @expected( """\ class mod.TestClassAttributeDocs A class code: CodeType | None An attribute """, ) class TestClassAttributeDocs: """A class""" code: Optional[CodeType] """An attribute""" @expected( """\ mod.func_with_examples_and_returns_after() f does the thing. -[ Examples ]- Here is an example Return type: "int" Returns: The index of the widget """, ) def func_with_examples_and_returns_after() -> int: """ f does the thing. Examples -------- Here is an example :returns: The index of the widget """ @expected( """\ mod.func_with_parameters_and_stuff_after(a, b) A func Parameters: * **a** ("int") -- a tells us something * **b** ("int") -- b tells us something Return type: "int" More info about the function here. """, ) def func_with_parameters_and_stuff_after(a: int, b: int) -> int: """A func :param a: a tells us something :param b: b tells us something More info about the function here. """ @expected( """\ mod.func_with_rtype_in_weird_spot(a, b) A func Parameters: * **a** ("int") -- a tells us something * **b** ("int") -- b tells us something -[ Examples ]- Here is an example Returns: The index of the widget More info about the function here. Return type: int """, ) def func_with_rtype_in_weird_spot(a: int, b: int) -> int: """A func :param a: a tells us something :param b: b tells us something Examples -------- Here is an example :returns: The index of the widget More info about the function here. :rtype: int """ @expected( """\ mod.empty_line_between_parameters(a, b) A func Parameters: * **a** ("int") -- One of the following possibilities: * a * b * c * **b** ("int") -- Whatever else we have to say. There is more of it And here too Return type: "int" More stuff here. """, ) def empty_line_between_parameters(a: int, b: int) -> int: """A func :param a: One of the following possibilities: - a - b - c :param b: Whatever else we have to say. There is more of it And here too More stuff here. """ @expected( """\ mod.func_with_code_block() A docstring. You would say: print("some python code here") Return type: "int" -[ Examples ]- Here are a couple of examples of how to use this function. """, ) def func_with_code_block() -> int: """ A docstring. You would say: .. code-block:: print("some python code here") .. rubric:: Examples Here are a couple of examples of how to use this function. """ @expected( """ mod.func_with_definition_list() Some text and then a definition list. Return type: "int" abc x xyz something """, ) def func_with_definition_list() -> int: """Some text and then a definition list. abc x xyz something """ # See https://github.com/tox-dev/sphinx-autodoc-typehints/issues/302 @expected( """\ mod.decorator_2(f) Run the decorated function with *asyncio.run*. Parameters: **f** ("Any") -- The function to wrap. Return type: "Any" -[ Examples ]- A """, ) def decorator_2(f: Any) -> Any: """Run the decorated function with `asyncio.run`. Parameters ---------- f The function to wrap. Examples -------- .. code-block:: python A """ assert f is not None @expected( """ class mod.ParamAndAttributeHaveSameName(blah) A Class Parameters: **blah** ("CodeType") -- Description of parameter blah blah: ModuleType Description of attribute blah """, ) class ParamAndAttributeHaveSameName: """ A Class Parameters ---------- blah: Description of parameter blah """ def __init__(self, blah: CodeType) -> None: pass blah: ModuleType """Description of attribute blah""" @expected( """ mod.napoleon_returns() A function. Return type: "CodeType" Returns: The info about the whatever. """, ) def napoleon_returns() -> CodeType: """ A function. Returns ------- The info about the whatever. """ @expected( """ mod.google_docstrings(arg1, arg2) Summary line. Extended description of function. Parameters: * **arg1** ("CodeType") -- Description of arg1 * **arg2** ("ModuleType") -- Description of arg2 Return type: "CodeType" Returns: Description of return value """, ) def google_docstrings(arg1: CodeType, arg2: ModuleType) -> CodeType: """Summary line. Extended description of function. Args: arg1: Description of arg1 arg2: Description of arg2 Returns: Description of return value """ @expected( """ mod.docstring_with_multiline_note_after_params(param) Do something. Parameters: **param** ("int") -- A parameter. Return type: "None" Note: Some notes. More notes """, ) def docstring_with_multiline_note_after_params(param: int) -> None: """Do something. Args: param: A parameter. Note: Some notes. More notes """ @expected( """ mod.docstring_with_bullet_list_after_params(param) Do something. Parameters: **param** ("int") -- A parameter. Return type: "None" * A: B * C: D """, ) def docstring_with_bullet_list_after_params(param: int) -> None: """Do something. Args: param: A parameter. * A: B * C: D """ @expected( """ mod.docstring_with_definition_list_after_params(param) Do something. Parameters: **param** ("int") -- A parameter. Return type: "None" Term A description maybe multiple lines Next Term Something about it """, ) def docstring_with_definition_list_after_params(param: int) -> None: """Do something. Args: param: A parameter. Term A description maybe multiple lines Next Term Something about it """ @expected( """ mod.docstring_with_enum_list_after_params(param) Do something. Parameters: **param** ("int") -- A parameter. Return type: "None" 1. A: B 2. C: D """, ) def docstring_with_enum_list_after_params(param: int) -> None: """Do something. Args: param: A parameter. 1. A: B 2. C: D """ @warns(WarningInfo(regexp="Definition list ends without a blank line", type="docutils")) @expected( """ mod.docstring_with_definition_list_after_params_no_blank_line(param) Do something. Parameters: **param** ("int") -- A parameter. Return type: "None" Term A description maybe multiple lines Next Term Something about it -[ Example ]- """, ) def docstring_with_definition_list_after_params_no_blank_line(param: int) -> None: """Do something. Args: param: A parameter. Term A description maybe multiple lines Next Term Something about it .. rubric:: Example """ @expected( """ mod.has_typevar(param) Do something. Parameters: **param** ("TypeVar"("T")) -- A parameter. Return type: "TypeVar"("T") """, ) def has_typevar[T](param: T) -> T: """Do something. Args: param: A parameter. """ return param @expected( """ mod.has_newtype(param) Do something. Parameters: **param** ("W" ("str")) -- A parameter. Return type: "W" ("str") """, ) def has_newtype(param: W) -> W: """Do something. Args: param: A parameter. """ return param AUTO_FUNCTION = ".. autofunction:: mod.{}" AUTO_CLASS = """\ .. autoclass:: mod.{} :members: """ AUTO_EXCEPTION = """\ .. autoexception:: mod.{} :members: """ @expected( """ mod.typehints_use_signature(a: AsyncGenerator) -> AsyncGenerator Do something. Parameters: **a** ("AsyncGenerator") -- blah Return type: "AsyncGenerator" """, typehints_use_signature=True, typehints_use_signature_return=True, ) def typehints_use_signature(a: AsyncGenerator) -> AsyncGenerator: """Do something. Args: a: blah """ return a @expected( """ mod.typehints_no_rtype_none() Do something. """, typehints_document_rtype_none=False, ) def typehints_no_rtype_none() -> None: """Do something.""" prolog = """ .. |test_node_start| replace:: {test_node_start} """.format(test_node_start="test_start") @expected( """ mod.docstring_with_multiline_note_after_params_prolog_replace(param) Do something. Parameters: **param** ("int") -- A parameter. Return type: "None" Note: Some notes. test_start More notes """, rst_prolog=prolog, ) def docstring_with_multiline_note_after_params_prolog_replace(param: int) -> None: """Do something. Args: param: A parameter. Note: Some notes. |test_node_start| More notes """ epilog = """ .. |test_node_end| replace:: {test_node_end} """.format(test_node_end="test_end") @expected( """ mod.docstring_with_multiline_note_after_params_epilog_replace(param) Do something. Parameters: **param** ("int") -- A parameter. Return type: "None" Note: Some notes. test_end More notes """, rst_epilog=epilog, ) def docstring_with_multiline_note_after_params_epilog_replace(param: int) -> None: """Do something. Args: param: A parameter. Note: Some notes. |test_node_end| More notes """ @expected( """ mod.docstring_with_see_also() Test See also: more info at `_. Return type: "str" """ ) def docstring_with_see_also() -> str: """ Test .. seealso:: more info at `_. """ return "" @expected( """ mod.has_doctest1() Test that we place the return type correctly when the function has a doctest. Return type: "None" >>> this is a fake doctest a >>> more doctest b """ ) def has_doctest1() -> None: r"""Test that we place the return type correctly when the function has a doctest. >>> this is a fake doctest a >>> more doctest b """ Unformatted = TypeVar("Unformatted") @warns(WarningInfo(regexp="cannot cache unpickleable configuration value: 'typehints_formatter'", type="config.cache")) @expected( """ mod.typehints_formatter_applied_to_signature(param: Formatted) -> Formatted Do nothing Parameters: **param** (Formatted) -- A parameter Return type: Formatted Returns: The return value """, typehints_use_signature=True, typehints_use_signature_return=True, typehints_formatter=lambda _, __=None: "Formatted", ) def typehints_formatter_applied_to_signature[Unformatted](param: Unformatted) -> Unformatted: """ Do nothing Args: param: A parameter Returns: The return value """ return param # Config settings for each test run. # Config Name: Sphinx Options as Dict. configs = { "default_conf": {}, "prolog_conf": {"rst_prolog": prolog}, "epilog_conf": { "rst_epilog": epilog, }, "bothlog_conf": { "rst_prolog": prolog, "rst_epilog": epilog, }, } @pytest.mark.parametrize("val", [x for x in globals().values() if hasattr(x, "EXPECTED")]) @pytest.mark.parametrize("conf_run", ["default_conf", "prolog_conf", "epilog_conf", "bothlog_conf"]) @pytest.mark.sphinx("text", testroot="integration") def test_integration( app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, val: Any, conf_run: str ) -> None: if isclass(val) and issubclass(val, BaseException): template = AUTO_EXCEPTION elif isclass(val): template = AUTO_CLASS else: template = AUTO_FUNCTION (Path(app.srcdir) / "index.rst").write_text(template.format(val.__name__)) app.config.__dict__.update(configs[conf_run]) app.config.__dict__.update(val.OPTIONS) app.config.always_use_bars_union = True monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__]) app.build() assert "build succeeded" in status.getvalue() # Build succeeded warning_info: Union[WarningInfo, None] = getattr(val, "WARNING", None) value = warning.getvalue().strip() if warning_info: warning_info.assert_warning(value) else: assert not value result = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) expected = normalize_sphinx_text(val.EXPECTED) try: assert result.strip() == dedent(expected).strip() except Exception: indented = indent(f'"""\n{result}\n"""', " " * 4) print(f"@expected(\n{indented}\n)\n") # noqa: T201 raise sphinx_autodoc_typehints-3.8.0/tests/test_integration_autodoc_type_aliases.py0000644000000000000000000000712013615410400025156 0ustar00from __future__ import annotations import re import sys from pathlib import Path from textwrap import dedent, indent from typing import TYPE_CHECKING, Any, Literal, NewType, TypeVar import pytest from conftest import normalize_sphinx_text if TYPE_CHECKING: from collections.abc import Callable from io import StringIO from sphinx.testing.util import SphinxTestApp T = TypeVar("T") W = NewType("W", str) def expected(expected: str, **options: Any) -> Callable[[T], T]: def dec(val: T) -> T: val.EXPECTED = expected # ty: ignore[unresolved-attribute] val.OPTIONS = options # ty: ignore[unresolved-attribute] return val return dec def warns(pattern: str) -> Callable[[T], T]: def dec(val: T) -> T: val.WARNING = pattern # ty: ignore[unresolved-attribute] return val return dec ArrayLike = Literal["test"] class _SchemaMeta(type): # noqa: PLW1641 def __eq__(cls, other: object) -> bool: return True class Schema(metaclass=_SchemaMeta): pass @expected( """ mod.f(s) Do something. Parameters: **s** ("Schema") -- Some schema. Return type: "Schema" """ ) def f(s: Schema) -> Schema: """ Do something. Args: s: Some schema. """ return s class AliasedClass: ... @expected( """ mod.g(s) Do something. Parameters: **s** ("Class Alias") -- Some schema. Return type: "Class Alias" """ ) def g(s: AliasedClass) -> AliasedClass: """ Do something. Args: s: Some schema. """ return s @expected( f"""\ mod.function(x, y) Function docstring. Parameters: * **x** ({'Array | "None"' if sys.version_info >= (3, 14) else '"Optional"[Array]'}) -- foo * **y** ("Schema") -- boo Returns: something Return type: bytes """, ) def function(x: ArrayLike | None, y: Schema) -> str: """ Function docstring. :param x: foo :param y: boo :return: something :rtype: bytes """ # Config settings for each test run. # Config Name: Sphinx Options as Dict. configs = {"default_conf": {"autodoc_type_aliases": {"ArrayLike": "Array", "AliasedClass": '"Class Alias"'}}} @pytest.mark.parametrize("val", [x for x in globals().values() if hasattr(x, "EXPECTED")]) @pytest.mark.parametrize("conf_run", list(configs.keys())) @pytest.mark.sphinx("text", testroot="integration") def test_integration( app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, val: Any, conf_run: str ) -> None: template = ".. autofunction:: mod.{}" (Path(app.srcdir) / "index.rst").write_text(template.format(val.__name__)) app.config.__dict__.update(configs[conf_run]) app.config.__dict__.update(val.OPTIONS) monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__]) app.build() assert "build succeeded" in status.getvalue() # Build succeeded regexp = getattr(val, "WARNING", None) value = warning.getvalue().strip() if regexp: msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}" assert re.search(regexp, value), msg elif not re.search(r"WARNING: Inline strong start-string without end-string.", value): assert not value result = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) expected = normalize_sphinx_text(val.EXPECTED) try: assert result.strip() == dedent(expected).strip() except Exception: indented = indent(f'"""\n{result}\n"""', " " * 4) print(f"@expected(\n{indented}\n)\n") # noqa: T201 raise sphinx_autodoc_typehints-3.8.0/tests/test_integration_issue_347.py0000644000000000000000000000606713615410400022514 0ustar00from __future__ import annotations import sys from pathlib import Path from typing import TYPE_CHECKING import pytest from conftest import normalize_sphinx_text numpydoc = pytest.importorskip("numpydoc") if TYPE_CHECKING: from io import StringIO from sphinx.testing.util import SphinxTestApp def simple_func(x: int, y: str) -> str: """ Do something simple. Parameters ---------- x : int The x value. y : str The y value. Returns ------- result : str The combined result. """ return f"{x}{y}" # pragma: no cover def raises_func(x: int) -> None: """ Raise on bad input. Parameters ---------- x : int The input value. Raises ------ ValueError If x is negative. TypeError If x is not an integer. """ def multi_return(x: int) -> tuple[str, int]: """ Return multiple values. Parameters ---------- x : int The input. Returns ------- name : str The name. value : int The value. """ return str(x), x # pragma: no cover def no_params() -> str: """ Function with no parameters. Returns ------- str A greeting. """ return "hello" # pragma: no cover def _build_and_get_output( app: SphinxTestApp, status: StringIO, monkeypatch: pytest.MonkeyPatch, func_name: str, ) -> str: template = f"""\ .. autofunction:: mod_numpy.{func_name} """ (Path(app.srcdir) / "index.rst").write_text(template) monkeypatch.setitem(sys.modules, "mod_numpy", sys.modules[__name__]) app.build() assert "build succeeded" in status.getvalue() return normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) @pytest.mark.sphinx("text", testroot="issue_347") def test_simple_params_and_return( app: SphinxTestApp, status: StringIO, monkeypatch: pytest.MonkeyPatch, ) -> None: result = _build_and_get_output(app, status, monkeypatch, "simple_func") assert "Parameters:" in result assert "**x** (" in result assert "**y** (" in result assert "Return type:" in result @pytest.mark.sphinx("text", testroot="issue_347") def test_raises_section( app: SphinxTestApp, status: StringIO, monkeypatch: pytest.MonkeyPatch, ) -> None: result = _build_and_get_output(app, status, monkeypatch, "raises_func") assert "Parameters:" in result assert "Raises:" in result assert "ValueError" in result assert "TypeError" in result @pytest.mark.sphinx("text", testroot="issue_347") def test_multi_return( app: SphinxTestApp, status: StringIO, monkeypatch: pytest.MonkeyPatch, ) -> None: result = _build_and_get_output(app, status, monkeypatch, "multi_return") assert "Return type:" in result @pytest.mark.sphinx("text", testroot="issue_347") def test_no_params( app: SphinxTestApp, status: StringIO, monkeypatch: pytest.MonkeyPatch, ) -> None: result = _build_and_get_output(app, status, monkeypatch, "no_params") assert "Return type:" in result sphinx_autodoc_typehints-3.8.0/tests/test_integration_issue_384.py0000644000000000000000000000602513615410400022507 0ustar00from __future__ import annotations import re import sys from pathlib import Path from textwrap import dedent, indent from typing import TYPE_CHECKING, Any, NewType, TypeVar import pytest from conftest import normalize_sphinx_text if TYPE_CHECKING: from collections.abc import Callable from io import StringIO from sphinx.testing.util import SphinxTestApp T = TypeVar("T") W = NewType("W", str) def expected(expected: str, **options: Any) -> Callable[[T], T]: def dec(val: T) -> T: val.EXPECTED = expected # ty: ignore[unresolved-attribute] val.OPTIONS = options # ty: ignore[unresolved-attribute] return val return dec def warns(pattern: str) -> Callable[[T], T]: def dec(val: T) -> T: val.WARNING = pattern # ty: ignore[unresolved-attribute] return val return dec @expected( """\ mod.function(x=5, y=10, z=15) Function docstring. Parameters: * **x** ("int") -- optional specifier line 2 (default: "5") * **y** ("int") -- another optional line 4 second paragraph for y (default: "10") * **z** ("int") -- yet another optional s line 6 (default: "15") Returns: something Return type: bytes """, ) def function(x: int = 5, y: int = 10, z: int = 15) -> str: """ Function docstring. :param x: optional specifier line 2 :param y: another optional line 4 second paragraph for y :param z: yet another optional s line 6 :return: something :rtype: bytes """ # Config settings for each test run. # Config Name: Sphinx Options as Dict. configs = {"default_conf": {"typehints_defaults": "braces-after"}} @pytest.mark.parametrize("val", [x for x in globals().values() if hasattr(x, "EXPECTED")]) @pytest.mark.parametrize("conf_run", list(configs.keys())) @pytest.mark.sphinx("text", testroot="integration") def test_integration( app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, val: Any, conf_run: str ) -> None: template = ".. autofunction:: mod.{}" (Path(app.srcdir) / "index.rst").write_text(template.format(val.__name__)) app.config.__dict__.update(configs[conf_run]) app.config.__dict__.update(val.OPTIONS) monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__]) app.build() assert "build succeeded" in status.getvalue() # Build succeeded regexp = getattr(val, "WARNING", None) value = warning.getvalue().strip() if regexp: msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}" assert re.search(regexp, value), msg else: assert not value result = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) expected = normalize_sphinx_text(val.EXPECTED) try: assert result.strip() == dedent(expected).strip() except Exception: indented = indent(f'"""\n{result}\n"""', " " * 4) print(f"@expected(\n{indented}\n)\n") # noqa: T201 raise sphinx_autodoc_typehints-3.8.0/tests/test_integration_issue_572.py0000644000000000000000000000454313615410400022511 0ustar00from __future__ import annotations import sys from pathlib import Path from typing import TYPE_CHECKING from unittest.mock import patch import pytest from conftest import normalize_sphinx_text from sphinx_autodoc_typehints._resolver._type_hints import _get_type_hint if sys.version_info >= (3, 14): import annotationlib if TYPE_CHECKING: from io import StringIO from sphinx.testing.util import SphinxTestApp SKIP_REASON = "annotationlib requires Python 3.14+" needs_314 = pytest.mark.skipif(sys.version_info < (3, 14), reason=SKIP_REASON) @needs_314 @pytest.mark.sphinx("text", testroot="issue_572") def test_forward_ref_builds_without_errors( app: SphinxTestApp, status: StringIO, warning: StringIO, # noqa: ARG001 ) -> None: """Forward-referencing module builds cleanly on 3.14+ using annotationlib.""" template = """\ .. autoclass:: mod_forward_ref.Tree :members: """ (Path(app.srcdir) / "index.rst").write_text(template) app.build() assert "build succeeded" in status.getvalue() result = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) assert "Tree" in result @needs_314 def test_get_type_hint_uses_annotationlib_on_name_error() -> None: """_get_type_hint falls back to annotationlib.get_annotations on 3.14+ NameError.""" sentinel = {"x": int} def dummy() -> None: ... with ( patch( "sphinx_autodoc_typehints._resolver._type_hints.get_type_hints", side_effect=NameError("name 'Foo' is not defined"), ), patch.object(annotationlib, "get_annotations", return_value=sentinel) as mock_get_ann, ): result = _get_type_hint([], "dummy", dummy, {}) mock_get_ann.assert_called_once_with(dummy, format=annotationlib.Format.FORWARDREF) assert result is sentinel @pytest.mark.skipif(sys.version_info >= (3, 14), reason="Tests pre-3.14 fallback path") def test_get_type_hint_falls_back_to_dunder_annotations_before_314() -> None: # pragma: <3.14 cover """_get_type_hint falls back to __annotations__ on pre-3.14 NameError.""" def dummy(x: int) -> str: ... with patch( "sphinx_autodoc_typehints._resolver._type_hints.get_type_hints", side_effect=NameError("name 'Foo' is not defined"), ): result = _get_type_hint([], "dummy", dummy, {}) assert result == dummy.__annotations__ sphinx_autodoc_typehints-3.8.0/tests/test_integration_issue_599.py0000644000000000000000000001020213615410400022507 0ustar00from __future__ import annotations import sys from pathlib import Path from textwrap import dedent, indent from typing import TYPE_CHECKING, Any, TypeVar, Union from unittest.mock import MagicMock import pytest from conftest import normalize_sphinx_text from sphinx.util.inspect import TypeAliasForwardRef from sphinx_autodoc_typehints import format_annotation if TYPE_CHECKING: from collections.abc import Callable from io import StringIO from sphinx.testing.util import SphinxTestApp T = TypeVar("T") UserId = Union[int, str] RequestData = dict[str, Any] TYPE_PREAMBLE = """\ type mod.UserId A user identifier that can be either an integer or a string. type mod.RequestData Request data dictionary. """ def expected(expected: str, **options: Any) -> Callable[[T], T]: def dec(val: T) -> T: val.EXPECTED = expected # ty: ignore[unresolved-attribute] val.OPTIONS = options # ty: ignore[unresolved-attribute] return val return dec @expected( TYPE_PREAMBLE + """\ mod.get_user(user_id) Get a user by ID. Parameters: **user_id** ("UserId") -- The user identifier Return type: "str" """ ) def get_user(user_id: UserId) -> str: """ Get a user by ID. Args: user_id: The user identifier """ return f"User {user_id}" @expected( TYPE_PREAMBLE + """\ mod.process_request(data) Process a request. Parameters: **data** ("RequestData") -- The request data Return type: "bool" """ ) def process_request(data: RequestData) -> bool: # noqa: ARG001 """ Process a request. Args: data: The request data """ return True configs = {"default_conf": {}} @pytest.mark.parametrize("val", [x for x in globals().values() if hasattr(x, "EXPECTED")]) @pytest.mark.parametrize("conf_run", list(configs.keys())) @pytest.mark.sphinx("text", testroot="issue_599") def test_integration( app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, val: Any, conf_run: str, ) -> None: template = """\ .. py:type:: mod.UserId A user identifier that can be either an integer or a string. .. py:type:: mod.RequestData Request data dictionary. .. autofunction:: mod.{} """ (Path(app.srcdir) / "index.rst").write_text(template.format(val.__name__)) app.config.__dict__.update(configs[conf_run]) app.config.__dict__.update(val.OPTIONS) monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__]) app.build() assert "build succeeded" in status.getvalue() value = warning.getvalue().strip() assert not value or "Inline strong start-string without end-string" in value result = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) expected_text = normalize_sphinx_text(val.EXPECTED) try: assert result.strip() == dedent(expected_text).strip() except Exception: indented = indent(f'"""\n{result}\n"""', " " * 4) print(f"@expected(\n{indented}\n)\n") # noqa: T201 raise @pytest.mark.sphinx("text", testroot="issue_599") def test_eager_annotations( app: SphinxTestApp, status: StringIO, warning: StringIO, ) -> None: """Test that non-deferred annotations (no ``from __future__ import annotations``) also resolve.""" template = """\ .. py:type:: mod_eager.UserId A user identifier. .. autofunction:: mod_eager.get_user_eager """ (Path(app.srcdir) / "index.rst").write_text(template) app.build() assert "build succeeded" in status.getvalue() value = warning.getvalue().strip() assert not value or "Inline strong start-string without end-string" in value result = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) assert '"UserId"' in result def test_format_annotation_type_alias_without_env() -> None: """TypeAliasForwardRef falls back to plain name when no env is available.""" config = MagicMock() config.typehints_formatter = None del config._typehints_env # noqa: SLF001 annotation = TypeAliasForwardRef("SomeAlias") assert format_annotation(annotation, config) == "SomeAlias" sphinx_autodoc_typehints-3.8.0/tests/test_method_lookup.py0000644000000000000000000000211413615410400021222 0ustar00from __future__ import annotations import sys from pathlib import Path from textwrap import dedent from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from io import StringIO from sphinx.testing.util import SphinxTestApp class MyClass: """A class with a property.""" @property def n_unique_nonzero(self) -> int: """Number of unique nonzero elements.""" return 0 def regular_method(self, x: int) -> int: # noqa: PLR6301 """Do something. Args: x: a number """ return x @pytest.mark.sphinx("text", testroot="integration") def test_method_lookup_does_not_crash( app: SphinxTestApp, status: StringIO, warning: StringIO, # noqa: ARG001 monkeypatch: pytest.MonkeyPatch, ) -> None: (Path(app.srcdir) / "index.rst").write_text( dedent("""\ Test ==== .. autoclass:: mod.MyClass :members: """) ) monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__]) app.build() assert "build succeeded" in status.getvalue() sphinx_autodoc_typehints-3.8.0/tests/test_pep695.py0000644000000000000000000000530213615410400017403 0ustar00from __future__ import annotations import sys import types from pathlib import Path from textwrap import dedent from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from io import StringIO from sphinx.testing.util import SphinxTestApp _mod_pep695 = types.ModuleType("_mod_pep695") _mod_pep695.__file__ = __file__ exec( # noqa: S102 dedent("""\ from __future__ import annotations class Foo[T]: \"\"\"A generic class.\"\"\" def __init__(self, thing: T) -> None: \"\"\"Init. :param thing: the thing \"\"\" def get(self) -> T: \"\"\"Get the thing. :return: the thing \"\"\" ... class Multi[K, V]: \"\"\"A class with multiple type params.\"\"\" def lookup(self, key: K) -> V: \"\"\"Look up. :param key: the key \"\"\" ... def identity[U](x: U) -> U: \"\"\"Identity function. :param x: input \"\"\" return x """), _mod_pep695.__dict__, ) @pytest.mark.sphinx("text", testroot="integration") def test_pep695_class_type_params( app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch ) -> None: (Path(app.srcdir) / "index.rst").write_text( dedent("""\ Test ==== .. autoclass:: mod.Foo :members: """) ) monkeypatch.setitem(sys.modules, "mod", _mod_pep695) app.build() assert "build succeeded" in status.getvalue() assert "Cannot resolve forward reference" not in warning.getvalue() @pytest.mark.sphinx("text", testroot="integration") def test_pep695_class_multiple_type_params( app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch ) -> None: (Path(app.srcdir) / "index.rst").write_text( dedent("""\ Test ==== .. autoclass:: mod.Multi :members: """) ) monkeypatch.setitem(sys.modules, "mod", _mod_pep695) app.build() assert "build succeeded" in status.getvalue() assert "Cannot resolve forward reference" not in warning.getvalue() @pytest.mark.sphinx("text", testroot="integration") def test_pep695_function_type_params( app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch ) -> None: (Path(app.srcdir) / "index.rst").write_text( dedent("""\ Test ==== .. autofunction:: mod.identity """) ) monkeypatch.setitem(sys.modules, "mod", _mod_pep695) app.build() assert "build succeeded" in status.getvalue() assert "Cannot resolve forward reference" not in warning.getvalue() sphinx_autodoc_typehints-3.8.0/tests/test_safe_parse.py0000644000000000000000000000357313615410400020473 0ustar00"""Tests that snippet parsing doesn't trigger extension directive side-effects.""" from __future__ import annotations import sys from pathlib import Path from textwrap import dedent from typing import TYPE_CHECKING, ClassVar import pytest from docutils.parsers.rst import Directive, directives if TYPE_CHECKING: from io import StringIO from sphinx.testing.util import SphinxTestApp @pytest.mark.sphinx("text", testroot="integration") def test_extension_directive_not_executed_during_snippet_parse( app: SphinxTestApp, status: StringIO, warning: StringIO, # noqa: ARG001 monkeypatch: pytest.MonkeyPatch, ) -> None: """A non-builtin directive in a docstring should only execute once (during the real build).""" directives.register_directive("tracking-directive", _TrackingDirective) _TrackingDirective.executions.clear() (Path(app.srcdir) / "index.rst").write_text( dedent("""\ Test ==== .. autofunction:: mod.func_with_tracking_directive """) ) src = dedent("""\ def func_with_tracking_directive(x: int) -> int: \"\"\"Do something. :param x: A number. .. tracking-directive:: unique-id-123 \"\"\" return x """) exec(compile(src, "", "exec"), (mod := {})) # noqa: S102 fake_module = type(sys)("mod") fake_module.__dict__.update(mod) monkeypatch.setitem(sys.modules, "mod", fake_module) app.build() assert "build succeeded" in status.getvalue() assert _TrackingDirective.executions.count("unique-id-123") == 1 class _TrackingDirective(Directive): """Directive that records each execution to detect double-processing.""" has_content = True executions: ClassVar[list[str]] = [] def run(self) -> list: _TrackingDirective.executions.append(self.content[0] if self.content else "") return [] sphinx_autodoc_typehints-3.8.0/tests/test_sphinx_autodoc_typehints.py0000644000000000000000000004657313615410400023530 0ustar00from __future__ import annotations import re import sys from functools import cmp_to_key from pathlib import Path from textwrap import dedent, indent from typing import TYPE_CHECKING, Any from unittest.mock import create_autospec, patch import pytest import typing_extensions from conftest import normalize_sphinx_text from sphinx.application import Sphinx from sphinx.config import Config from sphinx.ext.autodoc import Options from sphinx_autodoc_typehints import process_docstring, process_signature if TYPE_CHECKING: from io import StringIO from types import FunctionType from sphinx.testing.util import SphinxTestApp class Slotted: __slots__ = () class HintedMethods: @classmethod def from_magic(cls) -> typing_extensions.Self: ... def method(self) -> typing_extensions.Self: ... def test_process_docstring_slot_wrapper() -> None: lines: list[str] = [] config = create_autospec( Config, typehints_fully_qualified=False, simplify_optional_unions=False, typehints_formatter=None, autodoc_mock_imports=[], ) app: Sphinx = create_autospec(Sphinx, config=config) process_docstring(app, "class", "SlotWrapper", Slotted, None, lines) assert not lines def test_process_docstring_wrapper_loop() -> None: """Regression test for #405: inspect.unwrap raises ValueError on wrapper loops.""" def func(x: int) -> str: ... func.__wrapped__ = func # type: ignore[attr-defined] # circular wrapper loop lines: list[str] = [] config = create_autospec( Config, typehints_fully_qualified=False, simplify_optional_unions=False, typehints_formatter=None, autodoc_mock_imports=[], ) app: Sphinx = create_autospec(Sphinx, config=config) process_docstring(app, "function", "func", func, None, lines) def test_process_signature_wrapper_loop() -> None: """Regression test for #405: inspect.unwrap raises ValueError on wrapper loops.""" def func(x: int) -> str: ... func.__wrapped__ = func # type: ignore[attr-defined] # circular wrapper loop config = create_autospec( Config, typehints_fully_qualified=False, simplify_optional_unions=False, typehints_formatter=None, typehints_use_signature=False, typehints_use_signature_return=False, autodoc_type_aliases={}, ) app: Sphinx = create_autospec(Sphinx, config=config) result = process_signature( app, "function", "func", func, Options(), "", "", ) assert result is None def set_python_path() -> None: test_path = Path(__file__).parent if str(test_path) not in sys.path: sys.path.insert(0, str(test_path)) @pytest.mark.parametrize("always_document_param_types", [True, False], ids=["doc_param_type", "no_doc_param_type"]) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_always_document_param_types( app: SphinxTestApp, status: StringIO, warning: StringIO, always_document_param_types: bool, ) -> None: set_python_path() app.config.always_document_param_types = always_document_param_types # create flag app.config.autodoc_mock_imports = ["mailbox"] # create flag for f in Path(app.srcdir).glob("*.rst"): f.unlink() (Path(app.srcdir) / "index.rst").write_text( dedent( """ .. autofunction:: dummy_module.undocumented_function .. autoclass:: dummy_module.DataClass :undoc-members: :special-members: __init__ """, ), ) app.build() assert "build succeeded" in status.getvalue() assert not warning.getvalue().strip() format_args = {} for indentation_level in range(2): key = f"undoc_params_{indentation_level}" if always_document_param_types: format_args[key] = indent('\n\n Parameters:\n **x** ("int")', " " * indentation_level) else: format_args[key] = "" contents = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) expected_contents = """\ dummy_module.undocumented_function(x) Hi{undoc_params_0} Return type: "str" class dummy_module.DataClass(x) Class docstring.{undoc_params_0} __init__(x){undoc_params_1} """ expected_contents = normalize_sphinx_text(dedent(expected_contents).format(**format_args)) assert contents == expected_contents @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_always_document_param_types_with_defaults_braces_after( app: SphinxTestApp, status: StringIO, warning: StringIO, # noqa: ARG001 ) -> None: """Regression test for #575: IndexError when combining always_document_param_types with braces-after.""" set_python_path() app.config.always_document_param_types = True app.config.typehints_defaults = "braces-after" for rst_file in Path(app.srcdir).glob("*.rst"): rst_file.unlink() index_content = """\ .. autofunction:: dummy_module.undocumented_function_with_defaults """ (Path(app.srcdir) / "index.rst").write_text(dedent(index_content)) app.build() assert "build succeeded" in status.getvalue() @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_namedtuple_new_no_warning( app: SphinxTestApp, status: StringIO, warning: StringIO, ) -> None: """Regression test for #601: NamedTuple __new__ causes 'NoneType' attribute error.""" set_python_path() for rst_file in Path(app.srcdir).glob("*.rst"): rst_file.unlink() index_content = """\ .. autoclass:: dummy_module.MyNamedTuple :special-members: __new__ """ (Path(app.srcdir) / "index.rst").write_text(dedent(index_content)) app.build() assert "build succeeded" in status.getvalue() assert "NoneType" not in warning.getvalue() @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) -> None: set_python_path() app.config.master_doc = "future_annotations" # create flag app.build() assert "build succeeded" in status.getvalue() contents = normalize_sphinx_text((Path(app.srcdir) / "_build/text/future_annotations.txt").read_text()) expected_contents = """\ Dummy Module ************ dummy_module_future_annotations.function_with_py310_annotations(self, x, y, z=None) Method docstring. Parameters: * **x** ("bool" | "None") -- foo * **y** ("int" | "str" | "float") -- bar * **z** ("str" | "None") -- baz Return type: "str" """ expected_contents = normalize_sphinx_text(dedent(expected_contents)) assert contents == expected_contents @pytest.mark.sphinx("pseudoxml", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_default_role(app: SphinxTestApp, status: StringIO) -> None: set_python_path() app.config.master_doc = "simple_default_role" # create flag app.config.default_role = "literal" app.build() assert "build succeeded" in status.getvalue() contents_lines = ( (Path(app.srcdir) / "_build/pseudoxml/simple_default_role.pseudoxml").read_text(encoding="utf-8").splitlines() ) list_item_idxs = [i for i, line in enumerate(contents_lines) if line.strip() == ""] foo_param = dedent("\n".join(contents_lines[list_item_idxs[0] : list_item_idxs[1]])) expected_foo_param = """\ x ( bool ) \N{EN DASH}\N{SPACE} foo """.rstrip() expected_foo_param = dedent(expected_foo_param) assert foo_param == expected_foo_param @pytest.mark.parametrize( ("defaults_config_val", "expected"), [ (None, '("int") -- bar'), ("comma", '("int", default: "1") -- bar'), ("braces", '("int" (default: "1")) -- bar'), ("braces-after", '("int") -- bar (default: "1")'), ("comma-after", Exception("needs to be one of")), ], ) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_defaults( app: SphinxTestApp, status: StringIO, defaults_config_val: str, expected: str | Exception, ) -> None: set_python_path() app.config.master_doc = "simple" # create flag app.config.typehints_defaults = defaults_config_val # create flag if isinstance(expected, Exception): with pytest.raises(Exception, match=re.escape(str(expected))): app.build() return app.build() assert "build succeeded" in status.getvalue() contents = normalize_sphinx_text((Path(app.srcdir) / "_build/text/simple.txt").read_text()) expected_contents = f"""\ Simple Module ************* dummy_module_simple.function(x, y=1) Function docstring. Parameters: * **x** ("bool") -- foo * **y** {expected} Return type: "str" """ assert contents == normalize_sphinx_text(dedent(expected_contents)) @pytest.mark.parametrize( ("formatter_config_val", "expected"), [ (None, ['("bool") -- foo', '("int") -- bar', '"str"']), (lambda ann, conf: "Test", ["(Test) -- foo", "(Test) -- bar", "Test"]), # noqa: ARG005 ("some string", Exception("needs to be callable or `None`")), ], ) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_formatter( app: SphinxTestApp, status: StringIO, formatter_config_val: str, expected: tuple[str, ...] | Exception, ) -> None: set_python_path() app.config.master_doc = "simple" # create flag app.config.typehints_formatter = formatter_config_val # create flag if isinstance(expected, Exception): with pytest.raises(Exception, match=re.escape(str(expected))): app.build() return app.build() assert "build succeeded" in status.getvalue() contents = normalize_sphinx_text((Path(app.srcdir) / "_build/text/simple.txt").read_text()) expected_contents = f"""\ Simple Module ************* dummy_module_simple.function(x, y=1) Function docstring. Parameters: * **x** {expected[0]} * **y** {expected[1]} Return type: {expected[2]} """ assert contents == normalize_sphinx_text(dedent(expected_contents)) @pytest.mark.parametrize("obj", [cmp_to_key, 1]) def test_default_no_signature(obj: Any) -> None: config = create_autospec( Config, typehints_fully_qualified=False, simplify_optional_unions=False, typehints_formatter=None, autodoc_mock_imports=[], ) app: Sphinx = create_autospec(Sphinx, config=config) lines: list[str] = [] process_docstring(app, "what", "name", obj, None, lines) assert lines == [] @pytest.mark.parametrize("method", [HintedMethods.from_magic, HintedMethods().method]) def test_bound_class_method(method: FunctionType) -> None: config = create_autospec( Config, typehints_fully_qualified=False, simplify_optional_unions=False, typehints_document_rtype=False, always_document_param_types=True, typehints_defaults=True, typehints_formatter=None, autodoc_mock_imports=[], ) app: Sphinx = create_autospec(Sphinx, config=config) process_docstring(app, "class", method.__qualname__, method, None, []) @pytest.mark.sphinx("text", testroot="resolve-typing-guard") def test_resolve_typing_guard_imports(app: SphinxTestApp, status: StringIO, warning: StringIO) -> None: set_python_path() app.config.autodoc_mock_imports = ["viktor"] # create flag app.build() out = status.getvalue() assert "build succeeded" in out assert "Failed guarded type import" not in warning.getvalue() @pytest.mark.sphinx("text", testroot="resolve-typing-guard-tmp") def test_resolve_typing_guard_attrs_imports(app: SphinxTestApp, status: StringIO, warning: StringIO) -> None: set_python_path() app.build() assert "build succeeded" in status.getvalue() assert not warning.getvalue() @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_formatter_no_use_rtype(app: SphinxTestApp, status: StringIO) -> None: set_python_path() app.config.master_doc = "simple_no_use_rtype" # create flag app.config.typehints_use_rtype = False app.build() assert "build succeeded" in status.getvalue() text_path = Path(app.srcdir) / "_build" / "text" / "simple_no_use_rtype.txt" text_contents = normalize_sphinx_text(text_path.read_text()) expected_contents = """\ Simple Module ************* dummy_module_simple_no_use_rtype.function_no_returns(x, y=1) Function docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar Return type: "str" dummy_module_simple_no_use_rtype.function_returns_with_type(x, y=1) Function docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar Returns: *CustomType* -- A string dummy_module_simple_no_use_rtype.function_returns_with_compound_type(x, y=1) Function docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar Returns: Union[str, int] -- A string or int dummy_module_simple_no_use_rtype.function_returns_without_type(x, y=1) Function docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar Returns: "str" -- A string """ assert text_contents == normalize_sphinx_text(dedent(expected_contents)) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_with_use_signature(app: SphinxTestApp, status: StringIO) -> None: set_python_path() app.config.master_doc = "simple" # create flag app.config.typehints_use_signature = True app.build() assert "build succeeded" in status.getvalue() text_path = Path(app.srcdir) / "_build" / "text" / "simple.txt" text_contents = normalize_sphinx_text(text_path.read_text()) expected_contents = """\ Simple Module ************* dummy_module_simple.function(x: bool, y: int = 1) Function docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar Return type: "str" """ assert text_contents == normalize_sphinx_text(dedent(expected_contents)) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_with_use_signature_return(app: SphinxTestApp, status: StringIO) -> None: set_python_path() app.config.master_doc = "simple" # create flag app.config.typehints_use_signature_return = True app.build() assert "build succeeded" in status.getvalue() text_path = Path(app.srcdir) / "_build" / "text" / "simple.txt" text_contents = normalize_sphinx_text(text_path.read_text()) expected_contents = """\ Simple Module ************* dummy_module_simple.function(x, y=1) -> str Function docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar Return type: "str" """ assert text_contents == normalize_sphinx_text(dedent(expected_contents)) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_with_use_signature_and_return(app: SphinxTestApp, status: StringIO) -> None: set_python_path() app.config.master_doc = "simple" # create flag app.config.typehints_use_signature = True app.config.typehints_use_signature_return = True app.build() assert "build succeeded" in status.getvalue() text_path = Path(app.srcdir) / "_build" / "text" / "simple.txt" text_contents = normalize_sphinx_text(text_path.read_text()) expected_contents = """\ Simple Module ************* dummy_module_simple.function(x: bool, y: int = 1) -> str Function docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar Return type: "str" """ assert text_contents == normalize_sphinx_text(dedent(expected_contents)) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_default_annotation_without_typehints(app: SphinxTestApp, status: StringIO) -> None: set_python_path() app.config.master_doc = "without_complete_typehints" # create flag app.config.typehints_defaults = "comma" app.build() assert "build succeeded" in status.getvalue() text_path = Path(app.srcdir) / "_build" / "text" / "without_complete_typehints.txt" text_contents = normalize_sphinx_text(text_path.read_text()) expected_contents = """\ Simple Module ************* dummy_module_without_complete_typehints.function_with_some_defaults_and_without_typehints(x, y=None) Function docstring. Parameters: * **x** -- foo * **y** (default: "None") -- bar dummy_module_without_complete_typehints.function_with_some_defaults_and_some_typehints(x, y=None) Function docstring. Parameters: * **x** ("int") -- foo * **y** (default: "None") -- bar dummy_module_without_complete_typehints.function_with_some_defaults_and_more_typehints(x, y=None) Function docstring. Parameters: * **x** ("int") -- foo * **y** (default: "None") -- bar Return type: "str" dummy_module_without_complete_typehints.function_with_defaults_and_some_typehints(x=0, y=None) Function docstring. Parameters: * **x** ("int", default: "0") -- foo * **y** (default: "None") -- bar Return type: "str" dummy_module_without_complete_typehints.function_with_defaults_and_type_information_in_docstring(x, y=0) Function docstring. Parameters: * **x** ("int") -- foo * **y** (int, default: "0") -- bar Return type: "str" """ assert text_contents == normalize_sphinx_text(dedent(expected_contents)) @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_wrong_module_path(app: SphinxTestApp, status: StringIO, warning: StringIO) -> None: set_python_path() app.config.master_doc = "wrong_module_path" # create flag app.config.default_role = "literal" app.config.nitpicky = True app.config.nitpick_ignore = {("py:data", "typing.Optional")} def fixup_module_name(mod: str) -> str: if not mod.startswith("wrong_module_path"): return mod return "export_module" + mod.removeprefix("wrong_module_path") app.config.suppress_warnings = ["config.cache"] app.config.typehints_fixup_module_name = fixup_module_name app.build() assert "build succeeded" in status.getvalue() assert not warning.getvalue().strip() sphinx_autodoc_typehints-3.8.0/tests/test_version.py0000644000000000000000000000021213615410400020033 0ustar00from __future__ import annotations from sphinx_autodoc_typehints import __version__ def test_version() -> None: assert __version__ sphinx_autodoc_typehints-3.8.0/tests/roots/test-annotated-doc/conf.py0000644000000000000000000000032713615410400023106 0ustar00from __future__ import annotations import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).parent)) master_doc = "index" extensions = [ "sphinx.ext.autodoc", "sphinx_autodoc_typehints", ] sphinx_autodoc_typehints-3.8.0/tests/roots/test-annotated-doc/index.rst0000644000000000000000000000001213615410400023437 0ustar00Test ==== sphinx_autodoc_typehints-3.8.0/tests/roots/test-annotated-doc/mod.py0000644000000000000000000000127113615410400022737 0ustar00from __future__ import annotations from typing import Annotated from typing_extensions import Doc def greet( name: Annotated[str, Doc("The person's name")], greeting: Annotated[str, Doc("The greeting phrase")] ) -> Annotated[str, Doc("The full greeting message")]: """Say hello.""" return f"{greeting}, {name}!" def partial_doc(x: Annotated[int, Doc("The x value")], y: int) -> int: """Compute sum. :param y: The y value """ return x + y def no_doc(x: Annotated[int, 42]) -> int: """Identity.""" return x def docstring_wins(x: Annotated[int, Doc("Doc description")]) -> int: """Override. :param x: Docstring description """ return x sphinx_autodoc_typehints-3.8.0/tests/roots/test-annotated-doc-numpydoc/conf.py0000644000000000000000000000041313615410400024736 0ustar00from __future__ import annotations import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).parent)) master_doc = "index" extensions = [ "sphinx.ext.autodoc", "numpydoc", "sphinx_autodoc_typehints", ] numpydoc_show_class_members = False sphinx_autodoc_typehints-3.8.0/tests/roots/test-annotated-doc-numpydoc/index.rst0000644000000000000000000000001213615410400025273 0ustar00Test ==== sphinx_autodoc_typehints-3.8.0/tests/roots/test-annotated-doc-numpydoc/mod.py0000644000000000000000000000102513615410400024570 0ustar00from __future__ import annotations from typing import Annotated from typing_extensions import Doc def compute( x: Annotated[int, Doc("The x input")], y: Annotated[int, Doc("The y input")] ) -> Annotated[int, Doc("The sum")]: """ Compute the sum. Parameters ---------- x Placeholder. y Placeholder. """ return x + y def transform(data: Annotated[str, Doc("The input data")]) -> Annotated[str, Doc("The transformed result")]: """Transform data.""" return data.upper() sphinx_autodoc_typehints-3.8.0/tests/roots/test-attrs/attrs_mod.py0000644000000000000000000000067613615410400022601 0ustar00from __future__ import annotations import attr import attrs @attr.s class ClassicAttrs: """A class using classic attr.s style.""" name = attr.ib(type=str) age = attr.ib(type=int) untyped = attr.ib() @attr.s(auto_attribs=True) class AutoAttribs: """A class using auto_attribs=True.""" name: str age: int @attrs.define class ModernAttrs: """A class using modern attrs.define.""" name: str age: int sphinx_autodoc_typehints-3.8.0/tests/roots/test-attrs/conf.py0000644000000000000000000000036313615410400021523 0ustar00from __future__ import annotations import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).parent)) master_doc = "index" extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_autodoc_typehints", ] sphinx_autodoc_typehints-3.8.0/tests/roots/test-attrs/index.rst0000644000000000000000000000001213615410400022054 0ustar00Test ==== sphinx_autodoc_typehints-3.8.0/tests/roots/test-dummy/conf.py0000644000000000000000000000044213615410400021517 0ustar00from __future__ import annotations import pathlib import sys # Make dummy_module.py available for autodoc. sys.path.insert(0, str(pathlib.Path(__file__).parent)) master_doc = "index" extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_autodoc_typehints", ] sphinx_autodoc_typehints-3.8.0/tests/roots/test-dummy/dummy_module.py0000644000000000000000000000070113615410400023270 0ustar00from __future__ import annotations from dataclasses import dataclass from typing import NamedTuple def undocumented_function(x: int) -> str: """Hi""" return str(x) def undocumented_function_with_defaults(x: int, y: str = "hello") -> str: """Hi""" return str(x) + y class MyNamedTuple(NamedTuple): """A named tuple.""" x: int y: str = "hello" @dataclass class DataClass: """Class docstring.""" x: int sphinx_autodoc_typehints-3.8.0/tests/roots/test-dummy/dummy_module_future_annotations.py0000644000000000000000000000044313615410400027302 0ustar00from __future__ import annotations def function_with_py310_annotations( self, # noqa: ANN001 x: bool | None, y: int | str | float, # noqa: PYI041 z: str | None = None, ) -> str: """ Method docstring. :param x: foo :param y: bar :param z: baz """ sphinx_autodoc_typehints-3.8.0/tests/roots/test-dummy/dummy_module_simple.py0000644000000000000000000000023413615410400024642 0ustar00from __future__ import annotations def function(x: bool, y: int = 1) -> str: """ Function docstring. :param x: foo :param y: bar """ sphinx_autodoc_typehints-3.8.0/tests/roots/test-dummy/dummy_module_simple_default_role.py0000644000000000000000000000023613615410400027371 0ustar00from __future__ import annotations def function(x: bool, y: int) -> str: """ Function docstring. :param x: `foo` :param y: ``bar`` """ sphinx_autodoc_typehints-3.8.0/tests/roots/test-dummy/dummy_module_simple_no_use_rtype.py0000644000000000000000000000130313615410400027433 0ustar00from __future__ import annotations def function_no_returns(x: bool, y: int = 1) -> str: """ Function docstring. :param x: foo :param y: bar """ def function_returns_with_type(x: bool, y: int = 1) -> str: """ Function docstring. :param x: foo :param y: bar :returns: *CustomType* -- A string """ def function_returns_with_compound_type(x: bool, y: int = 1) -> str: """ Function docstring. :param x: foo :param y: bar :returns: Union[str, int] -- A string or int """ def function_returns_without_type(x: bool, y: int = 1) -> str: """ Function docstring. :param x: foo :param y: bar :returns: A string """ sphinx_autodoc_typehints-3.8.0/tests/roots/test-dummy/dummy_module_without_complete_typehints.py0000644000000000000000000000163013615410400031054 0ustar00from __future__ import annotations def function_with_some_defaults_and_without_typehints(x, y=None): # noqa: ANN001, ANN201 """ Function docstring. :param x: foo :param y: bar """ def function_with_some_defaults_and_some_typehints(x: int, y=None): # noqa: ANN001, ANN201 """ Function docstring. :param x: foo :param y: bar """ def function_with_some_defaults_and_more_typehints(x: int, y=None) -> str: # noqa: ANN001 """ Function docstring. :param x: foo :param y: bar """ def function_with_defaults_and_some_typehints(x: int = 0, y=None) -> str: # noqa: ANN001 """ Function docstring. :param x: foo :param y: bar """ def function_with_defaults_and_type_information_in_docstring(x, y=0) -> str: # noqa: ANN001 """ Function docstring. :type x: int :type y: int :param x: foo :param y: bar """ sphinx_autodoc_typehints-3.8.0/tests/roots/test-dummy/export_module.py0000644000000000000000000000013513615410400023457 0ustar00from __future__ import annotations from wrong_module_path import A, f __all__ = ["A", "f"] sphinx_autodoc_typehints-3.8.0/tests/roots/test-dummy/future_annotations.rst0000644000000000000000000000016713615410400024705 0ustar00:orphan: Dummy Module ============ .. autofunction:: dummy_module_future_annotations.function_with_py310_annotations sphinx_autodoc_typehints-3.8.0/tests/roots/test-dummy/simple.rst0000644000000000000000000000012613615410400022242 0ustar00:orphan: Simple Module ============= .. autofunction:: dummy_module_simple.function sphinx_autodoc_typehints-3.8.0/tests/roots/test-dummy/simple_default_role.rst0000644000000000000000000000014313615410400024766 0ustar00:orphan: Simple Module ============= .. autofunction:: dummy_module_simple_default_role.function sphinx_autodoc_typehints-3.8.0/tests/roots/test-dummy/simple_no_use_rtype.rst0000644000000000000000000000054413615410400025041 0ustar00:orphan: Simple Module ============= .. autofunction:: dummy_module_simple_no_use_rtype.function_no_returns .. autofunction:: dummy_module_simple_no_use_rtype.function_returns_with_type .. autofunction:: dummy_module_simple_no_use_rtype.function_returns_with_compound_type .. autofunction:: dummy_module_simple_no_use_rtype.function_returns_without_type sphinx_autodoc_typehints-3.8.0/tests/roots/test-dummy/without_complete_typehints.rst0000644000000000000000000000107413615410400026456 0ustar00:orphan: Simple Module ============= .. autofunction:: dummy_module_without_complete_typehints.function_with_some_defaults_and_without_typehints .. autofunction:: dummy_module_without_complete_typehints.function_with_some_defaults_and_some_typehints .. autofunction:: dummy_module_without_complete_typehints.function_with_some_defaults_and_more_typehints .. autofunction:: dummy_module_without_complete_typehints.function_with_defaults_and_some_typehints .. autofunction:: dummy_module_without_complete_typehints.function_with_defaults_and_type_information_in_docstring sphinx_autodoc_typehints-3.8.0/tests/roots/test-dummy/wrong_module_path.py0000644000000000000000000000012013615410400024300 0ustar00from __future__ import annotations class A: pass def f() -> A: pass sphinx_autodoc_typehints-3.8.0/tests/roots/test-dummy/wrong_module_path.rst0000644000000000000000000000011013615410400024457 0ustar00:orphan: .. class:: export_module.A .. autofunction:: export_module.f sphinx_autodoc_typehints-3.8.0/tests/roots/test-integration/conf.py0000644000000000000000000000044213615410400022707 0ustar00from __future__ import annotations import pathlib import sys # Make dummy_module.py available for autodoc. sys.path.insert(0, str(pathlib.Path(__file__).parent)) master_doc = "index" extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_autodoc_typehints", ] sphinx_autodoc_typehints-3.8.0/tests/roots/test-issue_347/conf.py0000644000000000000000000000053113615410400022110 0ustar00from __future__ import annotations import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).parent)) master_doc = "index" extensions = [ "sphinx.ext.autodoc", "numpydoc", "sphinx_autodoc_typehints", ] # Disable numpydoc's class members listing to avoid interfering with autodoc numpydoc_show_class_members = False sphinx_autodoc_typehints-3.8.0/tests/roots/test-issue_347/index.rst0000644000000000000000000000001213615410400022444 0ustar00Test ==== sphinx_autodoc_typehints-3.8.0/tests/roots/test-issue_572/conf.py0000644000000000000000000000036313615410400022113 0ustar00from __future__ import annotations import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).parent)) master_doc = "index" extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_autodoc_typehints", ] sphinx_autodoc_typehints-3.8.0/tests/roots/test-issue_572/index.rst0000644000000000000000000000001213615410400022444 0ustar00Test ==== sphinx_autodoc_typehints-3.8.0/tests/roots/test-issue_572/mod_forward_ref.py0000644000000000000000000000037213615410400024325 0ustar00"""Module without deferred annotations that uses forward references.""" class Tree: def children(self) -> list["Tree"]: """ Get child nodes. Returns: The children of this node. """ return [] sphinx_autodoc_typehints-3.8.0/tests/roots/test-issue_599/conf.py0000644000000000000000000000036313615410400022124 0ustar00from __future__ import annotations import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).parent)) master_doc = "index" extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_autodoc_typehints", ] sphinx_autodoc_typehints-3.8.0/tests/roots/test-issue_599/index.rst0000644000000000000000000000001213615410400022455 0ustar00Test ==== sphinx_autodoc_typehints-3.8.0/tests/roots/test-issue_599/mod_eager.py0000644000000000000000000000050413615410400023116 0ustar00"""Module without deferred annotations to test the globals-scanning path.""" from typing import Any, Union UserId = Union[int, str] RequestData = dict[str, Any] def get_user_eager(user_id: UserId) -> str: """ Get a user by ID. Args: user_id: The user identifier """ return f"User {user_id}" sphinx_autodoc_typehints-3.8.0/tests/roots/test-pyi-stubs/conf.py0000644000000000000000000000036313615410400022325 0ustar00from __future__ import annotations import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).parent)) master_doc = "index" extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_autodoc_typehints", ] sphinx_autodoc_typehints-3.8.0/tests/roots/test-pyi-stubs/index.rst0000644000000000000000000000001213615410400022656 0ustar00Test ==== sphinx_autodoc_typehints-3.8.0/tests/roots/test-pyi-stubs/stub_mod.py0000644000000000000000000000052713615410400023216 0ustar00"""Module with no type annotations, simulating a C extension.""" def greet(name, greeting): return f"{greeting}, {name}!" class Calculator: value = 0 def add(self, x): self.value += x return self class Inner: def process(self, data): return data async def fetch(url): return url sphinx_autodoc_typehints-3.8.0/tests/roots/test-pyi-stubs/stub_mod.pyi0000644000000000000000000000035413615410400023365 0ustar00def greet(name: str, greeting: str) -> str: ... class Calculator: value: int def add(self, x: int) -> Calculator: ... class Inner: def process(self, data: bytes) -> bytes: ... async def fetch(url: str) -> str: ... sphinx_autodoc_typehints-3.8.0/tests/roots/test-resolve-typing-guard/conf.py0000644000000000000000000000032613615410400024454 0ustar00from __future__ import annotations import pathlib import sys master_doc = "index" sys.path.insert(0, str(pathlib.Path(__file__).parent)) extensions = [ "sphinx.ext.autodoc", "sphinx_autodoc_typehints", ] sphinx_autodoc_typehints-3.8.0/tests/roots/test-resolve-typing-guard/demo_typing_guard.py0000644000000000000000000000264113615410400027231 0ustar00"""Module demonstrating imports that are type guarded""" from __future__ import annotations import typing from builtins import ValueError # handle does not have __module__ # noqa: A004 from functools import cmp_to_key # has __module__ but cannot get module as is builtin from typing import TYPE_CHECKING from demo_typing_guard_dummy import AnotherClass if TYPE_CHECKING: from collections.abc import Sequence from decimal import Decimal from demo_typing_guard_dummy import Literal # guarded by another `if TYPE_CHECKING` in demo_typing_guard_dummy if typing.TYPE_CHECKING: from typing import AnyStr if TYPE_CHECKING: # bad import from functools import missing # noqa: F401 def a[AnyStr: (bytes, str)](f: Decimal, s: AnyStr) -> Sequence[AnyStr | Decimal]: """ Do. :param f: first :param s: second :return: result """ return [f, s] class SomeClass: """This class do something.""" def create(self, item: Decimal) -> None: """ Create something. :param item: the item in question """ if TYPE_CHECKING: # Classes doesn't have `__globals__` attribute def guarded(self, item: Decimal) -> None: """ Guarded method. :param item: some item """ def func(_x: Literal) -> None: ... __all__ = [ "AnotherClass", "SomeClass", "ValueError", "a", "cmp_to_key", ] sphinx_autodoc_typehints-3.8.0/tests/roots/test-resolve-typing-guard/demo_typing_guard_dummy.py0000644000000000000000000000043613615410400030444 0ustar00from __future__ import annotations from typing import TYPE_CHECKING from viktor import AI # module part of autodoc_mock_imports # noqa: F401 if TYPE_CHECKING: # Nested type guard from typing import Literal # noqa: F401 class AnotherClass: """Another class is here""" sphinx_autodoc_typehints-3.8.0/tests/roots/test-resolve-typing-guard/index.rst0000644000000000000000000000005713615410400025017 0ustar00.. automodule:: demo_typing_guard :members: sphinx_autodoc_typehints-3.8.0/tests/roots/test-resolve-typing-guard-tmp/conf.py0000644000000000000000000000032613615410400025252 0ustar00from __future__ import annotations import pathlib import sys master_doc = "index" sys.path.insert(0, str(pathlib.Path(__file__).parent)) extensions = [ "sphinx.ext.autodoc", "sphinx_autodoc_typehints", ] sphinx_autodoc_typehints-3.8.0/tests/roots/test-resolve-typing-guard-tmp/demo_typing_guard.py0000644000000000000000000000223613615410400030027 0ustar00"""Module demonstrating imports that are type guarded""" from __future__ import annotations import datetime from attrs import define @define() class SomeClass: """This class does something.""" date: datetime.date """Date to handle""" @classmethod def from_str(cls, input_value: str) -> SomeClass: """ Initialize from string :param input_value: Input :return: result """ return cls(input_value) @classmethod def from_date(cls, input_value: datetime.date) -> SomeClass: """ Initialize from date :param input_value: Input :return: result """ return cls(input_value) @classmethod def from_time(cls, input_value: datetime.time) -> SomeClass: """ Initialize from time :param input_value: Input :return: result """ return cls(input_value) def calculate_thing(self, number: float) -> datetime.timedelta: # noqa: PLR6301 """ Calculate a thing :param number: Input :return: result """ return datetime.timedelta(number) __all__ = ["SomeClass"] sphinx_autodoc_typehints-3.8.0/tests/roots/test-resolve-typing-guard-tmp/index.rst0000644000000000000000000000005713615410400025615 0ustar00.. automodule:: demo_typing_guard :members: sphinx_autodoc_typehints-3.8.0/tests/test_resolver/__init__.py0000644000000000000000000000000013615410400021741 0ustar00sphinx_autodoc_typehints-3.8.0/tests/test_resolver/test_attrs.py0000644000000000000000000000616613615410400022421 0ustar00from __future__ import annotations import sys from pathlib import Path from typing import TYPE_CHECKING, Any import attr import attrs import pytest from conftest import normalize_sphinx_text from sphinx_autodoc_typehints._resolver._attrs import backfill_attrs_annotations if TYPE_CHECKING: from io import StringIO from sphinx.testing.util import SphinxTestApp ATTRS_ROOT = Path(__file__).parent.parent / "roots" / "test-attrs" @attr.s class _ClassicAttrs: name = attr.ib(type=str) age = attr.ib(type=int) untyped = attr.ib() @attr.s(auto_attribs=True) class _AutoAttribs: name: str age: int @attrs.define class _ModernAttrs: name: str age: int def test_backfill_classic_attrs() -> None: backfill_attrs_annotations(_ClassicAttrs) annotations = _ClassicAttrs.__annotations__ assert annotations["name"] is str assert annotations["age"] is int assert "untyped" not in annotations def test_backfill_does_not_override_existing_annotations() -> None: original = _AutoAttribs.__annotations__.copy() backfill_attrs_annotations(_AutoAttribs) assert _AutoAttribs.__annotations__ == original def test_backfill_modern_attrs() -> None: original = _ModernAttrs.__annotations__.copy() backfill_attrs_annotations(_ModernAttrs) assert _ModernAttrs.__annotations__ == original def test_backfill_non_attrs_class() -> None: class Plain: pass backfill_attrs_annotations(Plain) assert not hasattr(Plain, "__annotations__") or Plain.__annotations__ == {} def test_backfill_without_attrs_installed() -> None: saved = sys.modules.copy() sys.modules["attrs"] = None # type: ignore[assignment] try: class Dummy: pass backfill_attrs_annotations(Dummy) finally: sys.modules.update(saved) def test_backfill_classic_attrs_creates_annotations_when_missing() -> None: @attr.s class NoAnnotations: x = attr.ib(type=int) NoAnnotations.__annotations__ = None # type: ignore[assignment] backfill_attrs_annotations(NoAnnotations) assert NoAnnotations.__annotations__["x"] is int @pytest.fixture def _attrs_mod() -> Any: sys.path.insert(0, str(ATTRS_ROOT)) try: import attrs_mod # noqa: PLC0415 # ty: ignore[unresolved-import] yield attrs_mod finally: sys.path.pop(0) sys.modules.pop("attrs_mod", None) @pytest.mark.sphinx("text", testroot="attrs") def test_sphinx_build_attrs_types(app: SphinxTestApp, status: StringIO, warning: StringIO) -> None: template = """\ .. autoclass:: attrs_mod.ClassicAttrs :members: :undoc-members: .. autoclass:: attrs_mod.AutoAttribs :members: :undoc-members: .. autoclass:: attrs_mod.ModernAttrs :members: :undoc-members: """ (Path(app.srcdir) / "index.rst").write_text(template) app.build() assert "build succeeded" in status.getvalue() result = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) assert "str" in result assert "int" in result warn_text = warning.getvalue() assert "attrs_mod" not in warn_text or "forward reference" not in warn_text sphinx_autodoc_typehints-3.8.0/tests/test_resolver/test_stubs.py0000644000000000000000000002124513615410400022417 0ustar00from __future__ import annotations import ast import sys import types from pathlib import Path from typing import TYPE_CHECKING, Any from unittest.mock import MagicMock, patch import pytest from conftest import normalize_sphinx_text from sphinx_autodoc_typehints._resolver._stubs import ( _STUB_AST_CACHE, _backfill_from_stub, _extract_annotations_from_stub, _extract_class_annotations, _extract_func_annotations, _find_ast_node, _find_stub_path, _parse_stub_ast, ) if TYPE_CHECKING: from io import StringIO from sphinx.testing.util import SphinxTestApp STUB_ROOT = Path(__file__).parent.parent / "roots" / "test-pyi-stubs" def _import_stub_mod() -> types.ModuleType: import stub_mod # noqa: PLC0415 # ty: ignore[unresolved-import] return stub_mod @pytest.fixture def stub_mod() -> Any: sys.path.insert(0, str(STUB_ROOT)) try: yield _import_stub_mod() finally: sys.path.pop(0) sys.modules.pop("stub_mod", None) def test_find_stub_path_locates_sibling_pyi(stub_mod: Any) -> None: result = _find_stub_path(stub_mod.greet) assert result is not None assert result.suffix == ".pyi" assert result.stem == "stub_mod" def test_find_stub_path_returns_none_for_no_stub() -> None: assert _find_stub_path(test_find_stub_path_returns_none_for_no_stub) is None def test_find_stub_path_returns_none_for_no_module() -> None: obj = MagicMock(spec=[]) obj.__module__ = None with patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getmodule", return_value=None): assert _find_stub_path(obj) is None def test_find_stub_path_returns_none_when_getfile_fails() -> None: module = types.ModuleType("fake_mod") with ( patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getmodule", return_value=module), patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getfile", side_effect=TypeError), ): assert _find_stub_path(lambda: None) is None def test_find_stub_path_package_init_pyi(tmp_path: Path) -> None: pkg_dir = tmp_path / "mypkg" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text("") no_stub_dir = tmp_path / "no_stubs" no_stub_dir.mkdir() stub_dir = tmp_path / "stubs" stub_dir.mkdir() (stub_dir / "__init__.pyi").write_text("x: int\n") module = types.ModuleType("mypkg") module.__path__ = [str(no_stub_dir), str(stub_dir)] module.__file__ = str(pkg_dir / "__init__.py") with patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getmodule", return_value=module): result = _find_stub_path(module) assert result is not None assert result.name == "__init__.pyi" def test_find_stub_path_package_no_init_pyi(tmp_path: Path) -> None: pkg_dir = tmp_path / "mypkg" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text("") module = types.ModuleType("mypkg") module.__path__ = [str(pkg_dir)] module.__file__ = str(pkg_dir / "__init__.py") with patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getmodule", return_value=module): assert _find_stub_path(module) is None def test_parse_stub_ast_valid_file() -> None: stub_path = STUB_ROOT / "stub_mod.pyi" result = _parse_stub_ast(stub_path) assert isinstance(result, ast.Module) def test_parse_stub_ast_caches_result() -> None: stub_path = STUB_ROOT / "stub_mod.pyi" _STUB_AST_CACHE.pop(stub_path, None) first = _parse_stub_ast(stub_path) second = _parse_stub_ast(stub_path) assert first is second _STUB_AST_CACHE.pop(stub_path, None) def test_parse_stub_ast_caches_none_for_bad_syntax(tmp_path: Path) -> None: bad_stub = tmp_path / "bad.pyi" bad_stub.write_text("def (broken syntax\n") result = _parse_stub_ast(bad_stub) assert result is None assert bad_stub in _STUB_AST_CACHE assert _STUB_AST_CACHE[bad_stub] is None _STUB_AST_CACHE.pop(bad_stub, None) def test_parse_stub_ast_returns_none_for_missing_file() -> None: missing = Path("/nonexistent/stub.pyi") _STUB_AST_CACHE.pop(missing, None) assert _parse_stub_ast(missing) is None _STUB_AST_CACHE.pop(missing, None) @pytest.mark.parametrize( ("source", "parts", "expected_type", "expected_name"), [ pytest.param("def foo(x: int) -> str: ...", ["foo"], ast.FunctionDef, "foo", id="top_level_function"), pytest.param( "class Outer:\n class Inner:\n def method(self) -> None: ...", ["Outer", "Inner", "method"], ast.FunctionDef, "method", id="nested_class_method", ), pytest.param("class Foo:\n x: int\n", ["Foo"], ast.ClassDef, "Foo", id="class"), pytest.param("def foo(): ...", ["bar"], None, None, id="missing"), ], ) def test_find_ast_node( source: str, parts: list[str], expected_type: type[ast.stmt] | None, expected_name: str | None ) -> None: tree = ast.parse(source) node = _find_ast_node(tree.body, parts) if expected_type is None: assert node is None else: assert isinstance(node, expected_type) assert node.name == expected_name # type: ignore[union-attr] @pytest.mark.parametrize( ("source", "expected"), [ pytest.param( "def greet(name: str, greeting: str) -> str: ...", {"name": "str", "greeting": "str", "return": "str"}, id="basic", ), pytest.param("def f(x: int): ...", {"x": "int"}, id="no_return"), pytest.param("async def fetch(url: str) -> str: ...", {"url": "str", "return": "str"}, id="async"), pytest.param("def f(self, x: int) -> None: ...", {"x": "int", "return": "None"}, id="skips_unannotated"), ], ) def test_extract_func_annotations(source: str, expected: dict[str, str]) -> None: node = ast.parse(source).body[0] assert isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) assert _extract_func_annotations(node) == expected @pytest.mark.parametrize( ("source", "expected"), [ pytest.param("class Foo:\n x: int\n y: str\n", {"x": "int", "y": "str"}, id="basic"), pytest.param("class Foo:\n x: int\n self.y: str\n", {"x": "int"}, id="ignores_non_name_targets"), ], ) def test_extract_class_annotations(source: str, expected: dict[str, str]) -> None: node = ast.parse(source).body[0] assert isinstance(node, ast.ClassDef) assert _extract_class_annotations(node) == expected @pytest.mark.parametrize( ("source", "qualname", "expected"), [ pytest.param( "def greet(name: str, greeting: str) -> str: ...", "greet", {"name": "str", "greeting": "str", "return": "str"}, id="function", ), pytest.param("class Foo:\n x: int\n y: str\n", "Foo", {"x": "int", "y": "str"}, id="class"), pytest.param("def greet(name: str) -> str: ...", "nonexistent", {}, id="missing_node"), pytest.param("x: int = 1\n", "x", {}, id="unsupported_node_type"), ], ) def test_extract_annotations_from_stub(source: str, qualname: str, expected: dict[str, str]) -> None: tree = ast.parse(source) obj = MagicMock() obj.__qualname__ = qualname assert _extract_annotations_from_stub(tree, obj) == expected def test_extract_annotations_from_stub_no_qualname() -> None: tree = ast.parse("def greet(name: str) -> str: ...") obj = MagicMock(spec=[]) assert _extract_annotations_from_stub(tree, obj) == {} @pytest.mark.parametrize( ("attr", "expected"), [ pytest.param("greet", {"name": "str", "greeting": "str", "return": "str"}, id="function"), pytest.param("Calculator.Inner.process", {"data": "bytes", "return": "bytes"}, id="nested_class"), pytest.param("fetch", {"url": "str", "return": "str"}, id="async_function"), ], ) def test_backfill_from_stub(stub_mod: Any, attr: str, expected: dict[str, str]) -> None: obj = stub_mod for part in attr.split("."): obj = getattr(obj, part) assert _backfill_from_stub(obj) == expected def test_backfill_from_stub_no_stub() -> None: assert _backfill_from_stub(test_backfill_from_stub_no_stub) == {} @pytest.mark.sphinx("text", testroot="pyi-stubs") def test_sphinx_build_uses_stub_types(app: SphinxTestApp, status: StringIO, warning: StringIO) -> None: template = """\ .. autofunction:: stub_mod.greet .. autoclass:: stub_mod.Calculator :members: .. autofunction:: stub_mod.fetch """ (Path(app.srcdir) / "index.rst").write_text(template) app.build() assert "build succeeded" in status.getvalue() result = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) assert "str" in result warn_text = warning.getvalue() assert "stub_mod" not in warn_text or "forward reference" not in warn_text sphinx_autodoc_typehints-3.8.0/tests/test_resolver/test_type_comments.py0000644000000000000000000001322613615410400024145 0ustar00from __future__ import annotations from textwrap import dedent from unittest.mock import MagicMock, patch from sphinx_autodoc_typehints._resolver._type_comments import ( _normalize_source_lines, _split_type_comment_args, backfill_type_hints, ) def test__normalize_source_lines_async_def() -> None: source = """ async def async_function(): class InnerClass: def __init__(self): ... """ expected = """ async def async_function(): class InnerClass: def __init__(self): ... """ assert _normalize_source_lines(dedent(source)) == dedent(expected) def test__normalize_source_lines_def_starting_decorator_parameter() -> None: source = """ @_with_parameters( _Parameter("self", _Parameter.POSITIONAL_OR_KEYWORD), *_proxy_instantiation_parameters, _project_id, _Parameter( "node_numbers", _Parameter.POSITIONAL_OR_KEYWORD, default=None, annotation=Optional[Iterable[int]], ), ) def __init__(bound_args): # noqa: N805 ... """ expected = """ @_with_parameters( _Parameter("self", _Parameter.POSITIONAL_OR_KEYWORD), *_proxy_instantiation_parameters, _project_id, _Parameter( "node_numbers", _Parameter.POSITIONAL_OR_KEYWORD, default=None, annotation=Optional[Iterable[int]], ), ) def __init__(bound_args): # noqa: N805 ... """ assert _normalize_source_lines(dedent(source)) == dedent(expected) def test_syntax_error_backfill() -> None: def func(x): # noqa: ANN202 ... backfill_type_hints(func, "func") def test__normalize_source_lines_no_def() -> None: source = "x = 1\ny = 2\n" assert _normalize_source_lines(source) == source def test__split_type_comment_args_empty() -> None: assert _split_type_comment_args("") == [] def test__split_type_comment_args_single() -> None: assert _split_type_comment_args("int") == ["int"] def test__split_type_comment_args_multiple() -> None: assert _split_type_comment_args("int, str, bool") == ["int", "str", "bool"] def test__split_type_comment_args_nested_brackets() -> None: assert _split_type_comment_args("List[int], Dict[str, int]") == ["List[int]", "Dict[str, int]"] def test__split_type_comment_args_strips_stars() -> None: assert _split_type_comment_args("*args, **kwargs") == ["args", "kwargs"] def test_backfill_type_hints_no_type_comment_attr() -> None: with patch("sphinx_autodoc_typehints._resolver._type_comments.inspect.getsource", return_value="import os\n"): assert backfill_type_hints(lambda: None, "test") == {} def test_backfill_type_hints_multi_child_ast() -> None: with patch( "sphinx_autodoc_typehints._resolver._type_comments.inspect.getsource", return_value="x = 1\ndef foo(): pass\n", ): assert backfill_type_hints(lambda: None, "test") == {} def test_backfill_type_hints_with_type_comment() -> None: result = backfill_type_hints(_typed_func, "_typed_func") assert result == {"x": "int", "y": "str", "return": "bool"} def test_backfill_type_hints_type_comment_self_insertion() -> None: result = backfill_type_hints(_BackfillClass.method, "_BackfillClass.method") assert result == {"x": "int", "return": "str"} def test_backfill_type_hints_type_comment_mismatch() -> None: result = backfill_type_hints(_mismatched_type_comment, "_mismatched_type_comment") assert result == {"return": "bool"} def test_backfill_type_hints_posonly_args() -> None: result = backfill_type_hints(_posonly_typed_func, "_posonly_typed_func") assert result == {"x": "int", "y": "str", "return": "bool"} def test_backfill_type_hints_empty_return() -> None: with patch( "sphinx_autodoc_typehints._resolver._type_comments.inspect.getsource", return_value="def f(x):\n # type: (int) -> \n pass\n", ): def f(x): ... # noqa: ANN202 result = backfill_type_hints(f, "f") assert result == {"x": "int"} def test_backfill_type_hints_unparseable_type_comment() -> None: with patch( "sphinx_autodoc_typehints._resolver._type_comments.inspect.getsource", return_value="def f(x):\n # type: bad_comment\n pass\n", ): def f(x): ... # noqa: ANN202 result = backfill_type_hints(f, "f") assert result == {} def test_backfill_type_hints_inline_type_comments() -> None: result = backfill_type_hints(_inline_typed_func, "_inline_typed_func") assert result == {"x": "int", "y": "str", "return": "bool"} def test_multi_child_ast_warning_includes_location() -> None: mock_logger = MagicMock() with ( patch( "sphinx_autodoc_typehints._resolver._type_comments.inspect.getsource", return_value="x = 1\ndef foo(): pass\n", ), patch("sphinx_autodoc_typehints._resolver._type_comments._LOGGER", mock_logger), ): backfill_type_hints(lambda: None, "test") mock_logger.warning.assert_called_once() kwargs = mock_logger.warning.call_args.kwargs assert "location" in kwargs # --- module-level fixtures for inspect.getsource --- class _BackfillClass: def method(self, x): # noqa: ANN202 # type: (int) -> str ... def _typed_func(x, y): # noqa: ANN202 # type: (int, str) -> bool ... def _posonly_typed_func(x, y, /): # noqa: ANN202 # type: (int, str) -> bool ... def _inline_typed_func( # noqa: ANN202 x, # type: int y, # type: str ): # type: (...) -> bool ... def _mismatched_type_comment(x, y, z): # noqa: ANN202 # type: (int) -> bool ... sphinx_autodoc_typehints-3.8.0/tests/test_resolver/test_type_hints.py0000644000000000000000000000611313615410400023442 0ustar00from __future__ import annotations import csv from csv import Error from typing import Any from unittest.mock import MagicMock, patch from sphinx_autodoc_typehints._resolver._type_hints import ( _execute_guarded_code, _future_annotations_imported, _get_type_hint, _resolve_type_guarded_imports, _run_guarded_import, _should_skip_guarded_import_resolution, ) def test_no_source_code_type_guard() -> None: _resolve_type_guarded_imports([], Error) def test_future_annotations_not_imported() -> None: assert not _future_annotations_imported(csv) def test_future_annotations_imported() -> None: assert _future_annotations_imported(test_future_annotations_imported) def test_should_skip_module_type() -> None: assert not _should_skip_guarded_import_resolution(csv) def test_should_skip_no_globals() -> None: assert _should_skip_guarded_import_resolution(42) def test_should_skip_builtin_module() -> None: fn: Any = type("FakeFunc", (), {"__globals__": {}, "__module__": "builtins"})() assert _should_skip_guarded_import_resolution(fn) def test_get_type_hint_recursion_error() -> None: def func(x: int) -> str: ... with patch("sphinx_autodoc_typehints._resolver._type_hints.get_type_hints", side_effect=RecursionError): assert _get_type_hint([], "test", func, {}) == {} def test_execute_guarded_code_catches_exception() -> None: module = type("FakeModule", (), {"__globals__": {}, "__dict__": {}})() with patch("sphinx_autodoc_typehints._resolver._type_hints._run_guarded_import", side_effect=RuntimeError("boom")): _execute_guarded_code([], module, "\nif TYPE_CHECKING:\n import os\nx = 1\n") def test_run_guarded_import_no_exc_name() -> None: ns: dict[str, Any] = {} obj: Any = type("FakeObj", (), {"__globals__": ns})() _run_guarded_import([], obj, "raise ImportError()") def test_forward_ref_warning_includes_module() -> None: def func(x: int) -> str: ... func.__module__ = "some_module" func.__annotations__ = {"x": "NonExistent"} mock_logger = MagicMock() with ( patch("sphinx_autodoc_typehints._resolver._type_hints.get_type_hints", side_effect=NameError("NonExistent")), patch("sphinx_autodoc_typehints._resolver._type_hints._LOGGER", mock_logger), ): _get_type_hint([], "func", func, {}) mock_logger.warning.assert_called_once() args = mock_logger.warning.call_args assert "some_module" in str(args) assert "location" in args.kwargs def test_guarded_import_warning_includes_module() -> None: module = type("FakeModule", (), {"__globals__": {}, "__dict__": {}, "__module__": "fake_mod"})() mock_logger = MagicMock() with ( patch("sphinx_autodoc_typehints._resolver._type_hints._run_guarded_import", side_effect=RuntimeError("boom")), patch("sphinx_autodoc_typehints._resolver._type_hints._LOGGER", mock_logger), ): _execute_guarded_code([], module, "\nif TYPE_CHECKING:\n import os\nx = 1\n") mock_logger.warning.assert_called_once() args = mock_logger.warning.call_args assert "fake_mod" in str(args) sphinx_autodoc_typehints-3.8.0/tests/test_resolver/test_util.py0000644000000000000000000000210013615410400022221 0ustar00from __future__ import annotations from unittest.mock import patch from sphinx_autodoc_typehints._resolver._util import get_obj_location def _typed_func(x: int, y: str) -> bool: ... class _BackfillClass: def method(self, x: int) -> str: ... def test_get_obj_location_with_function() -> None: location = get_obj_location(_typed_func) assert location is not None assert location.endswith(".py:" + str(_typed_func.__code__.co_firstlineno)) assert "test_util.py" in location def test_get_obj_location_with_class() -> None: location = get_obj_location(_BackfillClass) assert location is not None assert "test_util.py" in location def test_get_obj_location_non_inspectable() -> None: assert get_obj_location(42) is None def test_get_obj_location_no_source_lines() -> None: with patch("sphinx_autodoc_typehints._resolver._util.inspect.getsourcelines", side_effect=OSError): location = get_obj_location(_typed_func) assert location is not None assert location.endswith(".py") assert ":" not in location.rsplit("/", 1)[-1] sphinx_autodoc_typehints-3.8.0/.gitignore0000644000000000000000000000013613615410400015570 0ustar00dist *.egg *.py[codz] *$py.class .tox .*_cache /src/sphinx_autodoc_typehints/version.py venv* sphinx_autodoc_typehints-3.8.0/LICENSE0000644000000000000000000000211513615410400014604 0ustar00MIT License Copyright (c) 2015-202x The sphinx-autodoc-typehints developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sphinx_autodoc_typehints-3.8.0/README.md0000644000000000000000000003702013615410400015061 0ustar00# sphinx-autodoc-typehints [![PyPI](https://img.shields.io/pypi/v/sphinx-autodoc-typehints?style=flat-square)](https://pypi.org/project/sphinx-autodoc-typehints/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/sphinx-autodoc-typehints.svg)](https://pypi.org/project/sphinx-autodoc-typehints/) [![Downloads](https://pepy.tech/badge/sphinx-autodoc-typehints/month)](https://pepy.tech/project/sphinx-autodoc-typehints) [![check](https://github.com/tox-dev/sphinx-autodoc-typehints/actions/workflows/check.yaml/badge.svg)](https://github.com/tox-dev/sphinx-autodoc-typehints/actions/workflows/check.yaml) This [Sphinx](https://www.sphinx-doc.org/) extension reads your Python [type hints](https://docs.python.org/3/library/typing.html) and automatically adds type information to your generated documentation -- so you write types once in code and they appear in your docs without duplication. **Features:** - Adds parameter and return types from annotations into docstrings - Resolves types from [`TYPE_CHECKING`](https://docs.python.org/3/library/typing.html#typing.TYPE_CHECKING) blocks and [`.pyi` stub files](https://typing.python.org/en/latest/spec/distributing.html#stub-files) - Renders [`@overload`](https://docs.python.org/3/library/typing.html#typing.overload) signatures in docstrings - Extracts types from [attrs](https://www.attrs.org/) and [dataclass](https://docs.python.org/3/library/dataclasses.html) classes - Shows default parameter values alongside types - Controls union display style (`Union[X, Y]` vs `X | Y`) - Supports custom type formatters and module name rewriting - Extracts descriptions from [`Annotated[T, Doc(...)]`](https://typing-extensions.readthedocs.io/en/latest/#Doc) metadata - Works with [Google and NumPy](https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html) docstring styles Sphinx has a built-in [`autodoc_typehints`](https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_typehints) setting (since v2.1) that can move type hints between signatures and descriptions. This extension replaces that with the features above. See [Avoid duplicate types with built-in Sphinx](#avoid-duplicate-types-with-built-in-sphinx). - [Installation](#installation) - [Quick start](#quick-start) - [How-to guides](#how-to-guides) - [Avoid duplicate types with built-in Sphinx](#avoid-duplicate-types-with-built-in-sphinx) - [Use with Google or NumPy docstring style](#use-with-google-or-numpy-docstring-style) - [Control return type display](#control-return-type-display) - [Change how union types look](#change-how-union-types-look) - [Show default parameter values](#show-default-parameter-values) - [Keep type hints in function signatures](#keep-type-hints-in-function-signatures) - [Handle circular imports](#handle-circular-imports) - [Resolve types from `TYPE_CHECKING` blocks](#resolve-types-from-type_checking-blocks) - [Show types for `attrs` or `dataclass` fields](#show-types-for-attrs-or-dataclass-fields) - [Write a custom type formatter](#write-a-custom-type-formatter) - [Document a `NewType` or `type` alias without expanding it](#document-a-newtype-or-type-alias-without-expanding-it) - [Add types for C extensions or packages without annotations](#add-types-for-c-extensions-or-packages-without-annotations) - [Fix cross-reference links for renamed modules](#fix-cross-reference-links-for-renamed-modules) - [Suppress warnings](#suppress-warnings) - [Reference](#reference) - [Configuration options](#configuration-options) - [Warning categories](#warning-categories) - [Explanation](#explanation) - [How it works](#how-it-works) - [How return type options interact](#how-return-type-options-interact) ## Installation ```bash pip install sphinx-autodoc-typehints ``` Then add the extension to your [`conf.py`](https://www.sphinx-doc.org/en/master/usage/configuration.html): ```python extensions = ["sphinx.ext.autodoc", "sphinx_autodoc_typehints"] ``` ## Quick start Instead of writing types in your docstrings, write them as Python type hints. The extension picks them up and adds them to your Sphinx output: ```python # Before: types repeated in docstrings def format_unit(value, unit): """ Format a value with its unit. :param float value: a numeric value :param str unit: the unit (kg, m, etc.) :rtype: str """ return f"{value} {unit}" # After: types only in annotations, docs generated automatically def format_unit(value: float, unit: str) -> str: """ Format a value with its unit. :param value: a numeric value :param unit: the unit (kg, m, etc.) """ return f"{value} {unit}" ``` The extension adds the type information to your docs during the Sphinx build. See an example at the [pyproject-api docs](https://pyproject-api.readthedocs.io/latest/api.html). ## How-to guides ### Avoid duplicate types with built-in Sphinx If types appear twice in your docs, you're likely running both this extension and Sphinx's built-in type hint processing. Set [`autodoc_typehints = "none"`](https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_typehints) in your `conf.py` to let this extension handle everything: ```python autodoc_typehints = "none" ``` ### Use with Google or NumPy docstring style If you use [`sphinx.ext.napoleon`](https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html) for Google-style or NumPy-style docstrings, load it **before** this extension: ```python extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_autodoc_typehints", ] ``` To avoid duplicate return type entries, disable the return type block in both extensions: ```python napoleon_use_rtype = False # sphinx.ext.napoleon setting typehints_use_rtype = False ``` See [`napoleon_use_rtype`](https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#confval-napoleon_use_rtype) in the Sphinx docs. ### Control return type display By default, return types appear as a separate block in your docs. You can change this: ```python # Don't show return types at all typehints_document_rtype = False # Don't show "None" return types, but show all others typehints_document_rtype_none = False # Show the return type inline with the return description # instead of as a separate block typehints_use_rtype = False ``` ### Change how union types look By default, union types display as `Union[str, int]` and `Optional[str]`. To use the shorter pipe syntax (`str | int`, `str | None`): ```python always_use_bars_union = True ``` On Python 3.14+, the [pipe syntax](https://docs.python.org/3/library/stdtypes.html#types-union) is always used regardless of this setting. By default, `Optional[Union[A, B]]` is simplified to `Union[A, B, None]`. To keep the `Optional` wrapper: ```python simplify_optional_unions = False ``` Note: with this set to `False`, any union containing `None` will display as `Optional`. ### Show default parameter values To include default values in your docs, set `typehints_defaults` to one of three styles: ```python # "param (int, default: 1) -- description" typehints_defaults = "comma" # "param (int) -- description (default: 1)" typehints_defaults = "braces" # "param (int) -- description (default: 1)" (at end of text) typehints_defaults = "braces-after" ``` ### Keep type hints in function signatures By default, type hints are removed from function signatures and shown in the parameter list below. To keep them visible in the signature line: ```python typehints_use_signature = True # show parameter types in signature typehints_use_signature_return = True # show return type in signature ``` ### Handle circular imports When two modules need to reference each other's types, you'll get circular import errors. Fix this by using [`from __future__ import annotations`](https://docs.python.org/3/library/__future__.html#module-__future__), which makes all type hints strings that are resolved later: ```python from __future__ import annotations import othermodule def process(item: othermodule.OtherClass) -> None: ... ``` ### Resolve types from `TYPE_CHECKING` blocks This extension automatically imports types from [`TYPE_CHECKING`](https://docs.python.org/3/library/typing.html#typing.TYPE_CHECKING) blocks at doc-build time. If a type still fails to resolve, the dependency is likely not installed in your docs environment. Either install it, or suppress the warning: ```python suppress_warnings = ["sphinx_autodoc_typehints.guarded_import"] ``` ### Show types for `attrs` or `dataclass` fields The extension backfills annotations from [attrs](https://www.attrs.org/) field metadata automatically. For [dataclasses](https://docs.python.org/3/library/dataclasses.html), annotations are read from the class body. Make sure the class is documented with `.. autoclass::` and `:members:` or `:undoc-members:`. ### Write a custom type formatter To control exactly how a type appears in your docs, provide a formatter function. It receives the type annotation and the Sphinx config, and returns [RST](https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html) markup (or `None` to use the default rendering): ```python def my_formatter(annotation, config): if annotation is bool: return ":class:`bool`" return None typehints_formatter = my_formatter ``` To always show the full module path for types (e.g., `collections.OrderedDict` instead of `OrderedDict`): ```python typehints_fully_qualified = True ``` ### Document a `NewType` or `type` alias without expanding it The extension preserves alias names only when they have a `.. py:type::` directive in your docs. Without that entry, the alias is expanded to its underlying type. Add a documentation entry for the alias, and it will render as a clickable link instead. ### Add types for C extensions or packages without annotations The extension reads [`.pyi` stub files](https://typing.python.org/en/latest/spec/distributing.html#stub-files) automatically. Place a `.pyi` file next to the `.so`/`.pyd` file (or as `__init__.pyi` in the package directory) with the type annotations, and they'll be picked up. ### Fix cross-reference links for renamed modules Some libraries expose types under a different module path than where they're documented. For example, GTK types live at `gi.repository.Gtk.Window` in Python, but their docs list them as `Gtk.Window`. This causes broken [intersphinx](https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html) links. Use `typehints_fixup_module_name` to rewrite the module path before links are generated: ```python def fixup_module_name(module: str) -> str: if module.startswith("gi.repository."): return module.removeprefix("gi.repository.") return module typehints_fixup_module_name = fixup_module_name ``` ### Suppress warnings To silence all warnings from this extension: ```python suppress_warnings = ["sphinx_autodoc_typehints"] ``` To suppress only specific warning types, see [Warning categories](#warning-categories) for the full list. ## Reference ### Configuration options | Option | Default | Description | | -------------------------------- | ------- | --------------------------------------------------------------------------------------------- | | `typehints_document_rtype` | `True` | Show the return type in docs. | | `typehints_document_rtype_none` | `True` | Show return type when it's `None`. | | `typehints_use_rtype` | `True` | Show return type as a separate block. When `False`, it's inlined with the return description. | | `always_use_bars_union` | `False` | Use `X \| Y` instead of `Union[X, Y]`. Always on for Python 3.14+. | | `simplify_optional_unions` | `True` | Flatten `Optional[Union[A, B]]` to `Union[A, B, None]`. | | `typehints_defaults` | `None` | Show default values: `"comma"`, `"braces"`, or `"braces-after"`. | | `typehints_use_signature` | `False` | Keep parameter types in the function signature. | | `typehints_use_signature_return` | `False` | Keep the return type in the function signature. | | `typehints_fully_qualified` | `False` | Show full module path for types (e.g., `module.Class` not `Class`). | | `always_document_param_types` | `False` | Add types even for parameters that don't have a `:param:` entry in the docstring. | | `typehints_formatter` | `None` | A function `(annotation, Config) -> str \| None` for custom type rendering. | | `typehints_fixup_module_name` | `None` | A function `(str) -> str` to rewrite module paths before generating cross-reference links. | ### Warning categories All warnings can be suppressed via Sphinx's [`suppress_warnings`](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-suppress_warnings) in `conf.py`: | Category | When it's raised | | --------------------------------------------- | --------------------------------------------------------------------------- | | `sphinx_autodoc_typehints` | Catch-all for every warning from this extension. | | `sphinx_autodoc_typehints.comment` | A type comment (`# type: ...`) couldn't be parsed. | | `sphinx_autodoc_typehints.forward_reference` | A forward reference (string annotation) couldn't be resolved. | | `sphinx_autodoc_typehints.guarded_import` | A type from a `TYPE_CHECKING` block couldn't be imported at runtime. | | `sphinx_autodoc_typehints.local_function` | A type annotation references a function defined inside another function. | | `sphinx_autodoc_typehints.multiple_ast_nodes` | A type comment matched multiple definitions and the right one is ambiguous. | ## Explanation ### How it works During the Sphinx build, this extension hooks into two [autodoc events](https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#event-autodoc-process-signature). First, it strips type annotations from function signatures (so they don't appear twice). Then, it reads the annotations and adds type information into the docstring -- parameter types go next to each `:param:` entry, and the return type becomes an `:rtype:` entry. Only parameters that already have a `:param:` line in the docstring get type information added. Set `always_document_param_types = True` to add types for all parameters, even undocumented ones. ### How return type options interact The return type options combine as follows: - **Both defaults** (`typehints_document_rtype = True`, `typehints_use_rtype = True`) -- return type appears as a separate `:rtype:` block below the description. - **Inline mode** (`typehints_document_rtype = True`, `typehints_use_rtype = False`) -- return type is appended to the `:return:` text. If there's no `:return:` entry, it falls back to a separate block. - **Disabled** (`typehints_document_rtype = False`) -- no return type shown, regardless of other settings. - **Skip None** (`typehints_document_rtype_none = False`) -- hides `None` return types specifically, other return types still appear. sphinx_autodoc_typehints-3.8.0/pyproject.toml0000644000000000000000000001271213615410400016517 0ustar00[build-system] build-backend = "hatchling.build" requires = [ "hatch-vcs>=0.5", "hatchling>=1.29", ] [project] name = "sphinx-autodoc-typehints" description = "Type hints (PEP 484) support for the Sphinx autodoc extension" readme.content-type = "text/markdown" readme.file = "README.md" keywords = [ "environments", "isolated", "testing", "virtual", ] license = "MIT" maintainers = [ { name = "Bernát Gábor", email = "gaborjbernat@gmail.com" }, ] authors = [ { name = "Bernát Gábor", email = "gaborjbernat@gmail.com" }, ] requires-python = ">=3.12" classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: Sphinx :: Extension", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Documentation :: Sphinx", ] dynamic = [ "version", ] dependencies = [ "sphinx>=9.1", ] urls.Changelog = "https://github.com/tox-dev/sphinx-autodoc-typehints/releases" urls.Homepage = "https://github.com/tox-dev/sphinx-autodoc-typehints" urls.Source = "https://github.com/tox-dev/sphinx-autodoc-typehints" urls.Tracker = "https://github.com/tox-dev/sphinx-autodoc-typehints/issues" [dependency-groups] dev = [ { include-group = "docs" }, { include-group = "lint" }, { include-group = "pkg-meta" }, { include-group = "test" }, { include-group = "type" }, ] test = [ "attrs>=22", "covdefaults>=2.3", "coverage>=7.13.4", "defusedxml>=0.7.1", "diff-cover>=10.2", "numpydoc>=1.8", "pytest>=9.0.2", "pytest-cov>=7", "sphobjinv>=2.3.1.3", "typing-extensions>=4.15", ] type = [ "ty>=0.0.18", { include-group = "docs" }, { include-group = "test" }, ] docs = [ "furo>=2025.12.19", ] lint = [ "pre-commit-uv>=4.2.1", ] pkg-meta = [ "check-wheel-contents>=0.6.3", "twine>=6.2", "uv>=0.10.5", ] [tool.hatch] build.hooks.vcs.version-file = "src/sphinx_autodoc_typehints/version.py" version.source = "vcs" [tool.ruff] line-length = 120 format.preview = true format.docstring-code-line-length = 100 format.docstring-code-format = true lint.select = [ "ALL", ] lint.ignore = [ "ANN401", # allow Any as type annotation "COM812", # Conflict with formatter "CPY", # No copyright statements "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible "DOC", # no sphinx support "ISC001", # Conflict with formatter "RUF067", # `__init__` module should only contain docstrings and re-exports "S104", # Possible binding to all interface ] lint.per-file-ignores."tests/**/*.py" = [ "D", # don't care about documentation in tests "FBT", # don't care about booleans as positional arguments in tests "INP001", # no implicit namespace "PLC2701", # private imports "PLR0913", # any number of arguments in tests "PLR0917", # any number of arguments in tests "PLR2004", # Magic value used in comparison, consider replacing with a constant variable "S101", # asserts allowed in tests "S603", # `subprocess` call: check for execution of untrusted input "UP006", # we test for old List/Tuple syntax "UP007", # we test for old Union syntax "UP045", # we test for old Optional syntax ] lint.per-file-ignores."tests/roots/test-issue_572/mod_forward_ref.py" = [ "I002", # intentionally omits `from __future__ import annotations` to test forward references "PLR6301", # method must be instance method to test class forward refs ] lint.per-file-ignores."tests/roots/test-issue_599/mod_eager.py" = [ "I002", # this file intentionally omits `from __future__ import annotations` to test non-deferred annotations ] lint.per-file-ignores."tests/roots/test-pyi-stubs/stub_mod.py" = [ "ANN", # intentionally has no type annotations to simulate a C extension "I002", # intentionally omits `from __future__ import annotations` "PLR6301", # method must be instance method to test stub resolution "RUF029", # async function must be async to test async stub resolution ] lint.per-file-ignores."tests/test_resolver/test_type_comments.py" = [ "ANN001", # type comment fixtures intentionally omit annotations "ARG001", # type comment fixtures have intentionally unused args ] lint.isort = { known-first-party = [ "sphinx_autodoc_typehints", "tests", ], required-imports = [ "from __future__ import annotations", ] } lint.preview = true [tool.codespell] builtin = "clear,usage,en-GB_to_en-US" ignore-words = "ignore-words.txt" write-changes = true count = true [tool.pyproject-fmt] max_supported_python = "3.14" [tool.pytest] ini_options.testpaths = [ "tests", ] ini_options.filterwarnings = [ "ignore:.*rm_rf.*:pytest.PytestWarning", ] [tool.coverage] run.parallel = true run.plugins = [ "covdefaults", ] paths.source = [ "src", ".tox/*/lib/python*/site-packages", ".tox/pypy*/site-packages", ".tox\\*\\Lib\\site-packages", ".tox/*/.venv/lib/python*/site-packages", ".tox/pypy*/.venv/site-packages", ".tox\\*\\.venv\\Lib\\site-packages", "*/src", "*\\src", ] report.fail_under = 88 report.omit = [] html.show_contexts = true html.skip_covered = false [tool.ty] environment.python-version = "3.14" src.exclude = [ "tests/roots/" ] overrides = [ { include = [ "tests/" ], rules.empty-body = "ignore" } ] sphinx_autodoc_typehints-3.8.0/PKG-INFO0000644000000000000000000004135413615410400014704 0ustar00Metadata-Version: 2.4 Name: sphinx-autodoc-typehints Version: 3.8.0 Summary: Type hints (PEP 484) support for the Sphinx autodoc extension Project-URL: Changelog, https://github.com/tox-dev/sphinx-autodoc-typehints/releases Project-URL: Homepage, https://github.com/tox-dev/sphinx-autodoc-typehints Project-URL: Source, https://github.com/tox-dev/sphinx-autodoc-typehints Project-URL: Tracker, https://github.com/tox-dev/sphinx-autodoc-typehints/issues Author-email: Bernát Gábor Maintainer-email: Bernát Gábor License-Expression: MIT License-File: LICENSE Keywords: environments,isolated,testing,virtual Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: Sphinx :: Extension Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Classifier: Topic :: Documentation :: Sphinx Requires-Python: >=3.12 Requires-Dist: sphinx>=9.1 Description-Content-Type: text/markdown # sphinx-autodoc-typehints [![PyPI](https://img.shields.io/pypi/v/sphinx-autodoc-typehints?style=flat-square)](https://pypi.org/project/sphinx-autodoc-typehints/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/sphinx-autodoc-typehints.svg)](https://pypi.org/project/sphinx-autodoc-typehints/) [![Downloads](https://pepy.tech/badge/sphinx-autodoc-typehints/month)](https://pepy.tech/project/sphinx-autodoc-typehints) [![check](https://github.com/tox-dev/sphinx-autodoc-typehints/actions/workflows/check.yaml/badge.svg)](https://github.com/tox-dev/sphinx-autodoc-typehints/actions/workflows/check.yaml) This [Sphinx](https://www.sphinx-doc.org/) extension reads your Python [type hints](https://docs.python.org/3/library/typing.html) and automatically adds type information to your generated documentation -- so you write types once in code and they appear in your docs without duplication. **Features:** - Adds parameter and return types from annotations into docstrings - Resolves types from [`TYPE_CHECKING`](https://docs.python.org/3/library/typing.html#typing.TYPE_CHECKING) blocks and [`.pyi` stub files](https://typing.python.org/en/latest/spec/distributing.html#stub-files) - Renders [`@overload`](https://docs.python.org/3/library/typing.html#typing.overload) signatures in docstrings - Extracts types from [attrs](https://www.attrs.org/) and [dataclass](https://docs.python.org/3/library/dataclasses.html) classes - Shows default parameter values alongside types - Controls union display style (`Union[X, Y]` vs `X | Y`) - Supports custom type formatters and module name rewriting - Extracts descriptions from [`Annotated[T, Doc(...)]`](https://typing-extensions.readthedocs.io/en/latest/#Doc) metadata - Works with [Google and NumPy](https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html) docstring styles Sphinx has a built-in [`autodoc_typehints`](https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_typehints) setting (since v2.1) that can move type hints between signatures and descriptions. This extension replaces that with the features above. See [Avoid duplicate types with built-in Sphinx](#avoid-duplicate-types-with-built-in-sphinx). - [Installation](#installation) - [Quick start](#quick-start) - [How-to guides](#how-to-guides) - [Avoid duplicate types with built-in Sphinx](#avoid-duplicate-types-with-built-in-sphinx) - [Use with Google or NumPy docstring style](#use-with-google-or-numpy-docstring-style) - [Control return type display](#control-return-type-display) - [Change how union types look](#change-how-union-types-look) - [Show default parameter values](#show-default-parameter-values) - [Keep type hints in function signatures](#keep-type-hints-in-function-signatures) - [Handle circular imports](#handle-circular-imports) - [Resolve types from `TYPE_CHECKING` blocks](#resolve-types-from-type_checking-blocks) - [Show types for `attrs` or `dataclass` fields](#show-types-for-attrs-or-dataclass-fields) - [Write a custom type formatter](#write-a-custom-type-formatter) - [Document a `NewType` or `type` alias without expanding it](#document-a-newtype-or-type-alias-without-expanding-it) - [Add types for C extensions or packages without annotations](#add-types-for-c-extensions-or-packages-without-annotations) - [Fix cross-reference links for renamed modules](#fix-cross-reference-links-for-renamed-modules) - [Suppress warnings](#suppress-warnings) - [Reference](#reference) - [Configuration options](#configuration-options) - [Warning categories](#warning-categories) - [Explanation](#explanation) - [How it works](#how-it-works) - [How return type options interact](#how-return-type-options-interact) ## Installation ```bash pip install sphinx-autodoc-typehints ``` Then add the extension to your [`conf.py`](https://www.sphinx-doc.org/en/master/usage/configuration.html): ```python extensions = ["sphinx.ext.autodoc", "sphinx_autodoc_typehints"] ``` ## Quick start Instead of writing types in your docstrings, write them as Python type hints. The extension picks them up and adds them to your Sphinx output: ```python # Before: types repeated in docstrings def format_unit(value, unit): """ Format a value with its unit. :param float value: a numeric value :param str unit: the unit (kg, m, etc.) :rtype: str """ return f"{value} {unit}" # After: types only in annotations, docs generated automatically def format_unit(value: float, unit: str) -> str: """ Format a value with its unit. :param value: a numeric value :param unit: the unit (kg, m, etc.) """ return f"{value} {unit}" ``` The extension adds the type information to your docs during the Sphinx build. See an example at the [pyproject-api docs](https://pyproject-api.readthedocs.io/latest/api.html). ## How-to guides ### Avoid duplicate types with built-in Sphinx If types appear twice in your docs, you're likely running both this extension and Sphinx's built-in type hint processing. Set [`autodoc_typehints = "none"`](https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_typehints) in your `conf.py` to let this extension handle everything: ```python autodoc_typehints = "none" ``` ### Use with Google or NumPy docstring style If you use [`sphinx.ext.napoleon`](https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html) for Google-style or NumPy-style docstrings, load it **before** this extension: ```python extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_autodoc_typehints", ] ``` To avoid duplicate return type entries, disable the return type block in both extensions: ```python napoleon_use_rtype = False # sphinx.ext.napoleon setting typehints_use_rtype = False ``` See [`napoleon_use_rtype`](https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#confval-napoleon_use_rtype) in the Sphinx docs. ### Control return type display By default, return types appear as a separate block in your docs. You can change this: ```python # Don't show return types at all typehints_document_rtype = False # Don't show "None" return types, but show all others typehints_document_rtype_none = False # Show the return type inline with the return description # instead of as a separate block typehints_use_rtype = False ``` ### Change how union types look By default, union types display as `Union[str, int]` and `Optional[str]`. To use the shorter pipe syntax (`str | int`, `str | None`): ```python always_use_bars_union = True ``` On Python 3.14+, the [pipe syntax](https://docs.python.org/3/library/stdtypes.html#types-union) is always used regardless of this setting. By default, `Optional[Union[A, B]]` is simplified to `Union[A, B, None]`. To keep the `Optional` wrapper: ```python simplify_optional_unions = False ``` Note: with this set to `False`, any union containing `None` will display as `Optional`. ### Show default parameter values To include default values in your docs, set `typehints_defaults` to one of three styles: ```python # "param (int, default: 1) -- description" typehints_defaults = "comma" # "param (int) -- description (default: 1)" typehints_defaults = "braces" # "param (int) -- description (default: 1)" (at end of text) typehints_defaults = "braces-after" ``` ### Keep type hints in function signatures By default, type hints are removed from function signatures and shown in the parameter list below. To keep them visible in the signature line: ```python typehints_use_signature = True # show parameter types in signature typehints_use_signature_return = True # show return type in signature ``` ### Handle circular imports When two modules need to reference each other's types, you'll get circular import errors. Fix this by using [`from __future__ import annotations`](https://docs.python.org/3/library/__future__.html#module-__future__), which makes all type hints strings that are resolved later: ```python from __future__ import annotations import othermodule def process(item: othermodule.OtherClass) -> None: ... ``` ### Resolve types from `TYPE_CHECKING` blocks This extension automatically imports types from [`TYPE_CHECKING`](https://docs.python.org/3/library/typing.html#typing.TYPE_CHECKING) blocks at doc-build time. If a type still fails to resolve, the dependency is likely not installed in your docs environment. Either install it, or suppress the warning: ```python suppress_warnings = ["sphinx_autodoc_typehints.guarded_import"] ``` ### Show types for `attrs` or `dataclass` fields The extension backfills annotations from [attrs](https://www.attrs.org/) field metadata automatically. For [dataclasses](https://docs.python.org/3/library/dataclasses.html), annotations are read from the class body. Make sure the class is documented with `.. autoclass::` and `:members:` or `:undoc-members:`. ### Write a custom type formatter To control exactly how a type appears in your docs, provide a formatter function. It receives the type annotation and the Sphinx config, and returns [RST](https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html) markup (or `None` to use the default rendering): ```python def my_formatter(annotation, config): if annotation is bool: return ":class:`bool`" return None typehints_formatter = my_formatter ``` To always show the full module path for types (e.g., `collections.OrderedDict` instead of `OrderedDict`): ```python typehints_fully_qualified = True ``` ### Document a `NewType` or `type` alias without expanding it The extension preserves alias names only when they have a `.. py:type::` directive in your docs. Without that entry, the alias is expanded to its underlying type. Add a documentation entry for the alias, and it will render as a clickable link instead. ### Add types for C extensions or packages without annotations The extension reads [`.pyi` stub files](https://typing.python.org/en/latest/spec/distributing.html#stub-files) automatically. Place a `.pyi` file next to the `.so`/`.pyd` file (or as `__init__.pyi` in the package directory) with the type annotations, and they'll be picked up. ### Fix cross-reference links for renamed modules Some libraries expose types under a different module path than where they're documented. For example, GTK types live at `gi.repository.Gtk.Window` in Python, but their docs list them as `Gtk.Window`. This causes broken [intersphinx](https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html) links. Use `typehints_fixup_module_name` to rewrite the module path before links are generated: ```python def fixup_module_name(module: str) -> str: if module.startswith("gi.repository."): return module.removeprefix("gi.repository.") return module typehints_fixup_module_name = fixup_module_name ``` ### Suppress warnings To silence all warnings from this extension: ```python suppress_warnings = ["sphinx_autodoc_typehints"] ``` To suppress only specific warning types, see [Warning categories](#warning-categories) for the full list. ## Reference ### Configuration options | Option | Default | Description | | -------------------------------- | ------- | --------------------------------------------------------------------------------------------- | | `typehints_document_rtype` | `True` | Show the return type in docs. | | `typehints_document_rtype_none` | `True` | Show return type when it's `None`. | | `typehints_use_rtype` | `True` | Show return type as a separate block. When `False`, it's inlined with the return description. | | `always_use_bars_union` | `False` | Use `X \| Y` instead of `Union[X, Y]`. Always on for Python 3.14+. | | `simplify_optional_unions` | `True` | Flatten `Optional[Union[A, B]]` to `Union[A, B, None]`. | | `typehints_defaults` | `None` | Show default values: `"comma"`, `"braces"`, or `"braces-after"`. | | `typehints_use_signature` | `False` | Keep parameter types in the function signature. | | `typehints_use_signature_return` | `False` | Keep the return type in the function signature. | | `typehints_fully_qualified` | `False` | Show full module path for types (e.g., `module.Class` not `Class`). | | `always_document_param_types` | `False` | Add types even for parameters that don't have a `:param:` entry in the docstring. | | `typehints_formatter` | `None` | A function `(annotation, Config) -> str \| None` for custom type rendering. | | `typehints_fixup_module_name` | `None` | A function `(str) -> str` to rewrite module paths before generating cross-reference links. | ### Warning categories All warnings can be suppressed via Sphinx's [`suppress_warnings`](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-suppress_warnings) in `conf.py`: | Category | When it's raised | | --------------------------------------------- | --------------------------------------------------------------------------- | | `sphinx_autodoc_typehints` | Catch-all for every warning from this extension. | | `sphinx_autodoc_typehints.comment` | A type comment (`# type: ...`) couldn't be parsed. | | `sphinx_autodoc_typehints.forward_reference` | A forward reference (string annotation) couldn't be resolved. | | `sphinx_autodoc_typehints.guarded_import` | A type from a `TYPE_CHECKING` block couldn't be imported at runtime. | | `sphinx_autodoc_typehints.local_function` | A type annotation references a function defined inside another function. | | `sphinx_autodoc_typehints.multiple_ast_nodes` | A type comment matched multiple definitions and the right one is ambiguous. | ## Explanation ### How it works During the Sphinx build, this extension hooks into two [autodoc events](https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#event-autodoc-process-signature). First, it strips type annotations from function signatures (so they don't appear twice). Then, it reads the annotations and adds type information into the docstring -- parameter types go next to each `:param:` entry, and the return type becomes an `:rtype:` entry. Only parameters that already have a `:param:` line in the docstring get type information added. Set `always_document_param_types = True` to add types for all parameters, even undocumented ones. ### How return type options interact The return type options combine as follows: - **Both defaults** (`typehints_document_rtype = True`, `typehints_use_rtype = True`) -- return type appears as a separate `:rtype:` block below the description. - **Inline mode** (`typehints_document_rtype = True`, `typehints_use_rtype = False`) -- return type is appended to the `:return:` text. If there's no `:return:` entry, it falls back to a separate block. - **Disabled** (`typehints_document_rtype = False`) -- no return type shown, regardless of other settings. - **Skip None** (`typehints_document_rtype_none = False`) -- hides `None` return types specifically, other return types still appear.