Skip to content

Commit 137b3ad

Browse files
pataccaAA-Turner
andauthored
Make stringify_annotation recursive on builtins with args (#11570)
Co-authored-by: Adam Turner <[email protected]>
1 parent 0bad447 commit 137b3ad

File tree

4 files changed

+76
-6
lines changed

4 files changed

+76
-6
lines changed

CHANGES

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ Features added
5151
* #10678: Emit "source-read" events for files read via
5252
the :dudir:`include` directive.
5353
Patch by Halldor Fannar.
54+
* #11570: Use short names when using :pep:`585` built-in generics.
55+
Patch by Riccardo Mori.
5456

5557
Bugs fixed
5658
----------

sphinx/util/typing.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ def restify(cls: type | None, mode: str = 'fully-qualified-except-typing') -> st
140140
return ' | '.join(restify(a, mode) for a in cls.__args__)
141141
elif cls.__module__ in ('__builtin__', 'builtins'):
142142
if hasattr(cls, '__args__'):
143+
if not cls.__args__: # Empty tuple, list, ...
144+
return fr':py:class:`{cls.__name__}`\ [{cls.__args__!r}]'
145+
143146
concatenated_args = ', '.join(restify(arg, mode) for arg in cls.__args__)
144147
return fr':py:class:`{cls.__name__}`\ [{concatenated_args}]'
145148
else:
@@ -276,8 +279,12 @@ def stringify_annotation(
276279
elif str(annotation).startswith('typing.Annotated'): # for py310+
277280
pass
278281
elif annotation_module == 'builtins' and annotation_qualname:
279-
if hasattr(annotation, '__args__'): # PEP 585 generic
280-
return repr(annotation)
282+
if (args := getattr(annotation, '__args__', None)) is not None: # PEP 585 generic
283+
if not args: # Empty tuple, list, ...
284+
return repr(annotation)
285+
286+
concatenated_args = ', '.join(stringify_annotation(arg, mode) for arg in args)
287+
return f'{annotation_qualname}[{concatenated_args}]'
281288
else:
282289
return annotation_qualname
283290
elif annotation is Ellipsis:

tests/test_ext_autodoc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2018,7 +2018,7 @@ def test_autodoc_TYPE_CHECKING(app):
20182018
' :type: ~_io.StringIO',
20192019
'',
20202020
'',
2021-
'.. py:function:: spam(ham: ~collections.abc.Iterable[str]) -> tuple[gettext.NullTranslations, bool]',
2021+
'.. py:function:: spam(ham: ~collections.abc.Iterable[str]) -> tuple[~gettext.NullTranslations, bool]',
20222022
' :module: target.TYPE_CHECKING',
20232023
'',
20242024
]

tests/test_util_typing.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ def test_restify_type_hints_typevars():
153153
assert restify(List[T]) == ":py:class:`~typing.List`\\ [:py:obj:`tests.test_util_typing.T`]"
154154
assert restify(List[T], "smart") == ":py:class:`~typing.List`\\ [:py:obj:`~tests.test_util_typing.T`]"
155155

156+
assert restify(list[T]) == ":py:class:`list`\\ [:py:obj:`tests.test_util_typing.T`]"
157+
assert restify(list[T], "smart") == ":py:class:`list`\\ [:py:obj:`~tests.test_util_typing.T`]"
158+
156159
if sys.version_info[:2] >= (3, 10):
157160
assert restify(MyInt) == ":py:class:`tests.test_util_typing.MyInt`"
158161
assert restify(MyInt, "smart") == ":py:class:`~tests.test_util_typing.MyInt`"
@@ -171,14 +174,20 @@ def test_restify_type_hints_custom_class():
171174

172175
def test_restify_type_hints_alias():
173176
MyStr = str
174-
MyTuple = Tuple[str, str]
177+
MyTypingTuple = Tuple[str, str]
178+
MyTuple = tuple[str, str]
175179
assert restify(MyStr) == ":py:class:`str`"
176-
assert restify(MyTuple) == ":py:class:`~typing.Tuple`\\ [:py:class:`str`, :py:class:`str`]"
180+
assert restify(MyTypingTuple) == ":py:class:`~typing.Tuple`\\ [:py:class:`str`, :py:class:`str`]"
181+
assert restify(MyTuple) == ":py:class:`tuple`\\ [:py:class:`str`, :py:class:`str`]"
177182

178183

179184
def test_restify_type_ForwardRef():
180185
from typing import ForwardRef # type: ignore[attr-defined]
181-
assert restify(ForwardRef("myint")) == ":py:class:`myint`"
186+
assert restify(ForwardRef("MyInt")) == ":py:class:`MyInt`"
187+
188+
assert restify(list[ForwardRef("MyInt")]) == ":py:class:`list`\\ [:py:class:`MyInt`]"
189+
190+
assert restify(Tuple[dict[ForwardRef("MyInt"), str], list[List[int]]]) == ":py:class:`~typing.Tuple`\\ [:py:class:`dict`\\ [:py:class:`MyInt`, :py:class:`str`], :py:class:`list`\\ [:py:class:`~typing.List`\\ [:py:class:`int`]]]" # type: ignore[attr-defined]
182191

183192

184193
def test_restify_type_Literal():
@@ -190,10 +199,26 @@ def test_restify_pep_585():
190199
assert restify(list[str]) == ":py:class:`list`\\ [:py:class:`str`]" # type: ignore[attr-defined]
191200
assert restify(dict[str, str]) == (":py:class:`dict`\\ " # type: ignore[attr-defined]
192201
"[:py:class:`str`, :py:class:`str`]")
202+
assert restify(tuple[str, ...]) == ":py:class:`tuple`\\ [:py:class:`str`, ...]"
203+
assert restify(tuple[str, str, str]) == (":py:class:`tuple`\\ "
204+
"[:py:class:`str`, :py:class:`str`, "
205+
":py:class:`str`]")
193206
assert restify(dict[str, tuple[int, ...]]) == (":py:class:`dict`\\ " # type: ignore[attr-defined]
194207
"[:py:class:`str`, :py:class:`tuple`\\ "
195208
"[:py:class:`int`, ...]]")
196209

210+
assert restify(tuple[()]) == ":py:class:`tuple`\\ [()]"
211+
212+
# Mix old typing with PEP 585
213+
assert restify(List[dict[str, Tuple[str, ...]]]) == (":py:class:`~typing.List`\\ "
214+
"[:py:class:`dict`\\ "
215+
"[:py:class:`str`, :py:class:`~typing.Tuple`\\ "
216+
"[:py:class:`str`, ...]]]")
217+
assert restify(tuple[MyList[list[int]], int]) == (":py:class:`tuple`\\ ["
218+
":py:class:`tests.test_util_typing.MyList`\\ "
219+
"[:py:class:`list`\\ [:py:class:`int`]], "
220+
":py:class:`int`]")
221+
197222

198223
@pytest.mark.skipif(sys.version_info[:2] <= (3, 9), reason='python 3.10+ is required.')
199224
def test_restify_type_union_operator():
@@ -313,9 +338,17 @@ def test_stringify_type_hints_pep_585():
313338
assert stringify_annotation(list[dict[str, tuple]], 'fully-qualified-except-typing') == "list[dict[str, tuple]]"
314339
assert stringify_annotation(list[dict[str, tuple]], "smart") == "list[dict[str, tuple]]"
315340

341+
assert stringify_annotation(MyList[tuple[int, int]], 'fully-qualified-except-typing') == "tests.test_util_typing.MyList[tuple[int, int]]"
342+
assert stringify_annotation(MyList[tuple[int, int]], "fully-qualified") == "tests.test_util_typing.MyList[tuple[int, int]]"
343+
assert stringify_annotation(MyList[tuple[int, int]], "smart") == "~tests.test_util_typing.MyList[tuple[int, int]]"
344+
316345
assert stringify_annotation(type[int], 'fully-qualified-except-typing') == "type[int]"
317346
assert stringify_annotation(type[int], "smart") == "type[int]"
318347

348+
# Mix typing and pep 585
349+
assert stringify_annotation(tuple[List[dict[int, str]], str, ...], 'fully-qualified-except-typing') == "tuple[List[dict[int, str]], str, ...]"
350+
assert stringify_annotation(tuple[List[dict[int, str]], str, ...], "smart") == "tuple[~typing.List[dict[int, str]], str, ...]"
351+
319352

320353
def test_stringify_Annotated():
321354
from typing import Annotated # type: ignore[attr-defined]
@@ -336,10 +369,18 @@ def test_stringify_type_hints_string():
336369
assert stringify_annotation(List["int"], 'fully-qualified') == "typing.List[int]"
337370
assert stringify_annotation(List["int"], "smart") == "~typing.List[int]"
338371

372+
assert stringify_annotation(list["int"], 'fully-qualified-except-typing') == "list[int]"
373+
assert stringify_annotation(list["int"], 'fully-qualified') == "list[int]"
374+
assert stringify_annotation(list["int"], "smart") == "list[int]"
375+
339376
assert stringify_annotation("Tuple[str]", 'fully-qualified-except-typing') == "Tuple[str]"
340377
assert stringify_annotation("Tuple[str]", 'fully-qualified') == "Tuple[str]"
341378
assert stringify_annotation("Tuple[str]", "smart") == "Tuple[str]"
342379

380+
assert stringify_annotation("tuple[str]", 'fully-qualified-except-typing') == "tuple[str]"
381+
assert stringify_annotation("tuple[str]", 'fully-qualified') == "tuple[str]"
382+
assert stringify_annotation("tuple[str]", "smart") == "tuple[str]"
383+
343384
assert stringify_annotation("unknown", 'fully-qualified-except-typing') == "unknown"
344385
assert stringify_annotation("unknown", 'fully-qualified') == "unknown"
345386
assert stringify_annotation("unknown", "smart") == "unknown"
@@ -401,6 +442,9 @@ def test_stringify_type_hints_typevars():
401442
assert stringify_annotation(List[T], 'fully-qualified-except-typing') == "List[tests.test_util_typing.T]"
402443
assert stringify_annotation(List[T], "smart") == "~typing.List[~tests.test_util_typing.T]"
403444

445+
assert stringify_annotation(list[T], 'fully-qualified-except-typing') == "list[tests.test_util_typing.T]"
446+
assert stringify_annotation(list[T], "smart") == "list[~tests.test_util_typing.T]"
447+
404448
if sys.version_info[:2] >= (3, 10):
405449
assert stringify_annotation(MyInt, 'fully-qualified-except-typing') == "tests.test_util_typing.MyInt"
406450
assert stringify_annotation(MyInt, "smart") == "~tests.test_util_typing.MyInt"
@@ -446,6 +490,9 @@ def test_stringify_type_union_operator():
446490
assert stringify_annotation(int | str | None) == "int | str | None" # type: ignore[attr-defined]
447491
assert stringify_annotation(int | str | None, "smart") == "int | str | None" # type: ignore[attr-defined]
448492

493+
assert stringify_annotation(int | tuple[dict[str, int | None], list[int | str]] | None) == "int | tuple[dict[str, int | None], list[int | str]] | None" # type: ignore[attr-defined]
494+
assert stringify_annotation(int | tuple[dict[str, int | None], list[int | str]] | None, "smart") == "int | tuple[dict[str, int | None], list[int | str]] | None" # type: ignore[attr-defined]
495+
449496
assert stringify_annotation(int | Struct) == "int | struct.Struct" # type: ignore[attr-defined]
450497
assert stringify_annotation(int | Struct, "smart") == "int | ~struct.Struct" # type: ignore[attr-defined]
451498

@@ -461,3 +508,17 @@ def test_stringify_mock():
461508
assert stringify_annotation(unknown, 'fully-qualified-except-typing') == 'unknown'
462509
assert stringify_annotation(unknown.secret.Class, 'fully-qualified-except-typing') == 'unknown.secret.Class'
463510
assert stringify_annotation(unknown.secret.Class, "smart") == 'unknown.secret.Class'
511+
512+
513+
def test_stringify_type_ForwardRef():
514+
from typing import ForwardRef # type: ignore[attr-defined]
515+
516+
assert stringify_annotation(ForwardRef("MyInt")) == "MyInt"
517+
assert stringify_annotation(ForwardRef("MyInt"), 'smart') == "MyInt"
518+
519+
assert stringify_annotation(list[ForwardRef("MyInt")]) == "list[MyInt]"
520+
assert stringify_annotation(list[ForwardRef("MyInt")], 'smart') == "list[MyInt]"
521+
522+
assert stringify_annotation(Tuple[dict[ForwardRef("MyInt"), str], list[List[int]]]) == "Tuple[dict[MyInt, str], list[List[int]]]" # type: ignore[attr-defined]
523+
assert stringify_annotation(Tuple[dict[ForwardRef("MyInt"), str], list[List[int]]], 'fully-qualified-except-typing') == "Tuple[dict[MyInt, str], list[List[int]]]" # type: ignore[attr-defined]
524+
assert stringify_annotation(Tuple[dict[ForwardRef("MyInt"), str], list[List[int]]], 'smart') == "~typing.Tuple[dict[MyInt, str], list[~typing.List[int]]]" # type: ignore[attr-defined]

0 commit comments

Comments
 (0)