Sphinx

# Source code for sphinx.util.typing

    """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, TypeAliasType

    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

        from typing_extensions import TypeIs

        from sphinx.application import Sphinx
        from sphinx.util.inventory import _InventoryItem

        type _RestifyMode = Literal[
            'fully-qualified-except-typing',
            'smart',
        ]
        type _StringifyMode = Literal[
            'fully-qualified-except-typing',
            'fully-qualified',
            'smart',
        ]

    AnyTypeAliasType: tuple[type, ...] = (TypeAliasType,)

    try:
        import typing_extensions
    except ImportError:
        pass
    else:
        AnyTypeAliasType += (typing_extensions.TypeAliasType,)

    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
    type TextlikeNode = nodes.Text | nodes.TextElement

    # path matcher
    type PathMatcher = 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:
        type RoleFunction = 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
    type OptionSpec = dict[str, Callable[[str], typing.Any]]

    # title getter functions for enumerable nodes (see sphinx.domains.std)
    type TitleGetter = Callable[[nodes.Node], str]

    # inventory data on memory
    type Inventory = dict[str, dict[str, _InventoryItem]]

    class ExtensionMetadata(typing.TypedDict, total=False):
        """The metadata returned by an extension's ``setup()`` function.

        See :ref:`ext-metadata`.
        """

        version: str
        """The extension version (default: ``'unknown version'``)."""
        env_version: int
        """An integer that identifies the version of env data added by the extension."""
        parallel_read_safe: bool
        """Indicate whether parallel reading of source files is supported
        by the extension.
        """
        parallel_write_safe: bool
        """Indicate whether parallel writing of output files is supported
        by the extension (default: ``True``).
        """

    if TYPE_CHECKING:
        type _ExtensionSetupFunc = Callable[[Sphinx], ExtensionMetadata]  # NoQA: PYI047 (false positive)

    def get_type_hints(
        obj: Any,
        globalns: dict[str, Any] | None = None,
        localns: Mapping[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._dynamic._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)
                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 isinstance(cls, AnyTypeAliasType):
                # TODO: Use ``__qualname__`` here unconditionally (not yet supported)
                if hasattr(cls, '__qualname__'):
                    return f':py:type:`{module_prefix}{cls.__module__}.{cls.__qualname__}`'
                return f':py:type:`{module_prefix}{cls.__module__}.{cls.__name__}`'  # type: ignore[attr-defined]
            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._dynamic._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, AnyTypeAliasType)
        ) 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):
            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):
                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)
                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