From dc64e013365e97eeb3da5edbc2ed4b4a375427d3 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 1 Jan 2023 22:41:36 +0000 Subject: [PATCH 1/3] Refactor ``util.typing.stringify_annotation`` --- doc/extdev/deprecated.rst | 5 + sphinx/ext/autodoc/__init__.py | 27 +-- sphinx/ext/autodoc/typehints.py | 7 +- sphinx/ext/napoleon/docstring.py | 6 +- sphinx/util/inspect.py | 3 +- sphinx/util/typing.py | 163 ++++++++++-------- tests/test_util_inspect.py | 6 +- tests/test_util_typing.py | 278 +++++++++++++++---------------- 8 files changed, 267 insertions(+), 228 deletions(-) diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index 2315413926f..ecfaeb64897 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -22,6 +22,11 @@ The following is a list of deprecated interfaces. - Removed - Alternatives + * - ``sphinx.util.typing.stringify`` + - 6.1 + - 8.0 + - ``sphinx.util.typing.stringify_annotation`` + * - HTML 4 support - 5.2 - 7.0 diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 35b16673e47..6c91e0024b1 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -27,8 +27,7 @@ from sphinx.util.docstrings import prepare_docstring, separate_metadata from sphinx.util.inspect import (evaluate_signature, getdoc, object_description, safe_getattr, stringify_signature) -from sphinx.util.typing import OptionSpec, get_type_hints, restify -from sphinx.util.typing import stringify as stringify_typehint +from sphinx.util.typing import OptionSpec, get_type_hints, restify, stringify_annotation if TYPE_CHECKING: from sphinx.ext.autodoc.directive import DocumenterBridge @@ -1902,9 +1901,10 @@ def update_content(self, more_content: StringList) -> None: attrs = [repr(self.object.__name__)] for constraint in self.object.__constraints__: if self.config.autodoc_typehints_format == "short": - attrs.append(stringify_typehint(constraint, "smart")) + attrs.append(stringify_annotation(constraint, "smart")) else: - attrs.append(stringify_typehint(constraint)) + attrs.append(stringify_annotation(constraint, + "fully-qualified-except-typing")) if self.object.__bound__: if self.config.autodoc_typehints_format == "short": bound = restify(self.object.__bound__, "smart") @@ -2027,10 +2027,11 @@ def add_directive_header(self, sig: str) -> None: self.config.autodoc_type_aliases) if self.objpath[-1] in annotations: if self.config.autodoc_typehints_format == "short": - objrepr = stringify_typehint(annotations.get(self.objpath[-1]), - "smart") + objrepr = stringify_annotation(annotations.get(self.objpath[-1]), + "smart") else: - objrepr = stringify_typehint(annotations.get(self.objpath[-1])) + objrepr = stringify_annotation(annotations.get(self.objpath[-1]), + "fully-qualified-except-typing") self.add_line(' :type: ' + objrepr, sourcename) try: @@ -2622,10 +2623,11 @@ def add_directive_header(self, sig: str) -> None: self.config.autodoc_type_aliases) if self.objpath[-1] in annotations: if self.config.autodoc_typehints_format == "short": - objrepr = stringify_typehint(annotations.get(self.objpath[-1]), - "smart") + objrepr = stringify_annotation(annotations.get(self.objpath[-1]), + "smart") else: - objrepr = stringify_typehint(annotations.get(self.objpath[-1])) + objrepr = stringify_annotation(annotations.get(self.objpath[-1]), + "fully-qualified-except-typing") self.add_line(' :type: ' + objrepr, sourcename) try: @@ -2750,9 +2752,10 @@ def add_directive_header(self, sig: str) -> None: type_aliases=self.config.autodoc_type_aliases) if signature.return_annotation is not Parameter.empty: if self.config.autodoc_typehints_format == "short": - objrepr = stringify_typehint(signature.return_annotation, "smart") + objrepr = stringify_annotation(signature.return_annotation, "smart") else: - objrepr = stringify_typehint(signature.return_annotation) + objrepr = stringify_annotation(signature.return_annotation, + "fully-qualified-except-typing") self.add_line(' :type: ' + objrepr, sourcename) except TypeError as exc: logger.warning(__("Failed to get a function signature for %s: %s"), diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py index 905bfc2afeb..903883dce6d 100644 --- a/sphinx/ext/autodoc/typehints.py +++ b/sphinx/ext/autodoc/typehints.py @@ -12,7 +12,8 @@ import sphinx from sphinx import addnodes from sphinx.application import Sphinx -from sphinx.util import inspect, typing +from sphinx.util import inspect +from sphinx.util.typing import stringify_annotation def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any, @@ -30,9 +31,9 @@ def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any, sig = inspect.signature(obj, type_aliases=app.config.autodoc_type_aliases) for param in sig.parameters.values(): if param.annotation is not param.empty: - annotation[param.name] = typing.stringify(param.annotation, mode) + annotation[param.name] = stringify_annotation(param.annotation, mode) if sig.return_annotation is not sig.empty: - annotation['return'] = typing.stringify(sig.return_annotation, mode) + annotation['return'] = stringify_annotation(sig.return_annotation, mode) except (TypeError, ValueError): pass diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index 8594cdfc32c..f108b083abf 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -12,8 +12,7 @@ from sphinx.config import Config as SphinxConfig from sphinx.locale import _, __ from sphinx.util import logging -from sphinx.util.inspect import stringify_annotation -from sphinx.util.typing import get_type_hints +from sphinx.util.typing import get_type_hints, stringify_annotation logger = logging.getLogger(__name__) @@ -876,7 +875,8 @@ def _lookup_annotation(self, _name: str) -> str: ) or {}) self._annotations = get_type_hints(self._obj, None, localns) if _name in self._annotations: - return stringify_annotation(self._annotations[_name]) + return stringify_annotation(self._annotations[_name], + 'fully-qualified-except-typing') # No annotation found return "" diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 10673ca57fc..5bae9b4b0d9 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -22,8 +22,7 @@ from sphinx.pycode.ast import unparse as ast_unparse from sphinx.util import logging -from sphinx.util.typing import ForwardRef -from sphinx.util.typing import stringify as stringify_annotation +from sphinx.util.typing import ForwardRef, stringify_annotation logger = logging.getLogger(__name__) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 53f32ed4659..e0110217bd1 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -11,6 +11,8 @@ from docutils import nodes from docutils.parsers.rst.states import Inliner +from sphinx.deprecation import RemovedInSphinx80Warning, deprecated_alias + try: from types import UnionType # type: ignore # python 3.10 or above except ImportError: @@ -205,9 +207,14 @@ def restify(cls: type | None, mode: str = 'fully-qualified-except-typing') -> st return inspect.object_description(cls) -def stringify(annotation: Any, mode: str = 'fully-qualified-except-typing') -> str: +def stringify_annotation( + annotation: Any, + /, + mode: str = 'fully-qualified-except-typing', +) -> str: """Stringify type annotation object. + :param annotation: The annotation to stringified. :param mode: Specify a method how annotations will be stringified. 'fully-qualified-except-typing' @@ -219,12 +226,20 @@ def stringify(annotation: Any, mode: str = 'fully-qualified-except-typing') -> s Show the module name and qualified name of the annotation. """ from sphinx.ext.autodoc.mock import ismock, ismockmodule # lazy loading - from sphinx.util import inspect # lazy loading + from sphinx.util.inspect import isNewType # lazy loading + + if mode not in {'fully-qualified-except-typing', 'fully-qualified', 'smart'}: + raise ValueError("'mode' must be one of 'fully-qualified-except-typing', " + f"'fully-qualified', or 'smart'; got {mode!r}.") if mode == 'smart': - modprefix = '~' + module_prefix = '~' else: - modprefix = '' + module_prefix = '' + + annotation_qualname = getattr(annotation, '__qualname__', '') + annotation_module = getattr(annotation, '__module__', '') + annotation_name = getattr(annotation, '__name__', '') if isinstance(annotation, str): if annotation.startswith("'") and annotation.endswith("'"): @@ -233,104 +248,120 @@ def stringify(annotation: Any, mode: str = 'fully-qualified-except-typing') -> s else: return annotation elif isinstance(annotation, TypeVar): - if (annotation.__module__ == 'typing' and - mode in ('fully-qualified-except-typing', 'smart')): - return annotation.__name__ + if (annotation_module == 'typing' + and mode in {'fully-qualified-except-typing', 'smart'}): + return annotation_name else: - return modprefix + '.'.join([annotation.__module__, annotation.__name__]) - elif inspect.isNewType(annotation): + return module_prefix + f'{annotation_module}.{annotation_name}' + elif isNewType(annotation): if sys.version_info[:2] >= (3, 10): # newtypes have correct module info since Python 3.10+ - return modprefix + f'{annotation.__module__}.{annotation.__name__}' + return module_prefix + f'{annotation_module}.{annotation_name}' else: - return annotation.__name__ + return annotation_name elif not annotation: return repr(annotation) elif annotation is NoneType: return 'None' elif ismockmodule(annotation): - return modprefix + annotation.__name__ + return module_prefix + annotation_name elif ismock(annotation): - return modprefix + f'{annotation.__module__}.{annotation.__name__}' + return module_prefix + f'{annotation_module}.{annotation_name}' elif is_invalid_builtin_class(annotation): - return modprefix + INVALID_BUILTIN_CLASSES[annotation] + return module_prefix + INVALID_BUILTIN_CLASSES[annotation] elif str(annotation).startswith('typing.Annotated'): # for py310+ pass - elif (getattr(annotation, '__module__', None) == 'builtins' and - getattr(annotation, '__qualname__', None)): + elif annotation_module == 'builtins' and annotation_qualname: if hasattr(annotation, '__args__'): # PEP 585 generic return repr(annotation) else: - return annotation.__qualname__ + return annotation_qualname elif annotation is Ellipsis: return '...' - module = getattr(annotation, '__module__', None) - modprefix = '' - if module == 'typing' and getattr(annotation, '__forward_arg__', None): - qualname = annotation.__forward_arg__ - elif module == 'typing': - if getattr(annotation, '_name', None): - qualname = annotation._name - elif getattr(annotation, '__qualname__', None): - qualname = annotation.__qualname__ - else: - qualname = stringify(annotation.__origin__).replace('typing.', '') # ex. Union - + module_prefix = '' + annotation_forward_arg = getattr(annotation, '__forward_arg__', None) + if (annotation_qualname + or (annotation_module == 'typing' and not annotation_forward_arg)): if mode == 'smart': - modprefix = '~%s.' % module + module_prefix = f'~{annotation_module}.' elif mode == 'fully-qualified': - modprefix = '%s.' % module - elif hasattr(annotation, '__qualname__'): - if mode == 'smart': - modprefix = '~%s.' % module + module_prefix = f'{annotation_module}.' + elif annotation_module != 'typing' and mode == 'fully-qualified-except-typing': + module_prefix = f'{annotation_module}.' + + if annotation_module == 'typing': + if annotation_forward_arg: + # handle ForwardRefs + qualname = annotation_forward_arg else: - modprefix = '%s.' % module - qualname = annotation.__qualname__ + _name = getattr(annotation, '_name', '') + if _name: + qualname = _name + elif annotation_qualname: + qualname = annotation_qualname + else: + qualname = stringify_annotation( + annotation.__origin__, 'fully-qualified-except-typing' + ).replace('typing.', '') # ex. Union + elif annotation_qualname: + qualname = annotation_qualname elif hasattr(annotation, '__origin__'): # instantiated generic provided by a user - qualname = stringify(annotation.__origin__, mode) - elif UnionType and isinstance(annotation, UnionType): # types.Union (for py3.10+) - qualname = 'types.Union' + qualname = stringify_annotation(annotation.__origin__, mode) + elif UnionType and isinstance(annotation, UnionType): # types.UnionType (for py3.10+) + qualname = 'types.UnionType' else: # we weren't able to extract the base type, appending arguments would # only make them appear twice return repr(annotation) - if getattr(annotation, '__args__', None): - if not isinstance(annotation.__args__, (list, tuple)): + 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'): - if len(annotation.__args__) > 1 and annotation.__args__[-1] is NoneType: - if len(annotation.__args__) > 2: - args = ', '.join(stringify(a, mode) for a in annotation.__args__[:-1]) - return f'{modprefix}Optional[{modprefix}Union[{args}]]' - else: - return f'{modprefix}Optional[{stringify(annotation.__args__[0], mode)}]' + elif qualname in {'Optional', 'Union'}: + if len(annotation_args) > 1 and annotation_args[-1] is NoneType: + # typing.Optional or typing.Union[..., None] + args = ', '.join(stringify_annotation(a, mode) for a in annotation_args[:-1]) + if len(annotation_args) > 2: + args = f'{module_prefix}Union[{args}]' + return f'{module_prefix}Optional[{args}]' else: - args = ', '.join(stringify(a, mode) for a in annotation.__args__) - return f'{modprefix}Union[{args}]' - elif qualname == 'types.Union': - if len(annotation.__args__) > 1 and None in annotation.__args__: - args = ' | '.join(stringify(a) for a in annotation.__args__ if a) - return f'{modprefix}Optional[{args}]' + # typing.Union + args = ', '.join(stringify_annotation(a, mode) for a in annotation_args) + return f'{module_prefix}Union[{args}]' + elif qualname == 'types.UnionType': + if len(annotation_args) > 1 and None in annotation_args: + args = ' | '.join(stringify_annotation(a) for a in annotation_args if a) + return f'{module_prefix}Optional[{args}]' else: - return ' | '.join(stringify(a) for a in annotation.__args__) + return ' | '.join(stringify_annotation(a, mode) for a in annotation_args) elif qualname == 'Callable': - args = ', '.join(stringify(a, mode) for a in annotation.__args__[:-1]) - returns = stringify(annotation.__args__[-1], mode) - return f'{modprefix}{qualname}[[{args}], {returns}]' + args = ', '.join(stringify_annotation(a, mode) for a in annotation_args[:-1]) + returns = stringify_annotation(annotation_args[-1], mode) + return f'{module_prefix}Callable[[{args}], {returns}]' elif qualname == 'Literal': - args = ', '.join(repr(a) for a in annotation.__args__) - return f'{modprefix}{qualname}[{args}]' + args = ', '.join(repr(a) for a in annotation_args) + return f'{module_prefix}Literal[{args}]' elif str(annotation).startswith('typing.Annotated'): # for py39+ - return stringify(annotation.__args__[0], mode) - elif all(is_system_TypeVar(a) for a in annotation.__args__): + 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]) - return modprefix + qualname + return module_prefix + qualname else: - args = ', '.join(stringify(a, mode) for a in annotation.__args__) - return f'{modprefix}{qualname}[{args}]' + args = ', '.join(stringify_annotation(a, mode) for a in annotation_args) + return f'{module_prefix}{qualname}[{args}]' + + return module_prefix + qualname + - return modprefix + qualname +deprecated_alias(__name__, + { + 'stringify': stringify_annotation, + }, + RemovedInSphinx80Warning, + { + 'stringify': 'sphinx.util.typing.stringify_annotation', + }) diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index c7f9914cc37..c7bb6f17606 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -15,15 +15,15 @@ from sphinx.util import inspect from sphinx.util.inspect import TypeAliasForwardRef, TypeAliasNamespace, stringify_signature -from sphinx.util.typing import stringify +from sphinx.util.typing import stringify_annotation def test_TypeAliasForwardRef(): alias = TypeAliasForwardRef('example') - assert stringify(alias) == 'example' + assert stringify_annotation(alias, 'fully-qualified-except-typing') == 'example' alias = Optional[alias] - assert stringify(alias) == 'Optional[example]' + assert stringify_annotation(alias, 'fully-qualified-except-typing') == 'Optional[example]' def test_TypeAliasNamespace(): diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py index 0cca1913e0d..c123b62c887 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util_typing.py @@ -10,7 +10,7 @@ import pytest from sphinx.ext.autodoc import mock -from sphinx.util.typing import restify, stringify +from sphinx.util.typing import restify, stringify_annotation class MyClass1: @@ -198,168 +198,168 @@ def test_restify_mock(): assert restify(unknown.secret.Class, "smart") == ':py:class:`~unknown.secret.Class`' -def test_stringify(): - assert stringify(int) == "int" - assert stringify(int, "smart") == "int" +def test_stringify_annotation(): + assert stringify_annotation(int, 'fully-qualified-except-typing') == "int" + assert stringify_annotation(int, "smart") == "int" - assert stringify(str) == "str" - assert stringify(str, "smart") == "str" + assert stringify_annotation(str, 'fully-qualified-except-typing') == "str" + assert stringify_annotation(str, "smart") == "str" - assert stringify(None) == "None" - assert stringify(None, "smart") == "None" + assert stringify_annotation(None, 'fully-qualified-except-typing') == "None" + assert stringify_annotation(None, "smart") == "None" - assert stringify(Integral) == "numbers.Integral" - assert stringify(Integral, "smart") == "~numbers.Integral" + assert stringify_annotation(Integral, 'fully-qualified-except-typing') == "numbers.Integral" + assert stringify_annotation(Integral, "smart") == "~numbers.Integral" - assert stringify(Struct) == "struct.Struct" - assert stringify(Struct, "smart") == "~struct.Struct" + assert stringify_annotation(Struct, 'fully-qualified-except-typing') == "struct.Struct" + assert stringify_annotation(Struct, "smart") == "~struct.Struct" - assert stringify(TracebackType) == "types.TracebackType" - assert stringify(TracebackType, "smart") == "~types.TracebackType" + assert stringify_annotation(TracebackType, 'fully-qualified-except-typing') == "types.TracebackType" + assert stringify_annotation(TracebackType, "smart") == "~types.TracebackType" - assert stringify(Any) == "Any" - assert stringify(Any, "fully-qualified") == "typing.Any" - assert stringify(Any, "smart") == "~typing.Any" + assert stringify_annotation(Any, 'fully-qualified-except-typing') == "Any" + assert stringify_annotation(Any, "fully-qualified") == "typing.Any" + assert stringify_annotation(Any, "smart") == "~typing.Any" def test_stringify_type_hints_containers(): - assert stringify(List) == "List" - assert stringify(List, "fully-qualified") == "typing.List" - assert stringify(List, "smart") == "~typing.List" + assert stringify_annotation(List, 'fully-qualified-except-typing') == "List" + assert stringify_annotation(List, "fully-qualified") == "typing.List" + assert stringify_annotation(List, "smart") == "~typing.List" - assert stringify(Dict) == "Dict" - assert stringify(Dict, "fully-qualified") == "typing.Dict" - assert stringify(Dict, "smart") == "~typing.Dict" + assert stringify_annotation(Dict, 'fully-qualified-except-typing') == "Dict" + assert stringify_annotation(Dict, "fully-qualified") == "typing.Dict" + assert stringify_annotation(Dict, "smart") == "~typing.Dict" - assert stringify(List[int]) == "List[int]" - assert stringify(List[int], "fully-qualified") == "typing.List[int]" - assert stringify(List[int], "smart") == "~typing.List[int]" + assert stringify_annotation(List[int], 'fully-qualified-except-typing') == "List[int]" + assert stringify_annotation(List[int], "fully-qualified") == "typing.List[int]" + assert stringify_annotation(List[int], "smart") == "~typing.List[int]" - assert stringify(List[str]) == "List[str]" - assert stringify(List[str], "fully-qualified") == "typing.List[str]" - assert stringify(List[str], "smart") == "~typing.List[str]" + assert stringify_annotation(List[str], 'fully-qualified-except-typing') == "List[str]" + assert stringify_annotation(List[str], "fully-qualified") == "typing.List[str]" + assert stringify_annotation(List[str], "smart") == "~typing.List[str]" - assert stringify(Dict[str, float]) == "Dict[str, float]" - assert stringify(Dict[str, float], "fully-qualified") == "typing.Dict[str, float]" - assert stringify(Dict[str, float], "smart") == "~typing.Dict[str, float]" + assert stringify_annotation(Dict[str, float], 'fully-qualified-except-typing') == "Dict[str, float]" + assert stringify_annotation(Dict[str, float], "fully-qualified") == "typing.Dict[str, float]" + assert stringify_annotation(Dict[str, float], "smart") == "~typing.Dict[str, float]" - assert stringify(Tuple[str, str, str]) == "Tuple[str, str, str]" - assert stringify(Tuple[str, str, str], "fully-qualified") == "typing.Tuple[str, str, str]" - assert stringify(Tuple[str, str, str], "smart") == "~typing.Tuple[str, str, str]" + assert stringify_annotation(Tuple[str, str, str], 'fully-qualified-except-typing') == "Tuple[str, str, str]" + assert stringify_annotation(Tuple[str, str, str], "fully-qualified") == "typing.Tuple[str, str, str]" + assert stringify_annotation(Tuple[str, str, str], "smart") == "~typing.Tuple[str, str, str]" - assert stringify(Tuple[str, ...]) == "Tuple[str, ...]" - assert stringify(Tuple[str, ...], "fully-qualified") == "typing.Tuple[str, ...]" - assert stringify(Tuple[str, ...], "smart") == "~typing.Tuple[str, ...]" + assert stringify_annotation(Tuple[str, ...], 'fully-qualified-except-typing') == "Tuple[str, ...]" + assert stringify_annotation(Tuple[str, ...], "fully-qualified") == "typing.Tuple[str, ...]" + assert stringify_annotation(Tuple[str, ...], "smart") == "~typing.Tuple[str, ...]" if sys.version_info[:2] <= (3, 10): - assert stringify(Tuple[()]) == "Tuple[()]" - assert stringify(Tuple[()], "fully-qualified") == "typing.Tuple[()]" - assert stringify(Tuple[()], "smart") == "~typing.Tuple[()]" + assert stringify_annotation(Tuple[()], 'fully-qualified-except-typing') == "Tuple[()]" + assert stringify_annotation(Tuple[()], "fully-qualified") == "typing.Tuple[()]" + assert stringify_annotation(Tuple[()], "smart") == "~typing.Tuple[()]" else: - assert stringify(Tuple[()]) == "Tuple" - assert stringify(Tuple[()], "fully-qualified") == "typing.Tuple" - assert stringify(Tuple[()], "smart") == "~typing.Tuple" + assert stringify_annotation(Tuple[()], 'fully-qualified-except-typing') == "Tuple" + assert stringify_annotation(Tuple[()], "fully-qualified") == "typing.Tuple" + assert stringify_annotation(Tuple[()], "smart") == "~typing.Tuple" - assert stringify(List[Dict[str, Tuple]]) == "List[Dict[str, Tuple]]" - assert stringify(List[Dict[str, Tuple]], "fully-qualified") == "typing.List[typing.Dict[str, typing.Tuple]]" - assert stringify(List[Dict[str, Tuple]], "smart") == "~typing.List[~typing.Dict[str, ~typing.Tuple]]" + assert stringify_annotation(List[Dict[str, Tuple]], 'fully-qualified-except-typing') == "List[Dict[str, Tuple]]" + assert stringify_annotation(List[Dict[str, Tuple]], "fully-qualified") == "typing.List[typing.Dict[str, typing.Tuple]]" + assert stringify_annotation(List[Dict[str, Tuple]], "smart") == "~typing.List[~typing.Dict[str, ~typing.Tuple]]" - assert stringify(MyList[Tuple[int, int]]) == "tests.test_util_typing.MyList[Tuple[int, int]]" - assert stringify(MyList[Tuple[int, int]], "fully-qualified") == "tests.test_util_typing.MyList[typing.Tuple[int, int]]" - assert stringify(MyList[Tuple[int, int]], "smart") == "~tests.test_util_typing.MyList[~typing.Tuple[int, int]]" + 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[typing.Tuple[int, int]]" + assert stringify_annotation(MyList[Tuple[int, int]], "smart") == "~tests.test_util_typing.MyList[~typing.Tuple[int, int]]" - assert stringify(Generator[None, None, None]) == "Generator[None, None, None]" - assert stringify(Generator[None, None, None], "fully-qualified") == "typing.Generator[None, None, None]" - assert stringify(Generator[None, None, None], "smart") == "~typing.Generator[None, None, 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]" @pytest.mark.skipif(sys.version_info[:2] <= (3, 8), reason='python 3.9+ is required.') def test_stringify_type_hints_pep_585(): - assert stringify(list[int]) == "list[int]" - assert stringify(list[int], "smart") == "list[int]" + assert stringify_annotation(list[int], 'fully-qualified-except-typing') == "list[int]" + assert stringify_annotation(list[int], "smart") == "list[int]" - assert stringify(list[str]) == "list[str]" - assert stringify(list[str], "smart") == "list[str]" + assert stringify_annotation(list[str], 'fully-qualified-except-typing') == "list[str]" + assert stringify_annotation(list[str], "smart") == "list[str]" - assert stringify(dict[str, float]) == "dict[str, float]" - assert stringify(dict[str, float], "smart") == "dict[str, float]" + assert stringify_annotation(dict[str, float], 'fully-qualified-except-typing') == "dict[str, float]" + assert stringify_annotation(dict[str, float], "smart") == "dict[str, float]" - assert stringify(tuple[str, str, str]) == "tuple[str, str, str]" - assert stringify(tuple[str, str, str], "smart") == "tuple[str, str, str]" + assert stringify_annotation(tuple[str, str, str], 'fully-qualified-except-typing') == "tuple[str, str, str]" + assert stringify_annotation(tuple[str, str, str], "smart") == "tuple[str, str, str]" - assert stringify(tuple[str, ...]) == "tuple[str, ...]" - assert stringify(tuple[str, ...], "smart") == "tuple[str, ...]" + assert stringify_annotation(tuple[str, ...], 'fully-qualified-except-typing') == "tuple[str, ...]" + assert stringify_annotation(tuple[str, ...], "smart") == "tuple[str, ...]" - assert stringify(tuple[()]) == "tuple[()]" - assert stringify(tuple[()], "smart") == "tuple[()]" + assert stringify_annotation(tuple[()], 'fully-qualified-except-typing') == "tuple[()]" + assert stringify_annotation(tuple[()], "smart") == "tuple[()]" - assert stringify(list[dict[str, tuple]]) == "list[dict[str, tuple]]" - assert stringify(list[dict[str, tuple]], "smart") == "list[dict[str, tuple]]" + 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(type[int]) == "type[int]" - assert stringify(type[int], "smart") == "type[int]" + assert stringify_annotation(type[int], 'fully-qualified-except-typing') == "type[int]" + assert stringify_annotation(type[int], "smart") == "type[int]" @pytest.mark.skipif(sys.version_info[:2] <= (3, 8), reason='python 3.9+ is required.') def test_stringify_Annotated(): from typing import Annotated # type: ignore - assert stringify(Annotated[str, "foo", "bar"]) == "str" - assert stringify(Annotated[str, "foo", "bar"], "smart") == "str" + assert stringify_annotation(Annotated[str, "foo", "bar"], 'fully-qualified-except-typing') == "str" + assert stringify_annotation(Annotated[str, "foo", "bar"], "smart") == "str" def test_stringify_type_hints_string(): - assert stringify("int") == "int" - assert stringify("int", "smart") == "int" + assert stringify_annotation("int", 'fully-qualified-except-typing') == "int" + assert stringify_annotation("int", "smart") == "int" - assert stringify("str") == "str" - assert stringify("str", "smart") == "str" + assert stringify_annotation("str", 'fully-qualified-except-typing') == "str" + assert stringify_annotation("str", "smart") == "str" - assert stringify(List["int"]) == "List[int]" - assert stringify(List["int"], "smart") == "~typing.List[int]" + assert stringify_annotation(List["int"], 'fully-qualified-except-typing') == "List[int]" + assert stringify_annotation(List["int"], "smart") == "~typing.List[int]" - assert stringify("Tuple[str]") == "Tuple[str]" - assert stringify("Tuple[str]", "smart") == "Tuple[str]" + assert stringify_annotation("Tuple[str]", 'fully-qualified-except-typing') == "Tuple[str]" + assert stringify_annotation("Tuple[str]", "smart") == "Tuple[str]" - assert stringify("unknown") == "unknown" - assert stringify("unknown", "smart") == "unknown" + assert stringify_annotation("unknown", 'fully-qualified-except-typing') == "unknown" + assert stringify_annotation("unknown", "smart") == "unknown" def test_stringify_type_hints_Callable(): - assert stringify(Callable) == "Callable" - assert stringify(Callable, "fully-qualified") == "typing.Callable" - assert stringify(Callable, "smart") == "~typing.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(Callable[[str], int]) == "Callable[[str], int]" - assert stringify(Callable[[str], int], "fully-qualified") == "typing.Callable[[str], int]" - assert stringify(Callable[[str], int], "smart") == "~typing.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(Callable[..., int]) == "Callable[[...], int]" - assert stringify(Callable[..., int], "fully-qualified") == "typing.Callable[[...], int]" - assert stringify(Callable[..., int], "smart") == "~typing.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(): - assert stringify(Optional[int]) == "Optional[int]" - assert stringify(Optional[int], "fully-qualified") == "typing.Optional[int]" - assert stringify(Optional[int], "smart") == "~typing.Optional[int]" + assert stringify_annotation(Optional[int], 'fully-qualified-except-typing') == "Optional[int]" + assert stringify_annotation(Optional[int], "fully-qualified") == "typing.Optional[int]" + assert stringify_annotation(Optional[int], "smart") == "~typing.Optional[int]" - assert stringify(Union[str, None]) == "Optional[str]" - assert stringify(Union[str, None], "fully-qualified") == "typing.Optional[str]" - assert stringify(Union[str, None], "smart") == "~typing.Optional[str]" + assert stringify_annotation(Union[str, None], 'fully-qualified-except-typing') == "Optional[str]" + assert stringify_annotation(Union[str, None], "fully-qualified") == "typing.Optional[str]" + assert stringify_annotation(Union[str, None], "smart") == "~typing.Optional[str]" - assert stringify(Union[int, str]) == "Union[int, str]" - assert stringify(Union[int, str], "fully-qualified") == "typing.Union[int, str]" - assert stringify(Union[int, str], "smart") == "~typing.Union[int, str]" + assert stringify_annotation(Union[int, str], 'fully-qualified-except-typing') == "Union[int, str]" + assert stringify_annotation(Union[int, str], "fully-qualified") == "typing.Union[int, str]" + assert stringify_annotation(Union[int, str], "smart") == "~typing.Union[int, str]" - assert stringify(Union[int, Integral]) == "Union[int, numbers.Integral]" - assert stringify(Union[int, Integral], "fully-qualified") == "typing.Union[int, numbers.Integral]" - assert stringify(Union[int, Integral], "smart") == "~typing.Union[int, ~numbers.Integral]" + assert stringify_annotation(Union[int, Integral], 'fully-qualified-except-typing') == "Union[int, numbers.Integral]" + assert stringify_annotation(Union[int, Integral], "fully-qualified") == "typing.Union[int, numbers.Integral]" + assert stringify_annotation(Union[int, Integral], "smart") == "~typing.Union[int, ~numbers.Integral]" - assert (stringify(Union[MyClass1, MyClass2]) == + assert (stringify_annotation(Union[MyClass1, MyClass2], 'fully-qualified-except-typing') == "Union[tests.test_util_typing.MyClass1, tests.test_util_typing.]") - assert (stringify(Union[MyClass1, MyClass2], "fully-qualified") == + assert (stringify_annotation(Union[MyClass1, MyClass2], "fully-qualified") == "typing.Union[tests.test_util_typing.MyClass1, tests.test_util_typing.]") - assert (stringify(Union[MyClass1, MyClass2], "smart") == + assert (stringify_annotation(Union[MyClass1, MyClass2], "smart") == "~typing.Union[~tests.test_util_typing.MyClass1, ~tests.test_util_typing.]") @@ -368,72 +368,72 @@ def test_stringify_type_hints_typevars(): T_co = TypeVar('T_co', covariant=True) T_contra = TypeVar('T_contra', contravariant=True) - assert stringify(T) == "tests.test_util_typing.T" - assert stringify(T, "smart") == "~tests.test_util_typing.T" + assert stringify_annotation(T, 'fully-qualified-except-typing') == "tests.test_util_typing.T" + assert stringify_annotation(T, "smart") == "~tests.test_util_typing.T" - assert stringify(T_co) == "tests.test_util_typing.T_co" - assert stringify(T_co, "smart") == "~tests.test_util_typing.T_co" + assert stringify_annotation(T_co, 'fully-qualified-except-typing') == "tests.test_util_typing.T_co" + assert stringify_annotation(T_co, "smart") == "~tests.test_util_typing.T_co" - assert stringify(T_contra) == "tests.test_util_typing.T_contra" - assert stringify(T_contra, "smart") == "~tests.test_util_typing.T_contra" + assert stringify_annotation(T_contra, 'fully-qualified-except-typing') == "tests.test_util_typing.T_contra" + assert stringify_annotation(T_contra, "smart") == "~tests.test_util_typing.T_contra" - assert stringify(List[T]) == "List[tests.test_util_typing.T]" - assert stringify(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") == "~typing.List[~tests.test_util_typing.T]" if sys.version_info[:2] >= (3, 10): - assert stringify(MyInt) == "tests.test_util_typing.MyInt" - assert stringify(MyInt, "smart") == "~tests.test_util_typing.MyInt" + assert stringify_annotation(MyInt, 'fully-qualified-except-typing') == "tests.test_util_typing.MyInt" + assert stringify_annotation(MyInt, "smart") == "~tests.test_util_typing.MyInt" else: - assert stringify(MyInt) == "MyInt" - assert stringify(MyInt, "smart") == "MyInt" + assert stringify_annotation(MyInt, 'fully-qualified-except-typing') == "MyInt" + assert stringify_annotation(MyInt, "smart") == "MyInt" def test_stringify_type_hints_custom_class(): - assert stringify(MyClass1) == "tests.test_util_typing.MyClass1" - assert stringify(MyClass1, "smart") == "~tests.test_util_typing.MyClass1" + assert stringify_annotation(MyClass1, 'fully-qualified-except-typing') == "tests.test_util_typing.MyClass1" + assert stringify_annotation(MyClass1, "smart") == "~tests.test_util_typing.MyClass1" - assert stringify(MyClass2) == "tests.test_util_typing." - assert stringify(MyClass2, "smart") == "~tests.test_util_typing." + assert stringify_annotation(MyClass2, 'fully-qualified-except-typing') == "tests.test_util_typing." + assert stringify_annotation(MyClass2, "smart") == "~tests.test_util_typing." def test_stringify_type_hints_alias(): MyStr = str MyTuple = Tuple[str, str] - assert stringify(MyStr) == "str" - assert stringify(MyStr, "smart") == "str" + assert stringify_annotation(MyStr, 'fully-qualified-except-typing') == "str" + assert stringify_annotation(MyStr, "smart") == "str" - assert stringify(MyTuple) == "Tuple[str, str]" # type: ignore - assert stringify(MyTuple, "smart") == "~typing.Tuple[str, str]" # type: ignore + assert stringify_annotation(MyTuple) == "Tuple[str, str]" # type: ignore + assert stringify_annotation(MyTuple, "smart") == "~typing.Tuple[str, str]" # type: ignore def test_stringify_type_Literal(): from typing import Literal # type: ignore - assert stringify(Literal[1, "2", "\r"]) == "Literal[1, '2', '\\r']" - assert stringify(Literal[1, "2", "\r"], "fully-qualified") == "typing.Literal[1, '2', '\\r']" - assert stringify(Literal[1, "2", "\r"], "smart") == "~typing.Literal[1, '2', '\\r']" + assert stringify_annotation(Literal[1, "2", "\r"], 'fully-qualified-except-typing') == "Literal[1, '2', '\\r']" + assert stringify_annotation(Literal[1, "2", "\r"], "fully-qualified") == "typing.Literal[1, '2', '\\r']" + assert stringify_annotation(Literal[1, "2", "\r"], "smart") == "~typing.Literal[1, '2', '\\r']" @pytest.mark.skipif(sys.version_info[:2] <= (3, 9), reason='python 3.10+ is required.') def test_stringify_type_union_operator(): - assert stringify(int | None) == "int | None" # type: ignore - assert stringify(int | None, "smart") == "int | None" # type: ignore + assert stringify_annotation(int | None) == "int | None" # type: ignore + assert stringify_annotation(int | None, "smart") == "int | None" # type: ignore - assert stringify(int | str) == "int | str" # type: ignore - assert stringify(int | str, "smart") == "int | str" # type: ignore + assert stringify_annotation(int | str) == "int | str" # type: ignore + assert stringify_annotation(int | str, "smart") == "int | str" # type: ignore - assert stringify(int | str | None) == "int | str | None" # type: ignore - assert stringify(int | str | None, "smart") == "int | str | None" # type: ignore + assert stringify_annotation(int | str | None) == "int | str | None" # type: ignore + assert stringify_annotation(int | str | None, "smart") == "int | str | None" # type: ignore def test_stringify_broken_type_hints(): - assert stringify(BrokenType) == 'tests.test_util_typing.BrokenType' - assert stringify(BrokenType, "smart") == '~tests.test_util_typing.BrokenType' + assert stringify_annotation(BrokenType, 'fully-qualified-except-typing') == 'tests.test_util_typing.BrokenType' + assert stringify_annotation(BrokenType, "smart") == '~tests.test_util_typing.BrokenType' def test_stringify_mock(): with mock(['unknown']): import unknown - assert stringify(unknown) == 'unknown' - assert stringify(unknown.secret.Class) == 'unknown.secret.Class' - assert stringify(unknown.secret.Class, "smart") == 'unknown.secret.Class' + 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' From 2795d1aa8f4734251c6cb1364be6e0053064ae95 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Mon, 2 Jan 2023 01:23:43 +0000 Subject: [PATCH 2/3] Use PEP 604 types in autodoc --- sphinx/util/typing.py | 19 ++--------------- tests/test_ext_autodoc.py | 2 +- tests/test_ext_autodoc_configs.py | 8 +++---- tests/test_util_inspect.py | 20 +++++++++--------- tests/test_util_typing.py | 35 ++++++++++++++++++------------- 5 files changed, 37 insertions(+), 47 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index e0110217bd1..282230e0cde 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -321,23 +321,8 @@ def stringify_annotation( if not isinstance(annotation_args, (list, tuple)): # broken __args__ found pass - elif qualname in {'Optional', 'Union'}: - if len(annotation_args) > 1 and annotation_args[-1] is NoneType: - # typing.Optional or typing.Union[..., None] - args = ', '.join(stringify_annotation(a, mode) for a in annotation_args[:-1]) - if len(annotation_args) > 2: - args = f'{module_prefix}Union[{args}]' - return f'{module_prefix}Optional[{args}]' - else: - # typing.Union - args = ', '.join(stringify_annotation(a, mode) for a in annotation_args) - return f'{module_prefix}Union[{args}]' - elif qualname == 'types.UnionType': - if len(annotation_args) > 1 and None in annotation_args: - args = ' | '.join(stringify_annotation(a) for a in annotation_args if a) - return f'{module_prefix}Optional[{args}]' - else: - return ' | '.join(stringify_annotation(a, mode) for a in annotation_args) + elif 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]) returns = stringify_annotation(annotation_args[-1], mode) diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index ec4388bf034..8f723deb4b3 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -808,7 +808,7 @@ def test_autodoc_imported_members(app): "imported-members": None, "ignore-module-all": None} actual = do_autodoc(app, 'module', 'target', options) - assert '.. py:function:: function_to_be_imported(app: ~typing.Optional[Sphinx]) -> str' in actual + assert '.. py:function:: function_to_be_imported(app: Sphinx | None) -> str' in actual @pytest.mark.sphinx('html', testroot='ext-autodoc') diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index 7ce8ed3d6d5..aa6a357a3f8 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -662,7 +662,7 @@ def test_mocked_module_imports(app, warning): confoverrides={'autodoc_typehints': "signature"}) def test_autodoc_typehints_signature(app): if sys.version_info[:2] <= (3, 10): - type_o = "~typing.Optional[~typing.Any]" + type_o = "~typing.Any | None" else: type_o = "~typing.Any" @@ -1304,7 +1304,7 @@ def test_autodoc_type_aliases(app): '', '.. py:data:: variable3', ' :module: target.autodoc_type_aliases', - ' :type: ~typing.Optional[int]', + ' :type: int | None', '', ' docstring', '', @@ -1375,7 +1375,7 @@ def test_autodoc_type_aliases(app): '', '.. py:data:: variable3', ' :module: target.autodoc_type_aliases', - ' :type: ~typing.Optional[myint]', + ' :type: myint | None', '', ' docstring', '', @@ -1409,7 +1409,7 @@ def test_autodoc_typehints_description_and_type_aliases(app): confoverrides={'autodoc_typehints_format': "fully-qualified"}) def test_autodoc_typehints_format_fully_qualified(app): if sys.version_info[:2] <= (3, 10): - type_o = "typing.Optional[typing.Any]" + type_o = "typing.Any | None" else: type_o = "typing.Any" diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index c7bb6f17606..08980463094 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -23,7 +23,7 @@ def test_TypeAliasForwardRef(): assert stringify_annotation(alias, 'fully-qualified-except-typing') == 'example' alias = Optional[alias] - assert stringify_annotation(alias, 'fully-qualified-except-typing') == 'Optional[example]' + assert stringify_annotation(alias, 'fully-qualified-except-typing') == 'example | None' def test_TypeAliasNamespace(): @@ -173,7 +173,7 @@ def test_signature_annotations(): # Union types sig = inspect.signature(f3) - assert stringify_signature(sig) == '(x: typing.Union[str, numbers.Integral]) -> None' + assert stringify_signature(sig) == '(x: str | numbers.Integral) -> None' # Quoted annotations sig = inspect.signature(f4) @@ -190,7 +190,7 @@ def test_signature_annotations(): # Space around '=' for defaults sig = inspect.signature(f7) if sys.version_info[:2] <= (3, 10): - assert stringify_signature(sig) == '(x: typing.Optional[int] = None, y: dict = {}) -> None' + assert stringify_signature(sig) == '(x: int | None = None, y: dict = {}) -> None' else: assert stringify_signature(sig) == '(x: int = None, y: dict = {}) -> None' @@ -215,12 +215,12 @@ def test_signature_annotations(): # optional sig = inspect.signature(f13) - assert stringify_signature(sig) == '() -> typing.Optional[str]' + assert stringify_signature(sig) == '() -> str | None' # optional union sig = inspect.signature(f20) - assert stringify_signature(sig) in ('() -> typing.Optional[typing.Union[int, str]]', - '() -> typing.Optional[typing.Union[str, int]]') + assert stringify_signature(sig) in ('() -> int | str | None', + '() -> str | int | None') # Any sig = inspect.signature(f14) @@ -239,7 +239,7 @@ def test_signature_annotations(): assert stringify_signature(sig) == '(*, arg3, arg4)' sig = inspect.signature(f18) - assert stringify_signature(sig) == ('(self, arg1: typing.Union[int, typing.Tuple] = 10) -> ' + assert stringify_signature(sig) == ('(self, arg1: int | typing.Tuple = 10) -> ' 'typing.List[typing.Dict]') # annotations for variadic and keyword parameters @@ -255,7 +255,7 @@ def test_signature_annotations(): assert stringify_signature(sig) == '(self) -> typing.List[tests.typing_test_data.Node]' sig = inspect.signature(Node.__init__) - assert stringify_signature(sig) == '(self, parent: typing.Optional[tests.typing_test_data.Node]) -> None' + assert stringify_signature(sig) == '(self, parent: tests.typing_test_data.Node | None) -> None' # show_annotation is False sig = inspect.signature(f7) @@ -264,14 +264,14 @@ def test_signature_annotations(): # show_return_annotation is False sig = inspect.signature(f7) if sys.version_info[:2] <= (3, 10): - assert stringify_signature(sig, show_return_annotation=False) == '(x: typing.Optional[int] = None, y: dict = {})' + assert stringify_signature(sig, show_return_annotation=False) == '(x: int | None = None, y: dict = {})' else: assert stringify_signature(sig, show_return_annotation=False) == '(x: int = None, y: dict = {})' # unqualified_typehints is True sig = inspect.signature(f7) if sys.version_info[:2] <= (3, 10): - assert stringify_signature(sig, unqualified_typehints=True) == '(x: ~typing.Optional[int] = None, y: dict = {}) -> None' + assert stringify_signature(sig, unqualified_typehints=True) == '(x: int | None = None, y: dict = {}) -> None' else: assert stringify_signature(sig, unqualified_typehints=True) == '(x: int = None, y: dict = {}) -> None' diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py index c123b62c887..89afb002109 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util_typing.py @@ -309,18 +309,23 @@ def test_stringify_Annotated(): def test_stringify_type_hints_string(): assert stringify_annotation("int", 'fully-qualified-except-typing') == "int" + assert stringify_annotation("int", 'fully-qualified') == "int" assert stringify_annotation("int", "smart") == "int" assert stringify_annotation("str", 'fully-qualified-except-typing') == "str" + assert stringify_annotation("str", 'fully-qualified') == "str" assert stringify_annotation("str", "smart") == "str" assert stringify_annotation(List["int"], 'fully-qualified-except-typing') == "List[int]" + assert stringify_annotation(List["int"], 'fully-qualified') == "typing.List[int]" assert stringify_annotation(List["int"], "smart") == "~typing.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("unknown", 'fully-qualified-except-typing') == "unknown" + assert stringify_annotation("unknown", 'fully-qualified') == "unknown" assert stringify_annotation("unknown", "smart") == "unknown" @@ -339,28 +344,28 @@ def test_stringify_type_hints_Callable(): def test_stringify_type_hints_Union(): - assert stringify_annotation(Optional[int], 'fully-qualified-except-typing') == "Optional[int]" - assert stringify_annotation(Optional[int], "fully-qualified") == "typing.Optional[int]" - assert stringify_annotation(Optional[int], "smart") == "~typing.Optional[int]" + assert stringify_annotation(Optional[int], 'fully-qualified-except-typing') == "int | None" + 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') == "Optional[str]" - assert stringify_annotation(Union[str, None], "fully-qualified") == "typing.Optional[str]" - assert stringify_annotation(Union[str, None], "smart") == "~typing.Optional[str]" + 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, str], 'fully-qualified-except-typing') == "Union[int, str]" - assert stringify_annotation(Union[int, str], "fully-qualified") == "typing.Union[int, str]" - assert stringify_annotation(Union[int, str], "smart") == "~typing.Union[int, str]" + assert stringify_annotation(Union[int, str], 'fully-qualified-except-typing') == "int | str" + assert stringify_annotation(Union[int, str], "fully-qualified") == "int | str" + assert stringify_annotation(Union[int, str], "smart") == "int | str" - assert stringify_annotation(Union[int, Integral], 'fully-qualified-except-typing') == "Union[int, numbers.Integral]" - assert stringify_annotation(Union[int, Integral], "fully-qualified") == "typing.Union[int, numbers.Integral]" - assert stringify_annotation(Union[int, Integral], "smart") == "~typing.Union[int, ~numbers.Integral]" + assert stringify_annotation(Union[int, Integral], 'fully-qualified-except-typing') == "int | numbers.Integral" + assert stringify_annotation(Union[int, Integral], "fully-qualified") == "int | numbers.Integral" + assert stringify_annotation(Union[int, Integral], "smart") == "int | ~numbers.Integral" assert (stringify_annotation(Union[MyClass1, MyClass2], 'fully-qualified-except-typing') == - "Union[tests.test_util_typing.MyClass1, tests.test_util_typing.]") + "tests.test_util_typing.MyClass1 | tests.test_util_typing.") assert (stringify_annotation(Union[MyClass1, MyClass2], "fully-qualified") == - "typing.Union[tests.test_util_typing.MyClass1, tests.test_util_typing.]") + "tests.test_util_typing.MyClass1 | tests.test_util_typing.") assert (stringify_annotation(Union[MyClass1, MyClass2], "smart") == - "~typing.Union[~tests.test_util_typing.MyClass1, ~tests.test_util_typing.]") + "~tests.test_util_typing.MyClass1 | ~tests.test_util_typing.") def test_stringify_type_hints_typevars(): From b91a140c967c1d01af433995e1f9519a4300f4bf Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Mon, 2 Jan 2023 02:56:08 +0000 Subject: [PATCH 3/3] Use PEP 604 types in the Python domain --- sphinx/domains/python.py | 24 ++++++++++++++++++++++++ tests/test_domain_py.py | 9 +++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index e43f72b5a36..d9c0d981e98 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -176,6 +176,8 @@ def unparse(node: ast.AST) -> list[Node]: elif isinstance(node, ast.Name): return [nodes.Text(node.id)] elif isinstance(node, ast.Subscript): + if getattr(node.value, 'id', '') in {'Optional', 'Union'}: + return _unparse_pep_604_annotation(node) result = unparse(node.value) result.append(addnodes.desc_sig_punctuation('', '[')) result.extend(unparse(node.slice)) @@ -206,6 +208,28 @@ def unparse(node: ast.AST) -> list[Node]: else: raise SyntaxError # unsupported syntax + def _unparse_pep_604_annotation(node: ast.Subscript) -> list[Node]: + subscript = node.slice + if isinstance(subscript, ast.Index): + # py38 only + subscript = subscript.value # type: ignore[assignment] + + flattened: list[Node] = [] + if isinstance(subscript, ast.Tuple): + flattened.extend(unparse(subscript.elts[0])) + for elt in subscript.elts[1:]: + flattened.extend(unparse(ast.BitOr())) + flattened.extend(unparse(elt)) + else: + # e.g. a Union[] inside an Optional[] + flattened.extend(unparse(subscript)) + + if getattr(node.value, 'id', '') == 'Optional': + flattened.extend(unparse(ast.BitOr())) + flattened.append(nodes.Text('None')) + + return flattened + try: tree = ast.parse(annotation, type_comments=True) result: list[Node] = [] diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index c4b87c73748..19fcfd36f5b 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -866,10 +866,11 @@ def test_pyattribute(app): assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "attr"], [desc_annotation, ([desc_sig_punctuation, ':'], desc_sig_space, - [pending_xref, "Optional"], - [desc_sig_punctuation, "["], [pending_xref, "str"], - [desc_sig_punctuation, "]"])], + desc_sig_space, + [desc_sig_punctuation, "|"], + desc_sig_space, + [pending_xref, "None"])], [desc_annotation, (desc_sig_space, [desc_sig_punctuation, '='], desc_sig_space, @@ -877,7 +878,7 @@ def test_pyattribute(app): )], [desc_content, ()])) assert_node(doctree[1][1][1][0][1][2], pending_xref, **{"py:class": "Class"}) - assert_node(doctree[1][1][1][0][1][4], pending_xref, **{"py:class": "Class"}) + assert_node(doctree[1][1][1][0][1][6], pending_xref, **{"py:class": "Class"}) assert 'Class.attr' in domain.objects assert domain.objects['Class.attr'] == ('index', 'Class.attr', 'attribute', False)