diff --git a/CHANGES.rst b/CHANGES.rst index ac359fc8cba..0d1f493bae4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -69,7 +69,6 @@ Features added parses the provided text into inline elements and text nodes. Patch by Adam Turner. - * #12258: Support ``typing_extensions.Unpack`` Patch by Bénédikt Tran and Adam Turner. * #12524: Add a ``class`` option to the :rst:dir:`toctree` directive. @@ -98,6 +97,9 @@ Features added * #12508: LaTeX: Revamped styling of all admonitions, with addition of a title row with icon. Patch by Jean-François B. +* #11773: Display :py:class:`~typing.Annotated` annotations + with their metadata in the Python domain. + Patch by Adam Turner and David Stansby. Bugs fixed ---------- diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index f19e054b30a..28ca86490b5 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -261,6 +261,14 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s # evaluated before determining whether *cls* is a mocked object # or not; instead of two try-except blocks, we keep it here. return f':py:class:`{module_prefix}{_INVALID_BUILTIN_CLASSES[cls]}`' + elif _is_annotated_form(cls): + args = restify(cls.__args__[0], mode) + meta = ', '.join(map(repr, cls.__metadata__)) + if sys.version_info[:2] <= (3, 11): + # Hardcoded to fix errors on Python 3.11 and earlier. + return fr':py:class:`~typing.Annotated`\ [{args}, {meta}]' + return (f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`' + fr'\ [{args}, {meta}]') elif inspect.isNewType(cls): if sys.version_info[:2] >= (3, 10): # newtypes have correct module info since Python 3.10+ @@ -497,7 +505,14 @@ def stringify_annotation( for a in annotation_args) return f'{module_prefix}Literal[{args}]' elif _is_annotated_form(annotation): # for py39+ - return stringify_annotation(annotation_args[0], mode) + args = stringify_annotation(annotation_args[0], mode) + meta = ', '.join(map(repr, annotation.__metadata__)) + if sys.version_info[:2] <= (3, 11): + if mode == 'fully-qualified-except-typing': + return f'Annotated[{args}, {meta}]' + module_prefix = module_prefix.replace('builtins', 'typing') + return f'{module_prefix}Annotated[{args}, {meta}]' + return f'{module_prefix}Annotated[{args}, {meta}]' elif all(is_system_TypeVar(a) for a in annotation_args): # Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT]) return module_prefix + qualname diff --git a/tests/test_util/test_util_typing.py b/tests/test_util/test_util_typing.py index 2873bb1a19c..044b4599058 100644 --- a/tests/test_util/test_util_typing.py +++ b/tests/test_util/test_util_typing.py @@ -1,5 +1,6 @@ """Tests util.typing functions.""" +import dataclasses import sys import typing as t from collections import abc @@ -73,6 +74,11 @@ class BrokenType: __args__ = int +@dataclasses.dataclass(frozen=True) +class Gt: + gt: float + + def test_restify(): assert restify(int) == ":py:class:`int`" assert restify(int, "smart") == ":py:class:`int`" @@ -187,10 +193,11 @@ def test_restify_type_hints_containers(): "[:py:obj:`None`]") -@pytest.mark.xfail(sys.version_info[:2] <= (3, 11), reason='Needs fixing.') def test_restify_Annotated(): - assert restify(Annotated[str, "foo", "bar"]) == ':py:class:`~typing.Annotated`\\ [:py:class:`str`]' - assert restify(Annotated[str, "foo", "bar"], 'smart') == ':py:class:`~typing.Annotated`\\ [:py:class:`str`]' + assert restify(Annotated[str, "foo", "bar"]) == ":py:class:`~typing.Annotated`\\ [:py:class:`str`, 'foo', 'bar']" + assert restify(Annotated[str, "foo", "bar"], 'smart') == ":py:class:`~typing.Annotated`\\ [:py:class:`str`, 'foo', 'bar']" + assert restify(Annotated[float, Gt(-10.0)]) == ':py:class:`~typing.Annotated`\\ [:py:class:`float`, Gt(gt=-10.0)]' + assert restify(Annotated[float, Gt(-10.0)], 'smart') == ':py:class:`~typing.Annotated`\\ [:py:class:`float`, Gt(gt=-10.0)]' def test_restify_type_hints_Callable(): @@ -499,9 +506,12 @@ def test_stringify_type_hints_pep_585(): assert stringify_annotation(tuple[List[dict[int, str]], str, ...], "smart") == "tuple[~typing.List[dict[int, str]], str, ...]" +@pytest.mark.xfail(sys.version_info[:2] <= (3, 9), reason='Needs fixing.') def test_stringify_Annotated(): - assert stringify_annotation(Annotated[str, "foo", "bar"], 'fully-qualified-except-typing') == "str" - assert stringify_annotation(Annotated[str, "foo", "bar"], "smart") == "str" + assert stringify_annotation(Annotated[str, "foo", "bar"], 'fully-qualified-except-typing') == "Annotated[str, 'foo', 'bar']" + assert stringify_annotation(Annotated[str, "foo", "bar"], 'smart') == "~typing.Annotated[str, 'foo', 'bar']" + assert stringify_annotation(Annotated[float, Gt(-10.0)], 'fully-qualified-except-typing') == "Annotated[float, Gt(gt=-10.0)]" + assert stringify_annotation(Annotated[float, Gt(-10.0)], 'smart') == "~typing.Annotated[float, Gt(gt=-10.0)]" def test_stringify_Unpack(): @@ -662,7 +672,6 @@ def test_stringify_type_hints_alias(): def test_stringify_type_Literal(): - from typing import Literal # type: ignore[attr-defined] assert stringify_annotation(Literal[1, "2", "\r"], 'fully-qualified-except-typing') == "Literal[1, '2', '\\r']" assert stringify_annotation(Literal[1, "2", "\r"], "fully-qualified") == "typing.Literal[1, '2', '\\r']" assert stringify_annotation(Literal[1, "2", "\r"], "smart") == "~typing.Literal[1, '2', '\\r']" @@ -704,8 +713,6 @@ def test_stringify_mock(): def test_stringify_type_ForwardRef(): - from typing import ForwardRef # type: ignore[attr-defined] - assert stringify_annotation(ForwardRef("MyInt")) == "MyInt" assert stringify_annotation(ForwardRef("MyInt"), 'smart') == "MyInt"