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__)

pairindextypes = {
    'module': 'module',
    'keyword': 'keyword',
    'operator': 'operator',
    'object': 'object',
    'exception': 'exception',
    'statement': 'statement',
    'builtin': 'built-in function',
}


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


[docs] 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', '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
[docs] 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.docname, node_id, objtype, aliased)
@property def modules(self) -> dict[str, ModuleEntry]: return self.data.setdefault('modules', {}) # modname -> ModuleEntry
[docs] 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.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 == '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, }