Display `typing.Annotated` metadata in the Python domain (#11785)

Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com>
This commit is contained in:
David Stansby
2024-07-14 01:52:55 +02:00
committed by GitHub
parent 02c265ce23
commit 159c26715b
3 changed files with 34 additions and 10 deletions

View File

@@ -71,7 +71,6 @@ Features added
parses the provided text into inline elements and text nodes. parses the provided text into inline elements and text nodes.
Patch by Adam Turner. Patch by Adam Turner.
* #12258: Support ``typing_extensions.Unpack`` * #12258: Support ``typing_extensions.Unpack``
Patch by Bénédikt Tran and Adam Turner. Patch by Bénédikt Tran and Adam Turner.
* #12524: Add a ``class`` option to the :rst:dir:`toctree` directive. * #12524: Add a ``class`` option to the :rst:dir:`toctree` directive.
@@ -100,6 +99,9 @@ Features added
* #12508: LaTeX: Revamped styling of all admonitions, with addition of a * #12508: LaTeX: Revamped styling of all admonitions, with addition of a
title row with icon. title row with icon.
Patch by Jean-François B. 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 Bugs fixed
---------- ----------

View File

@@ -261,6 +261,14 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s
# evaluated before determining whether *cls* is a mocked object # evaluated before determining whether *cls* is a mocked object
# or not; instead of two try-except blocks, we keep it here. # or not; instead of two try-except blocks, we keep it here.
return f':py:class:`{module_prefix}{_INVALID_BUILTIN_CLASSES[cls]}`' 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): elif inspect.isNewType(cls):
if sys.version_info[:2] >= (3, 10): if sys.version_info[:2] >= (3, 10):
# newtypes have correct module info since Python 3.10+ # newtypes have correct module info since Python 3.10+
@@ -497,7 +505,14 @@ def stringify_annotation(
for a in annotation_args) for a in annotation_args)
return f'{module_prefix}Literal[{args}]' return f'{module_prefix}Literal[{args}]'
elif _is_annotated_form(annotation): # for py39+ 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): elif all(is_system_TypeVar(a) for a in annotation_args):
# Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT]) # Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT])
return module_prefix + qualname return module_prefix + qualname

View File

@@ -1,5 +1,6 @@
"""Tests util.typing functions.""" """Tests util.typing functions."""
import dataclasses
import sys import sys
import typing as t import typing as t
from collections import abc from collections import abc
@@ -73,6 +74,11 @@ class BrokenType:
__args__ = int __args__ = int
@dataclasses.dataclass(frozen=True)
class Gt:
gt: float
def test_restify(): def test_restify():
assert restify(int) == ":py:class:`int`" assert restify(int) == ":py:class:`int`"
assert restify(int, "smart") == ":py:class:`int`" assert restify(int, "smart") == ":py:class:`int`"
@@ -187,10 +193,11 @@ def test_restify_type_hints_containers():
"[:py:obj:`None`]") "[:py:obj:`None`]")
@pytest.mark.xfail(sys.version_info[:2] <= (3, 11), reason='Needs fixing.')
def test_restify_Annotated(): def test_restify_Annotated():
assert restify(Annotated[str, "foo", "bar"]) == ':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`]' 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(): 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, ...]" 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(): def test_stringify_Annotated():
assert stringify_annotation(Annotated[str, "foo", "bar"], 'fully-qualified-except-typing') == "str" assert stringify_annotation(Annotated[str, "foo", "bar"], 'fully-qualified-except-typing') == "Annotated[str, 'foo', 'bar']"
assert stringify_annotation(Annotated[str, "foo", "bar"], "smart") == "str" 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(): def test_stringify_Unpack():
@@ -662,7 +672,6 @@ def test_stringify_type_hints_alias():
def test_stringify_type_Literal(): 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-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"], "fully-qualified") == "typing.Literal[1, '2', '\\r']"
assert stringify_annotation(Literal[1, "2", "\r"], "smart") == "~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(): def test_stringify_type_ForwardRef():
from typing import ForwardRef # type: ignore[attr-defined]
assert stringify_annotation(ForwardRef("MyInt")) == "MyInt" assert stringify_annotation(ForwardRef("MyInt")) == "MyInt"
assert stringify_annotation(ForwardRef("MyInt"), 'smart') == "MyInt" assert stringify_annotation(ForwardRef("MyInt"), 'smart') == "MyInt"