From 09d05a085853b1c134f9b5b5dd3db8ee53bb1574 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sat, 13 Apr 2024 12:38:42 +0100 Subject: [PATCH] Don't render types with ``Optional[...]`` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- sphinx/util/typing.py | 20 +------ .../test_ext_autodoc_autoclass.py | 3 +- tests/test_util/test_util_typing.py | 60 +++++++++++++------ 3 files changed, 45 insertions(+), 38 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index a31731e7b..fcb6aa6aa 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -186,11 +186,7 @@ def restify(cls: type | None, mode: str = 'fully-qualified-except-typing') -> st else: return ':py:class:`%s`' % cls.__name__ elif UnionType and isinstance(cls, UnionType): - if len(cls.__args__) > 1 and None in cls.__args__: - args = ' | '.join(restify(a, mode) for a in cls.__args__ if a) - return 'Optional[%s]' % args - else: - return ' | '.join(restify(a, mode) for a in cls.__args__) + 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, ... @@ -203,19 +199,7 @@ def restify(cls: type | None, mode: str = 'fully-qualified-except-typing') -> st elif (inspect.isgenericalias(cls) and cls.__module__ == 'typing' and cls.__origin__ is Union): # type: ignore[attr-defined] - if (len(cls.__args__) > 1 # type: ignore[attr-defined] - and cls.__args__[-1] is NoneType): # type: ignore[attr-defined] - if len(cls.__args__) > 2: # type: ignore[attr-defined] - args = ', '.join(restify(a, mode) - for a in cls.__args__[:-1]) # type: ignore[attr-defined] - return ':py:obj:`~typing.Optional`\\ [:obj:`~typing.Union`\\ [%s]]' % args - else: - return ':py:obj:`~typing.Optional`\\ [%s]' % restify( - cls.__args__[0], mode) # type: ignore[attr-defined] - else: - args = ', '.join(restify(a, mode) - for a in cls.__args__) # type: ignore[attr-defined] - return ':py:obj:`~typing.Union`\\ [%s]' % args + return ' | '.join(restify(a, mode) for a in cls.__args__) # type: ignore[attr-defined] elif inspect.isgenericalias(cls): if isinstance(cls.__origin__, typing._SpecialForm): # type: ignore[attr-defined] text = restify(cls.__origin__, mode) # type: ignore[attr-defined,arg-type] diff --git a/tests/test_extensions/test_ext_autodoc_autoclass.py b/tests/test_extensions/test_ext_autodoc_autoclass.py index a7c97d32e..3e68d60a6 100644 --- a/tests/test_extensions/test_ext_autodoc_autoclass.py +++ b/tests/test_extensions/test_ext_autodoc_autoclass.py @@ -276,8 +276,7 @@ def test_show_inheritance_for_subclass_of_generic_type(app): '.. py:class:: Quux(iterable=(), /)', ' :module: target.classes', '', - ' Bases: :py:class:`~typing.List`\\ ' - '[:py:obj:`~typing.Union`\\ [:py:class:`int`, :py:class:`float`]]', + ' Bases: :py:class:`~typing.List`\\ [:py:class:`int` | :py:class:`float`]', '', ' A subclass of List[Union[int, float]]', '', diff --git a/tests/test_util/test_util_typing.py b/tests/test_util/test_util_typing.py index 619fc3442..9c280297f 100644 --- a/tests/test_util/test_util_typing.py +++ b/tests/test_util/test_util_typing.py @@ -190,24 +190,44 @@ def test_restify_type_hints_Callable(): def test_restify_type_hints_Union(): - assert restify(Optional[int]) == ":py:obj:`~typing.Optional`\\ [:py:class:`int`]" - assert restify(Union[str, None]) == ":py:obj:`~typing.Optional`\\ [:py:class:`str`]" - assert restify(Union[int, str]) == (":py:obj:`~typing.Union`\\ " - "[:py:class:`int`, :py:class:`str`]") - assert restify(Union[int, Integral]) == (":py:obj:`~typing.Union`\\ " - "[:py:class:`int`, :py:class:`numbers.Integral`]") - assert restify(Union[int, Integral], "smart") == (":py:obj:`~typing.Union`\\ " - "[:py:class:`int`," - " :py:class:`~numbers.Integral`]") + assert restify(Union[int]) == ":py:class:`int`" + assert restify(Union[int, str]) == ":py:class:`int` | :py:class:`str`" + assert restify(Optional[int]) == ":py:class:`int` | :py:obj:`None`" + + assert restify(Union[str, None]) == ":py:class:`str` | :py:obj:`None`" + assert restify(Union[None, str]) == ":py:obj:`None` | :py:class:`str`" + assert restify(Optional[str]) == ":py:class:`str` | :py:obj:`None`" + + assert restify(Union[int, str, None]) == ( + ":py:class:`int` | :py:class:`str` | :py:obj:`None`" + ) + assert restify(Optional[Union[int, str]]) in { + ":py:class:`str` | :py:class:`int` | :py:obj:`None`", + ":py:class:`int` | :py:class:`str` | :py:obj:`None`", + } + + assert restify(Union[int, Integral]) == ( + ":py:class:`int` | :py:class:`numbers.Integral`" + ) + assert restify(Union[int, Integral], "smart") == ( + ":py:class:`int` | :py:class:`~numbers.Integral`" + ) assert (restify(Union[MyClass1, MyClass2]) == - (":py:obj:`~typing.Union`\\ " - "[:py:class:`tests.test_util.test_util_typing.MyClass1`, " - ":py:class:`tests.test_util.test_util_typing.`]")) + (":py:class:`tests.test_util.test_util_typing.MyClass1`" + " | :py:class:`tests.test_util.test_util_typing.`")) assert (restify(Union[MyClass1, MyClass2], "smart") == - (":py:obj:`~typing.Union`\\ " - "[:py:class:`~tests.test_util.test_util_typing.MyClass1`," - " :py:class:`~tests.test_util.test_util_typing.`]")) + (":py:class:`~tests.test_util.test_util_typing.MyClass1`" + " | :py:class:`~tests.test_util.test_util_typing.`")) + + assert (restify(Optional[Union[MyClass1, MyClass2]]) == + (":py:class:`tests.test_util.test_util_typing.MyClass1`" + " | :py:class:`tests.test_util.test_util_typing.`" + " | :py:obj:`None`")) + assert (restify(Optional[Union[MyClass1, MyClass2]], "smart") == + (":py:class:`~tests.test_util.test_util_typing.MyClass1`" + " | :py:class:`~tests.test_util.test_util_typing.`" + " | :py:obj:`None`")) def test_restify_type_hints_typevars(): @@ -300,6 +320,7 @@ def test_restify_pep_585(): @pytest.mark.skipif(sys.version_info[:2] <= (3, 9), reason='python 3.10+ is required.') def test_restify_type_union_operator(): assert restify(int | None) == ":py:class:`int` | :py:obj:`None`" # type: ignore[attr-defined] + assert restify(None | int) == ":py:obj:`None` | :py:class:`int`" # type: ignore[attr-defined] assert restify(int | str) == ":py:class:`int` | :py:class:`str`" # type: ignore[attr-defined] assert restify(int | str | None) == (":py:class:`int` | :py:class:`str` | " # type: ignore[attr-defined] ":py:obj:`None`") @@ -486,9 +507,12 @@ def test_stringify_type_hints_Union(): assert stringify_annotation(Optional[int], "fully-qualified") == "int | None" assert stringify_annotation(Optional[int], "smart") == "int | None" - assert stringify_annotation(Union[str, None], 'fully-qualified-except-typing') == "str | None" - assert stringify_annotation(Union[str, None], "fully-qualified") == "str | None" - assert stringify_annotation(Union[str, None], "smart") == "str | None" + assert stringify_annotation(Union[int, None], 'fully-qualified-except-typing') == "int | None" + assert stringify_annotation(Union[None, int], 'fully-qualified-except-typing') == "None | int" + assert stringify_annotation(Union[int, None], "fully-qualified") == "int | None" + assert stringify_annotation(Union[None, int], "fully-qualified") == "None | int" + assert stringify_annotation(Union[int, None], "smart") == "int | None" + assert stringify_annotation(Union[None, int], "smart") == "None | int" assert stringify_annotation(Union[int, str], 'fully-qualified-except-typing') == "int | str" assert stringify_annotation(Union[int, str], "fully-qualified") == "int | str"