Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ jobs:
fail-fast: false
matrix:
env:
- "3.14t"
- "3.14"
- "3.13"
- "3.12"
- "3.11"
Expand All @@ -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 }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,8 +34,8 @@ repos:
hooks:
- id: prettier
additional_dependencies:
- prettier@3.5.1
- "@prettier/[email protected].1"
- prettier@3.6.2
- "@prettier/[email protected].2"
- repo: meta
hooks:
- id: check-hooks-apply
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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.
Expand Down
20 changes: 10 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[build-system]
build-backend = "hatchling.build"
requires = [
"hatch-vcs>=0.4",
"hatch-vcs>=0.5",
"hatchling>=1.27",
]

Expand Down Expand Up @@ -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"
Expand Down
22 changes: 17 additions & 5 deletions src/sphinx_autodoc_typehints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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 = ""
Expand Down Expand Up @@ -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 {}

Expand All @@ -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:
Expand Down
24 changes: 12 additions & 12 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class mod.Class(x, y, z=None)

* **y** ("int") -- bar

* **z** ("Optional"["str"]) -- baz
* **z** ("str" | "None") -- baz

class InnerClass

Expand All @@ -117,7 +117,7 @@ class InnerClass

* **y** ("int") -- bar

* **z** ("Optional"["str"]) -- baz
* **z** ("str" | "None") -- baz

Return type:
"str"
Expand All @@ -131,7 +131,7 @@ class InnerClass

* **y** ("int") -- bar

* **z** ("Optional"["str"]) -- baz
* **z** ("str" | "None") -- baz

Return type:
"str"
Expand All @@ -149,7 +149,7 @@ class InnerClass

* **y** ("int") -- bar

* **z** ("Optional"["str"]) -- baz
* **z** ("str" | "None") -- baz

Return type:
"str"
Expand Down Expand Up @@ -284,7 +284,7 @@ def __init__(self, message: str) -> None:

* **y** ("int") -- bar

* **z_** ("Optional"["str"]) -- baz
* **z_** ("str" | "None") -- baz

Returns:
something
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -747,7 +746,7 @@ class mod.TestClassAttributeDocs

A class

code: "Optional"["CodeType"]
code: "CodeType" | "None"

An attribute
""",
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tests/test_integration_autodoc_type_aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
54 changes: 37 additions & 17 deletions tests/test_sphinx_autodoc_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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:`...<Ellipsis>`]",
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],
Expand Down Expand Up @@ -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:`...<Ellipsis>`]",
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",
),
]


Expand Down Expand Up @@ -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:(?P<role>class|data|func):`~(?P<name>[^`]+)`", result)
assert m, "No match"
name = m.group("name")
Expand Down
Loading