From d3466d723839b45f1296093cdf2607a5cae3d9e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 14 Apr 2024 11:01:14 +0200 Subject: [PATCH 01/14] cleanup --- sphinx/util/typing.py | 104 ++++++++++++++++++++++++++++++------------ 1 file changed, 74 insertions(+), 30 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 39056f91b44..2fcd520896d 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -8,16 +8,25 @@ from collections.abc import Sequence from contextvars import Context, ContextVar, Token from struct import Struct -from typing import TYPE_CHECKING, Any, Callable, ForwardRef, TypedDict, TypeVar, Union +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + Callable, + ForwardRef, + TypedDict, + TypeVar, + Union, +) from docutils import nodes from docutils.parsers.rst.states import Inliner if TYPE_CHECKING: from collections.abc import Mapping - from typing import Final, Literal + from typing import Final, Literal, Protocol - from typing_extensions import TypeAlias + from typing_extensions import TypeAlias, TypeGuard from sphinx.application import Sphinx @@ -31,6 +40,9 @@ 'smart', ] + class _SpecialFormInterface(Protocol): + _name: str + if sys.version_info >= (3, 10): from types import UnionType else: @@ -164,6 +176,25 @@ def is_system_TypeVar(typ: Any) -> bool: return modname == 'typing' and isinstance(typ, TypeVar) +def _is_special_form(obj: Any) -> TypeGuard[_SpecialFormInterface]: + """Check if *obj* is a typing special form. + + The guarded type is a protocol with the members that Sphinx needs in + this module and not the native ``typing._SpecialForm`` from typeshed, + but the runtime type of *obj* must be a true special form instance. + """ + return isinstance(obj, typing._SpecialForm) + + +def _is_annotated_form(obj: Any) -> TypeGuard[Annotated[Any, ...]]: + """Check if *obj* is an annotated type.""" + return typing.get_origin(obj) is Annotated or str(obj).startswith('typing.Annotated') + + +def _get_typing_internal_name(obj: Any) -> str | None: + return getattr(obj, '_name', None) + + def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> str: """Convert python class to a reST reference. @@ -185,7 +216,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s raise ValueError(msg) # things that are not types - if cls is None or cls is NoneType: + if cls in {None, NoneType}: return ':py:obj:`None`' if cls is Ellipsis: return '...' @@ -241,25 +272,32 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s else: text = restify(cls.__origin__, mode) - origin = getattr(cls, '__origin__', None) - if not hasattr(cls, '__args__'): # NoQA: SIM114 - pass - elif all(is_system_TypeVar(a) for a in cls.__args__): - # Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT]) - pass - elif cls.__module__ == 'typing' and cls._name == 'Callable': - args = ', '.join(restify(a, mode) for a in cls.__args__[:-1]) - text += fr'\ [[{args}], {restify(cls.__args__[-1], mode)}]' - elif cls.__module__ == 'typing' and getattr(origin, '_name', None) == 'Literal': - args = ', '.join(_format_literal_arg_restify(a, mode=mode) - for a in cls.__args__) - text += fr"\ [{args}]" - elif cls.__args__: - text += fr"\ [{', '.join(restify(a, mode) for a in cls.__args__)}]" - - return text - elif isinstance(cls, typing._SpecialForm): - return f':py:obj:`~{cls.__module__}.{cls._name}`' # type: ignore[attr-defined] + __args__ = getattr(cls, '__args__', ()) + if not __args__: + return text + + if all(map(is_system_TypeVar, __args__)): + # do not print the arguments they are all type variables + return text + + params: str | None = None + + if cls.__module__ == 'typing': + if _get_typing_internal_name(cls) == 'Callable': + vargs = ', '.join(restify(a, mode) for a in __args__[:-1]) + rtype = restify(__args__[-1], mode) + params = f'[{vargs}], {rtype}' + elif _get_typing_internal_name(cls.__origin__) == 'Literal': + params = ', '.join(_format_literal_arg_restify(a, mode) + for a in cls.__args__) + + if params is None: + # generic representation of the parameters + params = ', '.join(restify(a, mode) for a in __args__) + + return rf'{text}\ [{params}]' + elif _is_special_form(cls): + return f':py:obj:`~{cls.__module__}.{cls._name}`' elif sys.version_info[:2] >= (3, 11) and cls is typing.Any: # handle bpo-46998 return f':py:obj:`~{cls.__module__}.{cls.__name__}`' @@ -315,7 +353,7 @@ def stringify_annotation( raise ValueError(msg) # things that are not types - if annotation is None or annotation is NoneType: + if annotation in {None, NoneType}: return 'None' if annotation is Ellipsis: return '...' @@ -338,6 +376,7 @@ def stringify_annotation( annotation_name: str = getattr(annotation, '__name__', '') annotation_module_is_typing = annotation_module == 'typing' + # extract the annotation's base type by considering formattable cases if isinstance(annotation, TypeVar): if annotation_module_is_typing and mode in {'fully-qualified-except-typing', 'smart'}: return annotation_name @@ -365,6 +404,9 @@ def stringify_annotation( return repr(annotation) concatenated_args = ', '.join(stringify_annotation(arg, mode) for arg in args) return f'{annotation_qualname}[{concatenated_args}]' + else: + # add other special cases that can be directly formatted + pass module_prefix = f'{annotation_module}.' annotation_forward_arg: str | None = getattr(annotation, '__forward_arg__', None) @@ -387,6 +429,8 @@ def stringify_annotation( elif annotation_qualname: qualname = annotation_qualname else: + # in this case, we know that the annotation is a member + # of :mod:`typing` and all of them define ``__origin__`` qualname = stringify_annotation( annotation.__origin__, 'fully-qualified-except-typing', ).replace('typing.', '') # ex. Union @@ -402,12 +446,12 @@ def stringify_annotation( # only make them appear twice return repr(annotation) - annotation_args = getattr(annotation, '__args__', None) - if annotation_args: - if not isinstance(annotation_args, (list, tuple)): - # broken __args__ found - pass - elif qualname in {'Optional', 'Union', 'types.UnionType'}: + # process the generic arguments (if any); they must be a list or a tuple + # otherwise they are considered as 'broken' + + annotation_args = getattr(annotation, '__args__', ()) + if annotation_args and isinstance(annotation_args, (list, tuple)): + if qualname in {'Optional', 'Union', 'types.UnionType'}: return ' | '.join(stringify_annotation(a, mode) for a in annotation_args) elif qualname == 'Callable': args = ', '.join(stringify_annotation(a, mode) for a in annotation_args[:-1]) From 866a2e1572fb439fca88f0ad64ec83b943059657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 15 Apr 2024 09:27:30 +0200 Subject: [PATCH 02/14] revert some changes in case ``__contains__`` raises errors --- sphinx/util/typing.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 2fcd520896d..9e950d835a7 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -226,25 +226,25 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s # If the mode is 'smart', we always use '~'. # If the mode is 'fully-qualified-except-typing', # we use '~' only for the objects in the ``typing`` module. - if mode == 'smart' or getattr(cls, '__module__', None) == 'typing': - modprefix = '~' - else: - modprefix = '' + # + # With an if-else block, mypy infers 'mode' to be a 'str' + # instead of a literal string (and we don't want to cast). + module_prefix = '~' if mode == 'smart' or getattr(cls, '__module__', None) == 'typing' else '' try: if ismockmodule(cls): - return f':py:class:`{modprefix}{cls.__name__}`' + return f':py:class:`{module_prefix}{cls.__name__}`' elif ismock(cls): - return f':py:class:`{modprefix}{cls.__module__}.{cls.__name__}`' + return f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`' elif is_invalid_builtin_class(cls): # The above predicate never raises TypeError but should not be # 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:`{modprefix}{_INVALID_BUILTIN_CLASSES[cls]}`' + return f':py:class:`{module_prefix}{_INVALID_BUILTIN_CLASSES[cls]}`' elif inspect.isNewType(cls): if sys.version_info[:2] >= (3, 10): # newtypes have correct module info since Python 3.10+ - return f':py:class:`{modprefix}{cls.__module__}.{cls.__name__}`' + return f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`' return f':py:class:`{cls.__name__}`' elif UnionType and isinstance(cls, UnionType): # Union types (PEP 585) retain their definition order when they @@ -268,7 +268,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s text = restify(cls.__origin__, mode) elif getattr(cls, '_name', None): cls_name = cls._name - text = f':py:class:`{modprefix}{cls.__module__}.{cls_name}`' + text = f':py:class:`{module_prefix}{cls.__module__}.{cls_name}`' else: text = restify(cls.__origin__, mode) @@ -302,12 +302,12 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s # handle bpo-46998 return f':py:obj:`~{cls.__module__}.{cls.__name__}`' elif hasattr(cls, '__qualname__'): - return f':py:class:`{modprefix}{cls.__module__}.{cls.__qualname__}`' + return f':py:class:`{module_prefix}{cls.__module__}.{cls.__qualname__}`' elif isinstance(cls, ForwardRef): return f':py:class:`{cls.__forward_arg__}`' else: # not a class (ex. TypeVar) - return f':py:obj:`{modprefix}{cls.__module__}.{cls.__name__}`' + return f':py:obj:`{module_prefix}{cls.__module__}.{cls.__name__}`' except (AttributeError, TypeError): return inspect.object_description(cls) From 2d147b50fb638398bb71407f709832c32aaf739d Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Tue, 23 Apr 2024 06:13:35 +0100 Subject: [PATCH 03/14] module_prefix->modprefix --- sphinx/util/typing.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 9e950d835a7..ff833657d80 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -229,22 +229,22 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s # # With an if-else block, mypy infers 'mode' to be a 'str' # instead of a literal string (and we don't want to cast). - module_prefix = '~' if mode == 'smart' or getattr(cls, '__module__', None) == 'typing' else '' + modprefix = '~' if mode == 'smart' or getattr(cls, '__module__', None) == 'typing' else '' try: if ismockmodule(cls): - return f':py:class:`{module_prefix}{cls.__name__}`' + return f':py:class:`{modprefix}{cls.__name__}`' elif ismock(cls): - return f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`' + return f':py:class:`{modprefix}{cls.__module__}.{cls.__name__}`' elif is_invalid_builtin_class(cls): # The above predicate never raises TypeError but should not be # 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]}`' + return f':py:class:`{modprefix}{_INVALID_BUILTIN_CLASSES[cls]}`' elif inspect.isNewType(cls): if sys.version_info[:2] >= (3, 10): # newtypes have correct module info since Python 3.10+ - return f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`' + return f':py:class:`{modprefix}{cls.__module__}.{cls.__name__}`' return f':py:class:`{cls.__name__}`' elif UnionType and isinstance(cls, UnionType): # Union types (PEP 585) retain their definition order when they @@ -268,7 +268,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s text = restify(cls.__origin__, mode) elif getattr(cls, '_name', None): cls_name = cls._name - text = f':py:class:`{module_prefix}{cls.__module__}.{cls_name}`' + text = f':py:class:`{modprefix}{cls.__module__}.{cls_name}`' else: text = restify(cls.__origin__, mode) @@ -288,7 +288,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s rtype = restify(__args__[-1], mode) params = f'[{vargs}], {rtype}' elif _get_typing_internal_name(cls.__origin__) == 'Literal': - params = ', '.join(_format_literal_arg_restify(a, mode) + params = ', '.join(_format_literal_arg_restify(a, mode=mode) for a in cls.__args__) if params is None: @@ -302,12 +302,12 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s # handle bpo-46998 return f':py:obj:`~{cls.__module__}.{cls.__name__}`' elif hasattr(cls, '__qualname__'): - return f':py:class:`{module_prefix}{cls.__module__}.{cls.__qualname__}`' + return f':py:class:`{modprefix}{cls.__module__}.{cls.__qualname__}`' elif isinstance(cls, ForwardRef): return f':py:class:`{cls.__forward_arg__}`' else: # not a class (ex. TypeVar) - return f':py:obj:`{module_prefix}{cls.__module__}.{cls.__name__}`' + return f':py:obj:`{modprefix}{cls.__module__}.{cls.__name__}`' except (AttributeError, TypeError): return inspect.object_description(cls) From 8e6ed22e8a10f9c8510b90f16c3c8e9e48f65c1c Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Tue, 23 Apr 2024 06:49:37 +0100 Subject: [PATCH 04/14] tmp --- sphinx/util/typing.py | 72 +++++++++--------------- tests/test_util/test_util_typing.py | 86 +++++++++++++++++++---------- 2 files changed, 82 insertions(+), 76 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index ff833657d80..24f09abe9ac 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: from collections.abc import Mapping - from typing import Final, Literal, Protocol + from typing import Final, Literal from typing_extensions import TypeAlias, TypeGuard @@ -40,9 +40,6 @@ 'smart', ] - class _SpecialFormInterface(Protocol): - _name: str - if sys.version_info >= (3, 10): from types import UnionType else: @@ -176,25 +173,11 @@ def is_system_TypeVar(typ: Any) -> bool: return modname == 'typing' and isinstance(typ, TypeVar) -def _is_special_form(obj: Any) -> TypeGuard[_SpecialFormInterface]: - """Check if *obj* is a typing special form. - - The guarded type is a protocol with the members that Sphinx needs in - this module and not the native ``typing._SpecialForm`` from typeshed, - but the runtime type of *obj* must be a true special form instance. - """ - return isinstance(obj, typing._SpecialForm) - - def _is_annotated_form(obj: Any) -> TypeGuard[Annotated[Any, ...]]: """Check if *obj* is an annotated type.""" return typing.get_origin(obj) is Annotated or str(obj).startswith('typing.Annotated') -def _get_typing_internal_name(obj: Any) -> str | None: - return getattr(obj, '_name', None) - - def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> str: """Convert python class to a reST reference. @@ -265,39 +248,38 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s return ' | '.join(restify(a, mode) for a in cls.__args__) elif inspect.isgenericalias(cls): if isinstance(cls.__origin__, typing._SpecialForm): + # ClassVar; Concatenate; Final; Literal; Unpack; TypeGuard + # Required/NotRequired text = restify(cls.__origin__, mode) - elif getattr(cls, '_name', None): - cls_name = cls._name - text = f':py:class:`{modprefix}{cls.__module__}.{cls_name}`' + elif cls.__name__: + text = f':py:class:`{modprefix}{cls.__module__}.{cls.__name__}`' else: text = restify(cls.__origin__, mode) __args__ = getattr(cls, '__args__', ()) if not __args__: return text - if all(map(is_system_TypeVar, __args__)): - # do not print the arguments they are all type variables + # Don't print the arguments; they're all system defined type variables. return text - params: str | None = None - - if cls.__module__ == 'typing': - if _get_typing_internal_name(cls) == 'Callable': - vargs = ', '.join(restify(a, mode) for a in __args__[:-1]) - rtype = restify(__args__[-1], mode) - params = f'[{vargs}], {rtype}' - elif _get_typing_internal_name(cls.__origin__) == 'Literal': - params = ', '.join(_format_literal_arg_restify(a, mode=mode) - for a in cls.__args__) + # Callable (either collections.abc or typing) + if (cls.__module__ in {'collections.abc', 'typing'} + and cls.__name__ == 'Callable'): + args = ', '.join(restify(a, mode) for a in __args__[:-1]) + returns = restify(__args__[-1], mode) + return fr'{text}\ [[{args}], {returns}]' - if params is None: + if cls.__module__ == 'typing' and cls.__origin__.__name__ == 'Literal': + params = ', '.join(_format_literal_arg_restify(a, mode=mode) + for a in cls.__args__) + else: # generic representation of the parameters params = ', '.join(restify(a, mode) for a in __args__) return rf'{text}\ [{params}]' - elif _is_special_form(cls): - return f':py:obj:`~{cls.__module__}.{cls._name}`' + elif isinstance(cls, typing._SpecialForm): + return f':py:obj:`~{cls.__module__}.{cls.__name__}`' # type: ignore[attr-defined] elif sys.version_info[:2] >= (3, 11) and cls is typing.Any: # handle bpo-46998 return f':py:obj:`~{cls.__module__}.{cls.__name__}`' @@ -365,10 +347,7 @@ def stringify_annotation( if not annotation: return repr(annotation) - if mode == 'smart': - module_prefix = '~' - else: - module_prefix = '' + module_prefix = '~' if mode == 'smart' else '' # The values below must be strings if the objects are well-formed. annotation_qualname: str = getattr(annotation, '__qualname__', '') @@ -376,7 +355,7 @@ def stringify_annotation( annotation_name: str = getattr(annotation, '__name__', '') annotation_module_is_typing = annotation_module == 'typing' - # extract the annotation's base type by considering formattable cases + # Extract the annotation's base type by considering formattable cases if isinstance(annotation, TypeVar): if annotation_module_is_typing and mode in {'fully-qualified-except-typing', 'smart'}: return annotation_name @@ -392,7 +371,7 @@ def stringify_annotation( return module_prefix + f'{annotation_module}.{annotation_name}' elif is_invalid_builtin_class(annotation): return module_prefix + _INVALID_BUILTIN_CLASSES[annotation] - elif str(annotation).startswith('typing.Annotated'): # for py39+ + elif _is_annotated_form(annotation): # for py39+ pass elif annotation_module == 'builtins' and annotation_qualname: args = getattr(annotation, '__args__', None) @@ -430,7 +409,7 @@ def stringify_annotation( qualname = annotation_qualname else: # in this case, we know that the annotation is a member - # of :mod:`typing` and all of them define ``__origin__`` + # of ``typing`` and all of them define ``__origin__`` qualname = stringify_annotation( annotation.__origin__, 'fully-qualified-except-typing', ).replace('typing.', '') # ex. Union @@ -446,9 +425,8 @@ def stringify_annotation( # only make them appear twice return repr(annotation) - # process the generic arguments (if any); they must be a list or a tuple - # otherwise they are considered as 'broken' - + # Process the generic arguments (if any). + # They must be a list or a tuple, otherwise they are considered 'broken'. annotation_args = getattr(annotation, '__args__', ()) if annotation_args and isinstance(annotation_args, (list, tuple)): if qualname in {'Optional', 'Union', 'types.UnionType'}: @@ -461,7 +439,7 @@ def stringify_annotation( args = ', '.join(_format_literal_arg_stringify(a, mode=mode) for a in annotation_args) return f'{module_prefix}Literal[{args}]' - elif str(annotation).startswith('typing.Annotated'): # for py39+ + elif _is_annotated_form(annotation): # for py39+ return stringify_annotation(annotation_args[0], mode) elif all(is_system_TypeVar(a) for a in annotation_args): # Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT]) diff --git a/tests/test_util/test_util_typing.py b/tests/test_util/test_util_typing.py index 9c280297f49..dd5ec266fdd 100644 --- a/tests/test_util/test_util_typing.py +++ b/tests/test_util/test_util_typing.py @@ -1,6 +1,8 @@ """Tests util.typing functions.""" import sys +import typing as t +from collections import abc from contextvars import Context, ContextVar, Token from enum import Enum from numbers import Integral @@ -29,10 +31,7 @@ ) from typing import ( Any, - Callable, Dict, - Generator, - Iterator, List, NewType, Optional, @@ -173,20 +172,29 @@ def test_restify_type_hints_containers(): assert restify(MyList[Tuple[int, int]]) == (":py:class:`tests.test_util.test_util_typing.MyList`\\ " "[:py:class:`~typing.Tuple`\\ " "[:py:class:`int`, :py:class:`int`]]") - assert restify(Generator[None, None, None]) == (":py:class:`~typing.Generator`\\ " - "[:py:obj:`None`, :py:obj:`None`, " - ":py:obj:`None`]") - assert restify(Iterator[None]) == (":py:class:`~typing.Iterator`\\ " - "[:py:obj:`None`]") + assert restify(t.Generator[None, None, None]) == (":py:class:`~typing.Generator`\\ " + "[:py:obj:`None`, :py:obj:`None`, " + ":py:obj:`None`]") + assert restify(abc.Generator[None, None, None]) == (":py:class:`collections.abc.Generator`\\ " + "[:py:obj:`None`, :py:obj:`None`, " + ":py:obj:`None`]") + assert restify(t.Iterator[None]) == (":py:class:`~typing.Iterator`\\ " + "[:py:obj:`None`]") + assert restify(abc.Iterator[None]) == (":py:class:`collections.abc.Iterator`\\ " + "[:py:obj:`None`]") def test_restify_type_hints_Callable(): - assert restify(Callable) == ":py:class:`~typing.Callable`" - - assert restify(Callable[[str], int]) == (":py:class:`~typing.Callable`\\ " - "[[:py:class:`str`], :py:class:`int`]") - assert restify(Callable[..., int]) == (":py:class:`~typing.Callable`\\ " - "[[...], :py:class:`int`]") + assert restify(t.Callable) == ":py:class:`~typing.Callable`" + assert restify(t.Callable[[str], int]) == (":py:class:`~typing.Callable`\\ " + "[[:py:class:`str`], :py:class:`int`]") + assert restify(t.Callable[..., int]) == (":py:class:`~typing.Callable`\\ " + "[[...], :py:class:`int`]") + assert restify(abc.Callable) == ":py:class:`collections.abc.Callable`" + assert restify(abc.Callable[[str], int]) == (":py:class:`collections.abc.Callable`\\ " + "[[:py:class:`str`], :py:class:`int`]") + assert restify(abc.Callable[..., int]) == (":py:class:`collections.abc.Callable`\\ " + "[[...], :py:class:`int`]") def test_restify_type_hints_Union(): @@ -409,13 +417,21 @@ def test_stringify_type_hints_containers(): assert stringify_annotation(MyList[Tuple[int, int]], "fully-qualified") == "tests.test_util.test_util_typing.MyList[typing.Tuple[int, int]]" assert stringify_annotation(MyList[Tuple[int, int]], "smart") == "~tests.test_util.test_util_typing.MyList[~typing.Tuple[int, int]]" - assert stringify_annotation(Generator[None, None, None], 'fully-qualified-except-typing') == "Generator[None, None, None]" - assert stringify_annotation(Generator[None, None, None], "fully-qualified") == "typing.Generator[None, None, None]" - assert stringify_annotation(Generator[None, None, None], "smart") == "~typing.Generator[None, None, None]" + assert stringify_annotation(t.Generator[None, None, None], 'fully-qualified-except-typing') == "Generator[None, None, None]" + assert stringify_annotation(t.Generator[None, None, None], "fully-qualified") == "typing.Generator[None, None, None]" + assert stringify_annotation(t.Generator[None, None, None], "smart") == "~typing.Generator[None, None, None]" + + assert stringify_annotation(abc.Generator[None, None, None], 'fully-qualified-except-typing') == "collections.abc.Generator[None, None, None]" + assert stringify_annotation(abc.Generator[None, None, None], "fully-qualified") == "collections.abc.Generator[None, None, None]" + assert stringify_annotation(abc.Generator[None, None, None], "smart") == "~collections.abc.Generator[None, None, None]" + + assert stringify_annotation(t.Iterator[None], 'fully-qualified-except-typing') == "Iterator[None]" + assert stringify_annotation(t.Iterator[None], "fully-qualified") == "typing.Iterator[None]" + assert stringify_annotation(t.Iterator[None], "smart") == "~typing.Iterator[None]" - assert stringify_annotation(Iterator[None], 'fully-qualified-except-typing') == "Iterator[None]" - assert stringify_annotation(Iterator[None], "fully-qualified") == "typing.Iterator[None]" - assert stringify_annotation(Iterator[None], "smart") == "~typing.Iterator[None]" + assert stringify_annotation(abc.Iterator[None], 'fully-qualified-except-typing') == "collections.abc.Iterator[None]" + assert stringify_annotation(abc.Iterator[None], "fully-qualified") == "collections.abc.Iterator[None]" + assert stringify_annotation(abc.Iterator[None], "smart") == "~collections.abc.Iterator[None]" def test_stringify_type_hints_pep_585(): @@ -489,17 +505,29 @@ def test_stringify_type_hints_string(): def test_stringify_type_hints_Callable(): - assert stringify_annotation(Callable, 'fully-qualified-except-typing') == "Callable" - assert stringify_annotation(Callable, "fully-qualified") == "typing.Callable" - assert stringify_annotation(Callable, "smart") == "~typing.Callable" + assert stringify_annotation(t.Callable, 'fully-qualified-except-typing') == "Callable" + assert stringify_annotation(t.Callable, "fully-qualified") == "typing.Callable" + assert stringify_annotation(t.Callable, "smart") == "~typing.Callable" + + assert stringify_annotation(t.Callable[[str], int], 'fully-qualified-except-typing') == "Callable[[str], int]" + assert stringify_annotation(t.Callable[[str], int], "fully-qualified") == "typing.Callable[[str], int]" + assert stringify_annotation(t.Callable[[str], int], "smart") == "~typing.Callable[[str], int]" + + assert stringify_annotation(t.Callable[..., int], 'fully-qualified-except-typing') == "Callable[[...], int]" + assert stringify_annotation(t.Callable[..., int], "fully-qualified") == "typing.Callable[[...], int]" + assert stringify_annotation(t.Callable[..., int], "smart") == "~typing.Callable[[...], int]" + + assert stringify_annotation(abc.Callable, 'fully-qualified-except-typing') == "collections.abc.Callable" + assert stringify_annotation(abc.Callable, "fully-qualified") == "collections.abc.Callable" + assert stringify_annotation(abc.Callable, "smart") == "~collections.abc.Callable" - assert stringify_annotation(Callable[[str], int], 'fully-qualified-except-typing') == "Callable[[str], int]" - assert stringify_annotation(Callable[[str], int], "fully-qualified") == "typing.Callable[[str], int]" - assert stringify_annotation(Callable[[str], int], "smart") == "~typing.Callable[[str], int]" + assert stringify_annotation(abc.Callable[[str], int], 'fully-qualified-except-typing') == "collections.abc.Callable[[str], int]" + assert stringify_annotation(abc.Callable[[str], int], "fully-qualified") == "collections.abc.Callable[[str], int]" + assert stringify_annotation(abc.Callable[[str], int], "smart") == "~collections.abc.Callable[[str], int]" - assert stringify_annotation(Callable[..., int], 'fully-qualified-except-typing') == "Callable[[...], int]" - assert stringify_annotation(Callable[..., int], "fully-qualified") == "typing.Callable[[...], int]" - assert stringify_annotation(Callable[..., int], "smart") == "~typing.Callable[[...], int]" + assert stringify_annotation(abc.Callable[..., int], 'fully-qualified-except-typing') == "collections.abc.Callable[[...], int]" + assert stringify_annotation(abc.Callable[..., int], "fully-qualified") == "collections.abc.Callable[[...], int]" + assert stringify_annotation(abc.Callable[..., int], "smart") == "~collections.abc.Callable[[...], int]" def test_stringify_type_hints_Union(): From c880c7dd0bdbeba5bf5747a6401f7fe313be685e Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Tue, 23 Apr 2024 07:22:22 +0100 Subject: [PATCH 05/14] module_prefix --- sphinx/util/typing.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 24f09abe9ac..da7cafe68ff 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -212,22 +212,22 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s # # With an if-else block, mypy infers 'mode' to be a 'str' # instead of a literal string (and we don't want to cast). - modprefix = '~' if mode == 'smart' or getattr(cls, '__module__', None) == 'typing' else '' + module_prefix = '~' if mode == 'smart' or getattr(cls, '__module__', None) == 'typing' else '' try: if ismockmodule(cls): - return f':py:class:`{modprefix}{cls.__name__}`' + return f':py:class:`{module_prefix}{cls.__name__}`' elif ismock(cls): - return f':py:class:`{modprefix}{cls.__module__}.{cls.__name__}`' + return f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`' elif is_invalid_builtin_class(cls): # The above predicate never raises TypeError but should not be # 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:`{modprefix}{_INVALID_BUILTIN_CLASSES[cls]}`' + return f':py:class:`{module_prefix}{_INVALID_BUILTIN_CLASSES[cls]}`' elif inspect.isNewType(cls): if sys.version_info[:2] >= (3, 10): # newtypes have correct module info since Python 3.10+ - return f':py:class:`{modprefix}{cls.__module__}.{cls.__name__}`' + return f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`' return f':py:class:`{cls.__name__}`' elif UnionType and isinstance(cls, UnionType): # Union types (PEP 585) retain their definition order when they @@ -252,7 +252,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s # Required/NotRequired text = restify(cls.__origin__, mode) elif cls.__name__: - text = f':py:class:`{modprefix}{cls.__module__}.{cls.__name__}`' + text = f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`' else: text = restify(cls.__origin__, mode) @@ -284,12 +284,12 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s # handle bpo-46998 return f':py:obj:`~{cls.__module__}.{cls.__name__}`' elif hasattr(cls, '__qualname__'): - return f':py:class:`{modprefix}{cls.__module__}.{cls.__qualname__}`' + return f':py:class:`{module_prefix}{cls.__module__}.{cls.__qualname__}`' elif isinstance(cls, ForwardRef): return f':py:class:`{cls.__forward_arg__}`' else: # not a class (ex. TypeVar) - return f':py:obj:`{modprefix}{cls.__module__}.{cls.__name__}`' + return f':py:obj:`{module_prefix}{cls.__module__}.{cls.__name__}`' except (AttributeError, TypeError): return inspect.object_description(cls) From f511e5488f27d3b129047cccdb40090f5dd43ba4 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Tue, 23 Apr 2024 07:26:13 +0100 Subject: [PATCH 06/14] cls_module_is_typing --- sphinx/util/typing.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index da7cafe68ff..28be7e91589 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -206,13 +206,12 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s if isinstance(cls, str): return cls + cls_module_is_typing = getattr(cls, '__module__', '') == 'typing' + # If the mode is 'smart', we always use '~'. # If the mode is 'fully-qualified-except-typing', # we use '~' only for the objects in the ``typing`` module. - # - # With an if-else block, mypy infers 'mode' to be a 'str' - # instead of a literal string (and we don't want to cast). - module_prefix = '~' if mode == 'smart' or getattr(cls, '__module__', None) == 'typing' else '' + module_prefix = '~' if mode == 'smart' or cls_module_is_typing else '' try: if ismockmodule(cls): @@ -242,7 +241,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s return fr':py:class:`{cls.__name__}`\ [{concatenated_args}]' return f':py:class:`{cls.__name__}`' elif (inspect.isgenericalias(cls) - and cls.__module__ == 'typing' + and cls_module_is_typing and cls.__origin__ is Union): # *cls* is defined in ``typing``, and thus ``__args__`` must exist return ' | '.join(restify(a, mode) for a in cls.__args__) @@ -270,7 +269,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s returns = restify(__args__[-1], mode) return fr'{text}\ [[{args}], {returns}]' - if cls.__module__ == 'typing' and cls.__origin__.__name__ == 'Literal': + if cls_module_is_typing and cls.__origin__.__name__ == 'Literal': params = ', '.join(_format_literal_arg_restify(a, mode=mode) for a in cls.__args__) else: From e1b0ab14ee06ba44ac671226b6116ef151ecd947 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Tue, 23 Apr 2024 07:28:22 +0100 Subject: [PATCH 07/14] revert --- sphinx/util/typing.py | 3 +- tests/test_util/test_util_typing.py | 86 ++++++++++------------------- 2 files changed, 30 insertions(+), 59 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 28be7e91589..75970894e5b 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -263,8 +263,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s return text # Callable (either collections.abc or typing) - if (cls.__module__ in {'collections.abc', 'typing'} - and cls.__name__ == 'Callable'): + if cls_module_is_typing and cls.__name__ == 'Callable': args = ', '.join(restify(a, mode) for a in __args__[:-1]) returns = restify(__args__[-1], mode) return fr'{text}\ [[{args}], {returns}]' diff --git a/tests/test_util/test_util_typing.py b/tests/test_util/test_util_typing.py index dd5ec266fdd..9c280297f49 100644 --- a/tests/test_util/test_util_typing.py +++ b/tests/test_util/test_util_typing.py @@ -1,8 +1,6 @@ """Tests util.typing functions.""" import sys -import typing as t -from collections import abc from contextvars import Context, ContextVar, Token from enum import Enum from numbers import Integral @@ -31,7 +29,10 @@ ) from typing import ( Any, + Callable, Dict, + Generator, + Iterator, List, NewType, Optional, @@ -172,29 +173,20 @@ def test_restify_type_hints_containers(): assert restify(MyList[Tuple[int, int]]) == (":py:class:`tests.test_util.test_util_typing.MyList`\\ " "[:py:class:`~typing.Tuple`\\ " "[:py:class:`int`, :py:class:`int`]]") - assert restify(t.Generator[None, None, None]) == (":py:class:`~typing.Generator`\\ " - "[:py:obj:`None`, :py:obj:`None`, " - ":py:obj:`None`]") - assert restify(abc.Generator[None, None, None]) == (":py:class:`collections.abc.Generator`\\ " - "[:py:obj:`None`, :py:obj:`None`, " - ":py:obj:`None`]") - assert restify(t.Iterator[None]) == (":py:class:`~typing.Iterator`\\ " - "[:py:obj:`None`]") - assert restify(abc.Iterator[None]) == (":py:class:`collections.abc.Iterator`\\ " - "[:py:obj:`None`]") + assert restify(Generator[None, None, None]) == (":py:class:`~typing.Generator`\\ " + "[:py:obj:`None`, :py:obj:`None`, " + ":py:obj:`None`]") + assert restify(Iterator[None]) == (":py:class:`~typing.Iterator`\\ " + "[:py:obj:`None`]") def test_restify_type_hints_Callable(): - assert restify(t.Callable) == ":py:class:`~typing.Callable`" - assert restify(t.Callable[[str], int]) == (":py:class:`~typing.Callable`\\ " - "[[:py:class:`str`], :py:class:`int`]") - assert restify(t.Callable[..., int]) == (":py:class:`~typing.Callable`\\ " - "[[...], :py:class:`int`]") - assert restify(abc.Callable) == ":py:class:`collections.abc.Callable`" - assert restify(abc.Callable[[str], int]) == (":py:class:`collections.abc.Callable`\\ " - "[[:py:class:`str`], :py:class:`int`]") - assert restify(abc.Callable[..., int]) == (":py:class:`collections.abc.Callable`\\ " - "[[...], :py:class:`int`]") + assert restify(Callable) == ":py:class:`~typing.Callable`" + + assert restify(Callable[[str], int]) == (":py:class:`~typing.Callable`\\ " + "[[:py:class:`str`], :py:class:`int`]") + assert restify(Callable[..., int]) == (":py:class:`~typing.Callable`\\ " + "[[...], :py:class:`int`]") def test_restify_type_hints_Union(): @@ -417,21 +409,13 @@ def test_stringify_type_hints_containers(): assert stringify_annotation(MyList[Tuple[int, int]], "fully-qualified") == "tests.test_util.test_util_typing.MyList[typing.Tuple[int, int]]" assert stringify_annotation(MyList[Tuple[int, int]], "smart") == "~tests.test_util.test_util_typing.MyList[~typing.Tuple[int, int]]" - assert stringify_annotation(t.Generator[None, None, None], 'fully-qualified-except-typing') == "Generator[None, None, None]" - assert stringify_annotation(t.Generator[None, None, None], "fully-qualified") == "typing.Generator[None, None, None]" - assert stringify_annotation(t.Generator[None, None, None], "smart") == "~typing.Generator[None, None, None]" - - assert stringify_annotation(abc.Generator[None, None, None], 'fully-qualified-except-typing') == "collections.abc.Generator[None, None, None]" - assert stringify_annotation(abc.Generator[None, None, None], "fully-qualified") == "collections.abc.Generator[None, None, None]" - assert stringify_annotation(abc.Generator[None, None, None], "smart") == "~collections.abc.Generator[None, None, None]" - - assert stringify_annotation(t.Iterator[None], 'fully-qualified-except-typing') == "Iterator[None]" - assert stringify_annotation(t.Iterator[None], "fully-qualified") == "typing.Iterator[None]" - assert stringify_annotation(t.Iterator[None], "smart") == "~typing.Iterator[None]" + assert stringify_annotation(Generator[None, None, None], 'fully-qualified-except-typing') == "Generator[None, None, None]" + assert stringify_annotation(Generator[None, None, None], "fully-qualified") == "typing.Generator[None, None, None]" + assert stringify_annotation(Generator[None, None, None], "smart") == "~typing.Generator[None, None, None]" - assert stringify_annotation(abc.Iterator[None], 'fully-qualified-except-typing') == "collections.abc.Iterator[None]" - assert stringify_annotation(abc.Iterator[None], "fully-qualified") == "collections.abc.Iterator[None]" - assert stringify_annotation(abc.Iterator[None], "smart") == "~collections.abc.Iterator[None]" + assert stringify_annotation(Iterator[None], 'fully-qualified-except-typing') == "Iterator[None]" + assert stringify_annotation(Iterator[None], "fully-qualified") == "typing.Iterator[None]" + assert stringify_annotation(Iterator[None], "smart") == "~typing.Iterator[None]" def test_stringify_type_hints_pep_585(): @@ -505,29 +489,17 @@ def test_stringify_type_hints_string(): def test_stringify_type_hints_Callable(): - assert stringify_annotation(t.Callable, 'fully-qualified-except-typing') == "Callable" - assert stringify_annotation(t.Callable, "fully-qualified") == "typing.Callable" - assert stringify_annotation(t.Callable, "smart") == "~typing.Callable" - - assert stringify_annotation(t.Callable[[str], int], 'fully-qualified-except-typing') == "Callable[[str], int]" - assert stringify_annotation(t.Callable[[str], int], "fully-qualified") == "typing.Callable[[str], int]" - assert stringify_annotation(t.Callable[[str], int], "smart") == "~typing.Callable[[str], int]" - - assert stringify_annotation(t.Callable[..., int], 'fully-qualified-except-typing') == "Callable[[...], int]" - assert stringify_annotation(t.Callable[..., int], "fully-qualified") == "typing.Callable[[...], int]" - assert stringify_annotation(t.Callable[..., int], "smart") == "~typing.Callable[[...], int]" - - assert stringify_annotation(abc.Callable, 'fully-qualified-except-typing') == "collections.abc.Callable" - assert stringify_annotation(abc.Callable, "fully-qualified") == "collections.abc.Callable" - assert stringify_annotation(abc.Callable, "smart") == "~collections.abc.Callable" + assert stringify_annotation(Callable, 'fully-qualified-except-typing') == "Callable" + assert stringify_annotation(Callable, "fully-qualified") == "typing.Callable" + assert stringify_annotation(Callable, "smart") == "~typing.Callable" - assert stringify_annotation(abc.Callable[[str], int], 'fully-qualified-except-typing') == "collections.abc.Callable[[str], int]" - assert stringify_annotation(abc.Callable[[str], int], "fully-qualified") == "collections.abc.Callable[[str], int]" - assert stringify_annotation(abc.Callable[[str], int], "smart") == "~collections.abc.Callable[[str], int]" + assert stringify_annotation(Callable[[str], int], 'fully-qualified-except-typing') == "Callable[[str], int]" + assert stringify_annotation(Callable[[str], int], "fully-qualified") == "typing.Callable[[str], int]" + assert stringify_annotation(Callable[[str], int], "smart") == "~typing.Callable[[str], int]" - assert stringify_annotation(abc.Callable[..., int], 'fully-qualified-except-typing') == "collections.abc.Callable[[...], int]" - assert stringify_annotation(abc.Callable[..., int], "fully-qualified") == "collections.abc.Callable[[...], int]" - assert stringify_annotation(abc.Callable[..., int], "smart") == "~collections.abc.Callable[[...], int]" + assert stringify_annotation(Callable[..., int], 'fully-qualified-except-typing') == "Callable[[...], int]" + assert stringify_annotation(Callable[..., int], "fully-qualified") == "typing.Callable[[...], int]" + assert stringify_annotation(Callable[..., int], "smart") == "~typing.Callable[[...], int]" def test_stringify_type_hints_Union(): From 6265a3e342e75b9c13f7b5ed1fee7ebbabbdbf09 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Tue, 23 Apr 2024 07:33:19 +0100 Subject: [PATCH 08/14] rf/fr --- sphinx/util/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 75970894e5b..456bd29687f 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -275,7 +275,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s # generic representation of the parameters params = ', '.join(restify(a, mode) for a in __args__) - return rf'{text}\ [{params}]' + return fr'{text}\ [{params}]' elif isinstance(cls, typing._SpecialForm): return f':py:obj:`~{cls.__module__}.{cls.__name__}`' # type: ignore[attr-defined] elif sys.version_info[:2] >= (3, 11) and cls is typing.Any: From 447cd3ba16721d062339d45ad9f32be76c02eb01 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Tue, 23 Apr 2024 07:34:20 +0100 Subject: [PATCH 09/14] comment --- sphinx/util/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 456bd29687f..a278b7bc774 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -262,7 +262,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s # Don't print the arguments; they're all system defined type variables. return text - # Callable (either collections.abc or typing) + # Callable has special formatting if cls_module_is_typing and cls.__name__ == 'Callable': args = ', '.join(restify(a, mode) for a in __args__[:-1]) returns = restify(__args__[-1], mode) From 2118f3e80c0faa1a32d5149f34f8b0b463ff43ba Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Tue, 23 Apr 2024 07:35:54 +0100 Subject: [PATCH 10/14] fixup! --- sphinx/util/typing.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index a278b7bc774..dd12762295f 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -269,13 +269,13 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s return fr'{text}\ [[{args}], {returns}]' if cls_module_is_typing and cls.__origin__.__name__ == 'Literal': - params = ', '.join(_format_literal_arg_restify(a, mode=mode) - for a in cls.__args__) - else: - # generic representation of the parameters - params = ', '.join(restify(a, mode) for a in __args__) + args = ', '.join(_format_literal_arg_restify(a, mode=mode) + for a in cls.__args__) + return fr'{text}\ [{args}]' - return fr'{text}\ [{params}]' + # generic representation of the parameters + args = ', '.join(restify(a, mode) for a in __args__) + return fr'{text}\ [{args}]' elif isinstance(cls, typing._SpecialForm): return f':py:obj:`~{cls.__module__}.{cls.__name__}`' # type: ignore[attr-defined] elif sys.version_info[:2] >= (3, 11) and cls is typing.Any: From d2ec93339b75963ec7cdc0cc44f4a71ea1636a33 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Tue, 23 Apr 2024 07:42:15 +0100 Subject: [PATCH 11/14] Check for ``_name`` --- sphinx/util/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index dd12762295f..b1a3c3ac547 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -250,7 +250,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s # ClassVar; Concatenate; Final; Literal; Unpack; TypeGuard # Required/NotRequired text = restify(cls.__origin__, mode) - elif cls.__name__: + elif hasattr(cls, '_name'): text = f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`' else: text = restify(cls.__origin__, mode) From 9aa00b1ceb82a7a8753833103dbad5c8e7bc4487 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Tue, 23 Apr 2024 08:13:54 +0100 Subject: [PATCH 12/14] name --- sphinx/util/typing.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index b1a3c3ac547..551f5f670f7 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -246,12 +246,17 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s # *cls* is defined in ``typing``, and thus ``__args__`` must exist return ' | '.join(restify(a, mode) for a in cls.__args__) elif inspect.isgenericalias(cls): + if sys.version_info[:2] >= (3, 10): + cls_name = cls.__name__ + else: + cls_name = getattr(cls, '_name', '') + if isinstance(cls.__origin__, typing._SpecialForm): # ClassVar; Concatenate; Final; Literal; Unpack; TypeGuard # Required/NotRequired text = restify(cls.__origin__, mode) - elif hasattr(cls, '_name'): - text = f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`' + elif cls_name: + text = f':py:class:`{module_prefix}{cls.__module__}.{cls_name}`' else: text = restify(cls.__origin__, mode) @@ -263,7 +268,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s return text # Callable has special formatting - if cls_module_is_typing and cls.__name__ == 'Callable': + if cls_module_is_typing and cls.__origin__.__name__ == 'Callable': args = ', '.join(restify(a, mode) for a in __args__[:-1]) returns = restify(__args__[-1], mode) return fr'{text}\ [[{args}], {returns}]' From 1bae54b74c1786e48509c9b55bbb06a3317e3268 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Tue, 23 Apr 2024 08:19:28 +0100 Subject: [PATCH 13/14] name --- sphinx/util/typing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 551f5f670f7..9422af4eda5 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -282,7 +282,9 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s args = ', '.join(restify(a, mode) for a in __args__) return fr'{text}\ [{args}]' elif isinstance(cls, typing._SpecialForm): - return f':py:obj:`~{cls.__module__}.{cls.__name__}`' # type: ignore[attr-defined] + if sys.version_info[:2] >= (3, 10): + return f':py:obj:`~{cls.__module__}.{cls.__name__}`' + return f':py:obj:`~{cls.__module__}.{cls._name}`' # type: ignore[attr-defined] elif sys.version_info[:2] >= (3, 11) and cls is typing.Any: # handle bpo-46998 return f':py:obj:`~{cls.__module__}.{cls.__name__}`' From 59b327db97e0e7f57af94614e91e5a88d73e423c Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Tue, 23 Apr 2024 08:29:59 +0100 Subject: [PATCH 14/14] name round three --- sphinx/util/typing.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 9422af4eda5..fce1087e0d6 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -178,6 +178,12 @@ def _is_annotated_form(obj: Any) -> TypeGuard[Annotated[Any, ...]]: return typing.get_origin(obj) is Annotated or str(obj).startswith('typing.Annotated') +def _get_typing_internal_name(obj: Any) -> str | None: + if sys.version_info[:2] >= (3, 10): + return obj.__name__ + return getattr(obj, '_name', None) + + def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> str: """Convert python class to a reST reference. @@ -246,10 +252,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s # *cls* is defined in ``typing``, and thus ``__args__`` must exist return ' | '.join(restify(a, mode) for a in cls.__args__) elif inspect.isgenericalias(cls): - if sys.version_info[:2] >= (3, 10): - cls_name = cls.__name__ - else: - cls_name = getattr(cls, '_name', '') + cls_name = _get_typing_internal_name(cls) if isinstance(cls.__origin__, typing._SpecialForm): # ClassVar; Concatenate; Final; Literal; Unpack; TypeGuard @@ -268,12 +271,12 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s return text # Callable has special formatting - if cls_module_is_typing and cls.__origin__.__name__ == 'Callable': + if cls_module_is_typing and _get_typing_internal_name(cls) == 'Callable': args = ', '.join(restify(a, mode) for a in __args__[:-1]) returns = restify(__args__[-1], mode) return fr'{text}\ [[{args}], {returns}]' - if cls_module_is_typing and cls.__origin__.__name__ == 'Literal': + if cls_module_is_typing and _get_typing_internal_name(cls.__origin__) == 'Literal': args = ', '.join(_format_literal_arg_restify(a, mode=mode) for a in cls.__args__) return fr'{text}\ [{args}]' @@ -282,9 +285,8 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s args = ', '.join(restify(a, mode) for a in __args__) return fr'{text}\ [{args}]' elif isinstance(cls, typing._SpecialForm): - if sys.version_info[:2] >= (3, 10): - return f':py:obj:`~{cls.__module__}.{cls.__name__}`' - return f':py:obj:`~{cls.__module__}.{cls._name}`' # type: ignore[attr-defined] + cls_name = _get_typing_internal_name(cls) + return f':py:obj:`~{cls.__module__}.{cls_name}`' elif sys.version_info[:2] >= (3, 11) and cls is typing.Any: # handle bpo-46998 return f':py:obj:`~{cls.__module__}.{cls.__name__}`'