Skip to content
4 changes: 3 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
----------
Expand Down
17 changes: 16 additions & 1 deletion sphinx/util/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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+
Expand Down Expand Up @@ -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
Expand Down
23 changes: 15 additions & 8 deletions tests/test_util/test_util_typing.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests util.typing functions."""

import dataclasses
import sys
import typing as t
from collections import abc
Expand Down Expand Up @@ -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`"
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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']"
Expand Down Expand Up @@ -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"

Expand Down