sphinx_autodoc_typehints-3.12.0/.markdownlint.yaml0000644000000000000000000000015013615410400017322 0ustar00MD013: code_blocks: false headers: false line_length: 120 tables: false MD046: style: fenced sphinx_autodoc_typehints-3.12.0/.pre-commit-config.yaml0000644000000000000000000000244513615410400020141 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.37.3" hooks: - id: check-github-workflows args: ["--verbose"] - repo: https://github.com/codespell-project/codespell rev: v2.4.2 hooks: - id: codespell additional_dependencies: ["tomli>=2.4"] - repo: https://github.com/tox-dev/tox-toml-fmt rev: "v1.9.3" hooks: - id: tox-toml-fmt - repo: https://github.com/tox-dev/pyproject-fmt rev: "v2.25.0" hooks: - id: pyproject-fmt - repo: https://github.com/astral-sh/ruff-pre-commit rev: "v0.15.18" 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.4" # 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: https://github.com/zizmorcore/zizmor-pre-commit rev: v1.26.1 hooks: - id: zizmor - repo: meta hooks: - id: check-hooks-apply - id: check-useless-excludes sphinx_autodoc_typehints-3.12.0/ignore-words.txt0000644000000000000000000000001613615410400017030 0ustar00master manuel sphinx_autodoc_typehints-3.12.0/tox.toml0000644000000000000000000000421213615410400015361 0ustar00requires = [ "tox>=4.46.3", "tox-uv>=1.33", ] 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.12.0/whitelist.txt0000644000000000000000000000000013615410400016416 0ustar00sphinx_autodoc_typehints-3.12.0/.github/FUNDING.yaml0000644000000000000000000000002413615410400017165 0ustar00github: gaborbernat sphinx_autodoc_typehints-3.12.0/.github/SECURITY.md0000644000000000000000000000055513615410400017011 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.12.0/.github/dependabot.yaml0000644000000000000000000000023113615410400020200 0ustar00version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" cooldown: default-days: 7 sphinx_autodoc_typehints-3.12.0/.github/release.yaml0000644000000000000000000000012613615410400017516 0ustar00changelog: exclude: authors: - dependabot[bot] - pre-commit-ci[bot] sphinx_autodoc_typehints-3.12.0/.github/workflows/check.yaml0000644000000000000000000000311113615410400021205 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 permissions: contents: read 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 persist-credentials: false - name: Install the latest version of uv uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: enable-cache: false 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.12.0/.github/workflows/release.yaml0000644000000000000000000000305113615410400021553 0ustar00name: Release to PyPI on: push: tags: ["*"] permissions: contents: read env: dists-artifact-name: python-package-distributions jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 persist-credentials: false - name: Install the latest version of uv uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: enable-cache: false 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: ${{ env.dists-artifact-name }} path: dist/ - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: attestations: true sphinx_autodoc_typehints-3.12.0/src/sphinx_autodoc_typehints/__init__.py0000644000000000000000000005277513615410400023731 0ustar00"""Sphinx autodoc type hints.""" from __future__ import annotations import inspect import re 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, _strip_inline_param_type from ._intersphinx import build_type_mapping from ._parser import parse from ._resolver import ( backfill_attrs_annotations, backfill_type_hints, collect_documented_type_aliases, get_all_type_hints, get_descriptor_type_hint, get_instance_var_annotations, 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 try: has_annotations = getattr(obj, "__annotations__", None) except (NameError, TypeError, AttributeError): # PEP 649 (Python 3.14+): annotation expressions only run on access and may fail at runtime # (TYPE_CHECKING-only names, subscripting a non-generic class). __annotate__ still signals # annotations exist, and sphinx_signature renders them without evaluation via FORWARDREF. has_annotations = getattr(obj, "__annotate__", None) if not has_annotations: 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) and not inspect.ismethod(obj): 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): _maybe_inject_descriptor_type(app, what, obj, lines) return if inspect.isclass(obj): backfill_attrs_annotations(obj) ivar_annotations = get_instance_var_annotations(obj) else: ivar_annotations = {} use_class_for_signature = False cls_for_hints = None if inspect.isclass(obj): if obj.__init__ is object.__init__ and obj.__new__ is not object.__new__: use_class_for_signature = True cls_for_hints = obj obj = obj.__new__ else: obj = obj.__init__ try: obj = inspect.unwrap(obj) except ValueError: return try: signature = sphinx_signature( original_obj if use_class_for_signature else 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, cls_for_hints or 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) _inject_ivar_types(ivar_annotations, app, lines) finally: del app.config._annotation_globals # noqa: SLF001 del app.config._typehints_env # noqa: SLF001 del app.config._typehints_module_prefix # noqa: SLF001 def _maybe_inject_descriptor_type(app: Sphinx, what: str, obj: Any, lines: list[str]) -> None: """C data descriptors document as plain attributes; lift their type from the stub.""" if what != "attribute" or not (inspect.isgetsetdescriptor(obj) or inspect.ismemberdescriptor(obj)): return if any(line.startswith(":type:") for line in lines): return if (hint := get_descriptor_type_hint(obj)) is None: return formatted = add_type_css_class( format_annotation(hint, app.config, short_literals=app.config.python_display_short_literal_types) ) lines.extend(["", f":type: {formatted}"]) def _inject_overload_signatures( app: Sphinx, what: str, name: str, # noqa: ARG001 obj: Any, lines: list[str], ) -> bool: if what not in {"function", "method"} or not app.config.typehints_document_overloads: return False if _strip_no_overloads_directive(lines): return False if (overloads := _resolve_overloads(obj)) is None: return False for line in reversed(_format_overload_lines(overloads, app)): lines.insert(0, line) return True def _strip_no_overloads_directive(lines: list[str]) -> bool: for idx, line in enumerate(lines): if line.strip() == ":no-overloads:": del lines[idx] return True return False def _resolve_overloads(obj: Any) -> list[inspect.Signature] | None: module_name = getattr(obj, "__module__", None) if not module_name or module_name not in _OVERLOADS_CACHE: return None qualname = getattr(obj, "__qualname__", None) if not qualname: return None return _OVERLOADS_CACHE[module_name].get(qualname) or None def _format_overload_lines(overloads: list[inspect.Signature], app: Sphinx) -> list[str]: short_literals = app.config.python_display_short_literal_types result = [":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}" result.append(f" * {', '.join(params)}{return_annotation}") result.append("") return result def format_default(app: Sphinx, default: Any, is_annotated: bool) -> str | None: # noqa: FBT001 if default is inspect.Parameter.empty: return None formatted = repr(default).replace("\\", "\\\\") if is_annotated: if app.config.typehints_defaults.startswith("braces"): return f" (default: ``{formatted}``)" return f", default: ``{formatted}``" if app.config.typehints_defaults == "braces-after": return f" (default: ``{formatted}``)" return f"default: ``{formatted}``" def _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, insert_index = _find_trailing_underscore_param(lines, arg_name, fmt) else: insert_index = fmt.find_param(lines, arg_name) if ( insert_index is not None and annotation is not None and (stripped := _strip_inline_param_type(lines[insert_index], arg_name)) ): lines[insert_index] = stripped 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 None: return has_preexisting = False if annotation is None: type_annotation, has_preexisting = fmt.find_preexisting_type(lines, arg_name) else: preexisting_line, has_preexisting = fmt.find_preexisting_type(lines, arg_name) if has_preexisting: insert_index = _remove_preexisting_type(lines, preexisting_line) type_annotation = ":type {}: {}".format( arg_name, add_type_css_class( format_annotation(annotation, app.config, short_literals=app.config.python_display_short_literal_types) ), ) if app.config.typehints_defaults and ( formatted_default := format_default(app, default, annotation is not None or has_preexisting) ): type_annotation = fmt.append_default( lines, insert_index, type_annotation, formatted_default, after=app.config.typehints_defaults.endswith("after"), ) lines.insert(insert_index, type_annotation) def _find_trailing_underscore_param(lines: list[str], arg_name: str, fmt: Any) -> tuple[str, int | None]: escaped_name = f"{arg_name[:-1]}\\_" if (insert_index := fmt.find_param(lines, escaped_name)) is not None: return escaped_name, insert_index if (insert_index := fmt.find_param(lines, arg_name)) is None: return escaped_name, None # napoleon only escapes the trailing underscore when strip_signature_backslash is on, and docutils # swallows an unescaped one as reference markup — see issue #708 rewritten = lines[insert_index].replace(f" {arg_name}:", f" {escaped_name}:", 1) if rewritten == lines[insert_index]: return arg_name, insert_index lines[insert_index] = rewritten return escaped_name, insert_index def _remove_preexisting_type(lines: list[str], preexisting_line: str) -> int: idx = lines.index(preexisting_line) end = idx + 1 while end < len(lines) and lines[end] and lines[end][0].isspace(): end += 1 del lines[idx:end] return idx 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) # Matches a well-formed Sphinx ``:ivar ...:`` field list entry. Supports both # ``:ivar name:`` and the inline-typed ``:ivar name:`` grammar. The # optional type is lazy so the final whitespace-delimited token before the # closing colon is always captured as the attribute name. Anything that does # not match (empty name, missing closing colon, bare ``:ivar :``) is skipped # rather than crashing the parser — see issue #683. _IVAR_FIELD_RE = re.compile( r""" ^:ivar # literal field marker (?:\s+(?P\S.*?))? # optional inline type expression \s+(?P\w+) # attribute name (Python identifier) \s*: # closing colon of the field name """, re.VERBOSE, ) def _inject_ivar_types(ivar_annotations: dict[str, Any], app: Sphinx, lines: list[str]) -> None: line_index = 0 while line_index < len(lines): match = _IVAR_FIELD_RE.match(lines[line_index]) if match is None: line_index += 1 continue attr_name = match["name"] has_inline_type = match["type"] is not None has_vartype = any(existing.startswith(f":vartype {attr_name}:") for existing in lines) if has_inline_type or has_vartype or attr_name not in ivar_annotations: line_index += 1 continue formatted = add_type_css_class( format_annotation( ivar_annotations[attr_name], app.config, short_literals=app.config.python_display_short_literal_types, ) ) lines.insert(line_index, f":vartype {attr_name}: {formatted}") line_index += 2 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) app.config._intersphinx_type_mapping = build_type_mapping(env) # noqa: SLF001 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 | str]: 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, "") app.add_config_value("typehints_document_overloads", True, "env") # noqa: FBT003 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 { "version": __version__, "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.12.0/src/sphinx_autodoc_typehints/_annotations.py0000644000000000000000000004144713615410400024660 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, Generator from sphinx.config import Config from typing import TypeAliasType _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"), *("FrameType", "FunctionType"), *("GeneratorType", "GetSetDescriptorType"), "LambdaType", *("MemberDescriptorType", "MethodDescriptorType", "MethodType", "MethodWrapperType"), "NoneType", "WrapperDescriptorType", # documented as ``py:class`` since the Python 3.13 docs *({"EllipsisType", "NotImplementedType"} if sys.version_info < (3, 13) else set()), } _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): crossref: bool = False 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: """ Format the annotation into reStructuredText cross-reference markup. The annotation is a tree (``dict[str, list[int]]`` nests three levels), so formatting it is a post-order walk. The walk is driven iteratively with an explicit stack rather than by recursion: a PEP 695 type alias may reference itself (``type T = int | list[T]``), which is a fully supported recursive type but would expand forever and exhaust the interpreter recursion limit if walked naively. Each frame on the stack is one suspended ``_format_node`` generator; the generator yields the child annotations it needs formatted and is resumed with their rendered strings. ``active`` holds the aliases currently being expanded along the path from the root, so when an alias references itself it is rendered as a cross-reference to its own name -- the natural representation of a recursive type -- instead of being expanded again. """ fully_qualified: bool = getattr(config, "typehints_fully_qualified", False) cycle_prefix = "" if fully_qualified else "~" stack = [(_format_node(annotation, config, short_literals=short_literals), annotation)] active = {id(annotation)} if isinstance(annotation, TypeAliasType) else set() rendered: str | None = None while True: gen, node = stack[-1] try: child = next(gen) if rendered is None else gen.send(rendered) except StopIteration as done: stack.pop() if isinstance(node, TypeAliasType): active.discard(id(node)) rendered = done.value if not stack: return rendered continue if isinstance(child, TypeAliasType) and id(child) in active: rendered = f":py:type:`{cycle_prefix}{child.__name__}`" continue stack.append((_format_node(child, config, short_literals=short_literals), child)) if isinstance(child, TypeAliasType): active.add(id(child)) rendered = None def _format_node( # noqa: C901, PLR0911, PLR0912, PLR0915, PLR0914 annotation: Any, config: Config, *, short_literals: bool, ) -> Generator[Any, str, str]: """ Format a single annotation node, delegating nested annotations to :func:`format_annotation`. Every nested annotation is ``yield``-ed and the driver sends back its formatted string, so the recursion lives in the driver's explicit stack rather than on the Python call stack. """ 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): parts = [] for item in annotation: parts.append((yield item)) # noqa: PERF401 # yield is illegal inside a comprehension if not parts: return "()" if len(parts) == 1: return f"({parts[0]}, )" return f"({', '.join(parts)})" if isinstance(annotation, TypeAliasForwardRef): fully_qualified: bool = getattr(config, "typehints_fully_qualified", False) prefix = "" if fully_qualified else "~" 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": return f":py:type:`{prefix}{candidate}`" if isinstance(annotation, MyTypeAliasForwardRef) and annotation.crossref: return f":py:type:`{prefix}{annotation.name}`" return annotation.name if isinstance(annotation, TypeAliasType): fully_qualified = getattr(config, "typehints_fully_qualified", False) prefix = "" if fully_qualified else "~" if (env := getattr(config, "_typehints_env", None)) is not None: py_domain = env.get_domain("py") module_prefix = getattr(config, "_typehints_module_prefix", "") prefix_parts = module_prefix.split(".") if module_prefix else [] # Walk up the module prefix to find a matching type in the py domain candidates = [ f"{'.'.join(prefix_parts[:n])}.{annotation.__name__}" for n in range(len(prefix_parts), 0, -1) ] candidates.append(annotation.__name__) for candidate in candidates: if candidate in py_domain.objects and py_domain.objects[candidate].objtype == "type": return f":py:type:`{prefix}{candidate}`" # Handle external type aliases canonical = _get_canonical_type_alias_name(annotation) current_top = module_prefix.split(".")[0] if module_prefix else "" if canonical and canonical.split(".")[0] != current_top: full_name = _fixup_module_name(config, canonical.rpartition(".")[0]) + "." + annotation.__name__ return f":py:obj:`{prefix}{full_name}`" return (yield annotation.__value__) 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) internal_path = f"{module}.{class_name}" if (mapping := getattr(config, "_intersphinx_type_mapping", None)) and (mapped := mapping.get(internal_path)): module, _, class_name = mapped.rpartition(".") full_name = f"{module}.{class_name}" if module != "builtins" else class_name fully_qualified = 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 = yield annotation.__supertype__ return f":py:class:`{newtype_prefix}{newtype_qualified}` ({supertype})" if full_name == "typing.Annotated": return (yield annotation.__origin__) 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: bound = yield params["bound"] params["bound"] = f" {bound}" 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 = [] for arg in args: fmt.append((yield arg)) # noqa: PERF401 # yield is illegal inside a comprehension 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`" union_parts = [] for arg in args: union_parts.append((yield arg)) # noqa: PERF401 # yield is illegal inside a comprehension return " | ".join(union_parts) if args and not formatted_args: fmt = [] for arg in args: fmt.append((yield arg)) formatted_args = args_format.format(", ".join(fmt)) escape = "\\ " if formatted_args else "" # B901: returning a value from a generator is intentional here — the driver reads it via StopIteration return f":py:{role}:`{prefix}{full_name}`{escape}{formatted_args}" # noqa: B901 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 _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 _get_canonical_type_alias_name(annotation: TypeAliasType) -> str: """ Get canonical public qualified name for a TypeAliasType. For types defined in private modules (e.g. ``numpy._typing.ArrayLike``), search ``sys.modules`` for a public re-export (e.g. ``numpy.typing.ArrayLike``). """ module = getattr(annotation, "__module__", "") or "" name = getattr(annotation, "__name__", "") or "" if not module or not name: return "" if not any(part.startswith("_") for part in module.split(".")): return f"{module}.{name}" top_pkg = module.split(".")[0] for mod_name in sorted(sys.modules): if not mod_name.startswith(top_pkg): continue mod = sys.modules[mod_name] if not isinstance(mod, types.ModuleType): continue if any(part.startswith("_") for part in mod_name.split(".")): continue if getattr(mod, name, None) is annotation: return f"{mod_name}.{name}" return f"{module}.{name}" 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.12.0/src/sphinx_autodoc_typehints/_intersphinx.py0000644000000000000000000000432713615410400024672 0ustar00"""Build a reverse mapping from runtime type paths to intersphinx-documented paths.""" from __future__ import annotations import importlib import types as _types from typing import TYPE_CHECKING if TYPE_CHECKING: from sphinx.environment import BuildEnvironment _ROLES = frozenset({"py:class", "py:function", "py:exception", "py:data"}) def build_type_mapping(env: BuildEnvironment) -> dict[str, str]: """Map internal ``__module__.__qualname__`` paths to their documented intersphinx names.""" if not hasattr(env, "intersphinx_inventory"): return {} inventory_data: dict[str, dict[str, object]] = env.intersphinx_inventory # ty: ignore[invalid-assignment] all_documented: set[str] = set() candidates: list[tuple[str, str]] = [] for role in _ROLES: if (entries := inventory_data.get(role)) is None: continue for documented_name in entries: all_documented.add(documented_name) mod_path, _, attr_name = documented_name.rpartition(".") if not mod_path: all_documented.add(f"builtins.{documented_name}") continue try: mod = importlib.import_module(mod_path) obj = getattr(mod, attr_name) # Union instances borrow __module__/__qualname__ from their type class, # so every X|Y alias resolves to the same "types.UnionType"/"typing.Union" # path and would corrupt all union annotations in the rendered docs. if isinstance(obj, _types.UnionType): continue internal_path = f"{obj.__module__}.{obj.__qualname__}" except Exception: # noqa: BLE001, S112 continue # typing.Union[X, Y] aliases on Python <3.14 are not types.UnionType instances # but still resolve to "typing.Union"; guard against those and any edge cases. if internal_path in {"types.UnionType", "typing.Union"}: continue if internal_path != documented_name: candidates.append((internal_path, documented_name)) return {internal: documented for internal, documented in candidates if internal not in all_documented} sphinx_autodoc_typehints-3.12.0/src/sphinx_autodoc_typehints/_parser.py0000644000000000000000000000171713615410400023613 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.12.0/src/sphinx_autodoc_typehints/attributes_patch.py0000644000000000000000000000414713615410400025525 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.12.0/src/sphinx_autodoc_typehints/patches.py0000644000000000000000000001214013615410400023577 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.12.0/src/sphinx_autodoc_typehints/py.typed0000644000000000000000000000000013615410400023265 0ustar00sphinx_autodoc_typehints-3.12.0/src/sphinx_autodoc_typehints/version.py0000644000000000000000000000101213615410400023631 0ustar00# file generated by vcs-versioning # don't change, don't track in version control from __future__ import annotations __all__ = [ "__version__", "__version_tuple__", "version", "version_tuple", "__commit_id__", "commit_id", ] version: str __version__: str __version_tuple__: tuple[int | str, ...] version_tuple: tuple[int | str, ...] commit_id: str | None __commit_id__: str | None __version__ = version = '3.12.0' __version_tuple__ = version_tuple = (3, 12, 0) __commit_id__ = commit_id = None sphinx_autodoc_typehints-3.12.0/src/sphinx_autodoc_typehints/_formats/__init__.py0000644000000000000000000000161213615410400025523 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.12.0/src/sphinx_autodoc_typehints/_formats/_base.py0000644000000000000000000000406613615410400025043 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.12.0/src/sphinx_autodoc_typehints/_formats/_numpydoc.py0000644000000000000000000001552413615410400025770 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.12.0/src/sphinx_autodoc_typehints/_formats/_sphinx.py0000644000000000000000000002220313615410400025433 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 if any(doc_name == prefix + arg_name for prefix in ("", "\\*", "\\**", "\\*\\*")): return True # Match `:param type name:` format where inline type precedes the argument name. return any(doc_name.endswith(f" {prefix}{arg_name}") for prefix in ("", "\\*", "\\**", "\\*\\*")) def _strip_inline_param_type(line: str, arg_name: str) -> str | None: parts = line.split(":", maxsplit=2) if len(parts) != 3: # noqa: PLR2004 return None directive_and_name = parts[1] for prefix in ("", "\\*", "\\**", "\\*\\*"): suffix = f" {prefix}{arg_name}" if directive_and_name.endswith(suffix): keyword = directive_and_name.split(maxsplit=1)[0] return f":{keyword} {prefix}{arg_name}:{parts[2]}" return None 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 (at := next((i for i, line in enumerate(lines) if line.startswith(":rtype:")), None)) is not None: del lines[at] return InsertIndexInfo(insert_index=at, found_return=True) 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.12.0/src/sphinx_autodoc_typehints/_resolver/__init__.py0000644000000000000000000000116513615410400025714 0ustar00"""Type hint resolution and backfilling from source annotations.""" from __future__ import annotations from ._attrs import backfill_attrs_annotations from ._instance_vars import get_instance_var_annotations from ._type_comments import backfill_type_hints from ._type_hints import get_all_type_hints, get_descriptor_type_hint 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_descriptor_type_hint", "get_instance_var_annotations", "get_obj_location", ] sphinx_autodoc_typehints-3.12.0/src/sphinx_autodoc_typehints/_resolver/_attrs.py0000644000000000000000000000117213615410400025447 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.12.0/src/sphinx_autodoc_typehints/_resolver/_instance_vars.py0000644000000000000000000000351713615410400027156 0ustar00from __future__ import annotations import ast import inspect import textwrap from typing import Any def get_instance_var_annotations(cls: type) -> dict[str, Any]: """ Extract instance variable annotations from ``__init__`` by walking its AST. ``self.x: T = ...`` is not stored in ``cls.__annotations__`` at runtime, so ``typing.get_type_hints`` cannot see it — AST inspection is the only option. Only top-level statements of ``__init__`` are scanned to avoid picking up annotations from nested functions. """ init = cls.__init__ if init is object.__init__: return {} try: source = textwrap.dedent(inspect.getsource(init)) tree = ast.parse(source) except (OSError, TypeError, SyntaxError, IndentationError): return {} raw: dict[str, str] = {} for node in ast.walk(tree): if isinstance(node, ast.FunctionDef) and node.name == "__init__": self_name = node.args.args[0].arg if node.args.args else "self" for stmt in node.body: if ( isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Attribute) and isinstance(stmt.target.value, ast.Name) and stmt.target.value.id == self_name ): raw[stmt.target.attr] = ast.unparse(stmt.annotation) break if not raw: return {} module = inspect.getmodule(cls) globalns: dict[str, Any] = vars(module) if module is not None else {} resolved: dict[str, Any] = {} for attr_name, ann_str in raw.items(): try: resolved[attr_name] = eval(ann_str, globalns) # noqa: S307 except Exception: # noqa: BLE001 resolved[attr_name] = ann_str return resolved __all__ = ["get_instance_var_annotations"] sphinx_autodoc_typehints-3.12.0/src/sphinx_autodoc_typehints/_resolver/_stubs.py0000644000000000000000000002711013615410400025452 0ustar00"""Stub file (.pyi) annotation backfill.""" from __future__ import annotations import ast import contextlib import importlib import inspect import sys from copy import copy from pathlib import Path from typing import TYPE_CHECKING, Any from ._type_comments import _load_args if TYPE_CHECKING: import types _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 _get_stub_context(obj: Any) -> tuple[dict[str, Any], set[str], str]: """ Return (localns, alias_names, owner_module_name) from the stub owning *obj*. Single lookup avoids duplicate ``_find_stub_owner`` / ``_parse_stub_ast`` calls. The owner module name lets callers use ``vars(sys.modules[name])`` as the correct globalns — important when a C extension function lives in a child module but its stub belongs to a parent package (e.g. ``cbor2._cbor2.dumps`` → ``cbor2/__init__.pyi``). """ if (info := _find_stub_owner(obj)) is None: return {}, set(), "" stub_path, owner_module = info if (tree := _parse_stub_ast(stub_path)) is None: return {}, set(), "" ns: dict[str, Any] = dict(vars(owner_module)) ns.update(_resolve_stub_imports(tree, _stub_owner_package(owner_module, stub_path))) return ns, _extract_type_alias_names(tree), owner_module.__name__ def _stub_owner_package(owner_module: types.ModuleType, stub_path: Path) -> str: # Native submodules expose ``__package__ = None``; derive the package from the stub's name # so relative imports inside ``.pyi`` resolve relative to its parent package. if owner_package := getattr(owner_module, "__package__", None): return owner_package owner_name = owner_module.__name__ if stub_path.name == "__init__.pyi": return owner_name return owner_name.rpartition(".")[0] def _resolve_stub_imports(tree: ast.Module, owner_package: str = "") -> dict[str, Any]: ns: dict[str, Any] = {} _resolve_stub_imports_from_body(tree.body, owner_package, ns) _resolve_stub_definitions(tree.body, ns) return ns def _resolve_stub_imports_from_body(body: list[ast.stmt], owner_package: str, ns: dict[str, Any]) -> None: for node in body: if isinstance(node, ast.Import): _resolve_import_node(node, ns) elif isinstance(node, ast.ImportFrom): _resolve_import_from_node(node, owner_package, ns) elif isinstance(node, ast.If): _resolve_stub_imports_from_body(node.body, owner_package, ns) _resolve_stub_imports_from_body(node.orelse, owner_package, ns) _STUB_DEFINITION_TYPES = (ast.Assign, ast.AnnAssign, ast.ClassDef, ast.TypeAlias) def _resolve_stub_definitions(body: list[ast.stmt], ns: dict[str, Any]) -> None: for node in body: if isinstance(node, _STUB_DEFINITION_TYPES): exec_node = _strip_class_decorators(node) if isinstance(node, ast.ClassDef) else node with contextlib.suppress(Exception): exec(compile(ast.Module(body=[exec_node], type_ignores=[]), "", "exec"), ns) # noqa: S102 elif isinstance(node, ast.If): _resolve_stub_definitions(node.body, ns) _resolve_stub_definitions(node.orelse, ns) def _strip_class_decorators(node: ast.ClassDef) -> ast.ClassDef: if not node.decorator_list: return node stripped = copy(node) stripped.decorator_list = [] return stripped def _resolve_import_node(node: ast.Import, ns: dict[str, Any]) -> None: for alias in node.names: with contextlib.suppress(ImportError): if alias.asname: ns[alias.asname] = importlib.import_module(alias.name) else: top = alias.name.split(".")[0] ns[top] = importlib.import_module(top) def _resolve_import_from_node(node: ast.ImportFrom, owner_package: str, ns: dict[str, Any]) -> None: if (module_name := _resolve_import_from(node, owner_package)) is None: return try: mod = importlib.import_module(module_name) except ImportError: return for alias in node.names: if alias.name == "*": continue name = alias.asname or alias.name if (val := getattr(mod, alias.name, None)) is not None: ns[name] = val def _resolve_import_from(node: ast.ImportFrom, owner_package: str) -> str | None: if not node.level: return node.module or None if not owner_package: return node.module or None parts = owner_package.rsplit(".", node.level - 1) if len(parts) < node.level: return None base = parts[0] return f"{base}.{node.module}" if node.module else base def _get_module(obj: Any) -> types.ModuleType | None: if module := inspect.getmodule(obj): return module # inspect.getmodule returns None for some C/Rust extension descriptors even when __module__ is valid. if (mod_name := getattr(obj, "__module__", None)) and (module := sys.modules.get(mod_name)): return module # Method descriptors on C extension types expose the owning class via __objclass__. if (owner_cls := getattr(obj, "__objclass__", None)) and (module := inspect.getmodule(owner_cls)): return module # Bound methods like __new__ on C extension classes have __self__ pointing to the class. if (owner_cls := getattr(obj, "__self__", None)) and inspect.isclass(owner_cls): return inspect.getmodule(owner_cls) return None def _find_stub_path(obj: Any) -> Path | None: if info := _find_stub_owner(obj): return info[0] return None def _find_stub_owner(obj: Any) -> tuple[Path, types.ModuleType] | None: if (module := _get_module(obj)) is None: return None if result := _find_stub_for_module(module): return result, module # PEP 561 re-export pattern: cbor2.__init__.pyi documents functions living in cbor2._cbor2 (C/Rust). Walk up to # find the parent package whose stub describes the re-exported public API. module_name = module.__name__ while "." in module_name: module_name = module_name.rsplit(".", 1)[0] if (parent := sys.modules.get(module_name)) and (result := _find_stub_for_module(parent)): return result, parent return None def _find_stub_for_module(module: types.ModuleType) -> Path | None: try: source_file = inspect.getfile(module) except TypeError: return _find_native_submodule_stub(module) source = Path(source_file) if (stub := source.with_name(f"{source.name.split('.')[0]}.pyi")).is_file(): return stub if hasattr(module, "__path__"): for pkg_dir in module.__path__: if (init_stub := Path(pkg_dir) / "__init__.pyi").is_file(): return init_stub return None def _find_native_submodule_stub(module: types.ModuleType) -> Path | None: """Locate the sibling ``.pyi`` for a PyO3-style submodule registered via ``PyModule_AddObject``.""" name = getattr(module, "__name__", "") parent_name, _, child = name.rpartition(".") if not parent_name or (parent := sys.modules.get(parent_name)) is None: return None for pkg_dir in getattr(parent, "__path__", []) or []: pkg_path = Path(pkg_dir) if (stub := pkg_path / f"{child}.pyi").is_file(): return stub if (init_stub := pkg_path / child / "__init__.pyi").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] _TYPE_ALIAS_ANNOTATIONS = {"TypeAlias", "typing.TypeAlias", "typing_extensions.TypeAlias"} def _extract_type_alias_names(tree: ast.Module) -> set[str]: names: set[str] = set() for node in tree.body: if ( isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and isinstance(node.annotation, ast.Name | ast.Attribute) and ast.unparse(node.annotation) in _TYPE_ALIAS_ANNOTATIONS ): names.add(node.target.id) elif isinstance(node, ast.TypeAlias) and isinstance(node.name, ast.Name): names.add(node.name.id) return names 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 _backfill_descriptor_annotation(obj: Any) -> str | None: """ Return the stub annotation string for a C data descriptor, or ``None``. getset and member descriptors carry no runtime annotations, so their type can only come from the owning class's stub: either a ``@property`` return annotation or a class-level annotated assignment with the same name. """ objclass = getattr(obj, "__objclass__", None) name = getattr(obj, "__name__", None) if objclass is None or not name: return None if (stub_path := _find_stub_path(obj)) is None or (tree := _parse_stub_ast(stub_path)) is None: return None node = _find_ast_node(tree.body, objclass.__qualname__.split(".")) if not isinstance(node, ast.ClassDef): return None members: list[ast.stmt] = list(node.body) while members: member = members.pop() if isinstance(member, ast.If): # stubs guard members behind `if sys.version_info >= ...` blocks (numpy's ndarray does) members.extend(member.body) members.extend(member.orelse) continue if isinstance(member, ast.AnnAssign) and isinstance(member.target, ast.Name) and member.target.id == name: return ast.unparse(member.annotation) if not isinstance(member, ast.FunctionDef) or member.name != name or member.returns is None: continue if any(isinstance(decorator, ast.Name) and decorator.id == "property" for decorator in member.decorator_list): return ast.unparse(member.returns) return None 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.12.0/src/sphinx_autodoc_typehints/_resolver/_type_comments.py0000644000000000000000000001133413615410400027201 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.12.0/src/sphinx_autodoc_typehints/_resolver/_type_hints.py0000644000000000000000000002404513615410400026504 0ustar00"""Type hint resolution with TYPE_CHECKING guard handling.""" from __future__ import annotations import contextlib import importlib import inspect import re import sys import textwrap import types from typing import Any, get_type_hints from sphinx.ext.autodoc.mock import mock from sphinx.util import logging from sphinx_autodoc_typehints._annotations import MyTypeAliasForwardRef from ._stubs import _backfill_descriptor_annotation, _backfill_from_stub, _get_stub_context from ._type_comments import backfill_type_hints from ._util import get_obj_location 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: stub_obj = _stub_target(obj) if inspect.isclass(obj) else obj result = backfill_type_hints(stub_obj, name) stub_localns: dict[str, Any] = {} stub_alias_names: set[str] = set() stub_owner_module: str = "" if not result: result = _backfill_from_stub(stub_obj) if result: stub_localns, stub_alias_names, stub_owner_module = _get_stub_context(stub_obj) combined_localns = {**stub_localns, **localns} for alias_name in stub_alias_names: ref = MyTypeAliasForwardRef(alias_name) ref.crossref = True combined_localns[alias_name] = ref try: obj.__annotations__ = result except (AttributeError, TypeError): result = _resolve_string_annotations(obj, result, combined_localns, stub_owner_module) else: result = _get_type_hint(autodoc_mock_imports, name, obj, combined_localns) with contextlib.suppress(AttributeError, TypeError): obj.__annotations__ = result return result def _stub_target(cls: type) -> Any: """Return the constructor method for stub/type-comment backfill when *cls* is a class.""" if cls.__init__ is not object.__init__: return cls.__init__ if cls.__new__ is not object.__new__: return cls.__new__ return cls def get_descriptor_type_hint(obj: Any) -> Any | None: """ Resolve the documented type of a C data descriptor from its stub, or ``None``. The signature-driven backfill never sees these objects because they are not callable; the annotation string from the stub is evaluated in the stub's namespace so aliases and forward references resolve the same way they do for function signatures. """ if (annotation := _backfill_descriptor_annotation(obj)) is None: return None localns, alias_names, owner_module = _get_stub_context(obj) for alias_name in alias_names: ref = MyTypeAliasForwardRef(alias_name) ref.crossref = True localns[alias_name] = ref return _resolve_string_annotations(obj, {"return": annotation}, localns, owner_module)["return"] def _resolve_string_annotations( obj: Any, annotations: dict[str, str], localns: dict[str, Any], owner_module: str = "" ) -> dict[str, Any]: # Use the stub owner module's namespace when available — the obj's __module__ may point at a C extension child # (e.g. cbor2._cbor2) while the stub lives in the parent (cbor2/__init__.pyi). module_name = owner_module or getattr(obj, "__module__", None) globalns = vars(sys.modules[module_name]) if module_name and module_name in sys.modules else {} resolved: dict[str, Any] = {} for key, value in annotations.items(): if isinstance(value, str): try: resolved[key] = eval(value, globalns, localns) # noqa: S307 except Exception: # noqa: BLE001 _LOGGER.debug( "Failed to resolve annotation %r=%r for %s", key, value, getattr(obj, "__qualname__", "?"), ) resolved[key] = value else: resolved[key] = value return resolved 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 = _build_localns(obj, localns) try: if getattr(obj, "__no_type_check__", False): # typing.get_type_hints() unconditionally returns {} for @typing.no_type_check # targets; inspect.get_annotations() has no such guard, so use it to still surface # annotations in the rendered docs (see issue #680). result = inspect.get_annotations(obj, locals=localns, eval_str=True) else: 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__ elif isinstance(exc, TypeError) and sys.version_info >= (3, 14): # pragma: >=3.14 cover result = _get_forward_ref_annotations(obj) 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 _get_forward_ref_annotations(obj: Any) -> dict[str, Any]: # pragma: >=3.14 cover # ForwardRef proxies keep unevaluatable annotations renderable as their source text — see #712 try: return annotationlib.get_annotations(obj, format=annotationlib.Format.FORWARDREF) except (NameError, TypeError, AttributeError, RecursionError): return {} 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 _build_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}} parts = (getattr(obj, "__qualname__", "") or "").split(".") if len(parts) <= 1: return localns if ns := (vars(module) if (module := inspect.getmodule(obj)) else getattr(obj, "__globals__", None)): current: Any = None for part in parts[:-1]: current = ( (ns if current is None else vars(current)).get(part) if current is None or hasattr(current, "__dict__") else None ) if current is None: break if inspect.isclass(current): localns = {**localns, part: current} if ancestor_params := getattr(current, "__type_params__", None): localns = {**localns, **{p.__name__: p for p in ancestor_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.12.0/src/sphinx_autodoc_typehints/_resolver/_util.py0000644000000000000000000000574713615410400025303 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]]: py_objects = env.get_domain("py").objects # ty: ignore[unresolved-attribute] deferred = _collect_module_type_aliases(module_prefix, py_objects) eager: dict[int, MyTypeAliasForwardRef] = {} # PEP 649 (Python 3.14+): annotation expressions only run on access and may fail at runtime # (TYPE_CHECKING-only names, subscripting a non-generic class) try: raw_annotations = getattr(obj, "__annotations__", {}) except (NameError, TypeError, AttributeError): return deferred, eager if not raw_annotations: return deferred, eager obj_globals = getattr(obj, "__globals__", {}) for annotation in raw_annotations.values(): if isinstance(annotation, str): if _is_documented_type(annotation, module_prefix, py_objects): ref = MyTypeAliasForwardRef(annotation) ref.crossref = True deferred[annotation] = ref 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) ): ref = MyTypeAliasForwardRef(var_name) ref.crossref = True eager[id(annotation)] = ref return deferred, eager def _collect_module_type_aliases(module_prefix: str, py_objects: dict[str, Any]) -> dict[str, MyTypeAliasForwardRef]: prefix_dot = f"{module_prefix}." result: dict[str, MyTypeAliasForwardRef] = {} for fqn, obj_info in py_objects.items(): if obj_info.objtype == "type" and (fqn.startswith(prefix_dot) or fqn == module_prefix): short_name = fqn.rsplit(".", 1)[-1] ref = MyTypeAliasForwardRef(short_name) ref.crossref = True result[short_name] = ref return result 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.12.0/tests/conftest.py0000644000000000000000000001111013615410400017206 0ustar00from __future__ import annotations import re import shutil import sys from contextlib import suppress from pathlib import Path from textwrap import dedent from typing import TYPE_CHECKING from unittest.mock import create_autospec, patch import pytest from sphinx.application import Sphinx from sphinx.config import Config from sphobjinv import Inventory if TYPE_CHECKING: from collections.abc import Callable, Iterator from sphinx.testing.util import SphinxTestApp 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) def make_sig_app(**overrides: object) -> Sphinx: defaults: dict[str, object] = { "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: object) -> Sphinx: defaults: dict[str, object] = { "autodoc_type_aliases": {}, "autodoc_mock_imports": [], "typehints_fully_qualified": False, "simplify_optional_unions": False, "typehints_formatter": None, "typehints_document_rtype": True, "typehints_document_rtype_none": True, "typehints_use_rtype": True, "typehints_defaults": None, "always_document_param_types": False, "python_display_short_literal_types": False, "typehints_document_overloads": True, } defaults.update(overrides) config = create_autospec(Config, **defaults) # ty: ignore[invalid-argument-type] config.__getitem__.side_effect = lambda key: getattr(config, key) app = create_autospec(Sphinx, config=config) app.env = None return app @pytest.fixture(autouse=True, scope="session") # noqa: RUF076 def _test_python_path() -> Iterator[None]: test_path = str(Path(__file__).parent) sys.path.insert(0, test_path) yield sys.path.remove(test_path) @pytest.fixture(scope="session") def wide_text_output() -> Iterator[None]: with patch("sphinx.writers.text.MAXWIDTH", 2000): yield @pytest.fixture(scope="session") def inv(pytestconfig: pytest.Config) -> Inventory: cache_path = f"python{sys.version_info.major}.{sys.version_info.minor}/objects.inv" assert pytestconfig.cache is not None if ( inv_dict := pytestconfig.cache.get(cache_path, None) ) is None: # pragma: no cover -- network fetch, CI has cache url = f"https://docs.python.org/{sys.version_info.major}.{sys.version_info.minor}/objects.inv" inv_dict = Inventory(url=url).json_dict() # ty: ignore[unknown-argument] pytestconfig.cache.set(cache_path, inv_dict) return Inventory(inv_dict) # ty: ignore[too-many-positional-arguments] @pytest.fixture(autouse=True) # noqa: RUF076 def _remove_sphinx_projects(request: pytest.FixtureRequest, sphinx_test_tempdir: Path) -> None: if not request.node.get_closest_marker("sphinx"): return 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" @pytest.fixture def write_rst(app: SphinxTestApp) -> Callable[[str], None]: def _write(content: str) -> None: for rst_file in Path(app.srcdir).glob("*.rst"): rst_file.unlink() (Path(app.srcdir) / "index.rst").write_text(dedent(content)) return _write def pytest_ignore_collect(collection_path: Path, config: pytest.Config) -> bool | None: # noqa: ARG001 version_re = re.compile(r"_py(\d)(\d)\.py$") if match := version_re.search(collection_path.name): 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.12.0/tests/test_annotated_doc.py0000644000000000000000000000662513615410400021241 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.12.0/tests/test_annotations.py0000644000000000000000000006642613615410400021001 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, TypeAliasType, TypeVar, Union, ) from unittest.mock import MagicMock, create_autospec import pytest import typing_extensions from sphinx.config import Config from sphinx.util.inspect import TypeAliasForwardRef from sphinx_autodoc_typehints import ( format_annotation, get_annotation_args, get_annotation_class_name, get_annotation_module, ) from sphinx_autodoc_typehints._annotations import MyTypeAliasForwardRef, _get_canonical_type_alias_name 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: """Get the type.""" 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, f":py:{'class' if sys.version_info >= (3, 13) else '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, f":py:{'class' if sys.version_info >= (3, 13) else '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 == "function": # pragma: no cover -- Python docs inventory always uses "func" 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 -- all tested annotations exist in both libs on 3.12+ 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,) def test_get_canonical_type_alias_name_public_module(monkeypatch: pytest.MonkeyPatch) -> None: pub = types.ModuleType("mypkg") exec("type MyAlias = str | int", pub.__dict__) # noqa: S102 alias: TypeAliasType = pub.__dict__["MyAlias"] monkeypatch.setitem(sys.modules, "mypkg", pub) assert _get_canonical_type_alias_name(alias) == "mypkg.MyAlias" def test_get_canonical_type_alias_name_private_to_public(monkeypatch: pytest.MonkeyPatch) -> None: priv = types.ModuleType("extpkg._priv") exec("type ExtAlias = str | int", priv.__dict__) # noqa: S102 alias: TypeAliasType = priv.__dict__["ExtAlias"] pub = types.ModuleType("extpkg") pub.ExtAlias = alias # type: ignore[attr-defined] monkeypatch.setitem(sys.modules, "extpkg._priv", priv) monkeypatch.setitem(sys.modules, "extpkg", pub) assert _get_canonical_type_alias_name(alias) == "extpkg.ExtAlias" def test_get_canonical_type_alias_name_no_public_reexport(monkeypatch: pytest.MonkeyPatch) -> None: priv = types.ModuleType("extpkg2._priv") exec("type ExtAlias2 = str | int", priv.__dict__) # noqa: S102 alias: TypeAliasType = priv.__dict__["ExtAlias2"] monkeypatch.setitem(sys.modules, "extpkg2._priv", priv) assert _get_canonical_type_alias_name(alias) == "extpkg2._priv.ExtAlias2" def _config_without_env(*, fully_qualified: bool = False) -> MagicMock: config = MagicMock() config.typehints_formatter = None config.typehints_fully_qualified = fully_qualified del config._typehints_env # noqa: SLF001 return config def test_format_annotation_type_alias_without_env() -> None: """TypeAliasForwardRef falls back to plain name when no env is available.""" assert format_annotation(TypeAliasForwardRef("SomeAlias"), _config_without_env()) == "SomeAlias" @pytest.mark.parametrize( ("crossref", "fully_qualified", "expected_result"), [ pytest.param(True, False, ":py:type:`~EncoderHook`", id="crossref"), pytest.param(True, True, ":py:type:`EncoderHook`", id="crossref-fully-qualified"), pytest.param(False, False, "EncoderHook", id="plain-name"), ], ) def test_format_annotation_crossref_alias(crossref: bool, fully_qualified: bool, expected_result: str) -> None: annotation = MyTypeAliasForwardRef("EncoderHook") annotation.crossref = crossref assert format_annotation(annotation, _config_without_env(fully_qualified=fully_qualified)) == expected_result def test_format_annotation_type_alias_found_in_py_domain() -> None: config = MagicMock() config.typehints_formatter = None config.typehints_fully_qualified = False config._typehints_module_prefix = "mymod" # noqa: SLF001 py_domain = MagicMock() py_domain.objects = {"mymod.SomeAlias": MagicMock(objtype="type")} env = MagicMock() env.get_domain.return_value = py_domain config._typehints_env = env # noqa: SLF001 annotation = TypeAliasForwardRef("SomeAlias") assert format_annotation(annotation, config) == ":py:type:`~mymod.SomeAlias`" def test_format_annotation_recursive_type_alias() -> None: """A self-referential PEP 695 alias renders as a cross-reference to itself, not forever (#720).""" namespace: dict[str, Any] = {} exec("type RecType = int | list[RecType]", namespace) # noqa: S102 result = format_annotation(namespace["RecType"], create_autospec(Config)) assert result == ":py:class:`int` | :py:class:`list`\\ \\[:py:type:`~RecType`]" def test_format_annotation_mutually_recursive_type_alias() -> None: """An indirect cycle between PEP 695 aliases resolves to a self cross-reference too (#720).""" namespace: dict[str, Any] = {} exec("type A = int | list[B]\ntype B = str | list[A]", namespace) # noqa: S102 result = format_annotation(namespace["A"], create_autospec(Config)) assert result == ":py:class:`int` | :py:class:`list`\\ \\[:py:class:`str` | :py:class:`list`\\ \\[:py:type:`~A`]]" sphinx_autodoc_typehints-3.12.0/tests/test_attributes_patch.py0000644000000000000000000000357713615410400022007 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.12.0/tests/test_conftest.py0000644000000000000000000000153613615410400020260 0ustar00from __future__ import annotations from typing import TYPE_CHECKING from unittest.mock import MagicMock from conftest import pytest_ignore_collect if TYPE_CHECKING: from pathlib import Path def test_ignore_collect_skips_future_python_version(tmp_path: Path) -> None: path = tmp_path / "test_something_py99.py" assert pytest_ignore_collect(path, MagicMock()) is True # ty: ignore[invalid-argument-type] def test_ignore_collect_allows_current_or_past_version(tmp_path: Path) -> None: path = tmp_path / "test_something_py30.py" assert pytest_ignore_collect(path, MagicMock()) is None # ty: ignore[invalid-argument-type] def test_ignore_collect_ignores_non_versioned_files(tmp_path: Path) -> None: path = tmp_path / "test_regular.py" assert pytest_ignore_collect(path, MagicMock()) is None # ty: ignore[invalid-argument-type] sphinx_autodoc_typehints-3.12.0/tests/test_formats_numpydoc.py0000644000000000000000000003102113615410400022014 0ustar00from __future__ import annotations import sys from importlib.util import find_spec from pathlib import Path from typing import TYPE_CHECKING, Any from unittest.mock import MagicMock import pytest from conftest import normalize_sphinx_text 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 if TYPE_CHECKING: from io import StringIO from sphinx.testing.util import SphinxTestApp 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) 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. """ 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. """ def no_params() -> str: """ Function with no parameters. Returns ------- str A greeting. """ @pytest.mark.skipif(find_spec("numpydoc") is None, reason="requires the numpydoc extension") @pytest.mark.parametrize( ("func", "expected_fragments"), [ pytest.param(simple_func, ["Parameters:", "**x** (", "**y** (", "Return type:"], id="params-and-return"), pytest.param(raises_func, ["Parameters:", "Raises:", "ValueError", "TypeError"], id="raises-section"), pytest.param(multi_return, ["Return type:"], id="multi-return"), pytest.param(no_params, ["Return type:"], id="no-params"), ], ) @pytest.mark.sphinx("text", testroot="numpydoc") def test_numpydoc_extension_build( app: SphinxTestApp, status: StringIO, monkeypatch: pytest.MonkeyPatch, func: Any, expected_fragments: list[str], ) -> None: """Regression test for #347: docstrings preprocessed by the numpydoc extension keep their types.""" (Path(app.srcdir) / "index.rst").write_text(f".. autofunction:: mod_numpy.{func.__name__}") monkeypatch.setitem(sys.modules, "mod_numpy", sys.modules[__name__]) app.build() assert "build succeeded" in status.getvalue() result = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) for fragment in expected_fragments: assert fragment in result sphinx_autodoc_typehints-3.12.0/tests/test_formats_sphinx.py0000644000000000000000000000737713615410400021510 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.12.0/tests/test_generator_yields.py0000644000000000000000000000553113615410400021771 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( self, arg1: int | None = None, ) -> Generator[tuple[str, ...], None, None]: """Summary. Args: arg1: argument 1 Yields: strings of things """ def iter_method(self) -> Iterator[int]: """Summary. Yields: integers """ async def async_gen_method(self) -> AsyncGenerator[str, None]: """Summary. Yields: strings """ async def async_iter_method(self) -> AsyncIterator[int]: """Summary. Yields: integers """ def no_yields_method(self) -> Generator[int, None, None]: """Summary without yields section.""" def _extract_section(text: str, method_name: str) -> str: pattern = rf"({method_name}\(\).*?)(?=(?:async\s+)?\w+\(\)|$)" match = re.search(pattern, text, re.DOTALL) assert match is not None, f"Method {method_name} not found in output" return match.group(1) 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.12.0/tests/test_guarded_import.py0000644000000000000000000000266713615410400021446 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.12.0/tests/test_init.py0000644000000000000000000003527713615410400017407 0ustar00from __future__ import annotations import inspect from typing import Any from unittest.mock import MagicMock, create_autospec, patch import pytest from conftest import make_docstring_app, make_sig_app from sphinx.application import Sphinx from sphinx.config import Config import sphinx_autodoc_typehints as sat from sphinx_autodoc_typehints import ( __version__, _inject_overload_signatures, process_docstring, process_signature, setup, ) 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 test_process_signature_class_attr_lookup_fails() -> None: """Line 111: class name not found on module returns None.""" def fake_method(self: object, 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_signature_class_instance() -> None: """Line 121: bound instance methods should not have their first argument removed.""" instance = _ClassWithPrivate() method = _ClassWithPrivate.__dict__["_ClassWithPrivate__secret"].__get__(instance) app = make_sig_app() result = process_signature(app, "method", "test_init._ClassWithPrivate.__secret", method, MagicMock(), "", "") assert result is not None sig_str, _ = result assert sig_str == "(x)" 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_process_docstring_descriptor_without_stub_is_untouched() -> None: """A C data descriptor with no stub keeps its docstring as-is.""" import array # noqa: PLC0415 app = make_docstring_app() lines = ["the typecode character used to create the array"] process_docstring(app, "attribute", "array.array.typecode", array.array.typecode, None, lines) assert lines == ["the typecode character used to create the array"] def test_process_docstring_descriptor_with_existing_type_field() -> None: """An explicit :type: field wins over the stub annotation.""" import array # noqa: PLC0415 app = make_docstring_app() lines = [":type: str"] process_docstring(app, "attribute", "array.array.typecode", array.array.typecode, None, lines) assert lines == [":type: str"] 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]} app = make_docstring_app() lines: list[str] = [] try: 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: object, 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_overload_global_disable() -> None: obj = MagicMock() obj.__module__ = "test_mod" obj.__qualname__ = "func" sig = inspect.Signature( parameters=[inspect.Parameter("x", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=int)], return_annotation=str, ) _OVERLOADS_CACHE["test_mod"] = {"func": [sig]} try: app = make_docstring_app(typehints_document_overloads=False) lines: list[str] = [] assert _inject_overload_signatures(app, "function", "name", obj, lines) is False assert lines == [] finally: _OVERLOADS_CACHE.pop("test_mod", None) def test_inject_overload_local_no_overloads_directive() -> None: obj = MagicMock() obj.__module__ = "test_mod" obj.__qualname__ = "func" sig = inspect.Signature( parameters=[inspect.Parameter("x", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=int)], return_annotation=str, ) _OVERLOADS_CACHE["test_mod"] = {"func": [sig]} try: app = make_docstring_app() lines = [":no-overloads:", "", "Some docstring."] assert _inject_overload_signatures(app, "function", "name", obj, lines) is False assert ":no-overloads:" not in lines assert lines == ["", "Some docstring."] finally: _OVERLOADS_CACHE.pop("test_mod", None) def test_inject_overload_local_directive_with_global_enabled() -> None: obj = MagicMock() obj.__module__ = "test_mod" obj.__qualname__ = "func" sig = inspect.Signature( parameters=[inspect.Parameter("x", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=int)], return_annotation=str, ) _OVERLOADS_CACHE["test_mod"] = {"func": [sig]} try: app = make_docstring_app(typehints_document_overloads=True) lines = [":no-overloads:", "", "Docs here."] assert _inject_overload_signatures(app, "function", "name", obj, lines) is False assert ":no-overloads:" not in lines finally: _OVERLOADS_CACHE.pop("test_mod", None) def test_process_docstring_uses_new_when_init_inherited() -> None: class _NewOnlyClass: def __new__(cls, x: int, y: str) -> _NewOnlyClass: # noqa: ARG004, PYI034 return super().__new__(cls) app = make_docstring_app(typehints_document_rtype=False) lines: list[str] = [":param x: the x", ":param y: the y"] process_docstring(app, "class", "test._NewOnlyClass", _NewOnlyClass, None, lines) joined = "\n".join(lines) assert ":type x:" in joined assert ":type y:" in joined def test_process_docstring_prefers_init_over_new() -> None: class _BothClass: def __new__(cls, a: float) -> _BothClass: # noqa: ARG004, PYI034 return super().__new__(cls) def __init__(self, x: int) -> None: self.x = x app = make_docstring_app(typehints_document_rtype=False) lines: list[str] = [":param x: the x"] process_docstring(app, "class", "test._BothClass", _BothClass, None, lines) joined = "\n".join(lines) assert ":type x:" in joined 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(s.startswith(":type") for s in lines) def test_process_docstring_replaces_preexisting_single_line_type() -> None: def func(x: int) -> None: ... app = make_docstring_app(typehints_document_rtype=False) lines: list[str] = [":param x: the x parameter", ":type x: str"] process_docstring(app, "function", "test.func", func, None, lines) type_lines = [line for line in lines if line.startswith(":type x:")] assert len(type_lines) == 1 assert "int" in type_lines[0] assert "str" not in "\n".join(lines) def test_process_docstring_replaces_preexisting_multiline_type() -> None: def func(callback: int) -> None: ... app = make_docstring_app(typehints_document_rtype=False) lines: list[str] = [ ":param callback: the callback parameter", ":type callback: ~collections.abc.Callable[[int,", " str], bool]", ] process_docstring(app, "function", "test.func", func, None, lines) type_lines = [line for line in lines if line.startswith(":type callback:")] assert len(type_lines) == 1 assert "int" in type_lines[0] assert "Callable" not in "\n".join(lines) def test_process_docstring_preserves_blank_line_after_preexisting_type() -> None: def func(x: int) -> None: ... app = make_docstring_app(typehints_document_rtype=False) lines: list[str] = [":param x: the x parameter", ":type x: str", "", ".. rubric:: Notes"] process_docstring(app, "function", "test.func", func, None, lines) blank_idx = next(i for i, line in enumerate(lines) if not line) assert lines[blank_idx + 1] == ".. rubric:: Notes" def test_process_docstring_strips_inline_param_type() -> None: def func(x: int) -> None: ... app = make_docstring_app(typehints_document_rtype=False) lines: list[str] = [":param str x: the x parameter"] process_docstring(app, "function", "test.func", func, None, lines) assert ":param x: the x parameter" in lines type_lines = [line for line in lines if line.startswith(":type x:")] assert len(type_lines) == 1 assert "int" in type_lines[0] def test_process_docstring_strips_complex_inline_param_type() -> None: def func(fp: int) -> None: ... app = make_docstring_app(typehints_document_rtype=False) lines: list[str] = [":param ~typing.IO[bytes] fp: the file"] process_docstring(app, "function", "test.func", func, None, lines) assert ":param fp: the file" in lines type_lines = [line for line in lines if line.startswith(":type fp:")] assert len(type_lines) == 1 assert "int" in type_lines[0] def _trailing_underscore_func(lambda_: float) -> None: ... def _trailing_underscore_starred_func(*args_: float) -> None: ... def _trailing_underscore_undocumented_func(x_: float) -> None: ... @pytest.mark.parametrize( ("func", "lines", "expected_param_line", "expected_type_name"), [ pytest.param( _trailing_underscore_func, [":param lambda_: description"], ":param lambda\\_: description", "lambda\\_", id="unescaped-line-rewritten", ), pytest.param( _trailing_underscore_starred_func, [":param \\*args_: description"], ":param \\*args_: description", "args_", id="starred-line-kept", ), pytest.param( _trailing_underscore_undocumented_func, [], ":param x\\_:", "x\\_", id="undocumented-escaped", ), ], ) def test_process_docstring_trailing_underscore_param( func: Any, lines: list[str], expected_param_line: str, expected_type_name: str ) -> None: """napoleon emits ``:param lambda_:`` unescaped by default; the param line must be rewritten to the escaped form and the type attached, except for forms that cannot be rewritten (issue #708).""" app = make_docstring_app(typehints_document_rtype=False, always_document_param_types=True) process_docstring(app, "function", "test.func", func, None, lines) assert expected_param_line in lines assert any(line.startswith(":type ") and expected_type_name in line and "float" in line for line in lines) @pytest.mark.parametrize( "error", [ pytest.param(NameError("Callable"), id="name-error"), pytest.param(TypeError("type 'DiGraph' is not subscriptable"), id="type-error"), ], ) def test_process_signature_annotations_error(error: Exception) -> None: """PEP 649 lazy annotation evaluation raising (NameError for TYPE_CHECKING-only names, TypeError for subscripting a non-generic class) must not propagate from process_signature (issue #712).""" class _Func: @property def __annotations__(self) -> dict[str, object]: # noqa: PLW3201 raise error def __call__(self) -> None: ... app = make_sig_app() result = process_signature(app, "function", "test.func", _Func(), MagicMock(), "", "") assert result is None def test_setup_returns_version() -> None: """setup() returns metadata dict with version from __version__.""" app = create_autospec(Sphinx) app.config = create_autospec(Config) result = setup(app) assert result["version"] == __version__ assert result["parallel_read_safe"] is True assert result["parallel_write_safe"] is True sphinx_autodoc_typehints-3.12.0/tests/test_integration.py0000644000000000000000000007516013615410400020762 0ustar00from __future__ import annotations import re import sys from dataclasses import dataclass from inspect import isclass from pathlib import Path from textwrap import dedent 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: ... 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: ... @expected( """\ mod.function(x, y, z_=None) Function docstring. Parameters: * **x** ("bool") -- foo * **y** ("int") -- bar * **z_** ("str" | "None") -- baz Returns: something Return type: "str" """, ) 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_trailing_underscore_numpy(a, lambda_=None) Do nothing. Parameters: * **a** ("int") -- Description for a. * **lambda_** ("float" | "None", default: "None") -- Description for lambda. Return type: "None" """, typehints_defaults="comma", ) def function_with_trailing_underscore_numpy(a: int, lambda_: float | None = None) -> None: """Do nothing. Parameters ---------- a Description for a. lambda_ Description for lambda. """ @expected( """\ mod.function_with_trailing_underscore_google(a, lambda_=None) Do nothing. Parameters: * **a** ("int") -- Description for a. * **lambda_** ("float" | "None", default: "None") -- Description for lambda. Return type: "None" """, typehints_defaults="comma", ) def function_with_trailing_underscore_google(a: int, lambda_: float | None = None) -> None: """Do nothing. Args: a: Description for a. lambda_: Description for lambda. """ @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 """ @expected( """\ mod.function_with_multiline_param_defaults(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: "str" """, typehints_defaults="braces-after", ) def function_with_multiline_param_defaults(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 """ ArrayLike = Literal["test"] ALIAS_OPTIONS = {"autodoc_type_aliases": {"ArrayLike": "Array", "AliasedClass": '"Class Alias"'}} class _SchemaMeta(type): # noqa: PLW1641 def __eq__(cls, other: object) -> bool: return True class Schema(metaclass=_SchemaMeta): pass class AliasedClass: ... @expected( """ mod.function_with_eq_metaclass(s) Do something. Parameters: **s** ("Schema") -- Some schema. Return type: "Schema" """, **ALIAS_OPTIONS, ) def function_with_eq_metaclass(s: Schema) -> Schema: """ Do something. Args: s: Some schema. """ @expected( """ mod.function_with_aliased_class(s) Do something. Parameters: **s** ("Class Alias") -- Some schema. Return type: "Class Alias" """, **ALIAS_OPTIONS, ) def function_with_aliased_class(s: AliasedClass) -> AliasedClass: """ Do something. Args: s: Some schema. """ @expected( """\ mod.function_with_type_aliases(x, y) Function docstring. Parameters: * **x** (Array | "None") -- foo * **y** ("Schema") -- boo Returns: something Return type: "str" """, **ALIAS_OPTIONS, ) def function_with_type_aliases(x: ArrayLike | None, y: Schema) -> str: """ Function docstring. :param x: foo :param y: boo :return: something :rtype: bytes """ @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 ... def foo( # noqa: ANN201 self, x, # type: str # noqa: ANN001 ): # type: (...) -> int """ Method docstring. :arg x: foo """ def method_without_typehint(self, x): # noqa: ANN001, ANN201 """ Method docstring. """ @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 ... def foo(self, x=1): # type: (Callable[[int, bytes], int]) -> int # noqa: ANN001, ANN201 """ Method docstring. :param x: foo """ @classmethod def mk( # noqa: ANN206 cls, x=None, # noqa: ANN001 ): # type: (Optional[Callable[[int, bytes], int]]) -> ClassWithTypehintsNotInline """ Method docstring. :param x: foo """ @expected( """\ mod.undocumented_function(x) Hi Return type: "str" """, ) def undocumented_function(x: int) -> str: """Hi""" @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: ... @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 """ @overload def func_with_overload_no_overloads(a: int, b: int) -> None: ... @overload def func_with_overload_no_overloads(a: str, b: str) -> None: ... @expected( """\ mod.func_with_overload_no_overloads(a, b) Accepts int or str pairs, see docs for details. Parameters: * **a** ("int" | "str") -- The first thing * **b** ("int" | "str") -- The second thing Return type: "None" """, ) def func_with_overload_no_overloads(a: Union[int, str], b: Union[int, str]) -> None: """:no-overloads: Accepts int or str pairs, see docs for details. Parameters ---------- a: The first thing b: The second thing """ @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 """ @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: ... 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. """ @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. """ 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 """ @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 `_. """ @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") @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 """ # 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 = dedent(normalize_sphinx_text(val.EXPECTED)).strip() assert result.strip() == expected sphinx_autodoc_typehints-3.12.0/tests/test_intersphinx_mapping.py0000644000000000000000000001114513615410400022516 0ustar00from __future__ import annotations import sys import threading import types as _types import typing from types import SimpleNamespace from typing import TYPE_CHECKING, Any, cast from unittest.mock import create_autospec import pytest from sphinx.config import Config from sphinx_autodoc_typehints import format_annotation from sphinx_autodoc_typehints._intersphinx import build_type_mapping if TYPE_CHECKING: from sphinx.environment import BuildEnvironment def _make_env(inventory_data: dict[str, dict[str, object]] | None = None) -> BuildEnvironment: if inventory_data is None: return cast("BuildEnvironment", SimpleNamespace()) return cast("BuildEnvironment", SimpleNamespace(intersphinx_inventory=inventory_data)) def test_build_type_mapping_threading_local() -> None: inventory_data: dict[str, dict[str, Any]] = { "py:class": {"threading.local": ("", "", "", "")}, } result = build_type_mapping(_make_env(inventory_data)) assert result["_thread._local"] == "threading.local" def test_build_type_mapping_no_intersphinx_inventory() -> None: assert build_type_mapping(_make_env()) == {} def test_build_type_mapping_skips_already_documented() -> None: inventory_data: dict[str, dict[str, Any]] = { "py:class": { "threading.local": ("", "", "", ""), "_thread._local": ("", "", "", ""), }, } result = build_type_mapping(_make_env(inventory_data)) assert "_thread._local" not in result def test_build_type_mapping_skips_unimportable() -> None: inventory_data: dict[str, dict[str, Any]] = { "py:class": {"nonexistent_module.SomeClass": ("", "", "", "")}, } assert build_type_mapping(_make_env(inventory_data)) == {} def test_build_type_mapping_skips_missing_attr() -> None: inventory_data: dict[str, dict[str, Any]] = { "py:class": {"threading.NoSuchAttribute": ("", "", "", "")}, } assert build_type_mapping(_make_env(inventory_data)) == {} def test_build_type_mapping_skips_no_qualname(monkeypatch: pytest.MonkeyPatch) -> None: obj_no_qualname = SimpleNamespace(__module__="os") monkeypatch.setattr("os.getcwd", obj_no_qualname) inventory_data: dict[str, dict[str, Any]] = { "py:function": {"os.getcwd": ("", "", "", "")}, } assert build_type_mapping(_make_env(inventory_data)) == {} def test_build_type_mapping_skips_same_path() -> None: inventory_data: dict[str, dict[str, Any]] = { "py:class": {"int": ("", "", "", "")}, } result = build_type_mapping(_make_env(inventory_data)) assert "builtins.int" not in result def test_build_type_mapping_multiple_roles() -> None: inventory_data: dict[str, dict[str, Any]] = { "py:class": {"threading.local": ("", "", "", "")}, "py:function": {}, "py:exception": {}, } result = build_type_mapping(_make_env(inventory_data)) assert result["_thread._local"] == "threading.local" def test_build_type_mapping_skips_builtin_alias() -> None: inventory_data: dict[str, dict[str, Any]] = { "py:class": { "dict": ("", "", "", ""), "wsgiref.types.WSGIEnvironment": ("", "", "", ""), }, } result = build_type_mapping(_make_env(inventory_data)) assert "builtins.dict" not in result def test_format_annotation_applies_intersphinx_mapping() -> None: conf = create_autospec( Config, _intersphinx_type_mapping={"_thread._local": "threading.local"}, always_use_bars_union=False, ) result = format_annotation(threading.local, conf) assert result == ":py:class:`~threading.local`" def test_format_annotation_without_mapping() -> None: conf = create_autospec(Config, always_use_bars_union=False) result = format_annotation(threading.local, conf) assert result == ":py:class:`~_thread._local`" @pytest.mark.parametrize( "alias", [ pytest.param(bytes | memoryview, id="X|Y"), pytest.param(typing.Union[bytes, memoryview], id="Union[X,Y]"), ], ) def test_build_type_mapping_skips_union_alias(monkeypatch: pytest.MonkeyPatch, alias: object) -> None: # Union instances all resolve to the same union-type class path, so including # them in the mapping would remap every union annotation in the docs. mypkg = _types.ModuleType("mypkg") mypkg.MyAlias = alias # type: ignore[attr-defined] monkeypatch.setitem(sys.modules, "mypkg", mypkg) inventory_data: dict[str, dict[str, Any]] = {"py:data": {"mypkg.MyAlias": ("", "", "", "")}} result = build_type_mapping(_make_env(inventory_data)) assert "types.UnionType" not in result assert "typing.Union" not in result sphinx_autodoc_typehints-3.12.0/tests/test_method_lookup.py0000644000000000000000000000203113615410400021273 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.""" def regular_method(self, x: int) -> int: """Do something. Args: x: a number """ @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.12.0/tests/test_pep695.py0000644000000000000000000004270613615410400017467 0ustar00from __future__ import annotations import sys import types from pathlib import Path from textwrap import dedent from typing import TYPE_CHECKING, Any, Union import pytest from conftest import normalize_sphinx_text if TYPE_CHECKING: from io import StringIO from sphinx.testing.util import SphinxTestApp _mod_pep695 = types.ModuleType("mod") _mod_pep695.__file__ = __file__ exec( # noqa: S102 dedent("""\ from __future__ import annotations type IntList = list[int] type StringOrInt = str | int def type_alias_func(x: IntList) -> StringOrInt: \"\"\"Function using PEP 695 type aliases. :param x: List of integers :return: String or integer \"\"\" ... 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 FooStringOrInt(Foo[StringOrInt]): \"\"\"A subclass of Foo with StringOrInt type param.\"\"\" def set(self, thing: StringOrInt) -> StringOrInt: \"\"\"Set the thing. :param thing: 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() @pytest.mark.sphinx("text", testroot="integration") def test_pep695_type_alias_in_function_undocumented( app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test that PEP 695 type aliases in function signatures are rendered as their literal types when the type aliases are not documented.""" (Path(app.srcdir) / "index.rst").write_text(".. autofunction:: mod.type_alias_func") monkeypatch.setitem(sys.modules, "mod", _mod_pep695) app.build() assert "build succeeded" in status.getvalue() assert not warning.getvalue().strip() result = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) expected = dedent("""\ mod.type_alias_func(x) Function using PEP 695 type aliases. Parameters: **x** ("list"["int"]) -- List of integers Return type: "str" | "int" Returns: String or integer """).strip() assert result.strip() == normalize_sphinx_text(expected) @pytest.mark.sphinx("text", testroot="integration") def test_pep695_type_alias_in_function_documented( app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test that PEP 695 type aliases in function signatures are rendered as their documented type names when the type aliases are documented.""" (Path(app.srcdir) / "index.rst").write_text( dedent("""\ .. py:type:: mod.IntList List of integers .. py:type:: mod.StringOrInt String or integer .. autofunction:: mod.type_alias_func """) ) monkeypatch.setitem(sys.modules, "mod", _mod_pep695) app.build() assert "build succeeded" in status.getvalue() assert not warning.getvalue().strip() result = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) expected = dedent("""\ type mod.IntList List of integers type mod.StringOrInt String or integer mod.type_alias_func(x) Function using PEP 695 type aliases. Parameters: **x** ("IntList") -- List of integers Return type: "StringOrInt" Returns: String or integer """).strip() assert result.strip() == normalize_sphinx_text(expected) @pytest.mark.sphinx("text", testroot="integration") def test_pep695_type_alias_in_method_undocumented( app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test that PEP 695 type aliases in method signatures are rendered as their literal types when the type aliases are not documented.""" (Path(app.srcdir) / "index.rst").write_text(".. autoclass:: mod.FooStringOrInt\n :members:") monkeypatch.setitem(sys.modules, "mod", _mod_pep695) app.build() assert "build succeeded" in status.getvalue() assert not warning.getvalue().strip() result = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) expected = dedent("""\ class mod.FooStringOrInt(thing) A subclass of Foo with StringOrInt type param. set(thing) Set the thing. Parameters: **thing** ("str" | "int") -- the thing Return type: "str" | "int" Returns: the thing """).strip() assert result.strip() == normalize_sphinx_text(expected) @pytest.mark.sphinx("text", testroot="integration") def test_pep695_type_alias_in_method_documented( app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test that PEP 695 type aliases in method signatures are rendered as their documented type names when the type aliases are documented.""" (Path(app.srcdir) / "index.rst").write_text( dedent("""\ .. py:type:: mod.StringOrInt String or integer .. autoclass:: mod.FooStringOrInt :members: """) ) monkeypatch.setitem(sys.modules, "mod", _mod_pep695) app.build() assert "build succeeded" in status.getvalue() assert not warning.getvalue().strip() result = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) expected = dedent("""\ type mod.StringOrInt String or integer class mod.FooStringOrInt(thing) A subclass of Foo with StringOrInt type param. set(thing) Set the thing. Parameters: **thing** ("StringOrInt") -- the thing Return type: "StringOrInt" Returns: the thing """).strip() assert result.strip() == normalize_sphinx_text(expected) @pytest.mark.sphinx("text", testroot="integration") def test_pep695_external_type_alias( app: SphinxTestApp, status: StringIO, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test that external TypeAliasType renders as the alias name, not the expanded value.""" # Define an external type alias in a private module ext_priv_mod = types.ModuleType("extpkg._priv") exec("type ExtAlias = str | int", ext_priv_mod.__dict__) # noqa: S102 ext_alias = ext_priv_mod.__dict__["ExtAlias"] # Reexport the type alias in a public module ext_pub_mod = types.ModuleType("extpkg") ext_pub_mod.ExtAlias = ext_alias # type: ignore[attr-defined] # Import and use the external type alias in user module user_mod = types.ModuleType("user_mod") user_mod.__dict__["ExtAlias"] = ext_alias exec( # noqa: S102 dedent("""\ from __future__ import annotations def ext_alias_func(x: ExtAlias) -> ExtAlias: \"\"\"Function using external type alias. :param x: the value :return: the value \"\"\" ... """), user_mod.__dict__, ) (Path(app.srcdir) / "index.rst").write_text(".. autofunction:: user_mod.ext_alias_func") monkeypatch.setitem(sys.modules, "user_mod", user_mod) app.build() assert "build succeeded" in status.getvalue() result = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) assert '"ExtAlias"' in result assert '"str" | "int"' not in result @pytest.mark.skipif(sys.version_info < (3, 14), reason="PEP 649 lazy annotation evaluation is Python 3.14+") @pytest.mark.sphinx("text", testroot="integration") def test_pep695_type_checking_only_annotation( app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, ) -> None: """Regression test for #703: PEP 695 generic functions with TYPE_CHECKING-only annotations must not raise NameError during Sphinx processing.""" mod = types.ModuleType("mod_703") mod.__file__ = __file__ exec( # noqa: S102 dedent("""\ import functools from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Callable def my_customizable_decorator[**P, R]() -> Callable[[Callable[P, R]], Callable[P, R]]: \"\"\"Return a decorator that wraps functions.\"\"\" def decorator[**P, R](func: Callable[P, R]) -> Callable[P, R]: @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: return func(*args, **kwargs) return wrapper return decorator """), mod.__dict__, ) (Path(app.srcdir) / "index.rst").write_text(".. autofunction:: mod_703.my_customizable_decorator") monkeypatch.setitem(sys.modules, "mod_703", mod) app.build() assert "build succeeded" in status.getvalue() # The fix prevents process_signature from throwing — a forward_reference warning # for unresolvable TYPE_CHECKING imports in exec'd modules is acceptable. assert "threw an exception" not in warning.getvalue() UserId = Union[int, str] RequestData = dict[str, Any] _TYPE_ALIAS_PREAMBLE = """\ .. 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.{} """ _TYPE_ALIAS_EXPECTED = """\ type mod.UserId A user identifier that can be either an integer or a string. type mod.RequestData Request data dictionary. {} """ def get_user(user_id: UserId) -> str: """ Get a user by ID. Args: user_id: The user identifier """ def process_request(data: RequestData) -> bool: """ Process a request. Args: data: The request data """ @pytest.mark.parametrize( ("documented", "expected_body"), [ pytest.param( get_user, """\ mod.get_user(user_id) Get a user by ID. Parameters: **user_id** ("UserId") -- The user identifier Return type: "str" """, id="get-user", ), pytest.param( process_request, """\ mod.process_request(data) Process a request. Parameters: **data** ("RequestData") -- The request data Return type: "bool" """, id="process-request", ), ], ) @pytest.mark.sphinx("text", testroot="integration") def test_documented_type_alias_crossref( app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, documented: Any, expected_body: str, ) -> None: (Path(app.srcdir) / "index.rst").write_text(_TYPE_ALIAS_PREAMBLE.format(documented.__name__)) 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 = dedent(normalize_sphinx_text(_TYPE_ALIAS_EXPECTED.format(expected_body))).strip() assert result.strip() == expected @pytest.mark.sphinx("text", testroot="integration") def test_recursive_type_alias_does_not_recurse_forever( app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, ) -> None: """A self-referential PEP 695 alias builds cleanly and renders as a self cross-reference (#720).""" mod = types.ModuleType("mod_720") mod.__file__ = __file__ exec( # noqa: S102 dedent("""\ from __future__ import annotations type RecType = int | list[RecType] \"\"\"A recursive type alias.\"\"\" def some_func(some_param: RecType) -> None: \"\"\"Describe. :param some_param: some description \"\"\" ... """), mod.__dict__, ) (Path(app.srcdir) / "index.rst").write_text(".. autofunction:: mod_720.some_func") monkeypatch.setitem(sys.modules, "mod_720", mod) 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 '"RecType"' in result @pytest.mark.sphinx("text", testroot="integration") def test_eager_annotations( app: SphinxTestApp, status: StringIO, warning: StringIO, ) -> None: """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 @pytest.mark.skipif(sys.version_info < (3, 14), reason="annotationlib requires Python 3.14+") @pytest.mark.sphinx("text", testroot="integration") def test_forward_ref_builds_without_errors( # pragma: >=3.14 cover app: SphinxTestApp, status: StringIO, ) -> None: """Forward-referencing module builds cleanly on 3.14+ using annotationlib.""" (Path(app.srcdir) / "index.rst").write_text(".. autoclass:: mod_forward_ref.Tree\n :members:\n") 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 @pytest.mark.skipif(sys.version_info < (3, 14), reason="PEP 649 lazy annotation evaluation is Python 3.14+") @pytest.mark.sphinx("text", testroot="integration") def test_non_subscriptable_generic_annotation( # pragma: >=3.14 cover app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, ) -> None: """Regression test for #712: annotations whose lazy evaluation raises (here TypeError from subscripting a non-generic class) must not crash the build; the hint degrades to its source text.""" mod = types.ModuleType("mod_712") mod.__file__ = __file__ source = dedent("""\ class DiGraph: pass def add_node_between_nodes(g: DiGraph[int]) -> None: \"\"\"Stub.\"\"\" """) # dont_inherit keeps this file's `from __future__ import annotations` (PEP 563) out of the # compiled module so its annotations stay lazily evaluated (PEP 649) exec(compile(source, "", "exec", dont_inherit=True), mod.__dict__) # noqa: S102 (Path(app.srcdir) / "index.rst").write_text(".. autofunction:: mod_712.add_node_between_nodes") monkeypatch.setitem(sys.modules, "mod_712", mod) app.config.__dict__["always_document_param_types"] = True app.build() assert "build succeeded" in status.getvalue() assert "threw an exception" not in warning.getvalue() result = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) assert "DiGraph[int]" in result sphinx_autodoc_typehints-3.12.0/tests/test_safe_parse.py0000644000000000000000000000357313615410400020546 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.12.0/tests/test_sphinx_autodoc_typehints.py0000644000000000000000000006106313615410400023572 0ustar00from __future__ import annotations import re from functools import cmp_to_key from pathlib import Path from textwrap import dedent, indent from typing import TYPE_CHECKING, Any import pytest import typing_extensions from conftest import make_docstring_app, make_sig_app, normalize_sphinx_text from sphinx.ext.autodoc import Options from sphinx_autodoc_typehints import process_docstring, process_signature pytestmark = pytest.mark.usefixtures("wide_text_output") if TYPE_CHECKING: from collections.abc import Callable 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] = [] app = make_docstring_app() 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] = [] app = make_docstring_app() 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 app = make_sig_app() result = process_signature( app, "function", "func", func, Options(), "", "", ) assert result is None @pytest.mark.parametrize("always_document_param_types", [True, False], ids=["doc_param_type", "no_doc_param_type"]) @pytest.mark.sphinx("text", testroot="dummy") def test_always_document_param_types( app: SphinxTestApp, status: StringIO, warning: StringIO, always_document_param_types: bool, write_rst: Callable[[str], None], ) -> None: app.config.always_document_param_types = always_document_param_types # create flag app.config.autodoc_mock_imports = ["mailbox"] # create flag write_rst( """ .. 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") def test_always_document_param_types_with_defaults_braces_after( app: SphinxTestApp, status: StringIO, warning: StringIO, # noqa: ARG001 write_rst: Callable[[str], None], ) -> None: """Regression test for #575: IndexError when combining always_document_param_types with braces-after.""" app.config.always_document_param_types = True app.config.typehints_defaults = "braces-after" write_rst("""\ .. autofunction:: dummy_module.undocumented_function_with_defaults """) app.build() assert "build succeeded" in status.getvalue() @pytest.mark.sphinx("text", testroot="dummy") def test_namedtuple_new_no_warning( app: SphinxTestApp, status: StringIO, warning: StringIO, write_rst: Callable[[str], None], ) -> None: """Regression test for #601: NamedTuple __new__ causes 'NoneType' attribute error.""" write_rst("""\ .. autoclass:: dummy_module.MyNamedTuple :special-members: __new__ """) app.build() assert "build succeeded" in status.getvalue() assert "NoneType" not in warning.getvalue() @pytest.mark.sphinx("text", testroot="dummy") def test_namedtuple_no_forward_ref_warning( app: SphinxTestApp, status: StringIO, warning: StringIO, write_rst: Callable[[str], None], ) -> None: """Regression test for #656: NamedTuple forward reference resolution fails.""" write_rst("""\ .. autoclass:: dummy_module.MyNamedTupleWithPath :undoc-members: """) app.build() assert "build succeeded" in status.getvalue() assert "Cannot resolve forward reference" not in warning.getvalue() @pytest.mark.sphinx("text", testroot="dummy") def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) -> None: 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") def test_sphinx_output_default_role(app: SphinxTestApp, status: StringIO) -> None: 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") def test_sphinx_output_defaults( app: SphinxTestApp, status: StringIO, defaults_config_val: str, expected: str | Exception, ) -> None: 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") def test_sphinx_output_formatter( app: SphinxTestApp, status: StringIO, formatter_config_val: str, expected: tuple[str, ...] | Exception, ) -> None: 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.sphinx("text", testroot="dummy") def test_typehints_formatter_builds_without_config_cache_warning( app: SphinxTestApp, status: StringIO, warning: StringIO, write_rst: Callable[[str], None], ) -> None: """Regression test for #695: callable typehints_formatter should not emit cache warnings. """ def formatter(_annotation: object, _config: object | None = None) -> str: return "Formatted" app.config.typehints_formatter = formatter write_rst("""\ .. autofunction:: dummy_module.undocumented_function """) app.build() assert "build succeeded" in status.getvalue() assert not warning.getvalue().strip() @pytest.mark.parametrize("obj", [cmp_to_key, 1]) def test_default_no_signature(obj: Any) -> None: app = make_docstring_app() 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: app = make_docstring_app( typehints_document_rtype=False, always_document_param_types=True, typehints_defaults=True, ) 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: 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: app.build() assert "build succeeded" in status.getvalue() assert not warning.getvalue() @pytest.mark.sphinx("text", testroot="dummy") def test_sphinx_output_formatter_no_use_rtype(app: SphinxTestApp, status: StringIO) -> None: 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") def test_sphinx_output_with_use_signature(app: SphinxTestApp, status: StringIO) -> None: 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") def test_sphinx_output_with_use_signature_return(app: SphinxTestApp, status: StringIO) -> None: 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") def test_sphinx_output_with_use_signature_and_return(app: SphinxTestApp, status: StringIO) -> None: 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") def test_default_annotation_without_typehints(app: SphinxTestApp, status: StringIO) -> None: 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") def test_wrong_module_path(app: SphinxTestApp, status: StringIO, warning: StringIO) -> None: 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() class _IvarClass: """Class with instance variable docs. :ivar bar: the bar value :ivar count: the count """ def __init__(self, bar: str = "baz", count: int = 0) -> None: self.bar: str = bar self.count: int = count class _IvarWithExistingVartype: """Class with pre-existing :vartype:. :ivar bar: the bar value :vartype bar: str """ def __init__(self) -> None: self.bar: str = "" class _IvarInlineType: """Class with inline-typed :ivar:. :ivar str bar: the bar value """ def __init__(self) -> None: self.bar: str = "" class _IvarNoAnnotation: """Class with ivar but no annotation. :ivar bar: the bar value """ def __init__(self) -> None: self.bar = "" def test_ivar_type_injected() -> None: assert _IvarClass().bar == "baz" # instantiate to cover __init__ lines = [":ivar bar: the bar value", ":ivar count: the count"] app = make_docstring_app() process_docstring(app, "class", "_IvarClass", _IvarClass, None, lines) assert any(line.startswith(":vartype bar:") and "str" in line for line in lines) assert any(line.startswith(":vartype count:") and "int" in line for line in lines) def test_ivar_vartype_inserted_before_ivar() -> None: lines = [":ivar bar: the bar value"] app = make_docstring_app() process_docstring(app, "class", "_IvarClass", _IvarClass, None, lines) bar_idx = lines.index(":ivar bar: the bar value") vartype_idx = next(i for i, line in enumerate(lines) if line.startswith(":vartype bar:")) assert vartype_idx < bar_idx @pytest.mark.parametrize( ("cls", "lines"), [ pytest.param( _IvarWithExistingVartype, [":ivar bar: the bar value", ":vartype bar: str"], id="existing_vartype", ), pytest.param( _IvarInlineType, [":ivar str bar: the bar value"], id="inline_type", ), ], ) def test_ivar_vartype_not_duplicated(cls: type, lines: list[str]) -> None: cls() # cover __init__ app = make_docstring_app() process_docstring(app, "class", cls.__name__, cls, None, lines) assert sum(1 for line in lines if line.startswith(":vartype bar:")) <= 1 def test_ivar_without_annotation_no_vartype() -> None: assert not _IvarNoAnnotation().bar # cover __init__ lines = [":ivar bar: the bar value"] app = make_docstring_app() process_docstring(app, "class", "_IvarNoAnnotation", _IvarNoAnnotation, None, lines) assert not any(line.startswith(":vartype bar:") for line in lines) def test_ivar_injection_only_for_class() -> None: def func(x: str) -> str: # needs annotations so process_docstring does not short-circuit return x # pragma: no cover lines = [":ivar bar: should not be processed"] app = make_docstring_app(typehints_document_rtype=False) process_docstring(app, "function", "func", func, None, lines) assert not any(line.startswith(":vartype bar:") for line in lines) @pytest.mark.parametrize( "malformed_line", [ pytest.param(":ivar :", id="single_space_empty_name"), pytest.param(":ivar :", id="double_space_empty_name"), pytest.param(":ivar :", id="triple_space_empty_name"), pytest.param(":ivar ", id="trailing_space_no_colon"), pytest.param(":ivar bar", id="no_closing_colon"), pytest.param(":ivar bar body", id="no_closing_colon_with_body"), ], ) def test_ivar_injection_skips_malformed_line(malformed_line: str) -> None: """Regression: malformed ``:ivar ...`` lines must not crash the parser (issue #683).""" assert _IvarClass().bar == "baz" # cover __init__ lines = [malformed_line] app = make_docstring_app() process_docstring(app, "class", "_IvarClass", _IvarClass, None, lines) assert lines == [malformed_line] def test_ivar_injection_survives_mixed_malformed_and_valid_lines() -> None: """A crashing malformed entry must not block injection of valid sibling entries.""" assert _IvarClass().bar == "baz" # cover __init__ lines = [ ":ivar :", ":ivar bar: the bar value", ":ivar :", ":ivar count: the count", ] app = make_docstring_app() process_docstring(app, "class", "_IvarClass", _IvarClass, None, lines) assert ":ivar :" in lines assert ":ivar :" in lines assert any(line.startswith(":vartype bar:") and "str" in line for line in lines) assert any(line.startswith(":vartype count:") and "int" in line for line in lines) @pytest.mark.parametrize( "non_field_line", [ pytest.param(":ivarname: not a real field", id="no_space_after_ivar"), pytest.param(":ivars bar: pluralised keyword", id="pluralised_ivar"), ], ) def test_ivar_injection_ignores_non_ivar_field(non_field_line: str) -> None: """Lines that merely share the ``:ivar`` prefix must not be mistaken for fields.""" assert _IvarClass().bar == "baz" # cover __init__ lines = [non_field_line] app = make_docstring_app() process_docstring(app, "class", "_IvarClass", _IvarClass, None, lines) assert lines == [non_field_line] @pytest.mark.parametrize( "valid_line", [ pytest.param(":ivar\tbar: the bar value", id="tab_separator"), pytest.param(":ivar bar: the bar value", id="multi_space_separator"), pytest.param(":ivar bar: the bar value", id="multi_space_around_body"), ], ) def test_ivar_injection_accepts_whitespace_variants(valid_line: str) -> None: """Non-ASCII-single-space whitespace between ``:ivar`` and name must still match.""" assert _IvarClass().bar == "baz" # cover __init__ lines = [valid_line] app = make_docstring_app() process_docstring(app, "class", "_IvarClass", _IvarClass, None, lines) assert any(line.startswith(":vartype bar:") and "str" in line for line in lines) @pytest.mark.parametrize( "inline_typed_line", [ pytest.param(":ivar Dict[str, int] bar: the bar value", id="dict_str_int"), pytest.param(":ivar Callable[[int], str] bar: the bar value", id="callable"), pytest.param(":ivar tuple[int, ...] bar: the bar value", id="tuple_ellipsis"), ], ) def test_ivar_injection_accepts_parameterised_inline_type(inline_typed_line: str) -> None: """Inline types containing spaces still count as inline-typed and suppress injection.""" assert _IvarClass().bar == "baz" # cover __init__ lines = [inline_typed_line] app = make_docstring_app() process_docstring(app, "class", "_IvarClass", _IvarClass, None, lines) assert not any(line.startswith(":vartype bar:") for line in lines) sphinx_autodoc_typehints-3.12.0/tests/test_version.py0000644000000000000000000000021213615410400020106 0ustar00from __future__ import annotations from sphinx_autodoc_typehints import __version__ def test_version() -> None: assert __version__ sphinx_autodoc_typehints-3.12.0/tests/roots/test_nested_attrs_localns.py0000644000000000000000000000015313615410400024005 0ustar00from __future__ import annotations class Outer: class Inner: def __init__(self) -> None: ... sphinx_autodoc_typehints-3.12.0/tests/roots/test-annotated-doc/conf.py0000644000000000000000000000032713615410400023161 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.12.0/tests/roots/test-annotated-doc/index.rst0000644000000000000000000000001213615410400023512 0ustar00Test ==== sphinx_autodoc_typehints-3.12.0/tests/roots/test-annotated-doc/mod.py0000644000000000000000000000127113615410400023012 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.12.0/tests/roots/test-annotated-doc-numpydoc/conf.py0000644000000000000000000000041313615410400025011 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.12.0/tests/roots/test-annotated-doc-numpydoc/index.rst0000644000000000000000000000001213615410400025346 0ustar00Test ==== sphinx_autodoc_typehints-3.12.0/tests/roots/test-annotated-doc-numpydoc/mod.py0000644000000000000000000000102513615410400024643 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.12.0/tests/roots/test-attrs/attrs_mod.py0000644000000000000000000000133213615410400022642 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 class Outer: """A class with nested attrs classes to test forward reference resolution.""" class Foo: """A nested class referenced by Bar.""" @attrs.define class Bar: """An attrs class referencing a sibling nested class.""" foo: Outer.Foo sphinx_autodoc_typehints-3.12.0/tests/roots/test-attrs/conf.py0000644000000000000000000000036313615410400021576 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.12.0/tests/roots/test-attrs/index.rst0000644000000000000000000000001213615410400022127 0ustar00Test ==== sphinx_autodoc_typehints-3.12.0/tests/roots/test-dummy/conf.py0000644000000000000000000000044213615410400021572 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.12.0/tests/roots/test-dummy/dummy_module.py0000644000000000000000000000117313615410400023347 0ustar00from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, NamedTuple if TYPE_CHECKING: from pathlib import Path 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" class MyNamedTupleWithPath(NamedTuple): """A named tuple with non-builtin types.""" path: Path version: str @dataclass class DataClass: """Class docstring.""" x: int sphinx_autodoc_typehints-3.12.0/tests/roots/test-dummy/dummy_module_future_annotations.py0000644000000000000000000000044313615410400027355 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.12.0/tests/roots/test-dummy/dummy_module_simple.py0000644000000000000000000000023413615410400024715 0ustar00from __future__ import annotations def function(x: bool, y: int = 1) -> str: """ Function docstring. :param x: foo :param y: bar """ sphinx_autodoc_typehints-3.12.0/tests/roots/test-dummy/dummy_module_simple_default_role.py0000644000000000000000000000023613615410400027444 0ustar00from __future__ import annotations def function(x: bool, y: int) -> str: """ Function docstring. :param x: `foo` :param y: ``bar`` """ sphinx_autodoc_typehints-3.12.0/tests/roots/test-dummy/dummy_module_simple_no_use_rtype.py0000644000000000000000000000130313615410400027506 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.12.0/tests/roots/test-dummy/dummy_module_without_complete_typehints.py0000644000000000000000000000163013615410400031127 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.12.0/tests/roots/test-dummy/export_module.py0000644000000000000000000000013513615410400023532 0ustar00from __future__ import annotations from wrong_module_path import A, f __all__ = ["A", "f"] sphinx_autodoc_typehints-3.12.0/tests/roots/test-dummy/future_annotations.rst0000644000000000000000000000016713615410400024760 0ustar00:orphan: Dummy Module ============ .. autofunction:: dummy_module_future_annotations.function_with_py310_annotations sphinx_autodoc_typehints-3.12.0/tests/roots/test-dummy/simple.rst0000644000000000000000000000012613615410400022315 0ustar00:orphan: Simple Module ============= .. autofunction:: dummy_module_simple.function sphinx_autodoc_typehints-3.12.0/tests/roots/test-dummy/simple_default_role.rst0000644000000000000000000000014313615410400025041 0ustar00:orphan: Simple Module ============= .. autofunction:: dummy_module_simple_default_role.function sphinx_autodoc_typehints-3.12.0/tests/roots/test-dummy/simple_no_use_rtype.rst0000644000000000000000000000054413615410400025114 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.12.0/tests/roots/test-dummy/without_complete_typehints.rst0000644000000000000000000000107413615410400026531 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.12.0/tests/roots/test-dummy/wrong_module_path.py0000644000000000000000000000012013615410400024353 0ustar00from __future__ import annotations class A: pass def f() -> A: pass sphinx_autodoc_typehints-3.12.0/tests/roots/test-dummy/wrong_module_path.rst0000644000000000000000000000011013615410400024532 0ustar00:orphan: .. class:: export_module.A .. autofunction:: export_module.f sphinx_autodoc_typehints-3.12.0/tests/roots/test-integration/conf.py0000644000000000000000000000044213615410400022762 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.12.0/tests/roots/test-integration/mod_eager.py0000644000000000000000000000050413615410400023756 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.12.0/tests/roots/test-integration/mod_forward_ref.py0000644000000000000000000000037213615410400025176 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.12.0/tests/roots/test-numpydoc/conf.py0000644000000000000000000000053113615410400022274 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.12.0/tests/roots/test-numpydoc/index.rst0000644000000000000000000000001213615410400022630 0ustar00Test ==== sphinx_autodoc_typehints-3.12.0/tests/roots/test-pyi-stubs/c_ext_mod.c0000644000000000000000000000435013615410400023206 0ustar00#include static PyObject *greet(PyObject *self, PyObject *args) { const char *name; if (!PyArg_ParseTuple(args, "s", &name)) return NULL; return PyUnicode_FromFormat("Hello, %s!", name); } static PyObject *with_hook(PyObject *self, PyObject *args) { (void)self; (void)args; Py_RETURN_NONE; } static PyObject *encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) { (void)args; (void)kwargs; return type->tp_alloc(type, 0); } static PyObject *encoder_get_depth(PyObject *self, void *closure) { (void)self; (void)closure; return PyLong_FromLong(0); } static PyObject *encoder_get_hook(PyObject *self, void *closure) { (void)self; (void)closure; Py_RETURN_NONE; } static PyGetSetDef encoder_getset[] = { {"depth", encoder_get_depth, NULL, "current nesting depth", NULL}, {"hook", encoder_get_hook, NULL, "the active encoder hook", NULL}, {"flags", encoder_get_depth, NULL, "encoder behavior flags", NULL}, {"guarded", encoder_get_depth, NULL, "a member guarded by sys.version_info in the stub", NULL}, {NULL, NULL, NULL, NULL, NULL} }; static PyTypeObject EncoderType = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "c_ext_mod.Encoder", .tp_doc = "Encoder class", .tp_basicsize = sizeof(PyObject), .tp_itemsize = 0, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_new = encoder_new, .tp_getset = encoder_getset, }; static int module_exec(PyObject *m) { if (PyType_Ready(&EncoderType) < 0) return -1; Py_INCREF(&EncoderType); if (PyModule_AddObject(m, "Encoder", (PyObject *)&EncoderType) < 0) { Py_DECREF(&EncoderType); return -1; } return 0; } static PyMethodDef methods[] = { {"greet", greet, METH_VARARGS, NULL}, {"with_hook", with_hook, METH_VARARGS, NULL}, {NULL, NULL, 0, NULL} }; static PyModuleDef_Slot module_slots[] = { {Py_mod_exec, module_exec}, #ifdef Py_mod_gil {Py_mod_gil, Py_MOD_GIL_NOT_USED}, #endif {0, NULL} }; static struct PyModuleDef module = { PyModuleDef_HEAD_INIT, "c_ext_mod", NULL, 0, methods, module_slots, NULL, NULL, NULL }; PyMODINIT_FUNC PyInit_c_ext_mod(void) { return PyModuleDef_Init(&module); } sphinx_autodoc_typehints-3.12.0/tests/roots/test-pyi-stubs/c_ext_mod.pyi0000644000000000000000000000100613615410400023560 0ustar00from collections.abc import Callable, Sequence from typing import Any, Self type GreetHook = Callable[[str], Any] type EncoderHook = Callable[[Any], Any] def greet(name: str, greeting: Sequence[str]) -> str: ... def with_hook(callback: GreetHook) -> None: ... class Encoder: flags: int def __new__(cls, default: EncoderHook | None = None) -> Self: ... @property def depth(self) -> int: ... @property def hook(self) -> EncoderHook | None: ... @property def guarded(self) -> bool: ... sphinx_autodoc_typehints-3.12.0/tests/roots/test-pyi-stubs/conf.py0000644000000000000000000000053013615410400022374 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.intersphinx", "sphinx.ext.napoleon", "sphinx_autodoc_typehints", ] intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} sphinx_autodoc_typehints-3.12.0/tests/roots/test-pyi-stubs/index.rst0000644000000000000000000000016513615410400022742 0ustar00 .. autofunction:: stub_mod.greet .. autoclass:: stub_mod.Calculator :members: .. autofunction:: stub_mod.fetch sphinx_autodoc_typehints-3.12.0/tests/roots/test-pyi-stubs/stub_mod.py0000644000000000000000000000113013615410400023260 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 class Converter: """Convert output. :param output: the output format """ def __new__(cls, output): instance = super().__new__(cls) instance.output = output return instance async def fetch(url): return url def transform(value): return value sphinx_autodoc_typehints-3.12.0/tests/roots/test-pyi-stubs/stub_mod.pyi0000644000000000000000000000073513615410400023443 0ustar00from collections.abc import Callable, Sequence type MyAlias = Callable[[str], str] def greet(name: str, greeting: str) -> str: ... class Calculator: value: int def add(self, x: int) -> Calculator: ... class Inner: def process(self, data: bytes) -> bytes: ... class Converter: output: str def __new__(cls, output: str) -> Converter: ... # noqa: PYI034 async def fetch(url: str) -> str: ... def transform(value: Sequence[int]) -> list[str]: ... sphinx_autodoc_typehints-3.12.0/tests/roots/test-pyi-stubs/native_pkg/__init__.py0000644000000000000000000000024013615410400025333 0ustar00"""PyO3-shaped reproduction package for discussion #698.""" from __future__ import annotations from . import _native # noqa: F401 from . import warc as warc sphinx_autodoc_typehints-3.12.0/tests/roots/test-pyi-stubs/native_pkg/_native.c0000644000000000000000000000524013615410400025020 0ustar00/* Mirrors the PyO3 native-submodule shape from discussion #698: * ``native_pkg.warc`` is built via ``PyModule_New`` and registered in ``sys.modules`` with no * ``__file__`` / ``__spec__`` / ``__path__``. */ #include static PyObject *parse(PyObject *self, PyObject *args) { (void)self; (void)args; Py_RETURN_NONE; } static PyObject *raw_parse(PyObject *self, PyObject *args) { (void)self; (void)args; Py_RETURN_NONE; } /* ``parse`` uses the PyO3-style Argument Clinic header (recoverable signature); * ``raw_parse`` omits it so we cover the no-signature path. */ static PyMethodDef warc_methods[] = { {"parse", parse, METH_VARARGS, "parse($module, data)\n--\n\nParse WARC records.\n\n:param data: input bytes"}, {"raw_parse", raw_parse, METH_VARARGS, "Parse WARC records.\n\n:param data: input bytes"}, {NULL, NULL, 0, NULL} }; static PyTypeObject WarcRecordType = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "native_pkg.warc.WarcRecord", .tp_doc = "A WARC record (native).", .tp_basicsize = sizeof(PyObject), .tp_itemsize = 0, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, .tp_new = PyType_GenericNew, }; static int register_warc_submodule(PyObject *parent) { PyObject *warc = PyModule_New("native_pkg.warc"); if (warc == NULL) { return -1; } if (PyType_Ready(&WarcRecordType) < 0) { Py_DECREF(warc); return -1; } Py_INCREF(&WarcRecordType); if (PyModule_AddObject(warc, "WarcRecord", (PyObject *)&WarcRecordType) < 0) { Py_DECREF(&WarcRecordType); Py_DECREF(warc); return -1; } if (PyModule_AddFunctions(warc, warc_methods) < 0) { Py_DECREF(warc); return -1; } Py_INCREF(warc); if (PyModule_AddObject(parent, "warc", warc) < 0) { Py_DECREF(warc); Py_DECREF(warc); return -1; } PyObject *sys_modules = PyImport_GetModuleDict(); if (sys_modules == NULL) { Py_DECREF(warc); return -1; } if (PyDict_SetItemString(sys_modules, "native_pkg.warc", warc) < 0) { Py_DECREF(warc); return -1; } Py_DECREF(warc); return 0; } static int native_exec(PyObject *m) { return register_warc_submodule(m); } static PyModuleDef_Slot native_slots[] = { {Py_mod_exec, native_exec}, #ifdef Py_mod_gil {Py_mod_gil, Py_MOD_GIL_NOT_USED}, #endif {0, NULL} }; static struct PyModuleDef native_module = { PyModuleDef_HEAD_INIT, "native_pkg._native", NULL, 0, NULL, native_slots, NULL, NULL, NULL }; PyMODINIT_FUNC PyInit__native(void) { return PyModuleDef_Init(&native_module); } sphinx_autodoc_typehints-3.12.0/tests/roots/test-pyi-stubs/native_pkg/warc.pyi0000644000000000000000000000040213615410400024701 0ustar00from collections.abc import Iterator from typing import Self class WarcRecord: record_id: str def __new__(cls, record_id: str) -> Self: ... def parse(data: bytes) -> Iterator[WarcRecord]: ... def raw_parse(data: bytes) -> Iterator[WarcRecord]: ... sphinx_autodoc_typehints-3.12.0/tests/roots/test-resolve-typing-guard/conf.py0000644000000000000000000000032613615410400024527 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.12.0/tests/roots/test-resolve-typing-guard/demo_typing_guard.py0000644000000000000000000000264113615410400027304 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.12.0/tests/roots/test-resolve-typing-guard/demo_typing_guard_dummy.py0000644000000000000000000000043613615410400030517 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.12.0/tests/roots/test-resolve-typing-guard/index.rst0000644000000000000000000000005713615410400025072 0ustar00.. automodule:: demo_typing_guard :members: sphinx_autodoc_typehints-3.12.0/tests/roots/test-resolve-typing-guard-tmp/conf.py0000644000000000000000000000032613615410400025325 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.12.0/tests/roots/test-resolve-typing-guard-tmp/demo_typing_guard.py0000644000000000000000000000223613615410400030102 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.12.0/tests/roots/test-resolve-typing-guard-tmp/index.rst0000644000000000000000000000005713615410400025670 0ustar00.. automodule:: demo_typing_guard :members: sphinx_autodoc_typehints-3.12.0/tests/test_resolver/__init__.py0000644000000000000000000000000013615410400022014 0ustar00sphinx_autodoc_typehints-3.12.0/tests/test_resolver/test_attrs.py0000644000000000000000000000666613615410400022501 0ustar00from __future__ import annotations import sys from pathlib import Path from typing import TYPE_CHECKING 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.mark.sphinx("text", testroot="attrs") def test_sphinx_build_nested_attrs_forward_ref(app: SphinxTestApp, status: StringIO, warning: StringIO) -> None: template = """\ .. autoclass:: attrs_mod.Outer :members: :undoc-members: .. autoclass:: attrs_mod.Outer.Bar :members: :undoc-members: """ (Path(app.srcdir) / "index.rst").write_text(template) app.build() assert "build succeeded" in status.getvalue() assert "Foo" in normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) assert "forward reference" not in warning.getvalue() @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.12.0/tests/test_resolver/test_instance_vars.py0000644000000000000000000000430513615410400024167 0ustar00from __future__ import annotations from typing import Optional import pytest from sphinx_autodoc_typehints._resolver._instance_vars import get_instance_var_annotations class _Simple: def __init__(self, bar: str = "baz") -> None: self.bar: str = bar class _Multi: def __init__(self) -> None: self.x: int = 0 self.y: str = "" self.z: float = 0.0 class _NoAnnotations: def __init__(self) -> None: self.x = 0 class _NestedFunction: def __init__(self) -> None: self.outer: int = 0 def inner() -> None: fake_self_attr: str = "" # noqa: F841 inner() class _ComplexTypes: def __init__(self) -> None: self.items: list[str] = [] self.mapping: dict[str, int] = {} self.maybe: Optional[int] = None class _NoInit: pass def test_basic_instance_var() -> None: assert _Simple().bar == "baz" assert get_instance_var_annotations(_Simple) == {"bar": str} def test_multiple_instance_vars() -> None: assert _Multi().x == 0 assert get_instance_var_annotations(_Multi) == {"x": int, "y": str, "z": float} def test_no_annotations_in_init() -> None: assert _NoAnnotations().x == 0 assert get_instance_var_annotations(_NoAnnotations) == {} def test_nested_function_annotations_ignored() -> None: assert _NestedFunction().outer == 0 result = get_instance_var_annotations(_NestedFunction) assert result == {"outer": int} assert "fake_self_attr" not in result @pytest.mark.parametrize( ("cls", "attr", "expected"), [ pytest.param(_ComplexTypes, "items", list[str], id="list"), pytest.param(_ComplexTypes, "mapping", dict[str, int], id="dict"), pytest.param(_ComplexTypes, "maybe", Optional[int], id="optional"), ], ) def test_complex_types(cls: type, attr: str, expected: object) -> None: cls() # cover __init__ result = get_instance_var_annotations(cls) assert result[attr] == expected @pytest.mark.parametrize( "cls", [ pytest.param(_NoInit, id="no_init"), pytest.param(object, id="object_itself"), ], ) def test_no_instance_vars(cls: type) -> None: assert get_instance_var_annotations(cls) == {} sphinx_autodoc_typehints-3.12.0/tests/test_resolver/test_no_type_check.py0000644000000000000000000000213613615410400024142 0ustar00from __future__ import annotations import typing from sphinx_autodoc_typehints._resolver._type_hints import _get_type_hint, get_all_type_hints @typing.no_type_check def _decorated_function(x: int, y: str) -> bool: return bool(x) and bool(y) @typing.no_type_check class _DecoratedClass: def method(self, value: int) -> str: # noqa: PLR6301 return str(value) class _Target: pass @typing.no_type_check def _deferred_forward_ref(obj: _Target) -> _Target: return obj def test_no_type_check_function_still_resolves_hints() -> None: result = get_all_type_hints([], _decorated_function, "_decorated_function", {}) assert result == {"x": int, "y": str, "return": bool} def test_no_type_check_method_still_resolves_hints() -> None: result = _get_type_hint([], "_DecoratedClass.method", _DecoratedClass.method, {}) assert result == {"value": int, "return": str} def test_no_type_check_resolves_future_string_annotations() -> None: result = _get_type_hint([], "_deferred_forward_ref", _deferred_forward_ref, {}) assert result == {"obj": _Target, "return": _Target} sphinx_autodoc_typehints-3.12.0/tests/test_resolver/test_stubs.py0000644000000000000000000010171013615410400022466 0ustar00from __future__ import annotations import ast import importlib import inspect import os import shutil import subprocess # noqa: S404 import sys import sysconfig import textwrap import types from collections.abc import Sequence from pathlib import Path from typing import TYPE_CHECKING, Any, Optional 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, _extract_type_alias_names, _find_ast_node, _find_native_submodule_stub, _find_stub_for_module, _find_stub_owner, _find_stub_path, _get_module, _get_stub_context, _parse_stub_ast, _resolve_import_from, _resolve_stub_imports, _stub_owner_package, ) if TYPE_CHECKING: from collections.abc import Iterator 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 @pytest.mark.parametrize( "ext_filename", [ pytest.param("_mod.cpython-312-x86_64-linux-gnu.so", id="cpython_linux"), pytest.param("_mod.cpython-313-darwin.so", id="cpython_darwin"), pytest.param("_mod.cpython-314t-x86_64-linux-gnu.so", id="cpython_free_threaded"), pytest.param("_mod.cpython-314d-x86_64-linux-gnu.so", id="cpython_debug"), pytest.param("_mod.cpython-314-aarch64-linux-musl.so", id="cpython_musl"), pytest.param("_mod.abi3.so", id="abi3_stable"), pytest.param("_mod.so", id="simple_so"), pytest.param("_mod.pyd", id="windows_pyd"), pytest.param("_mod.cp314-win_amd64.pyd", id="windows_tagged_pyd"), pytest.param("_mod.dll", id="cygwin_dll"), pytest.param("_mod.pypy39-pp73-x86_64-linux-gnu.so", id="pypy"), ], ) def test_find_stub_path_extension_module(tmp_path: Path, ext_filename: str) -> None: (tmp_path / ext_filename).write_bytes(b"") (tmp_path / "_mod.pyi").write_text("def f(x: int) -> None: ...\n") module = types.ModuleType("_mod") module.__file__ = str(tmp_path / ext_filename) with ( patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getmodule", return_value=module), patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getfile", return_value=module.__file__), ): result = _find_stub_path(lambda: None) assert result is not None assert result.name == "_mod.pyi" def test_find_stub_path_extension_module_no_stub(tmp_path: Path) -> None: ext_filename = "_mod.cpython-312-x86_64-linux-gnu.so" (tmp_path / ext_filename).write_bytes(b"") module = types.ModuleType("_mod") module.__file__ = str(tmp_path / ext_filename) with ( patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getmodule", return_value=module), patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getfile", return_value=module.__file__), ): assert _find_stub_path(lambda: None) is None def test_find_stub_path_extension_module_package_fallback(tmp_path: Path) -> None: ext_filename = "_mod.cpython-312-x86_64-linux-gnu.so" (tmp_path / ext_filename).write_bytes(b"") stub_dir = tmp_path / "stubs" stub_dir.mkdir() (stub_dir / "__init__.pyi").write_text("x: int\n") module = types.ModuleType("_mod") module.__file__ = str(tmp_path / ext_filename) module.__path__ = [str(stub_dir)] with ( patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getmodule", return_value=module), patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getfile", return_value=module.__file__), ): result = _find_stub_path(lambda: None) assert result is not None assert result.name == "__init__.pyi" 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( "class Foo:\n def __new__(cls, x: int) -> Foo: ...\n", "Foo.__new__", {"x": "int", "return": "Foo"}, id="class_new", ), 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("Converter.__new__", {"output": "str", "return": "Converter"}, id="class_new"), pytest.param("fetch", {"url": "str", "return": "str"}, id="async_function"), pytest.param("transform", {"value": "Sequence[int]", "return": "list[str]"}, id="typing_imports"), ], ) 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.parametrize( ("source", "expected"), [ pytest.param("import os\nimport sys\n", {"os": os, "sys": sys}, id="basic_import"), pytest.param("import os as operating_system\n", {"operating_system": os}, id="import_as"), pytest.param("import os.path\n", {"os": os}, id="dotted_import"), pytest.param("from typing import Optional, Any\n", {"Optional": Optional, "Any": Any}, id="from_import"), pytest.param("from typing import Optional as Opt\n", {"Opt": Optional}, id="from_import_as"), pytest.param("from typing import *\n", {}, id="star_import_skipped"), pytest.param("import nonexistent_xyz\nfrom nonexistent_abc import Foo\n", {}, id="missing_module_skipped"), pytest.param("from typing import NonExistentThing\n", {}, id="missing_attr_skipped"), ], ) def test_resolve_stub_imports(source: str, expected: dict[str, Any]) -> None: ns = _resolve_stub_imports(ast.parse(source)) for key, val in expected.items(): assert ns[key] is val if not expected: assert ns == {} @pytest.mark.parametrize( ("source", "expected"), [ pytest.param( "from typing import TypeAlias\nFoo: TypeAlias = int\n", {"Foo"}, id="typing_TypeAlias", ), pytest.param( "import typing\nBar: typing.TypeAlias = str\n", {"Bar"}, id="qualified_TypeAlias", ), pytest.param( "from typing_extensions import TypeAlias\nQux: TypeAlias = int\n", {"Qux"}, id="typing_extensions_TypeAlias", ), pytest.param( "import typing_extensions\nQuux: typing_extensions.TypeAlias = str\n", {"Quux"}, id="qualified_typing_extensions_TypeAlias", ), pytest.param( "type Baz = int | str\n", {"Baz"}, id="type_statement", ), pytest.param( "x: int = 1\ndef f(): ...\n", set(), id="no_aliases", ), ], ) def test_extract_type_alias_names(source: str, expected: set[str]) -> None: assert _extract_type_alias_names(ast.parse(source)) == expected def test_get_stub_context_returns_aliases(stub_mod: Any) -> None: ns, names, owner = _get_stub_context(stub_mod.greet) assert "MyAlias" in names assert ns["Sequence"] is Sequence assert owner == "stub_mod" def test_get_stub_context_returns_empty_for_no_stub() -> None: ns, names, owner = _get_stub_context(test_get_stub_context_returns_empty_for_no_stub) assert ns == {} assert names == set() assert not owner def test_get_stub_context_returns_empty_for_bad_syntax(stub_mod: Any, tmp_path: Path) -> None: bad_stub = tmp_path / "stub_mod.pyi" bad_stub.write_text("def (broken syntax\n") with patch("sphinx_autodoc_typehints._resolver._stubs._find_stub_owner", return_value=(bad_stub, stub_mod)): ns, names, owner = _get_stub_context(stub_mod.greet) assert ns == {} assert names == set() assert not owner _STUB_AST_CACHE.pop(bad_stub, None) @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: .. autoclass:: stub_mod.Converter :members: .. autofunction:: stub_mod.fetch .. autofunction:: stub_mod.transform """ (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 "list" in result warn_text = warning.getvalue() assert "stub_mod" not in warn_text or "forward reference" not in warn_text sys.modules.pop("stub_mod", None) @pytest.mark.sphinx("text", testroot="pyi-stubs") def test_sphinx_build_class_new_uses_param_not_class_var(app: SphinxTestApp, status: StringIO) -> None: template = """\ .. autoclass:: stub_mod.Converter :members: """ (Path(app.srcdir) / "index.rst").write_text(template) app.build() assert "build succeeded" in status.getvalue() result = (Path(app.srcdir) / "_build/text/index.txt").read_text() assert '"str"' in result sys.modules.pop("stub_mod", None) @pytest.mark.sphinx("pseudoxml", testroot="pyi-stubs") def test_sphinx_build_stub_types_produce_crossrefs(app: SphinxTestApp, status: StringIO) -> None: template = """\ .. autofunction:: stub_mod.transform """ (Path(app.srcdir) / "index.rst").write_text(template) app.build() assert "build succeeded" in status.getvalue() result = (Path(app.srcdir) / "_build/pseudoxml/index.pseudoxml").read_text() assert 'classes="xref py py-class"' in result assert "docs.python.org" in result def test_find_stub_path_falls_back_to_parent_package(tmp_path: Path) -> None: pkg_dir = tmp_path / "mypkg" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text("") (pkg_dir / "__init__.pyi").write_text("def greet(name: str) -> str: ...\n") ext_file = pkg_dir / "_ext.cpython-314-darwin.so" ext_file.write_bytes(b"") parent_module = types.ModuleType("mypkg") parent_module.__file__ = str(pkg_dir / "__init__.py") parent_module.__path__ = [str(pkg_dir)] child_module = types.ModuleType("mypkg._ext") child_module.__file__ = str(ext_file) with ( patch.dict(sys.modules, {"mypkg": parent_module, "mypkg._ext": child_module}), patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getmodule", return_value=child_module), patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getfile", side_effect=lambda m: m.__file__), ): result = _find_stub_path(lambda: None) assert result is not None assert result.name == "__init__.pyi" assert result.parent == pkg_dir def test_find_stub_owner_returns_parent_module(tmp_path: Path) -> None: pkg_dir = tmp_path / "mypkg" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text("") (pkg_dir / "__init__.pyi").write_text("def greet(name: str) -> str: ...\n") ext_file = pkg_dir / "_ext.cpython-314-darwin.so" ext_file.write_bytes(b"") parent_module = types.ModuleType("mypkg") parent_module.__file__ = str(pkg_dir / "__init__.py") parent_module.__path__ = [str(pkg_dir)] child_module = types.ModuleType("mypkg._ext") child_module.__file__ = str(ext_file) with ( patch.dict(sys.modules, {"mypkg": parent_module, "mypkg._ext": child_module}), patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getmodule", return_value=child_module), patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getfile", side_effect=lambda m: m.__file__), ): info = _find_stub_owner(lambda: None) assert info is not None stub_path, owner = info assert stub_path.name == "__init__.pyi" assert owner is parent_module def test_find_stub_for_module_returns_none_when_getfile_raises() -> None: module = types.ModuleType("fake") with patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getfile", side_effect=TypeError): assert _find_stub_for_module(module) is None def test_get_module_returns_inspected_module() -> None: assert _get_module(test_get_module_returns_inspected_module) is sys.modules[__name__] def test_get_module_falls_back_to_dunder_module() -> None: obj = MagicMock(spec=[]) obj.__module__ = "sys" with patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getmodule", return_value=None): assert _get_module(obj) is sys def test_get_module_falls_back_to_objclass() -> None: owner = MagicMock() obj = type("FakeDescriptor", (), {"__objclass__": owner, "__module__": "nonexistent_xyz"})() with patch( "sphinx_autodoc_typehints._resolver._stubs.inspect.getmodule", side_effect=[None, sys.modules[__name__]] ): assert _get_module(obj) is sys.modules[__name__] def test_get_module_falls_back_to_self_class() -> None: class _FakeClass: pass obj = MagicMock(spec=[]) obj.__module__ = None obj.__self__ = _FakeClass with patch( "sphinx_autodoc_typehints._resolver._stubs.inspect.getmodule", side_effect=[None, sys.modules[__name__]] ): assert _get_module(obj) is sys.modules[__name__] def test_get_module_returns_none_when_all_fail() -> None: obj = type("Opaque", (), {"__module__": "nonexistent_xyz"})() with patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getmodule", return_value=None): assert _get_module(obj) is None def test_get_stub_context_includes_owner_module_vars(tmp_path: Path) -> None: pkg_dir = tmp_path / "mypkg" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text("") (pkg_dir / "__init__.pyi").write_text("from collections.abc import Sequence\ndef greet(name: str) -> str: ...\n") ext_file = pkg_dir / "_ext.cpython-314-darwin.so" ext_file.write_bytes(b"") parent_module = types.ModuleType("mypkg") parent_module.__file__ = str(pkg_dir / "__init__.py") parent_module.__path__ = [str(pkg_dir)] parent_module.MyAlias = int # type: ignore[attr-defined] child_module = types.ModuleType("mypkg._ext") child_module.__file__ = str(ext_file) with ( patch.dict(sys.modules, {"mypkg": parent_module, "mypkg._ext": child_module}), patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getmodule", return_value=child_module), patch("sphinx_autodoc_typehints._resolver._stubs.inspect.getfile", side_effect=lambda m: m.__file__), ): ns, _names, owner = _get_stub_context(lambda: None) assert ns["Sequence"] is Sequence assert ns["MyAlias"] is int assert owner == "mypkg" @pytest.mark.parametrize( ("source", "owner_package", "expected"), [ pytest.param("from typing import Any", "", "typing", id="absolute"), pytest.param("from .typing import ColorType", "matplotlib", "matplotlib.typing", id="rel_level1"), pytest.param("from . import utils", "matplotlib", "matplotlib", id="rel_level1_bare"), pytest.param("from ..typing import X", "matplotlib.figure", "matplotlib.typing", id="rel_level2"), pytest.param("from .sub import Y", "", "sub", id="rel_no_package"), ], ) def test_resolve_import_from(source: str, owner_package: str, expected: str) -> None: tree = ast.parse(source) node = tree.body[0] assert isinstance(node, ast.ImportFrom) assert _resolve_import_from(node, owner_package) == expected def test_resolve_import_from_level_beyond_package() -> None: node = ast.parse("from ...x import Y").body[0] assert isinstance(node, ast.ImportFrom) assert _resolve_import_from(node, "a") is None def test_resolve_stub_imports_handles_conditional_blocks() -> None: source = textwrap.dedent("""\ import os if sys.version_info >= (3, 11): from typing import Self else: from typing_extensions import Self """) tree = ast.parse(source) ns = _resolve_stub_imports(tree, "") assert "os" in ns assert "Self" in ns def test_resolve_stub_definitions_evaluates_typevars_and_classes() -> None: source = textwrap.dedent("""\ from typing import TypeVar, TypedDict _T = TypeVar("_T") class MyDict(TypedDict): x: int """) tree = ast.parse(source) ns = _resolve_stub_imports(tree, "") assert "_T" in ns assert "MyDict" in ns def test_resolve_stub_imports_relative() -> None: source = "from .typing import ColorType\nfrom typing import Any\n" tree = ast.parse(source) # matplotlib.typing.ColorType is a type alias, check it resolves to something ns = _resolve_stub_imports(tree, "matplotlib") assert "Any" in ns assert ns["Any"] is Any def test_get_stub_context_resolves_relative_imports(tmp_path: Path) -> None: pkg_dir = tmp_path / "relpkg" pkg_dir.mkdir() sub_dir = pkg_dir / "sub" sub_dir.mkdir() (pkg_dir / "__init__.py").write_text("") (sub_dir / "__init__.py").write_text("") (sub_dir / "mod.py").write_text("class Cls:\n pass\n") (sub_dir / "mod.pyi").write_text("from typing import Any\nfrom ..helpers import Helper\nclass Cls:\n x: Any\n") (pkg_dir / "helpers.py").write_text("class Helper:\n pass\n") sys.path.insert(0, str(tmp_path)) try: import relpkg.sub.mod # noqa: PLC0415 # ty: ignore[unresolved-import] ns, _names, owner = _get_stub_context(relpkg.sub.mod.Cls) assert owner == "relpkg.sub.mod" assert "Helper" in ns finally: sys.path.pop(0) for mod_name in [k for k in sys.modules if k.startswith("relpkg")]: sys.modules.pop(mod_name, None) _STUB_AST_CACHE.clear() @pytest.mark.parametrize( ("owner_name", "package_attr", "stub_name", "expected"), [ pytest.param("pkg", "pkg", "__init__.pyi", "pkg", id="package_with_attr"), pytest.param("pkg.warc", None, "warc.pyi", "pkg", id="native_module_no_attr"), pytest.param("pkg.sub", None, "__init__.pyi", "pkg.sub", id="native_subpackage_no_attr"), pytest.param("solo", "", "solo.pyi", "", id="top_level_module"), pytest.param("a.b.c", None, "c.pyi", "a.b", id="deep_module"), ], ) def test_stub_owner_package(owner_name: str, package_attr: str | None, stub_name: str, expected: str) -> None: module = types.ModuleType(owner_name) if package_attr is None: del module.__package__ else: module.__package__ = package_attr assert _stub_owner_package(module, Path(stub_name)) == expected @pytest.fixture(scope="module") def native_subpkg_layout( tmp_path_factory: pytest.TempPathFactory, ) -> Iterator[tuple[Path, types.ModuleType, types.ModuleType]]: root = tmp_path_factory.mktemp("native_subpkg") pkg_dir = root / "natpkg_edge" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text("") (pkg_dir / "warc.pyi").write_text( textwrap.dedent("""\ class WarcRecord: record_id: str """) ) parent = types.ModuleType("natpkg_edge") parent.__file__ = str(pkg_dir / "__init__.py") parent.__path__ = [str(pkg_dir)] child = types.ModuleType("natpkg_edge.warc") yield pkg_dir, parent, child _STUB_AST_CACHE.clear() def test_find_native_submodule_stub_sibling_pyi( native_subpkg_layout: tuple[Path, types.ModuleType, types.ModuleType], ) -> None: _, parent, child = native_subpkg_layout with patch.dict(sys.modules, {"natpkg_edge": parent, "natpkg_edge.warc": child}): result = _find_native_submodule_stub(child) assert result is not None assert result.name == "warc.pyi" def test_find_native_submodule_stub_subpackage_init_pyi(tmp_path: Path) -> None: pkg_dir = tmp_path / "natpkg_sub" pkg_dir.mkdir() sub_stub_dir = pkg_dir / "sub" sub_stub_dir.mkdir() (sub_stub_dir / "__init__.pyi").write_text("x: int\n") parent = types.ModuleType("natpkg_sub") parent.__path__ = [str(pkg_dir)] child = types.ModuleType("natpkg_sub.sub") with patch.dict(sys.modules, {"natpkg_sub": parent, "natpkg_sub.sub": child}): result = _find_native_submodule_stub(child) assert result is not None assert result.name == "__init__.pyi" assert result.parent.name == "sub" @pytest.mark.parametrize( "module_name", [ pytest.param("top_level_native", id="no_dot_in_name"), pytest.param("absent.child", id="parent_missing_in_sys_modules"), ], ) def test_find_native_submodule_stub_returns_none_without_parent(module_name: str) -> None: sys.modules.pop(module_name.partition(".")[0], None) assert _find_native_submodule_stub(types.ModuleType(module_name)) is None def test_find_native_submodule_stub_parent_without_path(tmp_path: Path) -> None: parent = types.ModuleType("flat_native") parent.__file__ = str(tmp_path / "flat.py") child = types.ModuleType("flat_native.child") with patch.dict(sys.modules, {"flat_native": parent, "flat_native.child": child}): assert _find_native_submodule_stub(child) is None def test_find_native_submodule_stub_no_matching_stub(tmp_path: Path) -> None: pkg_dir = tmp_path / "natpkg_empty" pkg_dir.mkdir() parent = types.ModuleType("natpkg_empty") parent.__path__ = [str(pkg_dir)] child = types.ModuleType("natpkg_empty.missing") with patch.dict(sys.modules, {"natpkg_empty": parent, "natpkg_empty.missing": child}): assert _find_native_submodule_stub(child) is None def test_find_stub_for_module_uses_native_submodule_fallback( native_subpkg_layout: tuple[Path, types.ModuleType, types.ModuleType], ) -> None: _, parent, child = native_subpkg_layout with patch.dict(sys.modules, {"natpkg_edge": parent, "natpkg_edge.warc": child}): result = _find_stub_for_module(child) assert result is not None assert result.name == "warc.pyi" @pytest.fixture(scope="module") def native_pkg(tmp_path_factory: pytest.TempPathFactory) -> Iterator[types.ModuleType]: """Compile and import the PyO3-shaped reproduction extension from discussion #698.""" if not sysconfig.get_config_var("LDSHARED"): # pragma: no cover pytest.skip("no C compiler available") build_dir = tmp_path_factory.mktemp("native_pkg_root") pkg_dir = build_dir / "native_pkg" pkg_dir.mkdir() src_pkg = STUB_ROOT / "native_pkg" for name in ("__init__.py", "warc.pyi"): shutil.copyfile(src_pkg / name, pkg_dir / name) ext_suffix = sysconfig.get_config_var("EXT_SUFFIX") so_file = pkg_dir / f"_native{ext_suffix}" include_dir = sysconfig.get_path("include") ldshared = sysconfig.get_config_var("LDSHARED") or f"{sysconfig.get_config_var('CC') or 'cc'} -shared" cflags = sysconfig.get_config_var("CFLAGS") or "" try: subprocess.check_call( [*ldshared.split(), f"-I{include_dir}", *cflags.split(), "-o", str(so_file), str(src_pkg / "_native.c")], cwd=str(pkg_dir), ) except (subprocess.CalledProcessError, FileNotFoundError): # pragma: no cover pytest.skip("C extension compilation failed") sys.path.insert(0, str(build_dir)) try: mod = importlib.import_module("native_pkg") finally: sys.path.pop(0) yield mod for name in [k for k in sys.modules if k == "native_pkg" or k.startswith("native_pkg.")]: sys.modules.pop(name, None) _STUB_AST_CACHE.clear() def test_native_submodule_has_no_file(native_pkg: types.ModuleType) -> None: warc = native_pkg.warc assert not hasattr(warc, "__file__") assert not hasattr(warc, "__path__") assert warc.__name__ == "native_pkg.warc" def test_find_stub_owner_resolves_native_submodule_stub(native_pkg: types.ModuleType) -> None: info = _find_stub_owner(native_pkg.warc.WarcRecord) assert info is not None stub_path, owner = info assert stub_path.name == "warc.pyi" assert owner is native_pkg.warc def test_backfill_from_stub_for_native_submodule(native_pkg: types.ModuleType) -> None: parse_annotations = _backfill_from_stub(native_pkg.warc.parse) assert parse_annotations == {"data": "bytes", "return": "Iterator[WarcRecord]"} def test_get_stub_context_resolves_relative_imports_for_native_submodule(native_pkg: types.ModuleType) -> None: ns, _names, owner = _get_stub_context(native_pkg.warc.parse) assert owner == "native_pkg.warc" assert ns["Iterator"].__module__ == "collections.abc" def test_native_function_signature_from_clinic_docstring(native_pkg: types.ModuleType) -> None: assert inspect.signature(native_pkg.warc.parse).parameters with pytest.raises(ValueError, match="no signature found"): inspect.signature(native_pkg.warc.raw_parse) @pytest.mark.usefixtures("native_pkg") @pytest.mark.sphinx("text", testroot="pyi-stubs") def test_sphinx_build_resolves_param_and_return_for_native_submodule( app: SphinxTestApp, status: StringIO, warning: StringIO ) -> None: (Path(app.srcdir) / "index.rst").write_text( textwrap.dedent("""\ .. autofunction:: native_pkg.warc.parse """) ) app.build() assert "build succeeded" in status.getvalue() result = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) assert "bytes" in result assert "Iterator" in result assert "WarcRecord" in result warn_text = warning.getvalue() assert "native_pkg" not in warn_text or "forward reference" not in warn_text @pytest.mark.usefixtures("native_pkg") @pytest.mark.sphinx("text", testroot="pyi-stubs") def test_sphinx_build_falls_back_to_return_only_without_signature(app: SphinxTestApp, status: StringIO) -> None: (Path(app.srcdir) / "index.rst").write_text( textwrap.dedent("""\ .. autofunction:: native_pkg.warc.raw_parse """) ) app.build() assert "build succeeded" in status.getvalue() result = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) assert "Iterator" in result assert "WarcRecord" in result @pytest.mark.usefixtures("native_pkg") @pytest.mark.sphinx("text", testroot="pyi-stubs") def test_sphinx_build_documents_native_class(app: SphinxTestApp, status: StringIO) -> None: (Path(app.srcdir) / "index.rst").write_text( textwrap.dedent("""\ .. autoclass:: native_pkg.warc.WarcRecord """) ) app.build() assert "build succeeded" in status.getvalue() result = normalize_sphinx_text((Path(app.srcdir) / "_build/text/index.txt").read_text()) assert "WarcRecord" in result sphinx_autodoc_typehints-3.12.0/tests/test_resolver/test_type_comments.py0000644000000000000000000001322613615410400024220 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.12.0/tests/test_resolver/test_type_hints.py0000644000000000000000000003236313615410400023523 0ustar00from __future__ import annotations import csv import importlib import subprocess # noqa: S404 import sys import sysconfig import types from collections.abc import Sequence from csv import Error from pathlib import Path from typing import Any, Union, get_args, get_origin from unittest.mock import MagicMock, patch if sys.version_info >= (3, 14): # pragma: >=3.14 cover import annotationlib import pytest from conftest import make_docstring_app from sphinx_autodoc_typehints import process_docstring from sphinx_autodoc_typehints._annotations import MyTypeAliasForwardRef from sphinx_autodoc_typehints._resolver._type_hints import ( _TYPE_GUARD_IMPORTS_RESOLVED, _build_localns, _execute_guarded_code, _future_annotations_imported, _get_type_hint, _resolve_string_annotations, _resolve_type_guarded_imports, _run_guarded_import, _should_skip_guarded_import_resolution, get_all_type_hints, get_descriptor_type_hint, ) STUB_ROOT = Path(__file__).parent.parent / "roots" / "test-pyi-stubs" 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, {}) == {} @pytest.fixture def non_subscriptable_generic_func() -> Any: # pragma: >=3.14 cover # dont_inherit keeps this file's `from __future__ import annotations` (PEP 563) out of the # compiled module so its annotations stay lazily evaluated (PEP 649) source = "class NotGeneric: ...\n\n\ndef func(g: NotGeneric[int]) -> None: ...\n" ns: dict[str, Any] = {} exec(compile(source, "", "exec", dont_inherit=True), ns) # noqa: S102 return ns["func"] @pytest.mark.skipif(sys.version_info < (3, 14), reason="PEP 649 lazy annotation evaluation is Python 3.14+") def test_get_type_hint_unevaluatable_annotation_falls_back_to_forward_ref( non_subscriptable_generic_func: Any, ) -> None: # pragma: >=3.14 cover """Annotations whose evaluation raises TypeError degrade to ForwardRef proxies (issue #712).""" result = _get_type_hint([], "test.func", non_subscriptable_generic_func, {}) assert result["g"].__forward_arg__ == "NotGeneric[int]" @pytest.mark.skipif(sys.version_info < (3, 14), reason="PEP 649 lazy annotation evaluation is Python 3.14+") def test_get_type_hint_forward_ref_fallback_failure_returns_empty( non_subscriptable_generic_func: Any, ) -> None: # pragma: >=3.14 cover """When even the FORWARDREF format cannot evaluate the annotations, fall back to no hints.""" with patch("sphinx_autodoc_typehints._resolver._type_hints.annotationlib.get_annotations", side_effect=TypeError): assert _get_type_hint([], "test.func", non_subscriptable_generic_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) def test_build_localns_adds_ancestor_classes() -> None: import tests.roots.test_nested_attrs_localns as mod # noqa: PLC0415 assert _build_localns(mod.Outer.Inner.__init__, {})["Outer"] is mod.Outer def test_build_localns_no_qualname() -> None: def func() -> None: ... func.__qualname__ = "func" localns: dict[Any, Any] = {"existing": 1} assert _build_localns(func, localns) == {"existing": 1} def test_build_localns_preserves_existing_localns() -> None: import tests.roots.test_nested_attrs_localns as mod # noqa: PLC0415 localns: dict[Any, Any] = {"key": "value"} assert (result := _build_localns(mod.Outer.Inner.__init__, localns))["key"] == "value" assert result["Outer"] is mod.Outer def test_resolve_string_annotations_keeps_unresolvable_strings() -> None: obj = MagicMock() obj.__module__ = "builtins" result = _resolve_string_annotations(obj, {"x": "NoSuchType", "y": "int"}, {}) assert result["x"] == "NoSuchType" assert result["y"] is int def test_resolve_string_annotations_passes_non_strings() -> None: obj = MagicMock() obj.__module__ = "builtins" result = _resolve_string_annotations(obj, {"x": int, "return": str}, {}) # type: ignore[dict-item] assert result["x"] is int assert result["return"] is str @pytest.fixture(scope="session") def c_ext_mod(tmp_path_factory: pytest.TempPathFactory) -> Any: if not sysconfig.get_config_var("LDSHARED"): pytest.skip("no C compiler available") build_dir = tmp_path_factory.mktemp("c_ext") ext_suffix = sysconfig.get_config_var("EXT_SUFFIX") so_file = build_dir / f"c_ext_mod{ext_suffix}" include_dir = sysconfig.get_path("include") cc = sysconfig.get_config_var("CC") or "cc" ldshared = sysconfig.get_config_var("LDSHARED") or f"{cc} -shared" cflags = sysconfig.get_config_var("CFLAGS") or "" c_src = str(STUB_ROOT / "c_ext_mod.c") try: subprocess.check_call( [*ldshared.split(), f"-I{include_dir}", *cflags.split(), "-o", str(so_file), c_src], cwd=str(STUB_ROOT), ) except (subprocess.CalledProcessError, FileNotFoundError): pytest.skip("C extension compilation failed") for stub in STUB_ROOT.glob("c_ext_mod.pyi"): (build_dir / stub.name).write_text(stub.read_text()) sys.path.insert(0, str(build_dir)) try: mod = importlib.import_module("c_ext_mod") finally: sys.path.pop(0) return mod def test_get_all_type_hints_resolves_stub_annotations_for_c_extension(c_ext_mod: Any) -> None: result = get_all_type_hints([], c_ext_mod.greet, "c_ext_mod.greet", {}) assert result["name"] is str assert result["greeting"] == Sequence[str] assert result["return"] is str def test_get_all_type_hints_preserves_stub_type_aliases(c_ext_mod: Any) -> None: result = get_all_type_hints([], c_ext_mod.with_hook, "c_ext_mod.with_hook", {}) assert isinstance(result["callback"], MyTypeAliasForwardRef) assert result["callback"].name == "GreetHook" def test_descriptor_type_hint_resolves_from_stub(c_ext_mod: Any) -> None: assert get_descriptor_type_hint(c_ext_mod.Encoder.depth) is int def test_descriptor_type_hint_preserves_stub_type_aliases(c_ext_mod: Any) -> None: hint = get_descriptor_type_hint(c_ext_mod.Encoder.hook) args = get_args(hint) assert len(args) == 2 encoder_hook = args[0] if isinstance(args[0], MyTypeAliasForwardRef) else args[1] assert encoder_hook.name == "EncoderHook" def test_descriptor_type_hint_resolves_class_annotation(c_ext_mod: Any) -> None: assert get_descriptor_type_hint(c_ext_mod.Encoder.flags) is int def test_descriptor_type_hint_inside_version_guard(c_ext_mod: Any) -> None: assert get_descriptor_type_hint(c_ext_mod.Encoder.guarded) is bool def test_descriptor_type_hint_for_non_class_stub_node(c_ext_mod: Any) -> None: fake_class = type("greet", (), {"__module__": c_ext_mod.__name__, "__qualname__": "greet"}) descriptor = types.SimpleNamespace(__objclass__=fake_class, __name__="depth") assert get_descriptor_type_hint(descriptor) is None def test_descriptor_type_hint_for_name_missing_from_stub(c_ext_mod: Any) -> None: descriptor = types.SimpleNamespace(__objclass__=c_ext_mod.Encoder, __name__="missing") assert get_descriptor_type_hint(descriptor) is None def test_process_docstring_injects_descriptor_type(c_ext_mod: Any) -> None: app = make_docstring_app() lines = ["current nesting depth"] process_docstring(app, "attribute", "c_ext_mod.Encoder.depth", c_ext_mod.Encoder.depth, None, lines) assert lines[0] == "current nesting depth" assert lines[-1].startswith(":type: ") assert "int" in lines[-1] def test_descriptor_type_hint_without_stub_is_none() -> None: import array # noqa: PLC0415 assert get_descriptor_type_hint(array.array.typecode) is None def test_descriptor_type_hint_ignores_non_descriptors() -> None: assert get_descriptor_type_hint(object()) is None def test_get_all_type_hints_resolves_c_extension_class_new(c_ext_mod: Any) -> None: result = get_all_type_hints([], c_ext_mod.Encoder.__new__, "c_ext_mod.Encoder.__new__", {}) default_type = result["default"] # Python 3.14+ produces types.UnionType for `X | None`, earlier versions produce typing.Union/Optional. assert get_origin(default_type) in {types.UnionType, Union} args = get_args(default_type) assert len(args) == 2 encoder_hook = args[0] if isinstance(args[0], MyTypeAliasForwardRef) else args[1] assert isinstance(encoder_hook, MyTypeAliasForwardRef) assert encoder_hook.name == "EncoderHook" def test_stub_annotations_not_polluted_on_repeated_calls(tmp_path: Path) -> None: pkg = tmp_path / "stubpkg" pkg.mkdir() (pkg / "__init__.py").write_text("") (pkg / "_types.py").write_text("class MyType: ...\n") (pkg / "mod.py").write_text("class Klass:\n def method(self, x): ...\n") (pkg / "mod.pyi").write_text( "from stubpkg._types import MyType\nclass Klass:\n def method(self, x: MyType) -> None: ...\n" ) sys.path.insert(0, str(tmp_path)) mod = importlib.import_module("stubpkg.mod") _TYPE_GUARD_IMPORTS_RESOLVED.discard("stubpkg.mod") my_type = importlib.import_module("stubpkg._types").MyType try: result1 = get_all_type_hints([], mod.Klass.method, "stubpkg.mod.Klass.method", {}) assert result1["x"] is my_type _TYPE_GUARD_IMPORTS_RESOLVED.discard("stubpkg.mod") result2 = _get_type_hint([], "another.Klass.method", mod.Klass.method, {}) assert result2.get("x") is my_type finally: sys.path.pop(0) for name in [n for n in sys.modules if n.startswith("stubpkg")]: del sys.modules[name] _TYPE_GUARD_IMPORTS_RESOLVED.discard("stubpkg.mod") @pytest.mark.skipif(sys.version_info < (3, 14), reason="annotationlib requires Python 3.14+") def test_get_type_hint_uses_annotationlib_on_name_error() -> None: # pragma: >=3.14 cover """_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.12.0/tests/test_resolver/test_util.py0000644000000000000000000000734713615410400022316 0ustar00from __future__ import annotations from typing import Any from unittest.mock import MagicMock, create_autospec, patch import pytest from sphinx.environment import BuildEnvironment from sphinx_autodoc_typehints._annotations import MyTypeAliasForwardRef from sphinx_autodoc_typehints._resolver._util import collect_documented_type_aliases, 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] def _make_env_with_types(type_names: list[str]) -> Any: obj_info = MagicMock() obj_info.objtype = "type" py_objects = dict.fromkeys(type_names, obj_info) domain = MagicMock() domain.objects = py_objects env = create_autospec(BuildEnvironment, instance=True) env.get_domain.return_value = domain return env def test_collect_documented_type_aliases_no_annotations() -> None: env = _make_env_with_types(["mymod.EncoderHook", "mymod.TagHook"]) obj = MagicMock(spec=[]) deferred, eager = collect_documented_type_aliases(obj, "mymod", env) assert "EncoderHook" in deferred assert "TagHook" in deferred assert isinstance(deferred["EncoderHook"], MyTypeAliasForwardRef) assert eager == {} def test_collect_documented_type_aliases_preserves_eager_path() -> None: env = _make_env_with_types(["mymod.MyAlias"]) def func(x: int) -> str: ... func.__annotations__ = {"x": int} func.__globals__["MyAlias"] = int deferred, eager = collect_documented_type_aliases(func, "mymod", env) assert "MyAlias" in deferred assert id(int) in eager def test_collect_documented_type_aliases_ignores_other_module_types() -> None: env = _make_env_with_types(["other.Foo"]) obj = MagicMock(spec=[]) deferred, eager = collect_documented_type_aliases(obj, "mymod", env) assert "Foo" not in deferred assert eager == {} def test_collect_documented_type_aliases_ignores_unqualified_names() -> None: env = _make_env_with_types(["EncoderHook"]) obj = MagicMock(spec=[]) deferred, eager = collect_documented_type_aliases(obj, "cbor2", env) assert "EncoderHook" not in deferred assert eager == {} @pytest.mark.parametrize( "error", [ pytest.param(NameError("Callable"), id="name-error"), pytest.param(TypeError("type 'DiGraph' is not subscriptable"), id="type-error"), ], ) def test_collect_documented_type_aliases_annotations_error(error: Exception) -> None: """PEP 649 lazy annotation evaluation raising (NameError for TYPE_CHECKING-only names, TypeError for subscripting a non-generic class) must not propagate (issue #712).""" class _AnnotationsRaiser: @property def __annotations__(self) -> dict[str, object]: # noqa: PLW3201 raise error env = _make_env_with_types(["mymod.MyType"]) deferred, eager = collect_documented_type_aliases(_AnnotationsRaiser(), "mymod", env) assert "MyType" in deferred assert eager == {} sphinx_autodoc_typehints-3.12.0/.gitignore0000644000000000000000000000013613615410400015643 0ustar00dist *.egg *.py[codz] *$py.class .tox .*_cache /src/sphinx_autodoc_typehints/version.py venv* sphinx_autodoc_typehints-3.12.0/LICENSE0000644000000000000000000000211513615410400014657 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.12.0/README.md0000644000000000000000000004342613615410400015143 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`) - Automatically fixes cross-references for stdlib types whose runtime module differs from their documented path - 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) - [Control overload signature display](#control-overload-signature-display) - [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 stdlib types](#fix-cross-reference-links-for-stdlib-types) - [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" ``` ### Control overload signature display When a function has [`@overload`](https://docs.python.org/3/library/typing.html#typing.overload) signatures, they are rendered automatically in the docstring. To disable this globally: ```python typehints_document_overloads = False ``` To disable overloads for a single function while keeping them everywhere else, add `:no-overloads:` to the docstring: ```python @overload def f(x: int) -> str: ... @overload def f(x: str) -> bool: ... def f(x): """:no-overloads: f accepts int or str, see docs for details. """ ``` The `:no-overloads:` directive is stripped from the rendered output. ### 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 stdlib types Many Python stdlib classes have a `__module__` that points to an internal module rather than their public documented path. For example, `threading.local` reports its module as `_thread._local`, and `concurrent.futures.Executor` as `concurrent.futures._base.Executor`. This produces broken cross-reference links because the extension builds links from `__module__.__qualname__`. When [intersphinx](https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html) is enabled, the extension automatically fixes this. After intersphinx loads its inventories, it builds a reverse mapping from runtime paths to documented paths and applies it during annotation formatting. No configuration is needed — just make sure intersphinx is loaded: ```python extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx_autodoc_typehints", ] intersphinx_mapping = { "python": ("https://docs.python.org/3", None), } ``` With this, a type hint like `threading.local` correctly links to `threading.local` in the Python docs instead of producing a broken `_thread._local` reference. ### Fix cross-reference links for renamed modules Some third-party 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_document_overloads` | `True` | Show `@overload` signatures in docs. Use `:no-overloads:` in a docstring for per-function control. | | `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.12.0/pyproject.toml0000644000000000000000000001370213615410400016572 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>=25.4", "covdefaults>=2.3", "coverage>=7.13.4", "defusedxml>=0.7.1", "diff-cover>=10.2", "numpydoc>=1.10", "pytest>=9.0.2", "pytest-cov>=7", "sphobjinv>=2.3.1.3", "typing-extensions>=4.15", ] type = [ "ty>=0.0.19,<0.0.25", { 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.6", ] [tool.hatch] version.source = "vcs" build.hooks.vcs.version-file = "src/sphinx_autodoc_typehints/version.py" [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" = [ "B903", # test fixture classes intentionally use plain __init__ without dataclass "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 "PYI033", # we test for legacy `# type:` comments "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-integration/mod_eager.py" = [ "I002", # this file intentionally omits `from __future__ import annotations` to test non-deferred annotations ] lint.per-file-ignores."tests/roots/test-integration/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-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/roots/test-pyi-stubs/stub_mod.pyi" = [ "I002", # stub files don't use `from __future__ import annotations` ] 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.ty] src.exclude = [ "tests/roots/" ] environment.python-version = "3.14" overrides = [ { include = [ "tests/" ], rules.empty-body = "ignore" } ] [tool.pytest] ini_options.testpaths = [ "tests", ] ini_options.filterwarnings = [ "error", "ignore:.*rm_rf.*:pytest.PytestWarning", "ignore::sphinx.deprecation.RemovedInSphinx11Warning", # numpydoc upstream: https://github.com/numpy/numpydoc/issues/679 "ignore:urllib3.*doesn't match a supported version", ] [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 = [ "tests/roots/*", ] html.show_contexts = true html.skip_covered = false sphinx_autodoc_typehints-3.12.0/PKG-INFO0000644000000000000000000004576313615410400014767 0ustar00Metadata-Version: 2.4 Name: sphinx-autodoc-typehints Version: 3.12.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`) - Automatically fixes cross-references for stdlib types whose runtime module differs from their documented path - 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) - [Control overload signature display](#control-overload-signature-display) - [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 stdlib types](#fix-cross-reference-links-for-stdlib-types) - [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" ``` ### Control overload signature display When a function has [`@overload`](https://docs.python.org/3/library/typing.html#typing.overload) signatures, they are rendered automatically in the docstring. To disable this globally: ```python typehints_document_overloads = False ``` To disable overloads for a single function while keeping them everywhere else, add `:no-overloads:` to the docstring: ```python @overload def f(x: int) -> str: ... @overload def f(x: str) -> bool: ... def f(x): """:no-overloads: f accepts int or str, see docs for details. """ ``` The `:no-overloads:` directive is stripped from the rendered output. ### 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 stdlib types Many Python stdlib classes have a `__module__` that points to an internal module rather than their public documented path. For example, `threading.local` reports its module as `_thread._local`, and `concurrent.futures.Executor` as `concurrent.futures._base.Executor`. This produces broken cross-reference links because the extension builds links from `__module__.__qualname__`. When [intersphinx](https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html) is enabled, the extension automatically fixes this. After intersphinx loads its inventories, it builds a reverse mapping from runtime paths to documented paths and applies it during annotation formatting. No configuration is needed — just make sure intersphinx is loaded: ```python extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx_autodoc_typehints", ] intersphinx_mapping = { "python": ("https://docs.python.org/3", None), } ``` With this, a type hint like `threading.local` correctly links to `threading.local` in the Python docs instead of producing a broken `_thread._local` reference. ### Fix cross-reference links for renamed modules Some third-party 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_document_overloads` | `True` | Show `@overload` signatures in docs. Use `:no-overloads:` in a docstring for per-function control. | | `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.