mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Separate cases in `stringify and restify` with greater precision (#12284)
Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com>
This commit is contained in:
@@ -8,7 +8,16 @@ import typing
|
|||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from contextvars import Context, ContextVar, Token
|
from contextvars import Context, ContextVar, Token
|
||||||
from struct import Struct
|
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 import nodes
|
||||||
from docutils.parsers.rst.states import Inliner
|
from docutils.parsers.rst.states import Inliner
|
||||||
@@ -17,7 +26,7 @@ if TYPE_CHECKING:
|
|||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from typing import Final, Literal
|
from typing import Final, Literal
|
||||||
|
|
||||||
from typing_extensions import TypeAlias
|
from typing_extensions import TypeAlias, TypeGuard
|
||||||
|
|
||||||
from sphinx.application import Sphinx
|
from sphinx.application import Sphinx
|
||||||
|
|
||||||
@@ -164,6 +173,17 @@ def is_system_TypeVar(typ: Any) -> bool:
|
|||||||
return modname == 'typing' and isinstance(typ, TypeVar)
|
return modname == 'typing' and isinstance(typ, TypeVar)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
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:
|
def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> str:
|
||||||
"""Convert python class to a reST reference.
|
"""Convert python class to a reST reference.
|
||||||
|
|
||||||
@@ -185,35 +205,34 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s
|
|||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
# things that are not types
|
# things that are not types
|
||||||
if cls is None or cls is NoneType:
|
if cls in {None, NoneType}:
|
||||||
return ':py:obj:`None`'
|
return ':py:obj:`None`'
|
||||||
if cls is Ellipsis:
|
if cls is Ellipsis:
|
||||||
return '...'
|
return '...'
|
||||||
if isinstance(cls, str):
|
if isinstance(cls, str):
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
cls_module_is_typing = getattr(cls, '__module__', '') == 'typing'
|
||||||
|
|
||||||
# If the mode is 'smart', we always use '~'.
|
# If the mode is 'smart', we always use '~'.
|
||||||
# If the mode is 'fully-qualified-except-typing',
|
# If the mode is 'fully-qualified-except-typing',
|
||||||
# we use '~' only for the objects in the ``typing`` module.
|
# we use '~' only for the objects in the ``typing`` module.
|
||||||
if mode == 'smart' or getattr(cls, '__module__', None) == 'typing':
|
module_prefix = '~' if mode == 'smart' or cls_module_is_typing else ''
|
||||||
modprefix = '~'
|
|
||||||
else:
|
|
||||||
modprefix = ''
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if ismockmodule(cls):
|
if ismockmodule(cls):
|
||||||
return f':py:class:`{modprefix}{cls.__name__}`'
|
return f':py:class:`{module_prefix}{cls.__name__}`'
|
||||||
elif ismock(cls):
|
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):
|
elif is_invalid_builtin_class(cls):
|
||||||
# The above predicate never raises TypeError but should not be
|
# The above predicate never raises TypeError but should not be
|
||||||
# evaluated before determining whether *cls* is a mocked object
|
# evaluated before determining whether *cls* is a mocked object
|
||||||
# or not; instead of two try-except blocks, we keep it here.
|
# 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):
|
elif inspect.isNewType(cls):
|
||||||
if sys.version_info[:2] >= (3, 10):
|
if sys.version_info[:2] >= (3, 10):
|
||||||
# newtypes have correct module info since Python 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__}`'
|
return f':py:class:`{cls.__name__}`'
|
||||||
elif UnionType and isinstance(cls, UnionType):
|
elif UnionType and isinstance(cls, UnionType):
|
||||||
# Union types (PEP 585) retain their definition order when they
|
# Union types (PEP 585) retain their definition order when they
|
||||||
@@ -228,48 +247,56 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s
|
|||||||
return fr':py:class:`{cls.__name__}`\ [{concatenated_args}]'
|
return fr':py:class:`{cls.__name__}`\ [{concatenated_args}]'
|
||||||
return f':py:class:`{cls.__name__}`'
|
return f':py:class:`{cls.__name__}`'
|
||||||
elif (inspect.isgenericalias(cls)
|
elif (inspect.isgenericalias(cls)
|
||||||
and cls.__module__ == 'typing'
|
and cls_module_is_typing
|
||||||
and cls.__origin__ is Union):
|
and cls.__origin__ is Union):
|
||||||
# *cls* is defined in ``typing``, and thus ``__args__`` must exist
|
# *cls* is defined in ``typing``, and thus ``__args__`` must exist
|
||||||
return ' | '.join(restify(a, mode) for a in cls.__args__)
|
return ' | '.join(restify(a, mode) for a in cls.__args__)
|
||||||
elif inspect.isgenericalias(cls):
|
elif inspect.isgenericalias(cls):
|
||||||
|
cls_name = _get_typing_internal_name(cls)
|
||||||
|
|
||||||
if isinstance(cls.__origin__, typing._SpecialForm):
|
if isinstance(cls.__origin__, typing._SpecialForm):
|
||||||
|
# ClassVar; Concatenate; Final; Literal; Unpack; TypeGuard
|
||||||
|
# Required/NotRequired
|
||||||
text = restify(cls.__origin__, mode)
|
text = restify(cls.__origin__, mode)
|
||||||
elif getattr(cls, '_name', None):
|
elif cls_name:
|
||||||
cls_name = cls._name
|
text = f':py:class:`{module_prefix}{cls.__module__}.{cls_name}`'
|
||||||
text = f':py:class:`{modprefix}{cls.__module__}.{cls_name}`'
|
|
||||||
else:
|
else:
|
||||||
text = restify(cls.__origin__, mode)
|
text = restify(cls.__origin__, mode)
|
||||||
|
|
||||||
origin = getattr(cls, '__origin__', None)
|
__args__ = getattr(cls, '__args__', ())
|
||||||
if not hasattr(cls, '__args__'): # NoQA: SIM114
|
if not __args__:
|
||||||
pass
|
return text
|
||||||
elif all(is_system_TypeVar(a) for a in cls.__args__):
|
if all(map(is_system_TypeVar, __args__)):
|
||||||
# Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT])
|
# Don't print the arguments; they're all system defined type variables.
|
||||||
pass
|
return text
|
||||||
elif cls.__module__ == 'typing' and cls._name == 'Callable':
|
|
||||||
args = ', '.join(restify(a, mode) for a in cls.__args__[:-1])
|
# Callable has special formatting
|
||||||
text += fr'\ [[{args}], {restify(cls.__args__[-1], mode)}]'
|
if cls_module_is_typing and _get_typing_internal_name(cls) == 'Callable':
|
||||||
elif cls.__module__ == 'typing' and getattr(origin, '_name', None) == 'Literal':
|
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 _get_typing_internal_name(cls.__origin__) == 'Literal':
|
||||||
args = ', '.join(_format_literal_arg_restify(a, mode=mode)
|
args = ', '.join(_format_literal_arg_restify(a, mode=mode)
|
||||||
for a in cls.__args__)
|
for a in cls.__args__)
|
||||||
text += fr"\ [{args}]"
|
return fr'{text}\ [{args}]'
|
||||||
elif cls.__args__:
|
|
||||||
text += fr"\ [{', '.join(restify(a, mode) for a in cls.__args__)}]"
|
|
||||||
|
|
||||||
return text
|
# generic representation of the parameters
|
||||||
|
args = ', '.join(restify(a, mode) for a in __args__)
|
||||||
|
return fr'{text}\ [{args}]'
|
||||||
elif isinstance(cls, typing._SpecialForm):
|
elif isinstance(cls, typing._SpecialForm):
|
||||||
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:
|
elif sys.version_info[:2] >= (3, 11) and cls is typing.Any:
|
||||||
# handle bpo-46998
|
# handle bpo-46998
|
||||||
return f':py:obj:`~{cls.__module__}.{cls.__name__}`'
|
return f':py:obj:`~{cls.__module__}.{cls.__name__}`'
|
||||||
elif hasattr(cls, '__qualname__'):
|
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):
|
elif isinstance(cls, ForwardRef):
|
||||||
return f':py:class:`{cls.__forward_arg__}`'
|
return f':py:class:`{cls.__forward_arg__}`'
|
||||||
else:
|
else:
|
||||||
# not a class (ex. TypeVar)
|
# 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):
|
except (AttributeError, TypeError):
|
||||||
return inspect.object_description(cls)
|
return inspect.object_description(cls)
|
||||||
|
|
||||||
@@ -315,7 +342,7 @@ def stringify_annotation(
|
|||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
# things that are not types
|
# things that are not types
|
||||||
if annotation is None or annotation is NoneType:
|
if annotation in {None, NoneType}:
|
||||||
return 'None'
|
return 'None'
|
||||||
if annotation is Ellipsis:
|
if annotation is Ellipsis:
|
||||||
return '...'
|
return '...'
|
||||||
@@ -327,10 +354,7 @@ def stringify_annotation(
|
|||||||
if not annotation:
|
if not annotation:
|
||||||
return repr(annotation)
|
return repr(annotation)
|
||||||
|
|
||||||
if mode == 'smart':
|
module_prefix = '~' if mode == 'smart' else ''
|
||||||
module_prefix = '~'
|
|
||||||
else:
|
|
||||||
module_prefix = ''
|
|
||||||
|
|
||||||
# The values below must be strings if the objects are well-formed.
|
# The values below must be strings if the objects are well-formed.
|
||||||
annotation_qualname: str = getattr(annotation, '__qualname__', '')
|
annotation_qualname: str = getattr(annotation, '__qualname__', '')
|
||||||
@@ -338,6 +362,7 @@ def stringify_annotation(
|
|||||||
annotation_name: str = getattr(annotation, '__name__', '')
|
annotation_name: str = getattr(annotation, '__name__', '')
|
||||||
annotation_module_is_typing = annotation_module == 'typing'
|
annotation_module_is_typing = annotation_module == 'typing'
|
||||||
|
|
||||||
|
# Extract the annotation's base type by considering formattable cases
|
||||||
if isinstance(annotation, TypeVar):
|
if isinstance(annotation, TypeVar):
|
||||||
if annotation_module_is_typing and mode in {'fully-qualified-except-typing', 'smart'}:
|
if annotation_module_is_typing and mode in {'fully-qualified-except-typing', 'smart'}:
|
||||||
return annotation_name
|
return annotation_name
|
||||||
@@ -353,7 +378,7 @@ def stringify_annotation(
|
|||||||
return module_prefix + f'{annotation_module}.{annotation_name}'
|
return module_prefix + f'{annotation_module}.{annotation_name}'
|
||||||
elif is_invalid_builtin_class(annotation):
|
elif is_invalid_builtin_class(annotation):
|
||||||
return module_prefix + _INVALID_BUILTIN_CLASSES[annotation]
|
return module_prefix + _INVALID_BUILTIN_CLASSES[annotation]
|
||||||
elif str(annotation).startswith('typing.Annotated'): # for py39+
|
elif _is_annotated_form(annotation): # for py39+
|
||||||
pass
|
pass
|
||||||
elif annotation_module == 'builtins' and annotation_qualname:
|
elif annotation_module == 'builtins' and annotation_qualname:
|
||||||
args = getattr(annotation, '__args__', None)
|
args = getattr(annotation, '__args__', None)
|
||||||
@@ -365,6 +390,9 @@ def stringify_annotation(
|
|||||||
return repr(annotation)
|
return repr(annotation)
|
||||||
concatenated_args = ', '.join(stringify_annotation(arg, mode) for arg in args)
|
concatenated_args = ', '.join(stringify_annotation(arg, mode) for arg in args)
|
||||||
return f'{annotation_qualname}[{concatenated_args}]'
|
return f'{annotation_qualname}[{concatenated_args}]'
|
||||||
|
else:
|
||||||
|
# add other special cases that can be directly formatted
|
||||||
|
pass
|
||||||
|
|
||||||
module_prefix = f'{annotation_module}.'
|
module_prefix = f'{annotation_module}.'
|
||||||
annotation_forward_arg: str | None = getattr(annotation, '__forward_arg__', None)
|
annotation_forward_arg: str | None = getattr(annotation, '__forward_arg__', None)
|
||||||
@@ -387,6 +415,8 @@ def stringify_annotation(
|
|||||||
elif annotation_qualname:
|
elif annotation_qualname:
|
||||||
qualname = annotation_qualname
|
qualname = annotation_qualname
|
||||||
else:
|
else:
|
||||||
|
# in this case, we know that the annotation is a member
|
||||||
|
# of ``typing`` and all of them define ``__origin__``
|
||||||
qualname = stringify_annotation(
|
qualname = stringify_annotation(
|
||||||
annotation.__origin__, 'fully-qualified-except-typing',
|
annotation.__origin__, 'fully-qualified-except-typing',
|
||||||
).replace('typing.', '') # ex. Union
|
).replace('typing.', '') # ex. Union
|
||||||
@@ -402,12 +432,11 @@ def stringify_annotation(
|
|||||||
# only make them appear twice
|
# only make them appear twice
|
||||||
return repr(annotation)
|
return repr(annotation)
|
||||||
|
|
||||||
annotation_args = getattr(annotation, '__args__', None)
|
# Process the generic arguments (if any).
|
||||||
if annotation_args:
|
# They must be a list or a tuple, otherwise they are considered 'broken'.
|
||||||
if not isinstance(annotation_args, (list, tuple)):
|
annotation_args = getattr(annotation, '__args__', ())
|
||||||
# broken __args__ found
|
if annotation_args and isinstance(annotation_args, (list, tuple)):
|
||||||
pass
|
if qualname in {'Optional', 'Union', 'types.UnionType'}:
|
||||||
elif qualname in {'Optional', 'Union', 'types.UnionType'}:
|
|
||||||
return ' | '.join(stringify_annotation(a, mode) for a in annotation_args)
|
return ' | '.join(stringify_annotation(a, mode) for a in annotation_args)
|
||||||
elif qualname == 'Callable':
|
elif qualname == 'Callable':
|
||||||
args = ', '.join(stringify_annotation(a, mode) for a in annotation_args[:-1])
|
args = ', '.join(stringify_annotation(a, mode) for a in annotation_args[:-1])
|
||||||
@@ -417,7 +446,7 @@ def stringify_annotation(
|
|||||||
args = ', '.join(_format_literal_arg_stringify(a, mode=mode)
|
args = ', '.join(_format_literal_arg_stringify(a, mode=mode)
|
||||||
for a in annotation_args)
|
for a in annotation_args)
|
||||||
return f'{module_prefix}Literal[{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)
|
return stringify_annotation(annotation_args[0], mode)
|
||||||
elif all(is_system_TypeVar(a) for a in annotation_args):
|
elif all(is_system_TypeVar(a) for a in annotation_args):
|
||||||
# Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT])
|
# Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT])
|
||||||
|
|||||||
Reference in New Issue
Block a user