Sphinx

# Source code for sphinx.domains.python

    """The Python domain."""

    from __future__ import annotations

    import builtins
    import inspect
    import typing
    from types import NoneType
    from typing import TYPE_CHECKING, NamedTuple, cast

    from docutils import nodes
    from docutils.parsers.rst import directives

    from sphinx import addnodes
    from sphinx.domains import Domain, Index, IndexEntry, ObjType
    from sphinx.domains.python._annotations import _parse_annotation
    from sphinx.domains.python._object import PyObject
    from sphinx.locale import _, __
    from sphinx.roles import XRefRole
    from sphinx.util import logging
    from sphinx.util.docutils import SphinxDirective
    from sphinx.util.nodes import (
        find_pending_xref_condition,
        make_id,
        make_refnode,
    )

    if TYPE_CHECKING:
        from collections.abc import Iterable, Iterator, Sequence, Set
        from typing import Any, ClassVar

        from docutils.nodes import Element, Node

        from sphinx.addnodes import desc_signature, pending_xref
        from sphinx.application import Sphinx
        from sphinx.builders import Builder
        from sphinx.environment import BuildEnvironment
        from sphinx.util.typing import ExtensionMetadata, OptionSpec

    # re-export objects for backwards compatibility
    # See: https://github.com/sphinx-doc/sphinx/issues/12295

    from sphinx.domains.python._annotations import (  # NoQA: F401
        _parse_arglist,  # for sphinx-immaterial
        type_to_xref,
    )
    from sphinx.domains.python._object import (  # NoQA: F401
        PyField,
        PyGroupedField,
        PyTypedField,
        PyXrefMixin,
        py_sig_re,
    )

    _TYPING_ALL = frozenset(typing.__all__)

    logger = logging.getLogger(__name__)

    class ObjectEntry(NamedTuple):
        docname: str
        node_id: str
        objtype: str
        aliased: bool

    class ModuleEntry(NamedTuple):
        docname: str
        node_id: str
        synopsis: str
        platform: str
        deprecated: bool

    class PyFunction(PyObject):
        """Description of a function."""

        option_spec: ClassVar[OptionSpec] = PyObject.option_spec.copy()
        option_spec.update({
            'async': directives.flag,
        })

        def get_signature_prefix(self, sig: str) -> Sequence[nodes.Node]:
            prefix: list[addnodes.desc_sig_element] = []
            if 'async' in self.options:
                prefix.extend((
                    addnodes.desc_sig_keyword('', 'async'),
                    addnodes.desc_sig_space(),
                ))
            return prefix

        def needs_arglist(self) -> bool:
            return True

        def add_target_and_index(
            self, name_cls: tuple[str, str], sig: str, signode: desc_signature
        ) -> None:
            super().add_target_and_index(name_cls, sig, signode)
            if 'no-index-entry' not in self.options:
                modname = self.options.get('module', self.env.ref_context.get('py:module'))
                node_id = signode['ids'][0]

                name, _cls = name_cls
                if modname:
                    text = _('%s() (in module %s)') % (name, modname)
                    self.indexnode['entries'].append(('single', text, node_id, '', None))
                else:
                    text = f'built-in function; {name}()'
                    self.indexnode['entries'].append(('pair', text, node_id, '', None))

        def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str:
            # add index in own add_target_and_index() instead.
            return ''

    class PyDecoratorFunction(PyFunction):
        """Description of a decorator."""

        def run(self) -> list[Node]:
            # a decorator function is a function after all
            self.name = 'py:function'
            return super().run()

        def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]:
            ret = super().handle_signature(sig, signode)
            signode.insert(0, addnodes.desc_addname('@', '@'))
            return ret

        def needs_arglist(self) -> bool:
            return False

    class PyVariable(PyObject):
        """Description of a variable."""

        option_spec: ClassVar[OptionSpec] = PyObject.option_spec.copy()
        option_spec.update({
            'type': directives.unchanged,
            'value': directives.unchanged,
        })

        def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]:
            fullname, prefix = super().handle_signature(sig, signode)

            typ = self.options.get('type')
            if typ:
                annotations = _parse_annotation(typ, self.env)
                signode += addnodes.desc_annotation(
                    typ,
                    '',
                    addnodes.desc_sig_punctuation('', ':'),
                    addnodes.desc_sig_space(),
                    *annotations,
                )

            value = self.options.get('value')
            if value:
                signode += addnodes.desc_annotation(
                    value,
                    '',
                    addnodes.desc_sig_space(),
                    addnodes.desc_sig_punctuation('', '='),
                    addnodes.desc_sig_space(),
                    nodes.Text(value),
                )

            return fullname, prefix

        def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str:
            name, _cls = name_cls
            if modname:
                return _('%s (in module %s)') % (name, modname)
            else:
                return _('%s (built-in variable)') % name

    class PyClasslike(PyObject):
        """Description of a class-like object (classes, interfaces, exceptions)."""

        option_spec: ClassVar[OptionSpec] = PyObject.option_spec.copy()
        option_spec.update({
            'abstract': directives.flag,
            'final': directives.flag,
        })

        allow_nesting = True

        def get_signature_prefix(self, sig: str) -> Sequence[nodes.Node]:
            prefix: list[addnodes.desc_sig_element] = []
            if 'final' in self.options:
                prefix.extend((
                    addnodes.desc_sig_keyword('', 'final'),
                    addnodes.desc_sig_space(),
                ))
            if 'abstract' in self.options:
                prefix.extend((
                    addnodes.desc_sig_keyword('', 'abstract'),
                    addnodes.desc_sig_space(),
                ))
            prefix.extend((
                addnodes.desc_sig_keyword('', self.objtype),
                addnodes.desc_sig_space(),
            ))
            return prefix

        def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str:
            if self.objtype == 'class':
                if not modname:
                    return _('%s (built-in class)') % name_cls[0]
                return _('%s (class in %s)') % (name_cls[0], modname)
            elif self.objtype == 'exception':
                return name_cls[0]
            else:
                return ''

    class PyMethod(PyObject):
        """Description of a method."""

        option_spec: ClassVar[OptionSpec] = PyObject.option_spec.copy()
        option_spec.update({
            'abstract': directives.flag,
            'abstractmethod': directives.flag,
            'async': directives.flag,
            'classmethod': directives.flag,
            'final': directives.flag,
            'staticmethod': directives.flag,
        })

        def needs_arglist(self) -> bool:
            return True

        def get_signature_prefix(self, sig: str) -> Sequence[nodes.Node]:
            prefix: list[addnodes.desc_sig_element] = []
            if 'final' in self.options:
                prefix.extend((
                    addnodes.desc_sig_keyword('', 'final'),
                    addnodes.desc_sig_space(),
                ))
            if 'abstract' in self.options or 'abstractmethod' in self.options:
                prefix.extend((
                    addnodes.desc_sig_keyword('', 'abstractmethod'),
                    addnodes.desc_sig_space(),
                ))
            if 'async' in self.options:
                prefix.extend((
                    addnodes.desc_sig_keyword('', 'async'),
                    addnodes.desc_sig_space(),
                ))
            if 'classmethod' in self.options:
                prefix.extend((
                    addnodes.desc_sig_keyword('', 'classmethod'),
                    addnodes.desc_sig_space(),
                ))
            if 'staticmethod' in self.options:
                prefix.extend((
                    addnodes.desc_sig_keyword('', 'static'),
                    addnodes.desc_sig_space(),
                ))
            return prefix

        def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str:
            name, _cls = name_cls
            try:
                clsname, methname = name.rsplit('.', 1)
                if modname and self.config.add_module_names:
                    clsname = f'{modname}.{clsname}'
            except ValueError:
                if modname:
                    return _('%s() (in module %s)') % (name, modname)
                else:
                    return '%s()' % name

            if 'classmethod' in self.options:
                return _('%s() (%s class method)') % (methname, clsname)
            elif 'staticmethod' in self.options:
                return _('%s() (%s static method)') % (methname, clsname)
            else:
                return _('%s() (%s method)') % (methname, clsname)

    class PyClassMethod(PyMethod):
        """Description of a classmethod."""

        option_spec: ClassVar[OptionSpec] = PyObject.option_spec.copy()

        def run(self) -> list[Node]:
            self.name = 'py:method'
            self.options['classmethod'] = True

            return super().run()

    class PyStaticMethod(PyMethod):
        """Description of a staticmethod."""

        option_spec: ClassVar[OptionSpec] = PyObject.option_spec.copy()

        def run(self) -> list[Node]:
            self.name = 'py:method'
            self.options['staticmethod'] = True

            return super().run()

    class PyDecoratorMethod(PyMethod):
        """Description of a decoratormethod."""

        def run(self) -> list[Node]:
            self.name = 'py:method'
            return super().run()

        def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]:
            ret = super().handle_signature(sig, signode)
            signode.insert(0, addnodes.desc_addname('@', '@'))
            return ret

        def needs_arglist(self) -> bool:
            return False

    class PyAttribute(PyObject):
        """Description of an attribute."""

        option_spec: ClassVar[OptionSpec] = PyObject.option_spec.copy()
        option_spec.update({
            'type': directives.unchanged,
            'value': directives.unchanged,
        })

        def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]:
            fullname, prefix = super().handle_signature(sig, signode)

            typ = self.options.get('type')
            if typ:
                annotations = _parse_annotation(typ, self.env)
                signode += addnodes.desc_annotation(
                    typ,
                    '',
                    addnodes.desc_sig_punctuation('', ':'),
                    addnodes.desc_sig_space(),
                    *annotations,
                )

            value = self.options.get('value')
            if value:
                signode += addnodes.desc_annotation(
                    value,
                    '',
                    addnodes.desc_sig_space(),
                    addnodes.desc_sig_punctuation('', '='),
                    addnodes.desc_sig_space(),
                    nodes.Text(value),
                )

            return fullname, prefix

        def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str:
            name, _cls = name_cls
            try:
                clsname, attrname = name.rsplit('.', 1)
                if modname and self.config.add_module_names:
                    clsname = f'{modname}.{clsname}'
            except ValueError:
                if modname:
                    return _('%s (in module %s)') % (name, modname)
                else:
                    return name

            return _('%s (%s attribute)') % (attrname, clsname)

    class PyProperty(PyObject):
        """Description of an attribute."""

        option_spec = PyObject.option_spec.copy()
        option_spec.update({
            'abstract': directives.flag,
            'abstractmethod': directives.flag,
            'classmethod': directives.flag,
            'type': directives.unchanged,
        })

        def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]:
            fullname, prefix = super().handle_signature(sig, signode)

            typ = self.options.get('type')
            if typ:
                annotations = _parse_annotation(typ, self.env)
                signode += addnodes.desc_annotation(
                    typ,
                    '',
                    addnodes.desc_sig_punctuation('', ':'),
                    addnodes.desc_sig_space(),
                    *annotations,
                )

            return fullname, prefix

        def get_signature_prefix(self, sig: str) -> Sequence[nodes.Node]:
            prefix: list[addnodes.desc_sig_element] = []
            if 'abstract' in self.options or 'abstractmethod' in self.options:
                prefix.extend((
                    addnodes.desc_sig_keyword('', 'abstract'),
                    addnodes.desc_sig_space(),
                ))
            if 'classmethod' in self.options:
                prefix.extend((
                    addnodes.desc_sig_keyword('', 'class'),
                    addnodes.desc_sig_space(),
                ))
            prefix.extend((
                addnodes.desc_sig_keyword('', 'property'),
                addnodes.desc_sig_space(),
            ))
            return prefix

        def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str:
            name, _cls = name_cls
            try:
                clsname, attrname = name.rsplit('.', 1)
                if modname and self.config.add_module_names:
                    clsname = f'{modname}.{clsname}'
            except ValueError:
                if modname:
                    return _('%s (in module %s)') % (name, modname)
                else:
                    return name

            return _('%s (%s property)') % (attrname, clsname)

    class PyTypeAlias(PyObject):
        """Description of a type alias."""

        option_spec: ClassVar[OptionSpec] = PyObject.option_spec.copy()
        option_spec.update({
            'canonical': directives.unchanged,
        })

        def get_signature_prefix(self, sig: str) -> Sequence[nodes.Node]:
            return [addnodes.desc_sig_keyword('', 'type'), addnodes.desc_sig_space()]

        def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]:
            fullname, prefix = super().handle_signature(sig, signode)
            if canonical := self.options.get('canonical'):
                canonical_annotations = _parse_annotation(canonical, self.env)
                signode += addnodes.desc_annotation(
                    canonical,
                    '',
                    addnodes.desc_sig_space(),
                    addnodes.desc_sig_punctuation('', '='),
                    addnodes.desc_sig_space(),
                    *canonical_annotations,
                )
            return fullname, prefix

        def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str:
            name, _cls = name_cls
            try:
                clsname, attrname = name.rsplit('.', 1)
                if modname and self.config.add_module_names:
                    clsname = f'{modname}.{clsname}'
            except ValueError:
                if modname:
                    return _('%s (in module %s)') % (name, modname)
                else:
                    return name

            return _('%s (type alias in %s)') % (attrname, clsname)

    class PyModule(SphinxDirective):
        """Directive to mark description of a new module."""

        has_content = True
        required_arguments = 1
        optional_arguments = 0
        final_argument_whitespace = False
        option_spec: ClassVar[OptionSpec] = {
            'platform': lambda x: x,
            'synopsis': lambda x: x,
            'no-index': directives.flag,
            'no-index-entry': directives.flag,
            'no-contents-entry': directives.flag,
            'no-typesetting': directives.flag,
            'noindex': directives.flag,
            'nocontentsentry': directives.flag,
            'deprecated': directives.flag,
        }

        def run(self) -> list[Node]:
            # Copy old option names to new ones
            # xref RemovedInSphinx90Warning
            # # deprecate noindex in Sphinx 9.0
            if 'no-index' not in self.options and 'noindex' in self.options:
                self.options['no-index'] = self.options['noindex']

            domain = self.env.domains.python_domain

            modname = self.arguments[0].strip()
            no_index = 'no-index' in self.options
            self.env.ref_context['py:module'] = modname

            content_nodes = self.parse_content_to_nodes(allow_section_headings=True)

            ret: list[Node] = []
            if not no_index:
                # note module to the domain
                node_id = make_id(self.env, self.state.document, 'module', modname)
                target = nodes.target('', '', ids=[node_id], ismod=True)
                self.set_source_info(target)
                self.state.document.note_explicit_target(target)

                domain.note_module(
                    name=modname,
                    node_id=node_id,
                    synopsis=self.options.get('synopsis', ''),
                    platform=self.options.get('platform', ''),
                    deprecated='deprecated' in self.options,
                )
                domain.note_object(modname, 'module', node_id, location=target)

                # the platform and synopsis aren't printed; in fact, they are only
                # used in the modindex currently

                if 'no-index-entry' not in self.options:
                    index_text = f'module; {modname}'
                    inode = addnodes.index(
                        entries=[('pair', index_text, node_id, '', None)]
                    )
                    # The node order is: index node first, then target node.
                    ret.append(inode)
                ret.append(target)
            ret.extend(content_nodes)
            return ret

    class PyCurrentModule(SphinxDirective):
        """This directive is just to tell Sphinx that we're documenting
        stuff in module foo, but links to module foo won't lead here.
        """

        has_content = False
        required_arguments = 1
        optional_arguments = 0
        final_argument_whitespace = False
        option_spec: ClassVar[OptionSpec] = {}

        def run(self) -> list[Node]:
            modname = self.arguments[0].strip()
            if modname == 'None':
                self.env.ref_context.pop('py:module', None)
            else:
                self.env.ref_context['py:module'] = modname
            return []

    class PyXRefRole(XRefRole):
        def process_link(
            self,
            env: BuildEnvironment,
            refnode: Element,
            has_explicit_title: bool,
            title: str,
            target: str,
        ) -> tuple[str, str]:
            refnode['py:module'] = env.ref_context.get('py:module')
            refnode['py:class'] = env.ref_context.get('py:class')
            if not has_explicit_title:
                title = title.lstrip('.')  # only has a meaning for the target
                target = target.lstrip('~')  # only has a meaning for the title
                # if the first character is a tilde, don't display the module/class
                # parts of the contents
                if title[0:1] == '~':
                    title = title[1:]
                    dot = title.rfind('.')
                    if dot != -1:
                        title = title[dot + 1 :]
            # if the first character is a dot, search more specific namespaces first
            # else search builtins first
            if target[0:1] == '.':
                target = target[1:]
                refnode['refspecific'] = True
            return title, target

    class _PyDecoXRefRole(PyXRefRole):
        def process_link(
            self,
            env: BuildEnvironment,
            refnode: Element,
            has_explicit_title: bool,
            title: str,
            target: str,
        ) -> tuple[str, str]:
            title, target = super().process_link(
                env, refnode, has_explicit_title, title, target
            )
            return f'@{title}', target

    def filter_meta_fields(
        app: Sphinx, domain: str, objtype: str, content: Element
    ) -> None:
        """Filter ``:meta:`` field from its docstring."""
        if domain != 'py':
            return

        for node in content:
            if isinstance(node, nodes.field_list):
                fields = cast('list[nodes.field]', node)
                # removing list items while iterating the list needs reversed()
                for field in reversed(fields):
                    field_name = cast('nodes.field_body', field[0]).astext().strip()
                    if field_name == 'meta' or field_name.startswith('meta '):
                        node.remove(field)

    class PythonModuleIndex(Index):
        """Index subclass to provide the Python module index."""

        name = 'modindex'
        localname = _('Python Module Index')
        shortname = _('modules')
        domain: PythonDomain

        def generate(
            self, docnames: Iterable[str] | None = None
        ) -> tuple[list[tuple[str, list[IndexEntry]]], bool]:
            doc_names = frozenset(docnames) if docnames is not None else None

            content: dict[str, list[IndexEntry]] = {}
            # list of prefixes to ignore
            ignores: list[str] = sorted(
                self.domain.env.config['modindex_common_prefix'], key=len, reverse=True
            )

            # list of all modules, sorted by module name
            modules = sorted(self.domain.modules.items(), key=lambda t: t[0].lower())

            # sort out collapsible modules
            prev_modname = ''

            num_top_levels = 0
            for modname, module in modules:
                if doc_names and module.docname not in doc_names:
                    continue

                for ignore in ignores:
                    if modname.startswith(ignore):
                        modname = modname.removeprefix(ignore)
                        stripped = ignore
                        break
                else:
                    stripped = ''

                # we stripped the whole module name?
                if not modname:
                    modname, stripped = stripped, ''

                entries = content.setdefault(modname[0].lower(), [])

                package = modname.partition('.')[0]
                if package != modname:
                    # it's a submodule
                    if prev_modname == package:
                        # first submodule - make parent a group head
                        if entries:
                            last = entries[-1]
                            entries[-1] = IndexEntry(
                                name=last.name,
                                subtype=1,
                                docname=last.docname,
                                anchor=last.anchor,
                                extra=last.extra,
                                qualifier=last.qualifier,
                                descr=last.descr,
                            )
                    elif not prev_modname.startswith(package):
                        # submodule without parent in list, add dummy entry
                        dummy_entry = IndexEntry(
                            name=stripped + package,
                            subtype=1,
                            docname='',
                            anchor='',
                            extra='',
                            qualifier='',
                            descr='',
                        )
                        entries.append(dummy_entry)
                    subtype = 2
                else:
                    num_top_levels += 1
                    subtype = 0

                entry = IndexEntry(
                    name=stripped + modname,
                    subtype=subtype,
                    docname=module.docname,
                    anchor=module.node_id,
                    extra=module.platform,
                    qualifier=_('Deprecated') if module.deprecated else '',
                    descr=module.synopsis,
                )
                entries.append(entry)
                prev_modname = modname

            # apply heuristics when to collapse modindex at page load:
            # only collapse if number of toplevel modules is larger than
            # number of submodules
            collapse = len(modules) - num_top_levels < num_top_levels

            # sort by first letter
            sorted_content = sorted(content.items())

            return sorted_content, collapse

    class PythonDomain(Domain):
        """Python language domain."""

        name = 'py'
        label = 'Python'
        object_types = {
            'function': ObjType(_('function'), 'func', 'obj'),
            'data': ObjType(_('data'), 'data', 'obj'),
            'class': ObjType(_('class'), 'class', 'exc', 'obj'),
            'exception': ObjType(_('exception'), 'exc', 'class', 'obj'),
            'method': ObjType(_('method'), 'meth', 'obj'),
            'classmethod': ObjType(_('class method'), 'meth', 'obj'),
            'staticmethod': ObjType(_('static method'), 'meth', 'obj'),
            'attribute': ObjType(_('attribute'), 'attr', 'obj'),
            'property': ObjType(_('property'), 'attr', '_prop', 'obj'),
            'type': ObjType(_('type alias'), 'type', 'class', 'obj'),
            'module': ObjType(_('module'), 'mod', 'obj'),
        }

        directives = {
            'function': PyFunction,
            'data': PyVariable,
            'class': PyClasslike,
            'exception': PyClasslike,
            'method': PyMethod,
            'classmethod': PyClassMethod,
            'staticmethod': PyStaticMethod,
            'attribute': PyAttribute,
            'property': PyProperty,
            'type': PyTypeAlias,
            'module': PyModule,
            'currentmodule': PyCurrentModule,
            'decorator': PyDecoratorFunction,
            'decoratormethod': PyDecoratorMethod,
        }
        roles = {
            'data': PyXRefRole(),
            'exc': PyXRefRole(),
            'func': PyXRefRole(fix_parens=True),
            'deco': _PyDecoXRefRole(),
            'class': PyXRefRole(),
            'const': PyXRefRole(),
            'attr': PyXRefRole(),
            'type': PyXRefRole(),
            'meth': PyXRefRole(fix_parens=True),
            'mod': PyXRefRole(),
            'obj': PyXRefRole(),
        }
        initial_data: ClassVar[dict[str, dict[str, tuple[Any]]]] = {
            'objects': {},  # fullname -> docname, objtype
            'modules': {},  # modname -> docname, synopsis, platform, deprecated
        }
        indices = [
            PythonModuleIndex,
        ]

        @property
        def objects(self) -> dict[str, ObjectEntry]:
            return self.data.setdefault('objects', {})  # fullname -> ObjectEntry

        def note_object(
            self,
            name: str,
            objtype: str,
            node_id: str,
            aliased: bool = False,
            location: Any = None,
        ) -> None:
            """Note a python object for cross reference.

            .. versionadded:: 2.1
            """
            if name in self.objects:
                other = self.objects[name]
                if other.aliased and aliased is False:
                    # The original definition found. Override it!
                    pass
                elif other.aliased is False and aliased:
                    # The original definition is already registered.
                    return
                else:
                    # duplicated
                    logger.warning(
                        __(
                            'duplicate object description of %s, '
                            'other instance in %s, use :no-index: for one of them'
                        ),
                        name,
                        other.docname,
                        location=location,
                    )
            self.objects[name] = ObjectEntry(
                self.env.current_document.docname, node_id, objtype, aliased
            )

        @property
        def modules(self) -> dict[str, ModuleEntry]:
            return self.data.setdefault('modules', {})  # modname -> ModuleEntry

        def note_module(
            self, name: str, node_id: str, synopsis: str, platform: str, deprecated: bool
        ) -> None:
            """Note a python module for cross reference.

            .. versionadded:: 2.1
            """
            self.modules[name] = ModuleEntry(
                docname=self.env.current_document.docname,
                node_id=node_id,
                synopsis=synopsis,
                platform=platform,
                deprecated=deprecated,
            )

        def clear_doc(self, docname: str) -> None:
            to_remove = [
                fullname for fullname, obj in self.objects.items() if obj.docname == docname
            ]
            for fullname in to_remove:
                del self.objects[fullname]
            to_remove = [
                modname for modname, mod in self.modules.items() if mod.docname == docname
            ]
            for fullname in to_remove:
                del self.modules[fullname]

        def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> None:
            # XXX check duplicates?
            for fullname, obj in otherdata['objects'].items():
                if obj.docname in docnames:
                    self.objects[fullname] = obj
            for modname, mod in otherdata['modules'].items():
                if mod.docname in docnames:
                    self.modules[modname] = mod

        def find_obj(
            self,
            env: BuildEnvironment,
            modname: str,
            classname: str,
            name: str,
            type: str | None,
            searchmode: int = 0,
        ) -> list[tuple[str, ObjectEntry]]:
            """Find a Python object for "name", perhaps using the given module
            and/or classname.  Returns a list of (name, object entry) tuples.
            """
            # skip parens
            name = name.removesuffix('()')

            if not name:
                return []

            matches: list[tuple[str, ObjectEntry]] = []

            newname = None
            if searchmode == 1:
                if type is None:
                    objtypes: list[str] | None = list(self.object_types)
                else:
                    objtypes = self.objtypes_for_role(type)
                if objtypes is not None:
                    if modname and classname:
                        fullname = modname + '.' + classname + '.' + name
                        if (
                            fullname in self.objects
                            and self.objects[fullname].objtype in objtypes
                        ):
                            newname = fullname
                    if not newname:
                        if (
                            modname
                            and f'{modname}.{name}' in self.objects
                            and self.objects[f'{modname}.{name}'].objtype in objtypes
                        ):
                            newname = f'{modname}.{name}'
                        elif (
                            name in self.objects and self.objects[name].objtype in objtypes
                        ):
                            newname = name
                        else:
                            # "fuzzy" searching mode
                            searchname = f'.{name}'
                            matches = [
                                (oname, self.objects[oname])
                                for oname in self.objects
                                if oname.endswith(searchname)
                                and self.objects[oname].objtype in objtypes
                            ]
            else:
                # NOTE: searching for exact match, object type is not considered
                if name in self.objects:
                    newname = name
                elif type == 'mod':
                    # only exact matches allowed for modules
                    return []
                elif classname and classname + '.' + name in self.objects:
                    newname = classname + '.' + name
                elif modname and modname + '.' + name in self.objects:
                    newname = modname + '.' + name
                elif (
                    modname
                    and classname
                    and modname + '.' + classname + '.' + name in self.objects
                ):
                    newname = modname + '.' + classname + '.' + name
            if newname is not None:
                matches.append((newname, self.objects[newname]))
            return matches

        def resolve_xref(
            self,
            env: BuildEnvironment,
            fromdocname: str,
            builder: Builder,
            type: str,
            target: str,
            node: pending_xref,
            contnode: Element,
        ) -> nodes.reference | None:
            modname = node.get('py:module')
            clsname = node.get('py:class')
            searchmode = 1 if node.hasattr('refspecific') else 0
            matches = self.find_obj(env, modname, clsname, target, type, searchmode)

            if not matches and type == 'class':
                # fallback to data/attr (for type aliases)
                # type aliases are documented as data/attr but referenced as class
                matches = self.find_obj(env, modname, clsname, target, 'data', searchmode)
                if not matches:
                    matches = self.find_obj(
                        env, modname, clsname, target, 'attr', searchmode
                    )
            if not matches and type == 'attr':
                # fallback to meth (for property; Sphinx 2.4.x)
                # this ensures that `:attr:` role continues to refer to the old property entry
                # that defined by ``method`` directive in old reST files.
                matches = self.find_obj(env, modname, clsname, target, 'meth', searchmode)
            if not matches and type == 'meth':
                # fallback to attr (for property)
                # this ensures that `:meth:` in the old reST files can refer to the property
                # entry that defined by ``property`` directive.
                #
                # Note: _prop is a secret role only for internal look-up.
                matches = self.find_obj(env, modname, clsname, target, '_prop', searchmode)

            if not matches:
                return None
            elif len(matches) > 1:
                canonicals = [m for m in matches if not m[1].aliased]
                if len(canonicals) == 1:
                    matches = canonicals
                else:
                    logger.warning(
                        __('more than one target found for cross-reference %r: %s'),
                        target,
                        ', '.join(match[0] for match in matches),
                        type='ref',
                        subtype='python',
                        location=node,
                    )
            name, obj = matches[0]

            if obj[2] == 'module':
                return self._make_module_refnode(builder, fromdocname, name, contnode)
            else:
                # determine the content of the reference by conditions
                content = find_pending_xref_condition(node, 'resolved')
                if content:
                    children = content.children
                else:
                    # if not found, use contnode
                    children = [contnode]

                return make_refnode(builder, fromdocname, obj[0], obj[1], children, name)

        def resolve_any_xref(
            self,
            env: BuildEnvironment,
            fromdocname: str,
            builder: Builder,
            target: str,
            node: pending_xref,
            contnode: Element,
        ) -> list[tuple[str, nodes.reference]]:
            modname = node.get('py:module')
            clsname = node.get('py:class')
            results: list[tuple[str, nodes.reference]] = []

            # always search in "refspecific" mode with the :any: role
            matches = self.find_obj(env, modname, clsname, target, None, 1)
            multiple_matches = len(matches) > 1

            for name, obj in matches:
                if multiple_matches and obj.aliased:
                    # Skip duplicated matches
                    continue

                if obj[2] == 'module':
                    results.append((
                        'py:mod',
                        self._make_module_refnode(builder, fromdocname, name, contnode),
                    ))
                else:
                    # determine the content of the reference by conditions
                    content = find_pending_xref_condition(node, 'resolved')
                    if content:
                        children = content.children
                    else:
                        # if not found, use contnode
                        children = [contnode]

                    role = 'py:' + self.role_for_objtype(obj[2])  # type: ignore[operator]
                    results.append((
                        role,
                        make_refnode(builder, fromdocname, obj[0], obj[1], children, name),
                    ))
            return results

        def _make_module_refnode(
            self, builder: Builder, fromdocname: str, name: str, contnode: Node
        ) -> nodes.reference:
            # get additional info for modules
            module: ModuleEntry = self.modules[name]
            title_parts = [name]
            if module.synopsis:
                title_parts.append(f': {module.synopsis}')
            if module.deprecated:
                title_parts.append(_(' (deprecated)'))
            if module.platform:
                title_parts.append(f' ({module.platform})')
            title = ''.join(title_parts)
            return make_refnode(
                builder, fromdocname, module.docname, module.node_id, contnode, title
            )

        def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]:
            for modname, mod in self.modules.items():
                yield modname, modname, 'module', mod.docname, mod.node_id, 0
            for refname, obj in self.objects.items():
                if obj.objtype != 'module':  # modules are already handled
                    if obj.aliased:
                        # aliased names are not full-text searchable.
                        yield refname, refname, obj.objtype, obj.docname, obj.node_id, -1
                    else:
                        yield refname, refname, obj.objtype, obj.docname, obj.node_id, 1

        def get_full_qualified_name(self, node: Element) -> str | None:
            modname = node.get('py:module')
            clsname = node.get('py:class')
            target = node.get('reftarget')
            if target is None:
                return None
            else:
                return '.'.join(filter(None, [modname, clsname, target]))

    def builtin_resolver(
        app: Sphinx, env: BuildEnvironment, node: pending_xref, contnode: Element
    ) -> Element | None:
        """Do not emit nitpicky warnings for built-in types."""
        if node.get('refdomain') != 'py':
            return None
        elif node.get('reftype') in {'class', 'obj'} and node.get('reftarget') == 'None':
            return contnode
        elif node.get('reftype') in {'class', 'obj', 'exc'}:
            reftarget = node.get('reftarget')
            if inspect.isclass(getattr(builtins, reftarget, None)):
                # built-in class
                return contnode
            if _is_typing(reftarget):
                # typing class
                return contnode

        return None

    def _is_typing(s: str, /) -> bool:
        return s.removeprefix('typing.') in _TYPING_ALL

    def setup(app: Sphinx) -> ExtensionMetadata:
        app.setup_extension('sphinx.directives')

        app.add_domain(PythonDomain)
        app.add_config_value(
            'python_use_unqualified_type_names', False, 'env', types=frozenset({bool})
        )
        app.add_config_value(
            'python_maximum_signature_line_length',
            None,
            'env',
            types=frozenset({int, NoneType}),
        )
        app.add_config_value(
            'python_trailing_comma_in_multi_line_signatures',
            True,
            'env',
            types=frozenset({bool}),
        )
        app.add_config_value(
            'python_display_short_literal_types', False, 'env', types=frozenset({bool})
        )
        app.connect('object-description-transform', filter_meta_fields)
        app.connect('missing-reference', builtin_resolver, priority=900)

        return {
            'version': 'builtin',
            'env_version': 4,
            'parallel_read_safe': True,
            'parallel_write_safe': True,
        }