Skip to content

Commit

Permalink
Make stringify_annotation recursive on builtins with args (#11570)
Browse files Browse the repository at this point in the history
Co-authored-by: Adam Turner <[email protected]>
  • Loading branch information
patacca and AA-Turner authored Aug 14, 2023
1 parent 0bad447 commit 137b3ad
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 6 deletions.
2 changes: 2 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ Features added
* #10678: Emit "source-read" events for files read via
the :dudir:`include` directive.
Patch by Halldor Fannar.
* #11570: Use short names when using :pep:`585` built-in generics.
Patch by Riccardo Mori.

Bugs fixed
----------
Expand Down
11 changes: 9 additions & 2 deletions sphinx/util/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ def restify(cls: type | None, mode: str = 'fully-qualified-except-typing') -> st
return ' | '.join(restify(a, mode) for a in cls.__args__)
elif cls.__module__ in ('__builtin__', 'builtins'):
if hasattr(cls, '__args__'):
if not cls.__args__: # Empty tuple, list, ...
return fr':py:class:`{cls.__name__}`\ [{cls.__args__!r}]'

concatenated_args = ', '.join(restify(arg, mode) for arg in cls.__args__)
return fr':py:class:`{cls.__name__}`\ [{concatenated_args}]'
else:
Expand Down Expand Up @@ -276,8 +279,12 @@ def stringify_annotation(
elif str(annotation).startswith('typing.Annotated'): # for py310+
pass
elif annotation_module == 'builtins' and annotation_qualname:
if hasattr(annotation, '__args__'): # PEP 585 generic
return repr(annotation)
if (args := getattr(annotation, '__args__', None)) is not None: # PEP 585 generic
if not args: # Empty tuple, list, ...
return repr(annotation)

concatenated_args = ', '.join(stringify_annotation(arg, mode) for arg in args)
return f'{annotation_qualname}[{concatenated_args}]'
else:
return annotation_qualname
elif annotation is Ellipsis:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_ext_autodoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2018,7 +2018,7 @@ def test_autodoc_TYPE_CHECKING(app):
' :type: ~_io.StringIO',
'',
'',
'.. py:function:: spam(ham: ~collections.abc.Iterable[str]) -> tuple[gettext.NullTranslations, bool]',
'.. py:function:: spam(ham: ~collections.abc.Iterable[str]) -> tuple[~gettext.NullTranslations, bool]',
' :module: target.TYPE_CHECKING',
'',
]
Expand Down
67 changes: 64 additions & 3 deletions tests/test_util_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ def test_restify_type_hints_typevars():
assert restify(List[T]) == ":py:class:`~typing.List`\\ [:py:obj:`tests.test_util_typing.T`]"
assert restify(List[T], "smart") == ":py:class:`~typing.List`\\ [:py:obj:`~tests.test_util_typing.T`]"

assert restify(list[T]) == ":py:class:`list`\\ [:py:obj:`tests.test_util_typing.T`]"
assert restify(list[T], "smart") == ":py:class:`list`\\ [:py:obj:`~tests.test_util_typing.T`]"

if sys.version_info[:2] >= (3, 10):
assert restify(MyInt) == ":py:class:`tests.test_util_typing.MyInt`"
assert restify(MyInt, "smart") == ":py:class:`~tests.test_util_typing.MyInt`"
Expand All @@ -171,14 +174,20 @@ def test_restify_type_hints_custom_class():

def test_restify_type_hints_alias():
MyStr = str
MyTuple = Tuple[str, str]
MyTypingTuple = Tuple[str, str]
MyTuple = tuple[str, str]
assert restify(MyStr) == ":py:class:`str`"
assert restify(MyTuple) == ":py:class:`~typing.Tuple`\\ [:py:class:`str`, :py:class:`str`]"
assert restify(MyTypingTuple) == ":py:class:`~typing.Tuple`\\ [:py:class:`str`, :py:class:`str`]"
assert restify(MyTuple) == ":py:class:`tuple`\\ [:py:class:`str`, :py:class:`str`]"


def test_restify_type_ForwardRef():
from typing import ForwardRef # type: ignore[attr-defined]
assert restify(ForwardRef("myint")) == ":py:class:`myint`"
assert restify(ForwardRef("MyInt")) == ":py:class:`MyInt`"

assert restify(list[ForwardRef("MyInt")]) == ":py:class:`list`\\ [:py:class:`MyInt`]"

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]


def test_restify_type_Literal():
Expand All @@ -190,10 +199,26 @@ def test_restify_pep_585():
assert restify(list[str]) == ":py:class:`list`\\ [:py:class:`str`]" # type: ignore[attr-defined]
assert restify(dict[str, str]) == (":py:class:`dict`\\ " # type: ignore[attr-defined]
"[:py:class:`str`, :py:class:`str`]")
assert restify(tuple[str, ...]) == ":py:class:`tuple`\\ [:py:class:`str`, ...]"
assert restify(tuple[str, str, str]) == (":py:class:`tuple`\\ "
"[:py:class:`str`, :py:class:`str`, "
":py:class:`str`]")
assert restify(dict[str, tuple[int, ...]]) == (":py:class:`dict`\\ " # type: ignore[attr-defined]
"[:py:class:`str`, :py:class:`tuple`\\ "
"[:py:class:`int`, ...]]")

assert restify(tuple[()]) == ":py:class:`tuple`\\ [()]"

# Mix old typing with PEP 585
assert restify(List[dict[str, Tuple[str, ...]]]) == (":py:class:`~typing.List`\\ "
"[:py:class:`dict`\\ "
"[:py:class:`str`, :py:class:`~typing.Tuple`\\ "
"[:py:class:`str`, ...]]]")
assert restify(tuple[MyList[list[int]], int]) == (":py:class:`tuple`\\ ["
":py:class:`tests.test_util_typing.MyList`\\ "
"[:py:class:`list`\\ [:py:class:`int`]], "
":py:class:`int`]")


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

assert stringify_annotation(MyList[tuple[int, int]], 'fully-qualified-except-typing') == "tests.test_util_typing.MyList[tuple[int, int]]"
assert stringify_annotation(MyList[tuple[int, int]], "fully-qualified") == "tests.test_util_typing.MyList[tuple[int, int]]"
assert stringify_annotation(MyList[tuple[int, int]], "smart") == "~tests.test_util_typing.MyList[tuple[int, int]]"

assert stringify_annotation(type[int], 'fully-qualified-except-typing') == "type[int]"
assert stringify_annotation(type[int], "smart") == "type[int]"

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


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

assert stringify_annotation(list["int"], 'fully-qualified-except-typing') == "list[int]"
assert stringify_annotation(list["int"], 'fully-qualified') == "list[int]"
assert stringify_annotation(list["int"], "smart") == "list[int]"

assert stringify_annotation("Tuple[str]", 'fully-qualified-except-typing') == "Tuple[str]"
assert stringify_annotation("Tuple[str]", 'fully-qualified') == "Tuple[str]"
assert stringify_annotation("Tuple[str]", "smart") == "Tuple[str]"

assert stringify_annotation("tuple[str]", 'fully-qualified-except-typing') == "tuple[str]"
assert stringify_annotation("tuple[str]", 'fully-qualified') == "tuple[str]"
assert stringify_annotation("tuple[str]", "smart") == "tuple[str]"

assert stringify_annotation("unknown", 'fully-qualified-except-typing') == "unknown"
assert stringify_annotation("unknown", 'fully-qualified') == "unknown"
assert stringify_annotation("unknown", "smart") == "unknown"
Expand Down Expand Up @@ -401,6 +442,9 @@ def test_stringify_type_hints_typevars():
assert stringify_annotation(List[T], 'fully-qualified-except-typing') == "List[tests.test_util_typing.T]"
assert stringify_annotation(List[T], "smart") == "~typing.List[~tests.test_util_typing.T]"

assert stringify_annotation(list[T], 'fully-qualified-except-typing') == "list[tests.test_util_typing.T]"
assert stringify_annotation(list[T], "smart") == "list[~tests.test_util_typing.T]"

if sys.version_info[:2] >= (3, 10):
assert stringify_annotation(MyInt, 'fully-qualified-except-typing') == "tests.test_util_typing.MyInt"
assert stringify_annotation(MyInt, "smart") == "~tests.test_util_typing.MyInt"
Expand Down Expand Up @@ -446,6 +490,9 @@ def test_stringify_type_union_operator():
assert stringify_annotation(int | str | None) == "int | str | None" # type: ignore[attr-defined]
assert stringify_annotation(int | str | None, "smart") == "int | str | None" # type: ignore[attr-defined]

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]
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]

assert stringify_annotation(int | Struct) == "int | struct.Struct" # type: ignore[attr-defined]
assert stringify_annotation(int | Struct, "smart") == "int | ~struct.Struct" # type: ignore[attr-defined]

Expand All @@ -461,3 +508,17 @@ def test_stringify_mock():
assert stringify_annotation(unknown, 'fully-qualified-except-typing') == 'unknown'
assert stringify_annotation(unknown.secret.Class, 'fully-qualified-except-typing') == 'unknown.secret.Class'
assert stringify_annotation(unknown.secret.Class, "smart") == 'unknown.secret.Class'


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"

assert stringify_annotation(list[ForwardRef("MyInt")]) == "list[MyInt]"
assert stringify_annotation(list[ForwardRef("MyInt")], 'smart') == "list[MyInt]"

assert stringify_annotation(Tuple[dict[ForwardRef("MyInt"), str], list[List[int]]]) == "Tuple[dict[MyInt, str], list[List[int]]]" # type: ignore[attr-defined]
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]
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 comments on commit 137b3ad

Please sign in to comment.