Skip to content

Commit 5ea2ec7

Browse files
authored
🐛 fix(annotations): link enum variants in Literals (#623)
Literal enum values like `Literal[Color.RED]` rendered as plain code literals with no cross-reference back to the enum class. Users expect clickable links to navigate to the enum definition, consistent with how other type references work throughout the documentation. Render enum instances inside Literal annotations as `:py:attr:` cross- references instead of bare ``repr()`` strings. Non-enum literal values continue to render as inline code. Closes #363
1 parent d4d14d2 commit 5ea2ec7

2 files changed

Lines changed: 28 additions & 2 deletions

File tree

src/sphinx_autodoc_typehints/__init__.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import ast
66
import collections.abc
7+
import enum
78
import importlib
89
import inspect
910
import re
@@ -212,6 +213,17 @@ def fixup_module_name(config: Config, module: str) -> str:
212213
return module
213214

214215

216+
def _format_literal_arg(arg: Any, config: Config) -> str:
217+
if isinstance(arg, enum.Enum):
218+
enum_cls = type(arg)
219+
module = fixup_module_name(config, enum_cls.__module__)
220+
fully_qualified = getattr(config, "typehints_fully_qualified", False)
221+
qualified = f"{module}.{enum_cls.__qualname__}.{arg.name}" if module else f"{enum_cls.__qualname__}.{arg.name}"
222+
prefix = "" if fully_qualified or not module else "~"
223+
return f":py:attr:`{prefix}{qualified}`"
224+
return f"``{arg!r}``"
225+
226+
215227
def format_annotation(annotation: Any, config: Config, *, short_literals: bool = False) -> str: # noqa: C901, PLR0911, PLR0912, PLR0915, PLR0914
216228
"""
217229
Format the annotation.
@@ -304,9 +316,10 @@ def format_annotation(annotation: Any, config: Config, *, short_literals: bool =
304316
fmt = [format_annotation(arg, config, short_literals=short_literals) for arg in args]
305317
formatted_args = f"\\[\\[{', '.join(fmt[:-1])}], {fmt[-1]}]"
306318
elif full_name == "typing.Literal":
319+
literal_parts = [_format_literal_arg(arg, config) for arg in args]
307320
if short_literals:
308-
return f"\\{' | '.join(f'``{arg!r}``' for arg in args)}"
309-
formatted_args = f"\\[{', '.join(f'``{arg!r}``' for arg in args)}]"
321+
return f"\\{' | '.join(literal_parts)}"
322+
formatted_args = f"\\[{', '.join(literal_parts)}]"
310323
elif is_bars_union:
311324
if not args:
312325
return f":py:{'class' if sys.version_info >= (3, 14) else 'data'}:`{prefix}typing.Union`"

tests/test_sphinx_autodoc_typehints.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import enum
34
import re
45
import sys
56
import types
@@ -60,6 +61,12 @@
6061
Z = TypeVar("Z", bound="A")
6162
S = TypeVar("S", bound="miss") # type: ignore[name-defined] # miss not defined on purpose # noqa: F821
6263
W = NewType("W", str)
64+
65+
66+
class SomeEnum(enum.Enum):
67+
VALUE = "val"
68+
69+
6370
P = typing_extensions.ParamSpec("P")
6471
P_args = P.args
6572
P_kwargs = P.kwargs
@@ -488,6 +495,12 @@ def test_always_use_bars_union(annotation: str, expected_result: str) -> None:
488495
pytest.param("ClassVar", int, ":py:data:`~typing.ClassVar`\\ \\[:py:class:`int`]", id="ClassVar"),
489496
pytest.param("NoReturn", None, ":py:data:`~typing.NoReturn`", id="NoReturn"),
490497
pytest.param("Literal", ("a", 1), ":py:data:`~typing.Literal`\\ \\[``'a'``, ``1``]", id="Literal"),
498+
pytest.param(
499+
"Literal",
500+
(SomeEnum.VALUE,),
501+
rf":py:data:`~typing.Literal`\ \[:py:attr:`~{__name__}.SomeEnum.VALUE`]",
502+
id="Literal-enum",
503+
),
491504
pytest.param("Type", None, ":py:class:`~typing.Type`", id="Type-none"),
492505
pytest.param("Type", (A,), rf":py:class:`~typing.Type`\ \[:py:class:`~{__name__}.A`]", id="Type-A"),
493506
],

0 commit comments

Comments
 (0)