diff --git a/.github/release.yml b/.github/release.yml index 9d1e0987..5f89818f 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -1,5 +1,5 @@ changelog: exclude: authors: - - dependabot - - pre-commit-ci + - dependabot[bot] + - pre-commit-ci[bot] diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 0316ecb7..61c26e88 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -19,29 +19,30 @@ jobs: fail-fast: false matrix: env: + - "3.14t" + - "3.14" - "3.13" - "3.12" - "3.11" - - "3.10" - type - dev - pkg_meta steps: - name: Install OS dependencies run: sudo apt-get install graphviz -y - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Install the latest version of uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v7 with: enable-cache: true cache-dependency-glob: "pyproject.toml" github-token: ${{ secrets.GITHUB_TOKEN }} - name: Install tox - run: uv tool install --python-preference only-managed --python 3.13 tox --with tox-uv + 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.13' + 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 }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8612092b..f773e029 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -10,17 +10,17 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Install the latest version of uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v7 with: enable-cache: true cache-dependency-glob: "pyproject.toml" github-token: ${{ secrets.GITHUB_TOKEN }} - name: Build package - run: uv build --python 3.13 --python-preference only-managed --sdist --wheel . --out-dir dist + run: uv build --python 3.14 --python-preference only-managed --sdist --wheel . --out-dir dist - name: Store the distribution packages uses: actions/upload-artifact@v4 with: @@ -38,11 +38,11 @@ jobs: id-token: write steps: - name: Download all the dists - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: ${{ env.dists-artifact-name }} path: dist/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.12.2 + uses: pypa/gh-action-pypi-publish@v1.13.0 with: attestations: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b88f80dd..62da4c0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,41 +1,41 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.4 + rev: "0.34.0" hooks: - id: check-github-workflows args: ["--verbose"] - repo: https://github.com/codespell-project/codespell - rev: v2.3.0 + rev: v2.4.1 hooks: - id: codespell - additional_dependencies: ["tomli>=2.0.1"] + additional_dependencies: ["tomli>=2.2.1"] - repo: https://github.com/tox-dev/tox-ini-fmt - rev: "1.4.1" + rev: "1.6.0" hooks: - id: tox-ini-fmt args: ["-p", "fix"] - repo: https://github.com/tox-dev/pyproject-fmt - rev: "v2.5.0" + rev: "v2.10.0" hooks: - id: pyproject-fmt - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.7.4" + rev: "v0.14.0" hooks: - id: ruff-format - - id: ruff + - id: ruff-check args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] - repo: https://github.com/rbubley/mirrors-prettier - rev: "v3.3.3" # Use the sha / tag you want to point at + rev: "v3.6.2" # Use the sha / tag you want to point at hooks: - id: prettier additional_dependencies: - - prettier@3.3.3 - - "@prettier/plugin-xml@3.4.1" + - prettier@3.6.2 + - "@prettier/plugin-xml@3.4.2" - repo: meta hooks: - id: check-hooks-apply diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index c17fb4cb..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,320 +0,0 @@ -# Changelog - -## 1.22 - -- Allow Sphinx explicitly to write in parallel. -- Fixed crash when documenting ParamSpecArgs - -## 1.21.7 - -- Fixed a bug where if a class has an attribute and a constructor argument with the same name, the constructor argument - type would be rendered incorrectly (issue 308) - -- Fixed napoleon handling of numpy docstrings with no specified return type. - -## 1.21.6 - -- Fix a `Field list ends without a blank line` warning (issue 305). - -## 1.21.5 - -- More robust determination of rtype location / fix issue 302 - -## 1.21.4 - -- Improvements to the location of the return type - -## 1.21.3 - -- Use format_annotation to render class attribute type annotations - -## 1.21.2 - -- Fix overloads support - -## 1.21.1 - -- Fix spacing between `:rtype:` and directives - -## 1.21 - -- Handle types from types module -- If module is \_io, use io instead -- Put rtype before examples or usage section -- Remove redundant return type for attributes -- Handle collections.abc.Callable as well as typing.Callable -- Put Literal args in code blocks - -## 1.20.2 - -- Fix Optional role to be data. - -## 1.20.1 - -- Fixed default options not displaying for parameters without type hints. - -## 1.20 - -- Use hatchling instead of setuptools -- Add support for typing.ParamSpec -- Allow star prefixes for parameter names in docstring - -## 1.19.2 - -- Fix incorrect domain used for collections.abc.Callable. - -## 1.19.1 - -- Fix bug for recursive type alias. - -## 1.19.0 - -- Support for CPython 3.11, no longer adds `Optional` when the argument is default per - [recommendation from PEP-484](https://github.com/tox-dev/sphinx-autodoc-typehints/pull/247). - -## 1.18.3 - -- Support and require `nptyping>=2.1.2` - -## 1.18.2 - -- Support and require `nptyping>=2.1.1` - -## 1.18.1 - -- Fix mocked module import not working when used as guarded import - -## 1.18.0 - -- Support and require `nptyping>=2` -- Handle `UnionType` - -## 1.17.1 - -- Mark it as requiring `nptyping<2` - -## 1.17.0 - -- Add `typehints_use_rtype` option -- Handles `TypeError` when getting source code via inspect - -## 1.16.0 - -- Add support for type subscriptions with multiple elements, where one or more elements are tuples; e.g., - `nptyping.NDArray[(Any, ...), nptyping.Float]` -- Fix bug for arbitrary types accepting singleton subscriptions; e.g., `nptyping.Float[64]` -- Resolve forward references -- Expand and better handle `TypeVar` -- Add intershpinx reference link for `...` to `Ellipsis` (as is just an alias) - -## 1.15.3 - -- Prevents reaching inner blocks that contains `if TYPE_CHECKING` - -## 1.15.2 - -- Log a warning instead of crashing when a type guard import fails to resolve -- When resolving type guard imports if the target module does not have source code (such is the case for C-extension - modules) do nothing instead of crashing - -## 1.15.1 - -- Fix `fully_qualified` should be `typehints_fully_qualified` - -## 1.15.0 - -- Resolve type guard imports before evaluating annotations for objects -- Remove `set_type_checking_flag` flag as this is now done by default -- Fix crash when the `inspect` module returns an invalid python syntax source -- Made formatting function configurable using the option `typehints_formatter` - -## 1.14.1 - -- Fixed `normalize_source_lines()` messing with the indentation of methods with decorators that have parameters starting - with `def`. -- Handle `ValueError` or `TypeError` being raised when signature of an object cannot be determined -- Fix `KeyError` being thrown when argument is not documented (e.g. `cls` argument for class methods, and `self` for - methods) - -## 1.14.0 - -- Added `typehints_defaults` config option allowing to automatically annotate parameter defaults. - -## 1.13.1 - -- Fixed `NewType` inserts a reference as first argument instead of a string - -## 1.13.0 - -- Dropped Python 3.6 support -- Python 3.10 support -- Normalize async functions properly -- Allow py310 style annotations (PEP-563) - -## 1.12.0 - -- Dropped Python 3.5 support -- Added the simplify_optional_unions config option (PR by tillhainbach) -- Fixed indentation of multiline strings (PR by Yuxin Wu) - -## 1.11.1 - -- Changed formatting of `None` to point to the Python stdlib docs (PR by Dominic Davis-Foster) -- Updated special dataclass handling (PR by Lihu Ben-Ezri-Ravin) - -## 1.11.0 - -- Dropped support for Sphinx \< 3.0 -- Added support for alternative parameter names (`arg`, `argument`, `parameter`) -- Fixed import path for Signature (PR by Matthew Treinish) -- Fixed `TypeError` when formatting a parametrized `typing.IO` annotation -- Fixed data class displaying a return type in its `__init__()` method - -## 1.10.3 - -- Fixed `TypeError` (or wrong rendered class name) when an annotation is a generic class that has a `name` property - -## 1.10.2 - -- Fixed inner classes missing their parent class name(s) when rendered - -## 1.10.1 - -- Fixed `KeyError` when encountering mocked annotations (`autodoc_mock_imports`) - -## 1.10.0 - -- Rewrote the annotation formatting logic (fixes Python 3.5.2 compatibility regressions and an `AttributeError` - regression introduced in v1.9.0) -- Fixed decorator classes not being processed as classes - -## 1.9.0 - -- Added support for [typing_extensions](https://pypi.org/project/typing-extensions/) -- Added the `typehints_document_rtype` option (PR by Simon-Martin Schröder) -- Fixed metaclasses as annotations causing `TypeError` -- Fixed rendering of `typing.Literal` -- Fixed OSError when generating docs for SQLAlchemy mapped classes -- Fixed unparametrized generic classes being rendered with their type parameters (e.g. `Dict[~KT, ~VT]`) - -## 1.8.0 - -- Fixed regression which caused `TypeError` or `OSError` when trying to set annotations due to PR #87 -- Fixed unintentional mangling of annotation type names -- Added proper `:py:data` targets for `NoReturn`, `ClassVar` and `Tuple` -- Added support for inline type comments (like `(int, str) -> None`) (PR by Bernát Gábor) -- Use the native AST parser for type comment support on Python 3.8+ - -## 1.7.0 - -- Dropped support for Python 3.4 -- Fixed unwrapped local functions causing errors (PR by Kimiyuki Onaka) -- Fixed `AttributeError` when documenting the `__init__()` method of a data class -- Added support for type hint comments (PR by Markus Unterwaditzer) -- Added flag for rendering classes with their fully qualified names (PR by Holly Becker) - -## 1.6.0 - -- Fixed `TypeError` when formatting annotations from a class that inherits from a concrete generic type (report and - tests by bpeake-illuscio) -- Added support for `typing_extensions.Protocol` (PR by Ian Good) -- Added support for `typing.NewType` (PR by George Leslie-Waksman) - -## 1.5.2 - -- Emit a warning instead of crashing when an unresolvable forward reference is encountered in type annotations - -## 1.5.1 - -- Fixed escape characters in parameter default values getting lost during signature processing -- Replaced use of the `config-inited` event (which inadvertently required Sphinx 1.8) with the `builder-inited` event - -## 1.5.0 - -- The setting of the `typing.TYPECHECKING` flag is now configurable using the `set_type_checking_flag` option - -## 1.4.0 - -- The extension now sets `typing.TYPECHECKING` to `True` during setup to include conditional imports which may be used - in type annotations -- Fixed parameters with trailing underscores (PR by Daniel Knell) -- Fixed KeyError with private methods (PR by Benito Palacios Sánchez) -- Fixed deprecation warning about the use of formatargspec (PR by Y. Somda) -- The minimum Sphinx version is now v1.7.0 - -## 1.3.1 - -- Fixed rendering of generic types outside the typing module (thanks to Tim Poterba for the PR) - -## 1.3.0 - -- Fixed crash when processing docstrings from nested classes (thanks to dilyanpalauzov for the fix) -- Added support for Python 3.7 -- Dropped support for Python 3.5.0 and 3.5.1 - -## 1.2.5 - -- Ensured that `:rtype:` doesn\'t get joined with a paragraph of text (thanks to Bruce Merry for the PR) - -## 1.2.4 - -- Removed support for `backports.typing` as it has been removed from the PyPI -- Fixed first parameter being cut out from class methods and static methods (thanks to Josiah Wolf Oberholtzer for the - PR) - -## 1.2.3 - -- Fixed `process_signature()` clobbering any explicitly overridden signatures from the docstring - -## 1.2.2 - -- Explicitly prefix `:class:`, `:mod:` et al with `:py:`, in case `py` is not the default domain of the project (thanks - Monty Taylor) - -## 1.2.1 - -- Fixed ``ValueError` when``getargspec()\`\` encounters a built-in function -- Fixed `AttributeError` when `Any` is combined with another type in a `Union` (thanks Davis Kirkendall) - -## 1.2.0 - -- Fixed compatibility with Python 3.6 and 3.5.3 -- Fixed `NameError` when processing signatures of wrapped functions with type hints -- Fixed handling of slotted classes with no `__init__()` method -- Fixed Sphinx warning about parallel reads -- Fixed return type being added to class docstring from its `__init__()` method (thanks to Manuel Krebber for the patch) -- Fixed return type hints of `@property` methods being omitted (thanks to pknight for the patch) -- Added a test suite (thanks Manuel Krebber) - -## 1.1.0 - -- Added proper support for `typing.Tuple` (pull request by Manuel Krebber) - -## 1.0.6 - -- Fixed wrong placement of `:rtype:` if a multi-line `:param:` or a `:returns:` is used - -## 1.0.5 - -- Fixed coroutine functions\' signatures not being processed when using sphinxcontrib-asyncio - -## 1.0.4 - -- Fixed compatibility with Sphinx 1.4 - -## 1.0.3 - -- Fixed \"self\" parameter not being removed from exception class constructor signatures -- Fixed process_signature() erroneously removing the first argument of a static method - -## 1.0.2 - -- Fixed exception classes not being processed like normal classes - -## 1.0.1 - -- Fixed errors caused by forward references not being looked up with the right globals - -## 1.0.0 - -- Initial release diff --git a/README.md b/README.md index 7ddbf092..db11c3d9 100644 --- a/README.md +++ b/README.md @@ -64,16 +64,17 @@ The following configuration options are accepted: `True`, add stub documentation for undocumented parameters to be able to add type info. - `always_use_bars_union ` (default: `False`): If `True`, display Union's using the | operator described in PEP 604. (e.g `X` | `Y` or `int` | `None`). If `False`, Unions will display with the typing in brackets. (e.g. `Union[X, Y]` - or `Optional[int]`) + or `Optional[int]`). Note that on 3.14 and later this will always be `True` and not configurable due the interpreter + no longer differentiating between the two types, and we have no way to determine what the user used. - `typehints_document_rtype` (default: `True`): If `False`, never add an `:rtype:` directive. If `True`, add the `:rtype:` directive if no existing `:rtype:` is found. +- `typehints_document_rtype_none` (default: `True`): If `False`, never add an `:rtype: None` directive. If `True`, add the `:rtype: None`. - `typehints_use_rtype` (default: `True`): Controls behavior when `typehints_document_rtype` is set to `True`. If `True`, document return type in the `:rtype:` directive. If `False`, document return type as part of the `:return:` directive, if present, otherwise fall back to using `:rtype:`. Use in conjunction with [napoleon_use_rtype](https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#confval-napoleon_use_rtype) to avoid generation of duplicate or redundant return type information. - `typehints_defaults` (default: `None`): If `None`, defaults are not added. Otherwise, adds a default annotation: - - `'comma'` adds it after the type, changing Sphinx’ default look to “**param** (_int_, default: `1`) -- text”. - `'braces'` adds `(default: ...)` after the type (useful for numpydoc like styles). - `'braces-after'` adds `(default: ...)` at the end of the parameter documentation text instead. @@ -88,6 +89,13 @@ The following configuration options are accepted: code or `None` to fall back to the default formatter. - `typehints_use_signature` (default: `False`): If `True`, typehints for parameters in the signature are shown. - `typehints_use_signature_return` (default: `False`): If `True`, return annotations in the signature are shown. +- `suppress_warnings`: sphinx-autodoc-typehints supports to suppress warning messages via Sphinx's `suppress_warnings`. It allows following additional warning types: + - `sphinx_autodoc_typehints` + - `sphinx_autodoc_typehints.comment` + - `sphinx_autodoc_typehints.forward_reference` + - `sphinx_autodoc_typehints.guarded_import` + - `sphinx_autodoc_typehints.local_function` + - `sphinx_autodoc_typehints.multiple_ast_nodes` ## How it works diff --git a/pyproject.toml b/pyproject.toml index 5c8cbfea..47b54378 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,8 @@ [build-system] build-backend = "hatchling.build" requires = [ - "hatch-vcs>=0.4", - "hatchling>=1.25", + "hatch-vcs>=0.5", + "hatchling>=1.27", ] [project] @@ -23,7 +23,7 @@ maintainers = [ authors = [ { name = "Bernát Gábor", email = "gaborjbernat@gmail.com" }, ] -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: Sphinx :: Extension", @@ -31,35 +31,32 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Documentation :: Sphinx", ] dynamic = [ "version", ] dependencies = [ - "sphinx>=8.0.2", + "sphinx>=8.2.3", ] optional-dependencies.docs = [ - "furo>=2024.8.6", -] -optional-dependencies.numpy = [ - "nptyping>=2.5", + "furo>=2025.9.25", ] optional-dependencies.testing = [ "covdefaults>=2.3", - "coverage>=7.6.1", - "defusedxml>=0.7.1", # required by sphinx.testing - "diff-cover>=9.1.1", - "pytest>=8.3.2", - "pytest-cov>=5", - "sphobjinv>=2.3.1.1", - "typing-extensions>=4.12.2", -] -urls.Changelog = "https://github.com/tox-dev/sphinx-autodoc-typehints/blob/main/CHANGELOG.md" + "coverage>=7.10.7", + "defusedxml>=0.7.1", # required by sphinx.testing + "diff-cover>=9.7.1", + "pytest>=8.4.2", + "pytest-cov>=7", + "sphobjinv>=2.3.1.3", + "typing-extensions>=4.15", +] +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" @@ -69,7 +66,6 @@ build.hooks.vcs.version-file = "src/sphinx_autodoc_typehints/version.py" version.source = "vcs" [tool.ruff] -target-version = "py310" line-length = 120 format.preview = true format.docstring-code-line-length = 100 @@ -78,7 +74,6 @@ lint.select = [ "ALL", ] lint.ignore = [ - "ANN101", # no type annotation for self "ANN401", # allow Any as type annotation "COM812", # Conflict with formatter "CPY", # No copyright statements @@ -98,6 +93,9 @@ lint.per-file-ignores."tests/**/*.py" = [ "PLR2004", # Magic value used in comparison, consider replacing with a constant variable "S101", # asserts allowed in tests "S603", # `subprocess` call: check for execution of untrusted input + "UP006", # we test for old List/Tuple syntax + "UP007", # we test for old Union syntax + "UP045", # we test for old Optional syntax ] lint.isort = { known-first-party = [ "sphinx_autodoc_typehints", @@ -114,7 +112,7 @@ write-changes = true count = true [tool.pyproject-fmt] -max_supported_python = "3.13" +max_supported_python = "3.14" [tool.pytest.ini_options] testpaths = [ @@ -135,7 +133,7 @@ paths.source = [ "*/src", "*\\src", ] -report.fail_under = 85 +report.fail_under = 88 report.omit = [ ] run.parallel = true @@ -144,7 +142,7 @@ run.plugins = [ ] [tool.mypy] -python_version = "3.10" +python_version = "3.11" strict = true exclude = "^(.*/roots/.*)|(tests/test_integration.*.py)$" overrides = [ diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py index e67180f8..1c1a3c2d 100644 --- a/src/sphinx_autodoc_typehints/__init__.py +++ b/src/sphinx_autodoc_typehints/__init__.py @@ -10,14 +10,14 @@ import textwrap import types from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, AnyStr, ForwardRef, NewType, TypeVar, get_type_hints +from typing import TYPE_CHECKING, Any, AnyStr, ForwardRef, NewType, TypeVar, Union, get_type_hints from docutils import nodes -from docutils.frontend import OptionParser +from docutils.frontend import get_default_settings from sphinx.ext.autodoc.mock import mock from sphinx.parsers import RSTParser from sphinx.util import logging, rst -from sphinx.util.inspect import TypeAliasForwardRef, TypeAliasNamespace, stringify_signature +from sphinx.util.inspect import TypeAliasForwardRef, stringify_signature from sphinx.util.inspect import signature as sphinx_signature from ._parser import parse @@ -36,7 +36,33 @@ from sphinx.ext.autodoc import Options _LOGGER = logging.getLogger(__name__) -_PYDATA_ANNOTATIONS = {"Any", "AnyStr", "Callable", "ClassVar", "Literal", "NoReturn", "Optional", "Tuple", "Union"} +_PYDATA_ANNOTS_TYPING = { + "Any", + "AnyStr", + "Callable", + "ClassVar", + "Literal", + "NoReturn", + "Optional", + "Tuple", + *({"Union"} if sys.version_info < (3, 14) else set()), +} +_PYDATA_ANNOTS_TYPES = { + *("AsyncGeneratorType", "BuiltinFunctionType", "BuiltinMethodType"), + *("CellType", "ClassMethodDescriptorType", "CoroutineType"), + "EllipsisType", + *("FrameType", "FunctionType"), + *("GeneratorType", "GetSetDescriptorType"), + "LambdaType", + *("MemberDescriptorType", "MethodDescriptorType", "MethodType", "MethodWrapperType"), + # NoneType is special, but included here for completeness' sake + *("NoneType", "NotImplementedType"), + "WrapperDescriptorType", +} +_PYDATA_ANNOTATIONS = { + *(("typing", n) for n in _PYDATA_ANNOTS_TYPING), + *(("types", n) for n in _PYDATA_ANNOTS_TYPES), +} # types has a bunch of things like ModuleType where ModuleType.__module__ is # "builtins" and ModuleType.__name__ is "module", so we have to check for this. @@ -45,6 +71,11 @@ _TYPES_DICT[types.FunctionType] = "FunctionType" +class MyTypeAliasForwardRef(TypeAliasForwardRef): + def __or__(self, value: Any) -> Any: + return Union[self, value] # noqa: UP007 + + def _get_types_type(obj: Any) -> str | None: try: return _TYPES_DICT.get(obj) @@ -64,7 +95,7 @@ def get_annotation_module(annotation: Any) -> str: return "builtins" if _get_types_type(annotation) is not None: return "types" - is_new_type = sys.version_info >= (3, 10) and isinstance(annotation, NewType) + is_new_type = isinstance(annotation, NewType) if ( is_new_type or isinstance(annotation, TypeVar) @@ -156,11 +187,11 @@ def get_annotation_args(annotation: Any, module: str, class_name: str) -> tuple[ return () if len(result) == 1 and result[0] == () else result # type: ignore[misc] -def format_internal_tuple(t: tuple[Any, ...], config: Config) -> str: - # An annotation can be a tuple, e.g., for nptyping: +def format_internal_tuple(t: tuple[Any, ...], config: Config, *, short_literals: bool = False) -> str: + # An annotation can be a tuple, e.g., for numpy.typing: # In this case, format_annotation receives: # This solution should hopefully be general for *any* type that allows tuples in annotations - fmt = [format_annotation(a, config) for a in t] + fmt = [format_annotation(a, config, short_literals=short_literals) for a in t] if len(fmt) == 0: return "()" if len(fmt) == 1: @@ -180,12 +211,13 @@ def fixup_module_name(config: Config, module: str) -> str: return module -def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PLR0911, PLR0912, PLR0915, PLR0914 +def format_annotation(annotation: Any, config: Config, *, short_literals: bool = False) -> str: # noqa: C901, PLR0911, PLR0912, PLR0915, PLR0914 """ Format the annotation. :param annotation: :param config: + :param short_literals: Render :py:class:`Literals` in PEP 604 style (``|``). :return: """ typehints_formatter: Callable[..., str] | None = getattr(config, "typehints_formatter", None) @@ -206,7 +238,7 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PL return format_internal_tuple(annotation, config) if isinstance(annotation, TypeAliasForwardRef): - return str(annotation) + return annotation.name try: module = get_annotation_module(annotation) @@ -219,13 +251,15 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PL full_name = f"{module}.{class_name}" if module != "builtins" else class_name fully_qualified: bool = getattr(config, "typehints_fully_qualified", False) prefix = "" if fully_qualified or full_name == class_name else "~" - role = "data" if module == "typing" and class_name in _PYDATA_ANNOTATIONS else "class" + 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 = full_name == "types.UnionType" or ( - always_use_bars_union and type(annotation).__qualname__ == "_UnionGenericAlias" + 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 = "" @@ -233,12 +267,12 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PL # Some types require special handling if full_name == "typing.NewType": args_format = f"\\(``{annotation.__name__}``, {{}})" - role = "class" if sys.version_info >= (3, 10) else "func" + role = "class" elif full_name in {"typing.TypeVar", "typing.ParamSpec"}: params = {k: getattr(annotation, f"__{k}__") for k in ("bound", "covariant", "contravariant")} params = {k: v for k, v in params.items() if v} if "bound" in params: - params["bound"] = f" {format_annotation(params['bound'], config)}" + params["bound"] = f" {format_annotation(params['bound'], config, short_literals=short_literals)}" args_format = f"\\(``{annotation.__name__}``{', {}' if args else ''}" if params: args_format += "".join(f", {k}={v}" for k, v in params.items()) @@ -259,20 +293,24 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PL args_format = f"\\[:py:data:`{prefix}typing.Union`\\[{{}}]]" args = tuple(x for x in args if x is not type(None)) elif full_name in {"typing.Callable", "collections.abc.Callable"} and args and args[0] is not ...: - fmt = [format_annotation(arg, config) for arg in args] + fmt = [format_annotation(arg, config, short_literals=short_literals) for arg in args] formatted_args = f"\\[\\[{', '.join(fmt[:-1])}], {fmt[-1]}]" elif full_name == "typing.Literal": + if short_literals: + return f"\\{' | '.join(f'``{arg!r}``' for arg in args)}" formatted_args = f"\\[{', '.join(f'``{arg!r}``' for arg in args)}]" elif is_bars_union: - return " | ".join([format_annotation(arg, config) for arg in args]) + if not args: + return f":py:{'class' if sys.version_info >= (3, 14) else 'data'}:`{prefix}typing.Union`" + return " | ".join([format_annotation(arg, config, short_literals=short_literals) for arg in args]) if args and not formatted_args: try: iter(args) except TypeError: - fmt = [format_annotation(args, config)] + fmt = [format_annotation(args, config, short_literals=short_literals)] else: - fmt = [format_annotation(arg, config) for arg in args] + fmt = [format_annotation(arg, config, short_literals=short_literals) for arg in args] formatted_args = args_format.format(", ".join(fmt)) escape = "\\ " if formatted_args else "" @@ -383,7 +421,12 @@ def _get_formatted_annotation(annotation: TypeVar) -> TypeVar: elif what == "method": # bail if it is a local method as we cannot determine if first argument needs to be deleted or not if "" in obj.__qualname__ and not _is_dataclass(name, what, obj.__qualname__): - _LOGGER.warning('Cannot handle as a local function: "%s" (use @functools.wraps)', name) + _LOGGER.warning( + 'Cannot handle as a local function: "%s" (use @functools.wraps)', + name, + type="sphinx_autodoc_typehints", + subtype="local_function", + ) return None outer = inspect.getmodule(obj) for class_name in obj.__qualname__.split(".")[:-1]: @@ -417,18 +460,17 @@ def _is_dataclass(name: str, what: str, qualname: str) -> bool: def _future_annotations_imported(obj: Any) -> bool: - _annotations = getattr(inspect.getmodule(obj), "annotations", None) - if _annotations is None: + annotations_ = getattr(inspect.getmodule(obj), "annotations", None) + if annotations_ is None: return False # Make sure that annotations is imported from __future__ - defined in cpython/Lib/__future__.py # annotations become strings at runtime - future_annotations = 0x100000 if sys.version_info[0:2] == (3, 7) else 0x1000000 - return bool(_annotations.compiler_flag == future_annotations) + return bool(annotations_.compiler_flag == 0x1000000) # pragma: no cover # noqa: PLR2004 def get_all_type_hints( - autodoc_mock_imports: list[str], obj: Any, name: str, localns: TypeAliasNamespace + 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: @@ -477,7 +519,9 @@ def _execute_guarded_code(autodoc_mock_imports: list[str], obj: Any, module_code with mock(autodoc_mock_imports): exec(guarded_code, getattr(obj, "__globals__", obj.__dict__)) # noqa: S102 except Exception as exc: # noqa: BLE001 - _LOGGER.warning("Failed guarded type import with %r", exc) + _LOGGER.warning( + "Failed guarded type import with %r", exc, type="sphinx_autodoc_typehints", subtype="guarded_import" + ) def _resolve_type_guarded_imports(autodoc_mock_imports: list[str], obj: Any) -> None: @@ -499,7 +543,9 @@ def _resolve_type_guarded_imports(autodoc_mock_imports: list[str], obj: Any) -> _execute_guarded_code(autodoc_mock_imports, obj, module_code) -def _get_type_hint(autodoc_mock_imports: list[str], name: str, obj: Any, localns: TypeAliasNamespace) -> dict[str, Any]: +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) try: result = get_type_hints(obj, None, localns) @@ -511,7 +557,13 @@ def _get_type_hint(autodoc_mock_imports: list[str], name: str, obj: Any, localns else: result = {} except NameError as exc: - _LOGGER.warning('Cannot resolve forward reference in type annotations of "%s": %s', name, exc) + _LOGGER.warning( + 'Cannot resolve forward reference in type annotations of "%s": %s', + name, + exc, + type="sphinx_autodoc_typehints", + subtype="forward_reference", + ) result = obj.__annotations__ return result @@ -529,7 +581,13 @@ def backfill_type_hints(obj: Any, name: str) -> dict[str, Any]: # noqa: C901, P def _one_child(module: Module) -> stmt | None: children = module.body # use the body to ignore type comments if len(children) != 1: - _LOGGER.warning('Did not get exactly one node from AST for "%s", got %s', name, len(children)) + _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", + ) return None return children[0] @@ -544,7 +602,7 @@ def _one_child(module: Module) -> stmt | None: return {} try: - type_comment = obj_ast.type_comment + type_comment = obj_ast.type_comment # type: ignore[attr-defined] except AttributeError: return {} @@ -554,14 +612,19 @@ def _one_child(module: Module) -> stmt | None: try: comment_args_str, comment_returns = type_comment.split(" -> ") except ValueError: - _LOGGER.warning('Unparseable type hint comment for "%s": Expected to contain ` -> `', name) + _LOGGER.warning( + 'Unparseable type hint comment for "%s": Expected to contain ` -> `', + name, + type="sphinx_autodoc_typehints", + subtype="comment", + ) return {} rv = {} if comment_returns: rv["return"] = comment_returns - args = load_args(obj_ast) + 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: @@ -569,7 +632,9 @@ def _one_child(module: Module) -> stmt | None: comment_args.insert(0, None) # self/cls may be omitted in type comments, insert blank if len(args) != len(comment_args): - _LOGGER.warning('Not enough type comments found on "%s"', name) + _LOGGER.warning( + 'Not enough type comments found on "%s"', name, type="sphinx_autodoc_typehints", subtype="comment" + ) return rv for at, arg in enumerate(args): @@ -671,7 +736,7 @@ def process_docstring( # noqa: PLR0913, PLR0917 except (ValueError, TypeError): signature = None - localns = TypeAliasNamespace(app.config["autodoc_type_aliases"]) + localns = {key: MyTypeAliasForwardRef(value) for key, value in app.config["autodoc_type_aliases"].items()} type_hints = get_all_type_hints(app.config.autodoc_mock_imports, obj, name, localns) app.config._annotation_globals = getattr(obj, "__globals__", {}) # noqa: SLF001 try: @@ -768,7 +833,10 @@ def _inject_signature( if annotation is None: type_annotation = f":type {arg_name}: " else: - formatted_annotation = add_type_css_class(format_annotation(annotation, app.config)) + short_literals = app.config.python_display_short_literal_types + formatted_annotation = add_type_css_class( + format_annotation(annotation, app.config, short_literals=short_literals) + ) type_annotation = f":type {arg_name}: {formatted_annotation}" if app.config.typehints_defaults: @@ -849,7 +917,7 @@ def get_insert_index(app: Sphinx, lines: list[str]) -> InsertIndexInfo | None: # 3. Insert after the parameters. # To find the parameters, parse as a docutils tree. - settings = OptionParser(components=(RSTParser,)).get_default_values() + settings = get_default_settings(RSTParser) # type: ignore[arg-type] settings.env = app.env doc = parse("\n".join(lines), settings) @@ -878,13 +946,15 @@ def get_insert_index(app: Sphinx, lines: list[str]) -> InsertIndexInfo | None: continue line_no = node_line_no(child) at = max(line_no - 2, 0) if line_no else len(lines) + if lines[at - 1]: # skip if something on this line + break return InsertIndexInfo(insert_index=at, found_directive=True) # 5. Otherwise, insert at end return InsertIndexInfo(insert_index=len(lines)) -def _inject_rtype( # noqa: PLR0913, PLR0917 +def _inject_rtype( # noqa: C901, PLR0913, PLR0917 type_hints: dict[str, Any], original_obj: Any, app: Sphinx, @@ -898,6 +968,8 @@ def _inject_rtype( # noqa: PLR0913, PLR0917 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 r = get_insert_index(app, lines) if r is None: @@ -908,7 +980,10 @@ def _inject_rtype( # noqa: PLR0913, PLR0917 if not app.config.typehints_use_rtype and r.found_return and " -- " in lines[insert_index]: return - formatted_annotation = add_type_css_class(format_annotation(type_hints["return"], app.config)) + 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) + ) if r.found_param and insert_index < len(lines) and lines[insert_index].strip(): insert_index -= 1 @@ -980,6 +1055,7 @@ def setup(app: Sphinx) -> dict[str, bool]: app.add_config_value("always_document_param_types", False, "html") # noqa: FBT003 app.add_config_value("typehints_fully_qualified", False, "env") # noqa: FBT003 app.add_config_value("typehints_document_rtype", True, "env") # noqa: FBT003 + app.add_config_value("typehints_document_rtype_none", True, "env") # noqa: FBT003 app.add_config_value("typehints_use_rtype", True, "env") # noqa: FBT003 app.add_config_value("typehints_defaults", None, "env") app.add_config_value("simplify_optional_unions", True, "env") # noqa: FBT003 diff --git a/src/sphinx_autodoc_typehints/attributes_patch.py b/src/sphinx_autodoc_typehints/attributes_patch.py index d038eae3..276dbd03 100644 --- a/src/sphinx_autodoc_typehints/attributes_patch.py +++ b/src/sphinx_autodoc_typehints/attributes_patch.py @@ -42,12 +42,12 @@ orig_handle_signature = PyAttribute.handle_signature -def _stringify_annotation(app: Sphinx, annotation: Any, mode: str = "") -> str: # noqa: ARG001 +def _stringify_annotation(app: Sphinx, annotation: Any, *args: Any, short_literals: bool = False, **kwargs: Any) -> str: # noqa: ARG001 # Format the annotation with sphinx-autodoc-typehints and inject our magic prefix to tell our patched # PyAttribute.handle_signature to treat it as rst. from . import format_annotation # noqa: PLC0415 - return TYPE_IS_RST_LABEL + format_annotation(annotation, app.config) + return TYPE_IS_RST_LABEL + format_annotation(annotation, app.config, short_literals=short_literals) def patch_attribute_documenter(app: Sphinx) -> None: diff --git a/src/sphinx_autodoc_typehints/patches.py b/src/sphinx_autodoc_typehints/patches.py index 236b38dd..583ed498 100644 --- a/src/sphinx_autodoc_typehints/patches.py +++ b/src/sphinx_autodoc_typehints/patches.py @@ -103,7 +103,7 @@ def _patched_base_admonition_run(self: BaseAdmonition) -> Any: def _patched_text_indent(self: Text, *args: Any) -> Any: _, line = self.state_machine.get_source_and_line() - result = orig_text_indent(self, *args) + result = orig_text_indent(self, *args) # type: ignore[no-untyped-call] node = self.parent[-1] if node.tagname == "system_message": node = self.parent[-2] @@ -114,6 +114,7 @@ def _patched_text_indent(self: Text, *args: Any) -> Any: 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()) n = nodes.doctest_block(data, data) @@ -128,9 +129,9 @@ def _patch_line_numbers() -> None: When the line numbers are missing, we have a hard time placing the :rtype:. """ - Text.indent = _patched_text_indent + 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 + Body.doctest = _patched_body_doctest # type: ignore[method-assign] def install_patches(app: Sphinx) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index a50bc0d5..19b09f38 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,11 +3,11 @@ import re import shutil import sys +from contextlib import suppress from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING import pytest -from sphinx.testing.path import path from sphobjinv import Inventory if TYPE_CHECKING: @@ -32,27 +32,24 @@ def inv(pytestconfig: Config) -> Inventory: @pytest.fixture(autouse=True) -def _remove_sphinx_projects(sphinx_test_tempdir: path) -> None: +def _remove_sphinx_projects(sphinx_test_tempdir: Path) -> None: # Remove any directory which appears to be a Sphinx project from # the temporary directory area. # See https://github.com/sphinx-doc/sphinx/issues/4040 - roots_path = Path(sphinx_test_tempdir) - for entry in roots_path.iterdir(): - try: + for entry in sphinx_test_tempdir.iterdir(): + with suppress(PermissionError): if entry.is_dir() and Path(entry, "_build").exists(): shutil.rmtree(str(entry)) - except PermissionError: # noqa: PERF203 - pass @pytest.fixture -def rootdir() -> path: - return path(str(Path(__file__).parent) or ".").abspath() / "roots" +def rootdir() -> Path: + return Path(str(Path(__file__).parent) or ".").absolute() / "roots" -def pytest_ignore_collect(path: Any, config: Config) -> bool | None: # noqa: ARG001 +def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None: # noqa: ARG001 version_re = re.compile(r"_py(\d)(\d)\.py$") - match = version_re.search(path.basename) + match = version_re.search(collection_path.name) if match: version = tuple(int(x) for x in match.groups()) if sys.version_info < version: diff --git a/tests/roots/test-resolve-typing-guard/demo_typing_guard.py b/tests/roots/test-resolve-typing-guard/demo_typing_guard.py index 0c42d6ee..e1c352b7 100644 --- a/tests/roots/test-resolve-typing-guard/demo_typing_guard.py +++ b/tests/roots/test-resolve-typing-guard/demo_typing_guard.py @@ -10,8 +10,8 @@ from demo_typing_guard_dummy import AnotherClass if TYPE_CHECKING: + from collections.abc import Sequence from decimal import Decimal - from typing import Sequence # noqa: UP035 from demo_typing_guard_dummy import Literal # guarded by another `if TYPE_CHECKING` in demo_typing_guard_dummy diff --git a/tests/test_integration.py b/tests/test_integration.py index 93aa1e52..4bd3904c 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -9,6 +9,7 @@ from typing import ( # no type comments TYPE_CHECKING, Any, + Literal, NewType, Optional, TypeVar, @@ -30,6 +31,28 @@ 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: dict[str, Any]) -> Callable[[T], T]: def dec(val: T) -> T: val.EXPECTED = expected @@ -39,9 +62,9 @@ def dec(val: T) -> T: return dec -def warns(pattern: str) -> Callable[[T], T]: +def warns(info: WarningInfo) -> Callable[[T], T]: def dec(val: T) -> T: - val.WARNING = pattern + val.WARNING = info return val return dec @@ -57,7 +80,7 @@ def wrapper(self) -> str: # noqa: ANN001 return wrapper -@warns("Cannot handle as a local function") +@warns(WarningInfo(regexp="Cannot handle as a local function", type="sphinx_autodoc_typehints.local_function")) @expected( """\ class mod.Class(x, y, z=None) @@ -69,7 +92,7 @@ class mod.Class(x, y, z=None) * **y** ("int") -- bar - * **z** ("Optional"["str"]) -- baz + * **z** ("str" | "None") -- baz class InnerClass @@ -94,7 +117,7 @@ class InnerClass * **y** ("int") -- bar - * **z** ("Optional"["str"]) -- baz + * **z** ("str" | "None") -- baz Return type: "str" @@ -108,7 +131,7 @@ class InnerClass * **y** ("int") -- bar - * **z** ("Optional"["str"]) -- baz + * **z** ("str" | "None") -- baz Return type: "str" @@ -126,7 +149,7 @@ class InnerClass * **y** ("int") -- bar - * **z** ("Optional"["str"]) -- baz + * **z** ("str" | "None") -- baz Return type: "str" @@ -148,10 +171,10 @@ class Class: :param z: baz """ - def __init__(self, x: bool, y: int, z: Optional[str] = None) -> None: # noqa: UP007 + def __init__(self, x: bool, y: int, z: Optional[str] = None) -> None: pass - def a_method(self, x: bool, y: int, z: Optional[str] = None) -> str: # noqa: UP007 + def a_method(self, x: bool, y: int, z: Optional[str] = None) -> str: """ Method docstring. @@ -182,7 +205,7 @@ def __magic_custom_method__(self, x: str) -> str: # noqa: PLW3201 """ @classmethod - def a_classmethod(cls, x: bool, y: int, z: Optional[str] = None) -> str: # noqa: UP007 + def a_classmethod(cls, x: bool, y: int, z: Optional[str] = None) -> str: """ Classmethod docstring. @@ -192,7 +215,7 @@ def a_classmethod(cls, x: bool, y: int, z: Optional[str] = None) -> str: # noqa """ @staticmethod - def a_staticmethod(x: bool, y: int, z: Optional[str] = None) -> str: # noqa: UP007 + def a_staticmethod(x: bool, y: int, z: Optional[str] = None) -> str: """ Staticmethod docstring. @@ -261,7 +284,7 @@ def __init__(self, message: str) -> None: * **y** ("int") -- bar - * **z_** ("Optional"["str"]) -- baz + * **z_** ("str" | "None") -- baz Returns: something @@ -270,7 +293,7 @@ def __init__(self, message: str) -> None: bytes """, ) -def function(x: bool, y: int, z_: Optional[str] = None) -> str: # noqa: UP007 +def function(x: bool, y: int, z_: Optional[str] = None) -> str: """ Function docstring. @@ -329,7 +352,11 @@ def function_with_escaped_default(x: str = "\b"): # noqa: ANN201 """ -@warns("Cannot resolve forward reference in type annotations") +@warns( + WarningInfo( + regexp="Cannot resolve forward reference in type annotations", type="sphinx_autodoc_typehints.forward_reference" + ) +) @expected( """\ mod.function_with_unresolvable_annotation(x) @@ -444,7 +471,7 @@ def method_without_typehint(self, x): # noqa: ANN001, ANN201, ARG002, PLR6301 Function docstring. Parameters: - * **x** ("Union"["str", "bytes", "None"]) -- foo + * **x** ("str" | "bytes" | "None") -- foo * **y** ("str") -- bar @@ -475,7 +502,7 @@ class mod.ClassWithTypehintsNotInline(x=None) Class docstring. Parameters: - **x** ("Optional"["Callable"[["int", "bytes"], "int"]]) -- foo + **x** ("Callable"[["int", "bytes"], "int"] | "None") -- foo foo(x=1) @@ -492,8 +519,7 @@ class mod.ClassWithTypehintsNotInline(x=None) Method docstring. Parameters: - **x** ("Optional"["Callable"[["int", "bytes"], "int"]]) -- - foo + **x** ("Callable"[["int", "bytes"], "int"] | "None") -- foo Return type: "ClassWithTypehintsNotInline" @@ -639,15 +665,15 @@ def func_with_overload(a: str, b: str) -> None: ... they must both have the same type. Parameters: - * **a** ("Union"["int", "str"]) -- The first thing + * **a** ("int" | "str") -- The first thing - * **b** ("Union"["int", "str"]) -- The second thing + * **b** ("int" | "str") -- The second thing Return type: "None" """, ) -def func_with_overload(a: Union[int, str], b: Union[int, str]) -> None: # noqa: UP007 +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. @@ -661,13 +687,66 @@ def func_with_overload(a: Union[int, str], b: Union[int, str]) -> None: # noqa: """ +@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: "Optional"["CodeType"] + code: "CodeType" | "None" An attribute """, @@ -675,7 +754,7 @@ class mod.TestClassAttributeDocs class TestClassAttributeDocs: """A class""" - code: Optional[CodeType] # noqa: UP007 + code: Optional[CodeType] """An attribute""" @@ -1142,7 +1221,7 @@ def docstring_with_enum_list_after_params(param: int) -> None: """ -@warns("Definition list ends without a blank line") +@warns(WarningInfo(regexp="Definition list ends without a blank line", type="docutils")) @expected( """ mod.docstring_with_definition_list_after_params_no_blank_line(param) @@ -1239,8 +1318,6 @@ def has_newtype(param: W) -> W: :members: """ -LT_PY310 = sys.version_info < (3, 10) - @expected( """ @@ -1267,6 +1344,19 @@ def typehints_use_signature(a: AsyncGenerator) -> AsyncGenerator: return a +@expected( + """ + mod.typehints_no_rtype_none() + + Do something. + + """, + typehints_document_rtype_none=False, +) +def typehints_no_rtype_none() -> None: + """Do something.""" + + prolog = """ .. |test_node_start| replace:: {test_node_start} """.format(test_node_start="test_start") @@ -1345,15 +1435,19 @@ def docstring_with_multiline_note_after_params_epilog_replace(param: int) -> Non """ mod.docstring_with_see_also() - Return type: - "str" + Test See also: more info at `_. + Return type: + "str" + """ ) def docstring_with_see_also() -> str: """ + Test + .. seealso:: more info at `_. """ return "" @@ -1388,7 +1482,7 @@ def has_doctest1() -> None: Unformatted = TypeVar("Unformatted") -@warns("cannot cache unpickable configuration value: 'typehints_formatter'") +@warns(WarningInfo(regexp="cannot cache unpickleable configuration value: 'typehints_formatter'", type="config.cache")) @expected( """ mod.typehints_formatter_applied_to_signature(param: Formatted) -> Formatted @@ -1452,23 +1546,21 @@ def test_integration( (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 - regexp = getattr(val, "WARNING", None) + warning_info: Union[WarningInfo, None] = getattr(val, "WARNING", None) value = warning.getvalue().strip() - if regexp: - msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}" - assert re.search(regexp, value), msg + if warning_info: + warning_info.assert_warning(value) else: assert not value result = (Path(app.srcdir) / "_build/text/index.txt").read_text() expected = val.EXPECTED - if LT_PY310: - expected = expected.replace("NewType", "NewType()") try: assert result.strip() == dedent(expected).strip() except Exception: diff --git a/tests/test_integration_autodoc_type_aliases.py b/tests/test_integration_autodoc_type_aliases.py index b3063fea..5ee1fa4a 100644 --- a/tests/test_integration_autodoc_type_aliases.py +++ b/tests/test_integration_autodoc_type_aliases.py @@ -97,13 +97,13 @@ def g(s: AliasedClass) -> AliasedClass: @expected( - """\ + f"""\ mod.function(x, y) Function docstring. Parameters: - * **x** (Array) -- foo + * **x** ({'Array | "None"' if sys.version_info >= (3, 14) else '"Optional"[Array]'}) -- foo * **y** ("Schema") -- boo @@ -115,7 +115,7 @@ def g(s: AliasedClass) -> AliasedClass: """, ) -def function(x: ArrayLike, y: Schema) -> str: +def function(x: ArrayLike | None, y: Schema) -> str: """ Function docstring. @@ -151,7 +151,7 @@ def test_integration( if regexp: msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}" assert re.search(regexp, value), msg - elif not re.search("WARNING: Inline strong start-string without end-string.", value): + elif not re.search(r"WARNING: Inline strong start-string without end-string.", value): assert not value result = (Path(app.srcdir) / "_build/text/index.txt").read_text() diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py index 4e8041ff..ea537113 100644 --- a/tests/test_sphinx_autodoc_typehints.py +++ b/tests/test_sphinx_autodoc_typehints.py @@ -1,28 +1,24 @@ from __future__ import annotations -import collections.abc import re import sys import types import typing +from collections.abc import Callable, Mapping from functools import cmp_to_key from io import StringIO from pathlib import Path from textwrap import dedent, indent -from types import FunctionType, ModuleType +from types import EllipsisType, FrameType, FunctionType, ModuleType, NotImplementedType, TracebackType from typing import ( # noqa: UP035 IO, Any, AnyStr, - Callable, Dict, Generic, List, - Mapping, - Match, NewType, Optional, - Pattern, Tuple, Type, TypeVar, @@ -50,11 +46,6 @@ from sphinx.testing.util import SphinxTestApp from sphobjinv import Inventory -try: - import nptyping -except ImportError: - nptyping = None # type: ignore[assignment] - T = TypeVar("T") U_co = TypeVar("U_co", covariant=True) V_contra = TypeVar("V_contra", contravariant=True) @@ -72,9 +63,9 @@ # Mypy does not support recursive type aliases, but # other type checkers do. -RecList = Union[int, List["RecList"]] # noqa: UP006, UP007 -MutualRecA = Union[bool, List["MutualRecB"]] # noqa: UP006, UP007 -MutualRecB = Union[str, List["MutualRecA"]] # noqa: UP006, UP007 +RecList = Union[int, List["RecList"]] +MutualRecA = Union[bool, List["MutualRecB"]] +MutualRecB = Union[str, List["MutualRecA"]] class A: @@ -107,32 +98,15 @@ class Metaclass(type): ... class HintedMethods: @classmethod - def from_magic(cls: type[T]) -> T: # type: ignore[empty-body] + def from_magic(cls) -> typing_extensions.Self: # type: ignore[empty-body] ... - def method(self: T) -> T: # type: ignore[empty-body] + def method(self) -> typing_extensions.Self: # type: ignore[empty-body] ... -PY310_PLUS = sys.version_info >= (3, 10) PY312_PLUS = sys.version_info >= (3, 12) -if sys.version_info >= (3, 9): # noqa: UP036 - AbcCallable = collections.abc.Callable # type: ignore[type-arg] -else: - # We could also set AbcCallable = typing.Callable and x fail the tests that - # use AbcCallable when in versions less than 3.9. - class MyGenericAlias(typing._VariadicGenericAlias, _root=True): # noqa: SLF001 - def __getitem__(self, params): # noqa: ANN001, ANN204 - result = super().__getitem__(params) - # Make a copy so we don't change the name of a cached annotation - result = result.copy_with(result.__args__) - result.__module__ = "collections.abc" - return result - - AbcCallable = MyGenericAlias(collections.abc.Callable, (), special=True) - AbcCallable.__module__ = "collections.abc" - @pytest.mark.parametrize( ("annotation", "module", "class_name", "args"), @@ -145,26 +119,28 @@ def __getitem__(self, params): # noqa: ANN001, ANN204 pytest.param(types.CoroutineType, "types", "CoroutineType", (), id="CoroutineType"), pytest.param(Any, "typing", "Any", (), id="Any"), pytest.param(AnyStr, "typing", "AnyStr", (), id="AnyStr"), - pytest.param(Dict, "typing", "Dict", (), id="Dict"), # noqa: UP006 - pytest.param(Dict[str, int], "typing", "Dict", (str, int), id="Dict_parametrized"), # noqa: UP006 - pytest.param(Dict[T, int], "typing", "Dict", (T, int), id="Dict_typevar"), # type: ignore[valid-type] # noqa: UP006 - pytest.param(Tuple, "typing", "Tuple", (), id="Tuple"), # noqa: UP006 - pytest.param(Tuple[str, int], "typing", "Tuple", (str, int), id="Tuple_parametrized"), # noqa: UP006 - pytest.param(Union[str, int], "typing", "Union", (str, int), id="Union"), # noqa: UP007 - pytest.param(Callable, "typing", "Callable", (), id="Callable"), - pytest.param(Callable[..., str], "typing", "Callable", (..., str), id="Callable_returntype"), - pytest.param(Callable[[int, str], str], "typing", "Callable", (int, str, str), id="Callable_all_types"), + pytest.param(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"), # type: ignore[valid-type] + 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( - AbcCallable[[int, str], str], # type: ignore[type-arg,misc,valid-type] + Callable[[int, str], str], "collections.abc", "Callable", (int, str, str), id="collections.abc.Callable_all_types", ), - pytest.param(Pattern, "typing", "Pattern", (), id="Pattern"), - pytest.param(Pattern[str], "typing", "Pattern", (str,), id="Pattern_parametrized"), - pytest.param(Match, "typing", "Match", (), id="Match"), - pytest.param(Match[str], "typing", "Match", (str,), id="Match_parametrized"), + pytest.param(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"), @@ -192,143 +168,182 @@ def test_parse_annotation(annotation: Any, module: str, class_name: str, args: t pytest.param(str, ":py:class:`str`", id="str"), pytest.param(int, ":py:class:`int`", id="int"), pytest.param(StringIO, ":py:class:`~io.StringIO`", id="StringIO"), - pytest.param(FunctionType, ":py:class:`~types.FunctionType`", id="FunctionType"), + pytest.param(EllipsisType, ":py:data:`~types.EllipsisType`", id="EllipsisType"), + pytest.param(FunctionType, ":py:data:`~types.FunctionType`", id="FunctionType"), + pytest.param(FrameType, ":py:data:`~types.FrameType`", id="FrameType"), pytest.param(ModuleType, ":py:class:`~types.ModuleType`", id="ModuleType"), + pytest.param(NotImplementedType, ":py:data:`~types.NotImplementedType`", id="NotImplementedType"), + pytest.param(TracebackType, ":py:class:`~types.TracebackType`", id="TracebackType"), pytest.param(type(None), ":py:obj:`None`", id="type None"), pytest.param(type, ":py:class:`type`", id="type"), - pytest.param(collections.abc.Callable, ":py:class:`~collections.abc.Callable`", id="abc-Callable"), - pytest.param(Type, ":py:class:`~typing.Type`", id="typing-Type"), # noqa: UP006 - pytest.param(Type[A], rf":py:class:`~typing.Type`\ \[:py:class:`~{__name__}.A`]", id="typing-A"), # noqa: UP006 + pytest.param(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:`~typing.Mapping`", id="Mapping"), + pytest.param(Generic[T], r":py:class:`~typing.Generic`\ \[:py:class:`~typing.TypeVar`\ \(``T``)]", id="Generic"), # type: ignore[index] + pytest.param(Mapping, ":py:class:`~collections.abc.Mapping`", id="Mapping"), pytest.param( Mapping[T, int], # type: ignore[valid-type] - r":py:class:`~typing.Mapping`\ \[:py:class:`~typing.TypeVar`\ \(``T``), :py:class:`int`]", + r":py:class:`~collections.abc.Mapping`\ \[:py:class:`~typing.TypeVar`\ \(``T``), :py:class:`int`]", id="Mapping-T-int", ), pytest.param( Mapping[str, V_contra], # type: ignore[valid-type] - r":py:class:`~typing.Mapping`\ \[:py:class:`str`, :py:class:`~typing.TypeVar`\ \(" + 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], # type: ignore[valid-type] - r":py:class:`~typing.Mapping`\ \[:py:class:`~typing.TypeVar`\ \(``T``), " + 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:`~typing.Mapping`\ \[:py:class:`str`, :py:class:`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"), # noqa: UP006 + pytest.param(Dict, ":py:class:`~typing.Dict`", id="Dict"), pytest.param( - Dict[T, int], # type: ignore[valid-type] # noqa: UP006 + Dict[T, int], # type: ignore[valid-type] r":py:class:`~typing.Dict`\ \[:py:class:`~typing.TypeVar`\ \(``T``), :py:class:`int`]", id="Dict-T-int", ), pytest.param( - Dict[str, V_contra], # type: ignore[valid-type] # noqa: UP006 + Dict[str, V_contra], # type: ignore[valid-type] r":py:class:`~typing.Dict`\ \[:py:class:`str`, :py:class:`~typing.TypeVar`\ \(``V_contra``, " r"contravariant=True)]", id="Dict-T-int-contra", ), pytest.param( - Dict[T, U_co], # type: ignore[valid-type] # noqa: UP006 + Dict[T, U_co], # type: ignore[valid-type] r":py:class:`~typing.Dict`\ \[:py:class:`~typing.TypeVar`\ \(``T``)," r" :py:class:`~typing.TypeVar`\ \(``U_co``, covariant=True)]", id="Dict-T-int-co", ), pytest.param( - Dict[str, bool], # noqa: UP006 + Dict[str, bool], r":py:class:`~typing.Dict`\ \[:py:class:`str`, :py:class:`bool`]", - id="Dict-str-bool", # noqa: RUF100, UP006 + id="Dict-str-bool", ), - pytest.param(Tuple, ":py:data:`~typing.Tuple`", id="Tuple"), # noqa: UP006 + pytest.param(Tuple, ":py:data:`~typing.Tuple`", id="Tuple"), pytest.param( - Tuple[str, bool], # noqa: UP006 + Tuple[str, bool], r":py:data:`~typing.Tuple`\ \[:py:class:`str`, :py:class:`bool`]", - id="Tuple-str-bool", # noqa: RUF100, UP006 + id="Tuple-str-bool", ), pytest.param( - Tuple[int, int, int], # noqa: UP006 + 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, ...], # noqa: UP006 + Tuple[str, ...], r":py:data:`~typing.Tuple`\ \[:py:class:`str`, :py:data:`...`]", id="Tuple-str-Ellipsis", ), - pytest.param(Union, ":py:data:`~typing.Union`", id="Union"), + pytest.param(Union, 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], # noqa: UP007 - r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:class:`bool`]", + 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], # noqa: UP007 - r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:class:`bool`, :py:obj:`None`]", + 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], # noqa: UP007 - r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:data:`~typing.Any`]", + 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], # noqa: UP007 - r":py:data:`~typing.Optional`\ \[:py:class:`str`]", + 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], # noqa: UP007 - r":py:data:`~typing.Optional`\ \[:py:class:`str`]", + 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( - Optional[str | bool], # noqa: UP007 - r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:class:`bool`, :py:obj:`None`]", + 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(Callable, ":py:data:`~typing.Callable`", id="Callable"), + 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:data:`~typing.Callable`\ \[:py:data:`...`, :py:class:`int`]", + r":py:class:`~collections.abc.Callable`\ \[:py:data:`...`, :py:class:`int`]", id="Callable-Ellipsis-int", ), pytest.param( Callable[[int], int], - r":py:data:`~typing.Callable`\ \[\[:py:class:`int`], :py:class:`int`]", + r":py:class:`~collections.abc.Callable`\ \[\[:py:class:`int`], :py:class:`int`]", id="Callable-int-int", ), pytest.param( Callable[[int, str], bool], - r":py:data:`~typing.Callable`\ \[\[:py:class:`int`, :py:class:`str`], :py:class:`bool`]", + 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:data:`~typing.Callable`\ \[\[:py:class:`int`, :py:class:`str`], :py:obj:`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:data:`~typing.Callable`\ \[\[:py:class:`~typing.TypeVar`\ \(``T``)]," + r":py:class:`~collections.abc.Callable`\ \[\[:py:class:`~typing.TypeVar`\ \(``T``)]," r" :py:class:`~typing.TypeVar`\ \(``T``)]", id="Callable-T-T", ), pytest.param( - AbcCallable[[int, str], bool], # type: ignore[valid-type,misc,type-arg] + Callable[[int, str], bool], r":py:class:`~collections.abc.Callable`\ \[\[:py:class:`int`, :py:class:`str`], :py:class:`bool`]", - id="AbcCallable-int-str-bool", + id="Callable-int-str-bool", ), - pytest.param(Pattern, ":py:class:`~typing.Pattern`", id="Pattern"), - pytest.param(Pattern[str], r":py:class:`~typing.Pattern`\ \[:py:class:`str`]", id="Pattern-str"), + pytest.param(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"), @@ -339,7 +354,7 @@ def test_parse_annotation(annotation: Any, module: str, class_name: str, args: t pytest.param(D, f":py:class:`~{__name__}.D`", id="D"), pytest.param(E, f":py:class:`~{__name__}.E`", id="E"), pytest.param(E[int], rf":py:class:`~{__name__}.E`\ \[:py:class:`int`]", id="E-int"), - pytest.param(W, rf":py:{'class' if PY310_PLUS else 'func'}:`~typing.NewType`\ \(``W``, :py:class:`str`)", id="W"), + pytest.param(W, r":py:class:`~typing.NewType`\ \(``W``, :py:class:`str`)", id="W"), pytest.param(T, r":py:class:`~typing.TypeVar`\ \(``T``)", id="T"), pytest.param(U_co, r":py:class:`~typing.TypeVar`\ \(``U_co``, covariant=True)", id="U-co"), pytest.param(V_contra, r":py:class:`~typing.TypeVar`\ \(``V_contra``, contravariant=True)", id="V-contra"), @@ -365,82 +380,22 @@ def test_parse_annotation(annotation: Any, module: str, class_name: str, args: t pytest.param(P_bound, r":py:class:`~typing.ParamSpec`\ \(``P_bound``, bound= :py:class:`str`)", id="P-bound"), # ## These test for correct internal tuple rendering, even if not all are valid Tuple types # Zero-length tuple remains - pytest.param(Tuple[()], ":py:data:`~typing.Tuple`", id="Tuple-p"), # noqa: UP006 + pytest.param(Tuple[()], ":py:data:`~typing.Tuple`", id="Tuple-p"), # Internal single tuple with simple types is flattened in the output - pytest.param(Tuple[int,], r":py:data:`~typing.Tuple`\ \[:py:class:`int`]", id="Tuple-p-int"), # noqa: UP006 + pytest.param(Tuple[int,], r":py:data:`~typing.Tuple`\ \[:py:class:`int`]", id="Tuple-p-int"), pytest.param( - Tuple[int, int], # noqa: UP006 + Tuple[int, int], r":py:data:`~typing.Tuple`\ \[:py:class:`int`, :py:class:`int`]", - id="Tuple-p-int-int", # noqa: RUF100, UP006 + id="Tuple-p-int-int", ), # Ellipsis in single tuple also gets flattened pytest.param( - Tuple[int, ...], # noqa: UP006 + Tuple[int, ...], r":py:data:`~typing.Tuple`\ \[:py:class:`int`, :py:data:`...`]", id="Tuple-p-Ellipsis", ), - pytest.param( - RecList, r":py:data:`~typing.Union`\ \[:py:class:`int`, :py:class:`~typing.List`\ \[RecList]]", id="RecList" - ), - pytest.param( - MutualRecA, - r":py:data:`~typing.Union`\ \[:py:class:`bool`, :py:class:`~typing.List`\ \[MutualRecB]]", - id="MutualRecA", - ), ] -if nptyping is not None: - _CASES.extend( - [ # Internal tuple with following additional type cannot be flattened (specific to nptyping?) - # These cases will fail if nptyping restructures its internal module hierarchy - pytest.param( - nptyping.NDArray[nptyping.Shape["*"], nptyping.Float], - ( - ":py:class:`~nptyping.ndarray.NDArray`\\ \\[:py:class:`~nptyping.base_meta_classes.Shape`\\ \\[*], " - ":py:class:`~numpy.float64`]" - ), - id="NDArray-star-float", - ), - pytest.param( - nptyping.NDArray[nptyping.Shape["64"], nptyping.Float], - ( - ":py:class:`~nptyping.ndarray.NDArray`\\ \\[:py:class:`~nptyping.base_meta_classes.Shape`\\ \\[64]," - " :py:class:`~numpy.float64`]" - ), - id="NDArray-64-float", - ), - pytest.param( - nptyping.NDArray[nptyping.Shape["*, *"], nptyping.Float], - ( - ":py:class:`~nptyping.ndarray.NDArray`\\ \\[:py:class:`~nptyping.base_meta_classes.Shape`\\ \\[*, " - "*], :py:class:`~numpy.float64`]" - ), - id="NDArray-star-star-float", - ), - pytest.param( - nptyping.NDArray[nptyping.Shape["*, ..."], nptyping.Float], - ":py:class:`~nptyping.ndarray.NDArray`\\ \\[:py:data:`~typing.Any`, :py:class:`~numpy.float64`]", - id="NDArray-star-Ellipsis-float", - ), - pytest.param( - nptyping.NDArray[nptyping.Shape["*, 3"], nptyping.Float], - ( - ":py:class:`~nptyping.ndarray.NDArray`\\ \\[:py:class:`~nptyping.base_meta_classes.Shape`\\ \\[*, 3" - "], :py:class:`~numpy.float64`]" - ), - id="NDArray-star-3-float", - ), - pytest.param( - nptyping.NDArray[nptyping.Shape["3, ..."], nptyping.Float], - ( - ":py:class:`~nptyping.ndarray.NDArray`\\ \\[:py:class:`~nptyping.base_meta_classes.Shape`\\ \\[3, " - "...], :py:class:`~numpy.float64`]" - ), - id="NDArray-3-Ellipsis-float", - ), - ], - ) - @pytest.mark.parametrize(("annotation", "expected_result"), _CASES) def test_format_annotation(inv: Inventory, annotation: Any, expected_result: str) -> None: @@ -476,9 +431,9 @@ def test_format_annotation(inv: Inventory, annotation: Any, expected_result: str assert format_annotation(annotation, conf) == expected_result_not_simplified # Test with the "fully_qualified" flag turned on - if "typing" in expected_result or "nptyping" in expected_result or __name__ in expected_result: + if "typing" in expected_result or __name__ in expected_result: expected_result = expected_result.replace("~typing", "typing") - expected_result = expected_result.replace("~nptyping", "nptyping") + expected_result = expected_result.replace("~collections.abc", "collections.abc") expected_result = expected_result.replace("~numpy", "numpy") expected_result = expected_result.replace("~" + __name__, __name__) conf = create_autospec( @@ -490,16 +445,16 @@ def test_format_annotation(inv: Inventory, annotation: Any, expected_result: str assert format_annotation(annotation, conf) == expected_result # Test for the correct role (class vs data) using the official Sphinx inventory - if "typing" in expected_result: - m = re.match("^:py:(?Pclass|data|func):`~(?P[^`]+)`", result) - assert m, "No match" - name = m.group("name") + 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: - if expected_role == "function": - expected_role = "func" - - assert m.group("role") == expected_role + if expected_role and expected_role == "function": + expected_role = "func" + assert match.group("role") == expected_role @pytest.mark.parametrize( @@ -515,7 +470,6 @@ def test_format_annotation(inv: Inventory, annotation: Any, expected_result: str ("Union[int, float] | str", ":py:class:`int` | :py:class:`float` | :py:class:`str`"), ], ) -@pytest.mark.skipif(not PY310_PLUS, reason="| union doesn't work before py310") def test_always_use_bars_union(annotation: str, expected_result: str) -> None: conf = create_autospec(Config, always_use_bars_union=True) result = format_annotation(eval(annotation), conf) # noqa: S307 @@ -627,19 +581,6 @@ class dummy_module.DataClass(x) assert contents == expected_contents -def maybe_fix_py310(expected_contents: str) -> str: - if sys.version_info >= (3, 11): - return expected_contents - if not PY310_PLUS: - return expected_contents.replace('"', "") - - for old, new in [ - ('"str" | "None"', '"Optional"["str"]'), - ]: - expected_contents = expected_contents.replace(old, new) - return expected_contents - - @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) -> None: @@ -670,7 +611,7 @@ def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) "str" """ expected_contents = dedent(expected_contents) - expected_contents = maybe_fix_py310(dedent(expected_contents)) + expected_contents = dedent(expected_contents) assert contents == expected_contents @@ -905,7 +846,7 @@ def test_resolve_typing_guard_imports(app: SphinxTestApp, status: StringIO, warn out = status.getvalue() assert "build succeeded" in out err = warning.getvalue() - r = re.compile("WARNING: Failed guarded type import") + r = re.compile(r"WARNING: Failed guarded type import") assert len(r.findall(err)) == 1 pat = r'WARNING: Failed guarded type import with ImportError\("cannot import name \'missing\' from \'functools\'' assert re.search(pat, err) diff --git a/tox.ini b/tox.ini index 7c952483..e7e0a31b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,15 @@ [tox] requires = - tox>=4.2 - tox-uv>=1.11.3 + tox>=4.30.2 + tox-uv>=1.28 env_list = fix + 3.14 3.13 3.12 3.11 - 3.10 type + 3.14t pkg_meta skip_missing_interpreters = true @@ -38,15 +39,15 @@ commands = description = format the code base to adhere to our styles, and complain about what we cannot do automatically skip_install = true deps = - pre-commit-uv>=4.1.1 + pre-commit-uv>=4.1.5 commands = pre-commit run --all-files --show-diff-on-failure [testenv:type] description = run type check on code base deps = - mypy==1.11.2 - types-docutils>=0.21.0.20240907 + mypy==1.18.2 + types-docutils>=0.22.2.20250924 commands = mypy src mypy tests @@ -55,9 +56,9 @@ commands = description = check that the long description is valid skip_install = true deps = - check-wheel-contents>=0.6 - twine>=5.1.1 - uv>=0.4.10 + check-wheel-contents>=0.6.3 + twine>=6.2 + uv>=0.8.22 commands = uv build --sdist --wheel --out-dir {env_tmp_dir} . twine check {env_tmp_dir}{/}* diff --git a/whitelist.txt b/whitelist.txt index 0e748bdc..e69de29b 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -1,90 +0,0 @@ -addnodes -arg1 -arg2 -ast3 -astext -autodoc -autouse -backfill -conf -contravariant -Coroutine -cpython -csv -dedent -delattr -desc -dirname -docnames -docstrings -Documenter -docutils -dunder -eval -exc -fget -fmt -fn -formatter -func -getitem -getmodule -getsource -globals -idx -inited -inv -isdatadescriptor -isfunction -iterdir -kwonlyargs -libs -lineno -lru -metaclass -ModuleType -multiline -newtype -numpy -nptyping -param -parametrized -params -parsers -pathlib -pos -prepend -py310 -pydata -pytestconfig -qualname -rootdir -rst -rtype -runtime -setitem -sig -signode -skipif -sph -sphobjinv -srcdir -stdlib -stmt -stringify -subclasses -supertype -tagname -tempdir -testroot -textwrap -toctree -typ -typehint -typehints -unittest -unlink -unresolvable -util -utils -vararg