diff options
Diffstat (limited to 'sphinx/util/typing.py')
-rw-r--r-- | sphinx/util/typing.py | 152 |
1 files changed, 84 insertions, 68 deletions
diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 53f32ed46..282230e0c 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -11,6 +11,8 @@ from typing import Any, Callable, Dict, ForwardRef, List, Tuple, TypeVar, Union 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,105 @@ 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)}]' - 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}]' - else: - return ' | '.join(stringify(a) 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(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', + }) |