diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 250c9b9..61c26e8 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -19,6 +19,8 @@ jobs: fail-fast: false matrix: env: + - "3.14t" + - "3.14" - "3.13" - "3.12" - "3.11" @@ -38,9 +40,9 @@ jobs: 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 a4c4f2f..f773e02 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -20,7 +20,7 @@ jobs: 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: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e2b624..73f64b7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,11 +20,11 @@ repos: - id: tox-ini-fmt args: ["-p", "fix"] - repo: https://github.com/tox-dev/pyproject-fmt - rev: "v2.7.0" + rev: "v2.8.0" hooks: - id: pyproject-fmt - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.13.3" + rev: "v0.14.0" hooks: - id: ruff-format - id: ruff @@ -34,8 +34,8 @@ repos: hooks: - id: prettier additional_dependencies: - - prettier@3.5.1 - - "@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/README.md b/README.md index 384e226..db11c3d 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,8 @@ 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`. @@ -74,7 +75,6 @@ The following configuration options are accepted: [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. diff --git a/pyproject.toml b/pyproject.toml index 37105b9..206a6cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = "hatchling.build" requires = [ - "hatch-vcs>=0.4", + "hatch-vcs>=0.5", "hatchling>=1.27", ] @@ -40,20 +40,20 @@ dynamic = [ "version", ] dependencies = [ - "sphinx>=8.2", + "sphinx>=8.2.3", ] optional-dependencies.docs = [ - "furo>=2024.8.6", + "furo>=2025.9.25", ] optional-dependencies.testing = [ "covdefaults>=2.3", - "coverage>=7.6.12", - "defusedxml>=0.7.1", # required by sphinx.testing - "diff-cover>=9.2.3", - "pytest>=8.3.4", - "pytest-cov>=6", - "sphobjinv>=2.3.1.2", - "typing-extensions>=4.12.2", + "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" diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py index c547569..537add4 100644 --- a/src/sphinx_autodoc_typehints/__init__.py +++ b/src/sphinx_autodoc_typehints/__init__.py @@ -36,7 +36,17 @@ from sphinx.ext.autodoc import Options _LOGGER = logging.getLogger(__name__) -_PYDATA_ANNOTS_TYPING = {"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"), @@ -246,8 +256,10 @@ def format_annotation(annotation: Any, config: Config, *, short_literals: bool = 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 = "" @@ -588,7 +600,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 {} @@ -610,7 +622,7 @@ def _one_child(module: Module) -> stmt | None: 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: diff --git a/tests/test_integration.py b/tests/test_integration.py index 02369f8..4bd3904 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -92,7 +92,7 @@ class mod.Class(x, y, z=None) * **y** ("int") -- bar - * **z** ("Optional"["str"]) -- baz + * **z** ("str" | "None") -- baz class InnerClass @@ -117,7 +117,7 @@ class InnerClass * **y** ("int") -- bar - * **z** ("Optional"["str"]) -- baz + * **z** ("str" | "None") -- baz Return type: "str" @@ -131,7 +131,7 @@ class InnerClass * **y** ("int") -- bar - * **z** ("Optional"["str"]) -- baz + * **z** ("str" | "None") -- baz Return type: "str" @@ -149,7 +149,7 @@ class InnerClass * **y** ("int") -- bar - * **z** ("Optional"["str"]) -- baz + * **z** ("str" | "None") -- baz Return type: "str" @@ -284,7 +284,7 @@ def __init__(self, message: str) -> None: * **y** ("int") -- bar - * **z_** ("Optional"["str"]) -- baz + * **z_** ("str" | "None") -- baz Returns: something @@ -471,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 @@ -502,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) @@ -519,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" @@ -666,9 +665,9 @@ 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" @@ -747,7 +746,7 @@ class mod.TestClassAttributeDocs A class - code: "Optional"["CodeType"] + code: "CodeType" | "None" An attribute """, @@ -1547,6 +1546,7 @@ 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 diff --git a/tests/test_integration_autodoc_type_aliases.py b/tests/test_integration_autodoc_type_aliases.py index f964042..5ee1fa4 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** ("Optional"[Array]) -- foo + * **x** ({'Array | "None"' if sys.version_info >= (3, 14) else '"Optional"[Array]'}) -- foo * **y** ("Schema") -- boo diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py index c0963c2..097c0dc 100644 --- a/tests/test_sphinx_autodoc_typehints.py +++ b/tests/test_sphinx_autodoc_typehints.py @@ -181,7 +181,7 @@ def test_parse_annotation(annotation: Any, module: str, class_name: str, args: t 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(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] @@ -244,37 +244,63 @@ def test_parse_annotation(annotation: Any, module: str, class_name: str, args: t 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, "" if sys.version_info >= (3, 14) else ":py:data:`~typing.Union`", id="Union"), pytest.param( Union[str, bool], - r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:class:`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], - r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:class:`bool`, :py:obj:`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], - r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:data:`~typing.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], - r":py:data:`~typing.Optional`\ \[:py:class:`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], - r":py:data:`~typing.Optional`\ \[:py:class:`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-None", ), pytest.param( Optional[str | bool], - r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:class:`bool`, :py:obj:`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="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], @@ -359,14 +385,6 @@ def test_parse_annotation(annotation: Any, module: str, class_name: str, args: t 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", - ), ] @@ -418,7 +436,9 @@ 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 any(modname in expected_result for modname in ("typing", "types")): + if any(modname in expected_result for modname in ("typing", "types")) and not ( + sys.version_info >= (3, 14) and isinstance(annotation, Union) + ): m = re.match(r"^:py:(?Pclass|data|func):`~(?P[^`]+)`", result) assert m, "No match" name = m.group("name") diff --git a/tox.ini b/tox.ini index 87924c5..e7e0a31 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,15 @@ [tox] requires = - tox>=4.24.1 - tox-uv>=1.24 + tox>=4.30.2 + tox-uv>=1.28 env_list = fix + 3.14 3.13 3.12 3.11 type + 3.14t pkg_meta skip_missing_interpreters = true @@ -37,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.4 + 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.15 - types-docutils>=0.21.0.20250604 + mypy==1.18.2 + types-docutils>=0.22.2.20250924 commands = mypy src mypy tests @@ -54,9 +56,9 @@ commands = description = check that the long description is valid skip_install = true deps = - check-wheel-contents>=0.6.1 - twine>=6.1 - uv>=0.6.1 + 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}{/}*