"""The composite types for Sphinx."""
from __future__ import annotations
import dataclasses
import sys
import types
import typing
from collections.abc import Callable, Sequence
from typing import TYPE_CHECKING
from docutils import nodes
from docutils.parsers.rst.states import Inliner
from sphinx.util import logging
if TYPE_CHECKING:
from collections.abc import Mapping
from typing import Annotated, Any, Final, Literal, Protocol, TypeAlias
from typing_extensions import TypeIs
from sphinx.application import Sphinx
from sphinx.util.inventory import _InventoryItem
_RestifyMode: TypeAlias = Literal[
'fully-qualified-except-typing',
'smart',
]
_StringifyMode: TypeAlias = Literal[
'fully-qualified-except-typing',
'fully-qualified',
'smart',
]
logger = logging.getLogger(__name__)
# classes that have an incorrect .__module__ attribute
# Map of (__module__, __qualname__) to the correct fully-qualified name
_INVALID_BUILTIN_CLASSES: Final[Mapping[tuple[str, str], str]] = {
# types from 'contextvars'
('_contextvars', 'Context'): 'contextvars.Context',
('_contextvars', 'ContextVar'): 'contextvars.ContextVar',
('_contextvars', 'Token'): 'contextvars.Token',
# types from 'ctypes':
('_ctypes', 'Array'): 'ctypes.Array',
('_ctypes', 'Structure'): 'ctypes.Structure',
('_ctypes', 'Union'): 'ctypes.Union',
# types from 'io':
('_io', 'BufferedRandom'): 'io.BufferedRandom',
('_io', 'BufferedReader'): 'io.BufferedReader',
('_io', 'BufferedRWPair'): 'io.BufferedRWPair',
('_io', 'BufferedWriter'): 'io.BufferedWriter',
('_io', 'BytesIO'): 'io.BytesIO',
('_io', 'FileIO'): 'io.FileIO',
('_io', 'StringIO'): 'io.StringIO',
('_io', 'TextIOWrapper'): 'io.TextIOWrapper',
# types from 'json':
('json.decoder', 'JSONDecoder'): 'json.JSONDecoder',
('json.encoder', 'JSONEncoder'): 'json.JSONEncoder',
# types from 'lzma':
('_lzma', 'LZMACompressor'): 'lzma.LZMACompressor',
('_lzma', 'LZMADecompressor'): 'lzma.LZMADecompressor',
# types from 'multiprocessing':
('multiprocessing.context', 'Process'): 'multiprocessing.Process',
# types from 'pathlib':
('pathlib._local', 'Path'): 'pathlib.Path',
('pathlib._local', 'PosixPath'): 'pathlib.PosixPath',
('pathlib._local', 'PurePath'): 'pathlib.PurePath',
('pathlib._local', 'PurePosixPath'): 'pathlib.PurePosixPath',
('pathlib._local', 'PureWindowsPath'): 'pathlib.PureWindowsPath',
('pathlib._local', 'WindowsPath'): 'pathlib.WindowsPath',
# types from 'pickle':
('_pickle', 'Pickler'): 'pickle.Pickler',
('_pickle', 'Unpickler'): 'pickle.Unpickler',
# types from 'struct':
('_struct', 'Struct'): 'struct.Struct',
# types from 'types':
('builtins', 'async_generator'): 'types.AsyncGeneratorType',
('builtins', 'builtin_function_or_method'): 'types.BuiltinMethodType',
('builtins', 'cell'): 'types.CellType',
('builtins', 'classmethod_descriptor'): 'types.ClassMethodDescriptorType',
('builtins', 'code'): 'types.CodeType',
('builtins', 'coroutine'): 'types.CoroutineType',
('builtins', 'ellipsis'): 'types.EllipsisType',
('builtins', 'frame'): 'types.FrameType',
('builtins', 'function'): 'types.LambdaType',
('builtins', 'generator'): 'types.GeneratorType',
('builtins', 'getset_descriptor'): 'types.GetSetDescriptorType',
('builtins', 'mappingproxy'): 'types.MappingProxyType',
('builtins', 'member_descriptor'): 'types.MemberDescriptorType',
('builtins', 'method'): 'types.MethodType',
('builtins', 'method-wrapper'): 'types.MethodWrapperType',
('builtins', 'method_descriptor'): 'types.MethodDescriptorType',
('builtins', 'module'): 'types.ModuleType',
('builtins', 'NoneType'): 'types.NoneType',
('builtins', 'NotImplementedType'): 'types.NotImplementedType',
('builtins', 'traceback'): 'types.TracebackType',
('builtins', 'wrapper_descriptor'): 'types.WrapperDescriptorType',
# types from 'weakref':
('_weakrefset', 'WeakSet'): 'weakref.WeakSet',
# types from 'zipfile':
('zipfile._path', 'CompleteDirs'): 'zipfile.CompleteDirs',
('zipfile._path', 'Path'): 'zipfile.Path',
}
def is_invalid_builtin_class(obj: Any) -> str:
"""Check *obj* is an invalid built-in class."""
try:
key = obj.__module__, obj.__qualname__
except AttributeError: # non-standard type
return ''
return _INVALID_BUILTIN_CLASSES.get(key, '')
# Text like nodes which are initialized with text and rawsource
TextlikeNode: TypeAlias = nodes.Text | nodes.TextElement
# path matcher
PathMatcher: TypeAlias = Callable[[str], bool]
# common role functions
if TYPE_CHECKING:
class RoleFunction(Protocol):
def __call__(
self,
name: str,
rawtext: str,
text: str,
lineno: int,
inliner: Inliner,
/,
options: dict[str, Any] | None = None,
content: Sequence[str] = (),
) -> tuple[list[nodes.Node], list[nodes.system_message]]: ...
else:
RoleFunction: TypeAlias = Callable[
[str, str, str, int, Inliner, dict[str, typing.Any], Sequence[str]],
tuple[list[nodes.Node], list[nodes.system_message]],
]
# A option spec for directive
OptionSpec: TypeAlias = dict[str, Callable[[str], typing.Any]]
# title getter functions for enumerable nodes (see sphinx.domains.std)
TitleGetter: TypeAlias = Callable[[nodes.Node], str]
# inventory data on memory
Inventory: TypeAlias = dict[str, dict[str, '_InventoryItem']]
if TYPE_CHECKING:
_ExtensionSetupFunc: TypeAlias = Callable[[Sphinx], ExtensionMetadata] # NoQA: PYI047 (false positive)
def get_type_hints(
obj: Any,
globalns: dict[str, Any] | None = None,
localns: dict[str, Any] | None = None,
include_extras: bool = False,
) -> dict[str, Any]:
"""Return a dictionary containing type hints for a function, method, module or class
object.
This is a simple wrapper of `typing.get_type_hints()` that does not raise an error on
runtime.
"""
from sphinx.util.inspect import safe_getattr # lazy loading
try:
return typing.get_type_hints(
obj, globalns, localns, include_extras=include_extras
)
except NameError:
# Failed to evaluate ForwardRef (maybe TYPE_CHECKING)
return safe_getattr(obj, '__annotations__', {})
except AttributeError:
# Failed to evaluate ForwardRef (maybe not runtime checkable)
return safe_getattr(obj, '__annotations__', {})
except TypeError:
# Invalid object is given. But try to get __annotations__ as a fallback.
return safe_getattr(obj, '__annotations__', {})
except KeyError:
# a broken class found
# See: https://github.com/sphinx-doc/sphinx/issues/8084
return {}
def is_system_TypeVar(typ: Any) -> bool:
"""Check *typ* is system defined TypeVar."""
modname = getattr(typ, '__module__', '')
return modname == 'typing' and isinstance(typ, typing.TypeVar)
def _is_annotated_form(obj: Any) -> TypeIs[Annotated[Any, ...]]:
"""Check if *obj* is an annotated type."""
return (
typing.get_origin(obj) is typing.Annotated
or str(obj).startswith('typing.Annotated')
) # fmt: skip
def _is_unpack_form(obj: Any) -> bool:
"""Check if the object is :class:`typing.Unpack` or equivalent."""
return typing.get_origin(obj) is typing.Unpack
def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> str:
"""Convert a type-like object to a reST reference.
:param mode: Specify a method how annotations will be stringified.
'fully-qualified-except-typing'
Show the module name and qualified name of the annotation except
the "typing" module.
'smart'
Show the name of the annotation.
"""
from sphinx.ext.autodoc.mock import ismock, ismockmodule # lazy loading
from sphinx.util.inspect import isgenericalias, object_description # lazy loading
valid_modes = {'fully-qualified-except-typing', 'smart'}
if mode not in valid_modes:
valid = ', '.join(map(repr, sorted(valid_modes)))
msg = f'mode must be one of {valid}; got {mode!r}'
raise ValueError(msg)
# things that are not types
if cls is None or cls == types.NoneType:
return ':py:obj:`None`'
if cls is Ellipsis:
return '...'
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.
module_prefix = '~' if mode == 'smart' or cls_module_is_typing else ''
try:
if ismockmodule(cls):
return f':py:class:`{module_prefix}{cls.__name__}`'
elif ismock(cls):
return f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`'
elif fixed_cls := 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}{fixed_cls}`'
elif _is_annotated_form(cls):
args = restify(cls.__args__[0], mode)
meta_args = []
for m in cls.__metadata__:
if isinstance(m, type):
meta_args.append(restify(m, mode))
elif dataclasses.is_dataclass(m):
# use restify for the repr of field values rather than repr
d_fields = ', '.join([
rf'{f.name}=\ {restify(getattr(m, f.name), mode)}'
for f in dataclasses.fields(m)
if f.repr
])
meta_args.append(rf'{restify(type(m), mode)}\ ({d_fields})')
else:
meta_args.append(repr(m))
meta = ', '.join(meta_args)
if sys.version_info[:2] <= (3, 11):
# Hardcoded to fix errors on Python 3.11 and earlier.
return rf':py:class:`~typing.Annotated`\ [{args}, {meta}]'
return (
f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`'
rf'\ [{args}, {meta}]'
)
elif isinstance(cls, typing.NewType):
return f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`' # type: ignore[attr-defined]
elif isinstance(cls, types.UnionType) or (
isgenericalias(cls)
and cls_module_is_typing
and cls.__origin__ is typing.Union
):
# Union types (PEP 585) retain their definition order when they
# are printed natively and ``None``-like types are kept as is.
# *cls* is defined in ``typing``, and thus ``__args__`` must exist
return ' | '.join(restify(a, mode) for a in cls.__args__)
elif cls.__module__ in {'__builtin__', 'builtins'}:
if hasattr(cls, '__args__'):
if not cls.__args__: # Empty tuple, list, ...
return rf':py:class:`{cls.__name__}`\ [{cls.__args__!r}]'
concatenated_args = ', '.join(
restify(arg, mode) for arg in cls.__args__
)
return rf':py:class:`{cls.__name__}`\ [{concatenated_args}]'
return f':py:class:`{cls.__name__}`'
elif isgenericalias(cls):
if cls.__name__ and not isinstance(cls.__origin__, typing._SpecialForm):
# Represent generic aliases as the classes in ``typing`` rather
# than the underlying aliased classes,
# e.g. ``~typing.Tuple`` instead of ``tuple``.
text = f':py:class:`{module_prefix}{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__)):
# Don't print the arguments; they're all system defined type variables.
return text
# Callable has special formatting
if (
(cls_module_is_typing and cls.__name__ == 'Callable')
or (cls.__module__ == 'collections.abc' and cls.__name__ == 'Callable')
): # fmt: skip
args = ', '.join(restify(a, mode) for a in __args__[:-1])
returns = restify(__args__[-1], mode)
return rf'{text}\ [[{args}], {returns}]'
if cls_module_is_typing and cls.__origin__.__name__ == 'Literal':
args = ', '.join(
_format_literal_arg_restify(a, mode=mode) for a in cls.__args__
)
return rf'{text}\ [{args}]'
# generic representation of the parameters
args = ', '.join(restify(a, mode) for a in __args__)
return rf'{text}\ [{args}]'
elif isinstance(cls, typing._SpecialForm):
return f':py:obj:`~{cls.__module__}.{cls.__name__}`' # type: ignore[attr-defined]
elif cls is typing.Any:
# 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__}`'
elif isinstance(cls, typing.ForwardRef):
return f':py:class:`{cls.__forward_arg__}`'
else:
# not a class (ex. TypeVar) but should have a __name__
return f':py:obj:`{module_prefix}{cls.__module__}.{cls.__name__}`'
except (AttributeError, TypeError) as exc:
logger.debug('restify on %r in mode %r failed: %r', cls, mode, exc)
return object_description(cls)
def _format_literal_arg_restify(arg: Any, /, *, mode: str) -> str:
from sphinx.util.inspect import isenumattribute # lazy loading
if isenumattribute(arg):
enum_cls = arg.__class__
if mode == 'smart' or enum_cls.__module__ == 'typing':
# MyEnum.member
return (
f':py:attr:`~{enum_cls.__module__}.{enum_cls.__qualname__}.{arg.name}`'
)
# module.MyEnum.member
return f':py:attr:`{enum_cls.__module__}.{enum_cls.__qualname__}.{arg.name}`'
return repr(arg)
def stringify_annotation(
annotation: Any,
/,
mode: _StringifyMode = 'fully-qualified-except-typing',
*,
short_literals: bool = False,
) -> 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'
Show the module name and qualified name of the annotation except
the "typing" module.
'smart'
Show the name of the annotation.
'fully-qualified'
Show the module name and qualified name of the annotation.
:param short_literals: Render :py:class:`Literals` in PEP 604 style (``|``).
"""
from sphinx.ext.autodoc.mock import ismock, ismockmodule # lazy loading
valid_modes = {'fully-qualified-except-typing', 'fully-qualified', 'smart'}
if mode not in valid_modes:
valid = ', '.join(map(repr, sorted(valid_modes)))
msg = f'mode must be one of {valid}; got {mode!r}'
raise ValueError(msg)
# things that are not types
if annotation is None or annotation == types.NoneType:
return 'None'
if annotation is Ellipsis:
return '...'
if isinstance(annotation, str):
if annotation.startswith("'") and annotation.endswith("'"):
# Might be a double Forward-ref'ed type. Go unquoting.
return annotation[1:-1]
return annotation
if not annotation:
return repr(annotation)
module_prefix = '~' if mode == 'smart' else ''
# The values below must be strings if the objects are well-formed.
annotation_qualname: str = getattr(annotation, '__qualname__', '')
annotation_module: str = getattr(annotation, '__module__', '')
annotation_name: str = getattr(annotation, '__name__', '')
annotation_module_is_typing = annotation_module == 'typing'
if sys.version_info[:2] >= (3, 14) and isinstance(annotation, typing.ForwardRef):
# ForwardRef moved from `typing` to `annotationlib` in Python 3.14.
annotation_module_is_typing = True
# Extract the annotation's base type by considering formattable cases
if isinstance(annotation, typing.TypeVar) and not _is_unpack_form(annotation):
# typing_extensions.Unpack is incorrectly determined as a TypeVar
if annotation_module_is_typing and mode in {
'fully-qualified-except-typing',
'smart',
}:
return annotation_name
return module_prefix + f'{annotation_module}.{annotation_name}'
elif isinstance(annotation, typing.NewType):
return module_prefix + f'{annotation_module}.{annotation_name}'
elif ismockmodule(annotation):
return module_prefix + annotation_name
elif ismock(annotation):
return module_prefix + f'{annotation_module}.{annotation_name}'
elif fixed_annotation := is_invalid_builtin_class(annotation):
return module_prefix + fixed_annotation
elif _is_annotated_form(annotation): # for py310+
pass
elif annotation_module == 'builtins' and annotation_qualname:
args = getattr(annotation, '__args__', None)
if args is None:
return annotation_qualname
# PEP 585 generic
if not args: # Empty tuple, list, ...
return repr(annotation)
concatenated_args = ', '.join(
stringify_annotation(arg, mode=mode, short_literals=short_literals)
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)
if annotation_qualname or (
annotation_module_is_typing and not annotation_forward_arg
):
if mode == 'smart':
module_prefix = f'~{module_prefix}'
if annotation_module_is_typing and mode == 'fully-qualified-except-typing':
module_prefix = ''
elif _is_unpack_form(annotation) and annotation_module == 'typing_extensions':
module_prefix = '~' if mode == 'smart' else ''
else:
module_prefix = ''
if annotation_module_is_typing:
if annotation_forward_arg:
# handle ForwardRefs
qualname = annotation_forward_arg
else:
if annotation_name:
qualname = annotation_name
elif annotation_qualname:
qualname = annotation_qualname
else:
# in this case, we know that the annotation is a member
# of ``typing`` and all of them define ``__origin__``
qualname = stringify_annotation(
annotation.__origin__,
mode='fully-qualified-except-typing',
short_literals=short_literals,
).replace('typing.', '') # ex. Union
elif annotation_qualname:
qualname = annotation_qualname
elif hasattr(annotation, '__origin__'):
# instantiated generic provided by a user
qualname = stringify_annotation(
annotation.__origin__, mode=mode, short_literals=short_literals
)
elif isinstance(annotation, types.UnionType):
qualname = 'types.UnionType'
else:
# we weren't able to extract the base type, appending arguments would
# 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 'broken'.
annotation_args = getattr(annotation, '__args__', ())
if annotation_args and isinstance(annotation_args, list | tuple):
if (
qualname in {'Union', 'types.UnionType'}
and all(getattr(a, '__origin__', ...) is typing.Literal for a in annotation_args)
): # fmt: skip
# special case to flatten a Union of Literals into a literal
flattened_args = typing.Literal[annotation_args].__args__ # type: ignore[attr-defined]
args = ', '.join(
_format_literal_arg_stringify(a, mode=mode) for a in flattened_args
)
return f'{module_prefix}Literal[{args}]'
if qualname in {'Optional', 'Union', 'types.UnionType'}:
return ' | '.join(
stringify_annotation(a, mode=mode, short_literals=short_literals)
for a in annotation_args
)
elif qualname == 'Callable':
args = ', '.join(
stringify_annotation(a, mode=mode, short_literals=short_literals)
for a in annotation_args[:-1]
)
returns = stringify_annotation(
annotation_args[-1], mode=mode, short_literals=short_literals
)
return f'{module_prefix}Callable[[{args}], {returns}]'
elif qualname == 'Literal':
if short_literals:
return ' | '.join(
_format_literal_arg_stringify(a, mode=mode) for a in annotation_args
)
args = ', '.join(
_format_literal_arg_stringify(a, mode=mode) for a in annotation_args
)
return f'{module_prefix}Literal[{args}]'
elif _is_annotated_form(annotation): # for py310+
args = stringify_annotation(
annotation_args[0], mode=mode, short_literals=short_literals
)
meta_args = []
for m in annotation.__metadata__:
if isinstance(m, type):
meta_args.append(
stringify_annotation(
m, mode=mode, short_literals=short_literals
)
)
elif dataclasses.is_dataclass(m):
# use stringify_annotation for the repr of field values rather than repr
d_fields = ', '.join([
f'{f.name}={stringify_annotation(getattr(m, f.name), mode=mode, short_literals=short_literals)}' # NoQA: E501
for f in dataclasses.fields(m)
if f.repr
])
meta_args.append(
f'{stringify_annotation(type(m), mode=mode, short_literals=short_literals)}({d_fields})' # NoQA: E501
)
else:
meta_args.append(repr(m))
meta = ', '.join(meta_args)
if sys.version_info[:2] <= (3, 11):
if mode == 'fully-qualified-except-typing':
return f'Annotated[{args}, {meta}]'
module_prefix = module_prefix.replace('builtins', 'typing')
return f'{module_prefix}Annotated[{args}, {meta}]'
return f'{module_prefix}Annotated[{args}, {meta}]'
elif all(is_system_TypeVar(a) for a in annotation_args):
# Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT])
return module_prefix + qualname
else:
args = ', '.join(
stringify_annotation(a, mode=mode, short_literals=short_literals)
for a in annotation_args
)
return f'{module_prefix}{qualname}[{args}]'
return module_prefix + qualname
def _format_literal_arg_stringify(arg: Any, /, *, mode: str) -> str:
from sphinx.util.inspect import isenumattribute # lazy loading
if isenumattribute(arg):
enum_cls = arg.__class__
if mode == 'smart' or enum_cls.__module__ == 'typing':
# MyEnum.member
return f'{enum_cls.__qualname__}.{arg.name}'
# module.MyEnum.member
return f'{enum_cls.__module__}.{enum_cls.__qualname__}.{arg.name}'
return repr(arg)
# deprecated name -> (object to return, canonical path or empty string, removal version)
_DEPRECATED_OBJECTS: dict[str, tuple[Any, str, tuple[int, int]]] = {
} # fmt: skip
def __getattr__(name: str) -> Any:
if name not in _DEPRECATED_OBJECTS:
msg = f'module {__name__!r} has no attribute {name!r}'
raise AttributeError(msg)
from sphinx.deprecation import _deprecation_warning
deprecated_object, canonical_name, remove = _DEPRECATED_OBJECTS[name]
_deprecation_warning(__name__, name, canonical_name, remove=remove)
return deprecated_object