diff --git a/CHANGES b/CHANGES index 1ca5b16f3..8645ac664 100644 --- a/CHANGES +++ b/CHANGES @@ -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 ---------- diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 0a14c99b1..6619384a1 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -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: @@ -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: diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index 657d57624..efb5c0395 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -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', '', ] diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py index 724330359..a0baf4ffd 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util_typing.py @@ -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`" @@ -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(): @@ -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(): @@ -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] @@ -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" @@ -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" @@ -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] @@ -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]