From 7ac92802e4761bcd849dc29fd296c338161ea81a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Thu, 9 Oct 2025 10:46:11 -0700 Subject: [PATCH] Support Union type on its own MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Bernát Gábor --- .pre-commit-config.yaml | 2 +- src/sphinx_autodoc_typehints/__init__.py | 2 ++ tests/test_sphinx_autodoc_typehints.py | 29 +++++++++++++++--------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 73f64b7..705e2e6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: 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.6.2" # Use the sha / tag you want to point at diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py index 537add4..1c1a3c2 100644 --- a/src/sphinx_autodoc_typehints/__init__.py +++ b/src/sphinx_autodoc_typehints/__init__.py @@ -300,6 +300,8 @@ def format_annotation(annotation: Any, config: Config, *, short_literals: bool = 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: + 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: diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py index 097c0dc..ea53711 100644 --- a/tests/test_sphinx_autodoc_typehints.py +++ b/tests/test_sphinx_autodoc_typehints.py @@ -244,7 +244,10 @@ 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, "" if sys.version_info >= (3, 14) else ":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], ":py:class:`str` | :py:class:`bool`" @@ -280,6 +283,12 @@ def test_parse_annotation(annotation: Any, module: str, class_name: str, args: t else r":py:data:`~typing.Optional`\ \[:py:class:`str`]", id="Optional-str-None", ), + pytest.param( + type[T] | types.UnionType, + ":py:class:`type`\\ \\[:py:class:`~typing.TypeVar`\\ \\(``T``)] | " + f":py:{'class' if sys.version_info >= (3, 14) else 'data'}:`~typing.Union`", + id="typevar union bar uniontype", + ), pytest.param( Optional[str | bool], ":py:class:`str` | :py:class:`bool` | :py:obj:`None`" @@ -436,18 +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 any(modname in expected_result for modname in ("typing", "types")) and not ( - sys.version_info >= (3, 14) and isinstance(annotation, Union) + if ( + result.count(":py:") == 1 + and ("typing" in result or "types" in result) + and (match := re.match(r"^:py:(?Pclass|data|func):`~(?P[^`]+)`", result)) ): - m = re.match(r"^:py:(?Pclass|data|func):`~(?P[^`]+)`", result) - assert m, "No match" - name = m.group("name") + 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(