From fbc8b22f940ab2f20123da811920303e118c16f4 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 4 Dec 2023 13:06:23 +0000 Subject: [PATCH 01/10] Add metadata to Annotated types --- CHANGES.rst | 2 ++ sphinx/util/typing.py | 3 ++- tests/test_util_typing.py | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index f0872e6dc85..1e4e68e2370 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,6 +22,8 @@ Features added .. _``: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/search +* #11773: Added metadata information to rendering of `~typing.Annotated` types. + Bugs fixed ---------- diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 171420df58b..5d3daf0811a 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -365,7 +365,8 @@ def format_literal_arg(arg): args = ', '.join(map(format_literal_arg, annotation_args)) return f'{module_prefix}Literal[{args}]' elif str(annotation).startswith('typing.Annotated'): # for py39+ - return stringify_annotation(annotation_args[0], mode) + meta = [stringify_annotation(meta, mode) for meta in annotation.__metadata__] + return stringify_annotation(annotation_args[0], mode) + f"[{', '.join(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_typing.py b/tests/test_util_typing.py index d79852e8bd4..47b2214b3f6 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util_typing.py @@ -360,8 +360,8 @@ def test_stringify_type_hints_pep_585(): def test_stringify_Annotated(): from typing import Annotated # type: ignore[attr-defined] - 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') == "str[foo, bar]" + assert stringify_annotation(Annotated[str, "foo", "bar"], "smart") == "str[foo, bar]" def test_stringify_type_hints_string(): From 288800cdb60184c2f1bb70531e6e20576b0e1785 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Wed, 24 Apr 2024 19:50:01 +0100 Subject: [PATCH 02/10] Post-merge --- CHANGES.rst | 3 +-- sphinx/util/typing.py | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index d948e112a45..1d50c37ffdb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,6 +18,7 @@ Features added Patch by James Addison and Adam Turner .. _officially recommended: https://jinja.palletsprojects.com/en/latest/templates/#template-file-extension +* #11773: Added metadata information to rendering of `~typing.Annotated` types. Bugs fixed ---------- @@ -180,8 +181,6 @@ Features added to annotate the return type of their ``setup`` function. Patch by Chris Sewell. -* #11773: Added metadata information to rendering of `~typing.Annotated` types. - Bugs fixed ---------- diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 45906678426..55f5643b00f 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -450,8 +450,9 @@ def stringify_annotation( for a in annotation_args) return f'{module_prefix}Literal[{args}]' elif _is_annotated_form(annotation): # for py39+ - meta = [stringify_annotation(meta, mode) for meta in annotation.__metadata__] - return stringify_annotation(annotation_args[0], mode) + f"[{', '.join(meta)}]" + meta = ', '.join(stringify_annotation(meta, mode) + for meta in annotation.__metadata__) + return stringify_annotation(annotation_args[0], mode) + f"[{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 From 6455bb36c597e20e0dddc3d19cb49d23f7f57d22 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Wed, 24 Apr 2024 19:59:02 +0100 Subject: [PATCH 03/10] Render with ``Annotated`` --- sphinx/util/typing.py | 6 +++--- tests/test_util/test_util_typing.py | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 55f5643b00f..f77f537b054 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -450,9 +450,9 @@ def stringify_annotation( for a in annotation_args) return f'{module_prefix}Literal[{args}]' elif _is_annotated_form(annotation): # for py39+ - meta = ', '.join(stringify_annotation(meta, mode) - for meta in annotation.__metadata__) - return stringify_annotation(annotation_args[0], mode) + f"[{meta}]" + args = stringify_annotation(annotation_args[0], mode) + meta = ', '.join(map(repr, annotation.__metadata__)) + 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 f20121e5384..3d9c5bf07c3 100644 --- a/tests/test_util/test_util_typing.py +++ b/tests/test_util/test_util_typing.py @@ -30,9 +30,12 @@ WrapperDescriptorType, ) from typing import ( + Annotated, Any, Dict, + ForwardRef, List, + Literal, NewType, Optional, Tuple, @@ -184,6 +187,11 @@ def test_restify_type_hints_containers(): "[:py:obj:`None`]") +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`]' + + def test_restify_type_hints_Callable(): assert restify(t.Callable) == ":py:class:`~typing.Callable`" assert restify(t.Callable[[str], int]) == (":py:class:`~typing.Callable`\\ " @@ -284,7 +292,6 @@ def test_restify_type_hints_alias(): def test_restify_type_ForwardRef(): - from typing import ForwardRef # type: ignore[attr-defined] assert restify(ForwardRef("MyInt")) == ":py:class:`MyInt`" assert restify(list[ForwardRef("MyInt")]) == ":py:class:`list`\\ [:py:class:`MyInt`]" @@ -293,7 +300,6 @@ def test_restify_type_ForwardRef(): def test_restify_type_Literal(): - from typing import Literal # type: ignore[attr-defined] assert restify(Literal[1, "2", "\r"]) == ":py:obj:`~typing.Literal`\\ [1, '2', '\\r']" assert restify(Literal[MyEnum.a], 'fully-qualified-except-typing') == ':py:obj:`~typing.Literal`\\ [:py:attr:`tests.test_util.test_util_typing.MyEnum.a`]' @@ -469,9 +475,8 @@ def test_stringify_type_hints_pep_585(): def test_stringify_Annotated(): - from typing import Annotated # type: ignore[attr-defined] - assert stringify_annotation(Annotated[str, "foo", "bar"], 'fully-qualified-except-typing') == "str[foo, bar]" - assert stringify_annotation(Annotated[str, "foo", "bar"], "smart") == "str[foo, bar]" + 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']" def test_stringify_type_hints_string(): @@ -606,7 +611,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']" @@ -648,8 +652,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" From 4c9c54c834fe01c36fe2a696af3f61c593e5807a Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 14 Jul 2024 00:04:03 +0100 Subject: [PATCH 04/10] Add annotated tests --- sphinx/util/typing.py | 5 +++++ tests/test_util/test_util_typing.py | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 6fee43a3d9e..153a32e3d46 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -261,6 +261,11 @@ 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__)) + 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+ diff --git a/tests/test_util/test_util_typing.py b/tests/test_util/test_util_typing.py index a316814569d..985eac67a3a 100644 --- a/tests/test_util/test_util_typing.py +++ b/tests/test_util/test_util_typing.py @@ -1,5 +1,5 @@ """Tests util.typing functions.""" - +import dataclasses import sys import typing as t from collections import abc @@ -73,6 +73,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 +192,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(): @@ -501,7 +507,9 @@ def test_stringify_type_hints_pep_585(): def test_stringify_Annotated(): 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[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(): From 45e0631efd6ec67d32380c1004946a3d38e89943 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 14 Jul 2024 00:17:28 +0100 Subject: [PATCH 05/10] Fix builtins errors on Python 3.11 and earlier --- CHANGES.rst | 4 +++- sphinx/util/typing.py | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 57805f8a23e..993064ee4a7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -98,7 +98,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: Added metadata information to rendering of `~typing.Annotated` types. +* #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 153a32e3d46..6f84357c44f 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -264,6 +264,10 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s 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 (f':py:class:`{module_prefix}typing.Annotated`' + fr'\ [{args}, {meta}]') return (f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`' fr'\ [{args}, {meta}]') elif inspect.isNewType(cls): @@ -504,6 +508,9 @@ def stringify_annotation( elif _is_annotated_form(annotation): # for py39+ args = stringify_annotation(annotation_args[0], mode) meta = ', '.join(map(repr, annotation.__metadata__)) + if sys.version_info[:2] <= (3, 11): + module_prefix = module_prefix.removesuffix('builtins.') + 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]) From 5f2e0029aa9e8d8127a444ef267314915f450979 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 14 Jul 2024 00:18:45 +0100 Subject: [PATCH 06/10] CHANGES --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 993064ee4a7..c796caf1ce0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -98,7 +98,7 @@ 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 +* #11773: Display :py:class:`~typing.Annotated` annotations with their metadata in the Python domain. Patch by Adam Turner and David Stansby. From 2cf4b9e07905a01f3bca17caa3908ebfe7287da5 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 14 Jul 2024 00:25:07 +0100 Subject: [PATCH 07/10] Fix builtins errors on Python 3.11 and earlier (v2) --- sphinx/util/typing.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 6f84357c44f..5cad0421c0d 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -266,8 +266,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s meta = ', '.join(map(repr, cls.__metadata__)) if sys.version_info[:2] <= (3, 11): # Hardcoded to fix errors on Python 3.11 and earlier. - return (f':py:class:`{module_prefix}typing.Annotated`' - fr'\ [{args}, {meta}]') + 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): @@ -509,7 +508,7 @@ def stringify_annotation( args = stringify_annotation(annotation_args[0], mode) meta = ', '.join(map(repr, annotation.__metadata__)) if sys.version_info[:2] <= (3, 11): - module_prefix = module_prefix.removesuffix('builtins.') + 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): From 0ac488eaf28e35ad41c0c17931d7d1c50344e76f Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 14 Jul 2024 00:32:48 +0100 Subject: [PATCH 08/10] f-q-e-t --- sphinx/util/typing.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 5cad0421c0d..28ca86490b5 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -508,6 +508,8 @@ def stringify_annotation( 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}]' From b9a44b0bd1e86a95ef7007e76199aa43e1b1100e Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 14 Jul 2024 00:38:57 +0100 Subject: [PATCH 09/10] Tolerate the 3.9 failure --- CHANGES.rst | 1 - tests/test_util/test_util_typing.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index c796caf1ce0..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. diff --git a/tests/test_util/test_util_typing.py b/tests/test_util/test_util_typing.py index 985eac67a3a..7531f8a853e 100644 --- a/tests/test_util/test_util_typing.py +++ b/tests/test_util/test_util_typing.py @@ -1,4 +1,5 @@ """Tests util.typing functions.""" + import dataclasses import sys import typing as t @@ -192,6 +193,7 @@ def test_restify_type_hints_containers(): "[:py:obj:`None`]") +@pytest.mark.xfail(sys.version_info[:2] <= (3, 9), reason='Needs fixing.') def test_restify_Annotated(): 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']" From 043054e87ab75c8d7ec8426468326b691745ebc4 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 14 Jul 2024 00:46:59 +0100 Subject: [PATCH 10/10] Tolerate the 3.9 failure --- tests/test_util/test_util_typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_util/test_util_typing.py b/tests/test_util/test_util_typing.py index 7531f8a853e..044b4599058 100644 --- a/tests/test_util/test_util_typing.py +++ b/tests/test_util/test_util_typing.py @@ -193,7 +193,6 @@ def test_restify_type_hints_containers(): "[:py:obj:`None`]") -@pytest.mark.xfail(sys.version_info[:2] <= (3, 9), reason='Needs fixing.') def test_restify_Annotated(): 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']" @@ -507,6 +506,7 @@ 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') == "Annotated[str, 'foo', 'bar']" assert stringify_annotation(Annotated[str, "foo", "bar"], 'smart') == "~typing.Annotated[str, 'foo', 'bar']"