mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Stop outputting trailing commas for C and C++, as it is invalid syntax. Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com> Co-authored-by: Jakob Lykke Andersen <jakobandersen@users.noreply.github.com>
1097 lines
36 KiB
Python
1097 lines
36 KiB
Python
"""The Python domain."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import builtins
|
|
import inspect
|
|
import typing
|
|
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, 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
|
|
# xref 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,
|
|
)
|
|
|
|
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) -> list[nodes.Node]:
|
|
if 'async' in self.options:
|
|
return [
|
|
addnodes.desc_sig_keyword('', 'async'),
|
|
addnodes.desc_sig_space(),
|
|
]
|
|
else:
|
|
return []
|
|
|
|
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({
|
|
'final': directives.flag,
|
|
})
|
|
|
|
allow_nesting = True
|
|
|
|
def get_signature_prefix(self, sig: str) -> list[nodes.Node]:
|
|
if 'final' in self.options:
|
|
return [
|
|
nodes.Text('final'),
|
|
addnodes.desc_sig_space(),
|
|
nodes.Text(self.objtype),
|
|
addnodes.desc_sig_space(),
|
|
]
|
|
else:
|
|
return [nodes.Text(self.objtype), addnodes.desc_sig_space()]
|
|
|
|
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({
|
|
'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) -> list[nodes.Node]:
|
|
prefix: list[nodes.Node] = []
|
|
if 'final' in self.options:
|
|
prefix.extend((
|
|
nodes.Text('final'),
|
|
addnodes.desc_sig_space(),
|
|
))
|
|
if 'abstractmethod' in self.options:
|
|
prefix.extend((
|
|
nodes.Text('abstract'),
|
|
addnodes.desc_sig_space(),
|
|
))
|
|
if 'async' in self.options:
|
|
prefix.extend((
|
|
nodes.Text('async'),
|
|
addnodes.desc_sig_space(),
|
|
))
|
|
if 'classmethod' in self.options:
|
|
prefix.extend((
|
|
nodes.Text('classmethod'),
|
|
addnodes.desc_sig_space(),
|
|
))
|
|
if 'staticmethod' in self.options:
|
|
prefix.extend((
|
|
nodes.Text('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({
|
|
'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) -> list[nodes.Node]:
|
|
prefix: list[nodes.Node] = []
|
|
if 'abstractmethod' in self.options:
|
|
prefix.extend((
|
|
nodes.Text('abstract'),
|
|
addnodes.desc_sig_space(),
|
|
))
|
|
if 'classmethod' in self.options:
|
|
prefix.extend((
|
|
nodes.Text('class'),
|
|
addnodes.desc_sig_space(),
|
|
))
|
|
|
|
prefix.extend((
|
|
nodes.Text('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) -> list[nodes.Node]:
|
|
return [nodes.Text('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-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
|
|
indextext = f'module; {modname}'
|
|
inode = addnodes.index(entries=[('pair', indextext, node_id, '', None)])
|
|
# The node order is: index node first, then target node.
|
|
ret.extend((inode, 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
|
|
|
|
|
|
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.split('.', maxsplit=1)[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: dict[str, ObjType] = {
|
|
'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),
|
|
'class': PyXRefRole(),
|
|
'const': PyXRefRole(),
|
|
'attr': PyXRefRole(),
|
|
'type': PyXRefRole(),
|
|
'meth': PyXRefRole(fix_parens=True),
|
|
'mod': PyXRefRole(),
|
|
'obj': PyXRefRole(),
|
|
}
|
|
initial_data: 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.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.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."""
|
|
|
|
def istyping(s: str) -> bool:
|
|
if s.startswith('typing.'):
|
|
s = s.split('.', 1)[1]
|
|
|
|
return s in typing.__all__
|
|
|
|
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 istyping(reftarget):
|
|
# typing class
|
|
return contnode
|
|
|
|
return None
|
|
|
|
|
|
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')
|
|
app.add_config_value(
|
|
'python_maximum_signature_line_length',
|
|
None,
|
|
'env',
|
|
types=frozenset({int, type(None)}),
|
|
)
|
|
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')
|
|
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,
|
|
}
|