mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
1378 lines
52 KiB
Python
1378 lines
52 KiB
Python
"""
|
|
sphinx.domains.python
|
|
~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
The Python domain.
|
|
|
|
:copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
|
|
:license: BSD, see LICENSE for details.
|
|
"""
|
|
|
|
import builtins
|
|
import inspect
|
|
import re
|
|
import typing
|
|
import warnings
|
|
from inspect import Parameter
|
|
from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Tuple
|
|
from typing import cast
|
|
|
|
from docutils import nodes
|
|
from docutils.nodes import Element, Node
|
|
from docutils.parsers.rst import directives
|
|
|
|
from sphinx import addnodes
|
|
from sphinx.addnodes import pending_xref, desc_signature
|
|
from sphinx.application import Sphinx
|
|
from sphinx.builders import Builder
|
|
from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning
|
|
from sphinx.directives import ObjectDescription
|
|
from sphinx.domains import Domain, ObjType, Index, IndexEntry
|
|
from sphinx.environment import BuildEnvironment
|
|
from sphinx.locale import _, __
|
|
from sphinx.pycode.ast import ast, parse as ast_parse
|
|
from sphinx.roles import XRefRole
|
|
from sphinx.util import logging
|
|
from sphinx.util.docfields import Field, GroupedField, TypedField
|
|
from sphinx.util.docutils import SphinxDirective
|
|
from sphinx.util.inspect import signature_from_str
|
|
from sphinx.util.nodes import make_id, make_refnode
|
|
from sphinx.util.typing import TextlikeNode
|
|
|
|
if False:
|
|
# For type annotation
|
|
from typing import Type # for python3.5.1
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# REs for Python signatures
|
|
py_sig_re = re.compile(
|
|
r'''^ ([\w.]*\.)? # class name(s)
|
|
(\w+) \s* # thing name
|
|
(?: \(\s*(.*)\s*\) # optional: arguments
|
|
(?:\s* -> \s* (.*))? # return annotation
|
|
)? $ # and nothing more
|
|
''', re.VERBOSE)
|
|
|
|
|
|
pairindextypes = {
|
|
'module': _('module'),
|
|
'keyword': _('keyword'),
|
|
'operator': _('operator'),
|
|
'object': _('object'),
|
|
'exception': _('exception'),
|
|
'statement': _('statement'),
|
|
'builtin': _('built-in function'),
|
|
}
|
|
|
|
ObjectEntry = NamedTuple('ObjectEntry', [('docname', str),
|
|
('node_id', str),
|
|
('objtype', str)])
|
|
ModuleEntry = NamedTuple('ModuleEntry', [('docname', str),
|
|
('node_id', str),
|
|
('synopsis', str),
|
|
('platform', str),
|
|
('deprecated', bool)])
|
|
|
|
|
|
def type_to_xref(text: str) -> addnodes.pending_xref:
|
|
"""Convert a type string to a cross reference node."""
|
|
if text == 'None':
|
|
reftype = 'obj'
|
|
else:
|
|
reftype = 'class'
|
|
|
|
return pending_xref('', nodes.Text(text),
|
|
refdomain='py', reftype=reftype, reftarget=text)
|
|
|
|
|
|
def _parse_annotation(annotation: str) -> List[Node]:
|
|
"""Parse type annotation."""
|
|
def unparse(node: ast.AST) -> List[Node]:
|
|
if isinstance(node, ast.Attribute):
|
|
return [nodes.Text("%s.%s" % (unparse(node.value)[0], node.attr))]
|
|
elif isinstance(node, ast.Expr):
|
|
return unparse(node.value)
|
|
elif isinstance(node, ast.Index):
|
|
return unparse(node.value)
|
|
elif isinstance(node, ast.List):
|
|
result = [addnodes.desc_sig_punctuation('', '[')] # type: List[Node]
|
|
for elem in node.elts:
|
|
result.extend(unparse(elem))
|
|
result.append(addnodes.desc_sig_punctuation('', ', '))
|
|
result.pop()
|
|
result.append(addnodes.desc_sig_punctuation('', ']'))
|
|
return result
|
|
elif isinstance(node, ast.Module):
|
|
return sum((unparse(e) for e in node.body), [])
|
|
elif isinstance(node, ast.Name):
|
|
return [nodes.Text(node.id)]
|
|
elif isinstance(node, ast.Subscript):
|
|
result = unparse(node.value)
|
|
result.append(addnodes.desc_sig_punctuation('', '['))
|
|
result.extend(unparse(node.slice))
|
|
result.append(addnodes.desc_sig_punctuation('', ']'))
|
|
return result
|
|
elif isinstance(node, ast.Tuple):
|
|
if node.elts:
|
|
result = []
|
|
for elem in node.elts:
|
|
result.extend(unparse(elem))
|
|
result.append(addnodes.desc_sig_punctuation('', ', '))
|
|
result.pop()
|
|
else:
|
|
result = [addnodes.desc_sig_punctuation('', '('),
|
|
addnodes.desc_sig_punctuation('', ')')]
|
|
|
|
return result
|
|
else:
|
|
raise SyntaxError # unsupported syntax
|
|
|
|
try:
|
|
tree = ast_parse(annotation)
|
|
result = unparse(tree)
|
|
for i, node in enumerate(result):
|
|
if isinstance(node, nodes.Text):
|
|
result[i] = type_to_xref(str(node))
|
|
return result
|
|
except SyntaxError:
|
|
return [type_to_xref(annotation)]
|
|
|
|
|
|
def _parse_arglist(arglist: str) -> addnodes.desc_parameterlist:
|
|
"""Parse a list of arguments using AST parser"""
|
|
params = addnodes.desc_parameterlist(arglist)
|
|
sig = signature_from_str('(%s)' % arglist)
|
|
last_kind = None
|
|
for param in sig.parameters.values():
|
|
if param.kind != param.POSITIONAL_ONLY and last_kind == param.POSITIONAL_ONLY:
|
|
# PEP-570: Separator for Positional Only Parameter: /
|
|
params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '/'))
|
|
if param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD,
|
|
param.POSITIONAL_ONLY,
|
|
None):
|
|
# PEP-3102: Separator for Keyword Only Parameter: *
|
|
params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '*'))
|
|
|
|
node = addnodes.desc_parameter()
|
|
if param.kind == param.VAR_POSITIONAL:
|
|
node += addnodes.desc_sig_operator('', '*')
|
|
node += addnodes.desc_sig_name('', param.name)
|
|
elif param.kind == param.VAR_KEYWORD:
|
|
node += addnodes.desc_sig_operator('', '**')
|
|
node += addnodes.desc_sig_name('', param.name)
|
|
else:
|
|
node += addnodes.desc_sig_name('', param.name)
|
|
|
|
if param.annotation is not param.empty:
|
|
children = _parse_annotation(param.annotation)
|
|
node += addnodes.desc_sig_punctuation('', ':')
|
|
node += nodes.Text(' ')
|
|
node += addnodes.desc_sig_name('', '', *children) # type: ignore
|
|
if param.default is not param.empty:
|
|
if param.annotation is not param.empty:
|
|
node += nodes.Text(' ')
|
|
node += addnodes.desc_sig_operator('', '=')
|
|
node += nodes.Text(' ')
|
|
else:
|
|
node += addnodes.desc_sig_operator('', '=')
|
|
node += nodes.inline('', param.default, classes=['default_value'],
|
|
support_smartquotes=False)
|
|
|
|
params += node
|
|
last_kind = param.kind
|
|
|
|
if last_kind == Parameter.POSITIONAL_ONLY:
|
|
# PEP-570: Separator for Positional Only Parameter: /
|
|
params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '/'))
|
|
|
|
return params
|
|
|
|
|
|
def _pseudo_parse_arglist(signode: desc_signature, arglist: str) -> None:
|
|
""""Parse" a list of arguments separated by commas.
|
|
|
|
Arguments can have "optional" annotations given by enclosing them in
|
|
brackets. Currently, this will split at any comma, even if it's inside a
|
|
string literal (e.g. default argument value).
|
|
"""
|
|
paramlist = addnodes.desc_parameterlist()
|
|
stack = [paramlist] # type: List[Element]
|
|
try:
|
|
for argument in arglist.split(','):
|
|
argument = argument.strip()
|
|
ends_open = ends_close = 0
|
|
while argument.startswith('['):
|
|
stack.append(addnodes.desc_optional())
|
|
stack[-2] += stack[-1]
|
|
argument = argument[1:].strip()
|
|
while argument.startswith(']'):
|
|
stack.pop()
|
|
argument = argument[1:].strip()
|
|
while argument.endswith(']') and not argument.endswith('[]'):
|
|
ends_close += 1
|
|
argument = argument[:-1].strip()
|
|
while argument.endswith('['):
|
|
ends_open += 1
|
|
argument = argument[:-1].strip()
|
|
if argument:
|
|
stack[-1] += addnodes.desc_parameter(argument, argument)
|
|
while ends_open:
|
|
stack.append(addnodes.desc_optional())
|
|
stack[-2] += stack[-1]
|
|
ends_open -= 1
|
|
while ends_close:
|
|
stack.pop()
|
|
ends_close -= 1
|
|
if len(stack) != 1:
|
|
raise IndexError
|
|
except IndexError:
|
|
# if there are too few or too many elements on the stack, just give up
|
|
# and treat the whole argument list as one argument, discarding the
|
|
# already partially populated paramlist node
|
|
paramlist = addnodes.desc_parameterlist()
|
|
paramlist += addnodes.desc_parameter(arglist, arglist)
|
|
signode += paramlist
|
|
else:
|
|
signode += paramlist
|
|
|
|
|
|
# This override allows our inline type specifiers to behave like :class: link
|
|
# when it comes to handling "." and "~" prefixes.
|
|
class PyXrefMixin:
|
|
def make_xref(self, rolename: str, domain: str, target: str,
|
|
innernode: "Type[TextlikeNode]" = nodes.emphasis,
|
|
contnode: Node = None, env: BuildEnvironment = None) -> Node:
|
|
result = super().make_xref(rolename, domain, target, # type: ignore
|
|
innernode, contnode, env)
|
|
result['refspecific'] = True
|
|
if target.startswith(('.', '~')):
|
|
prefix, result['reftarget'] = target[0], target[1:]
|
|
if prefix == '.':
|
|
text = target[1:]
|
|
elif prefix == '~':
|
|
text = target.split('.')[-1]
|
|
for node in result.traverse(nodes.Text):
|
|
node.parent[node.parent.index(node)] = nodes.Text(text)
|
|
break
|
|
return result
|
|
|
|
def make_xrefs(self, rolename: str, domain: str, target: str,
|
|
innernode: "Type[TextlikeNode]" = nodes.emphasis,
|
|
contnode: Node = None, env: BuildEnvironment = None) -> List[Node]:
|
|
delims = r'(\s*[\[\]\(\),](?:\s*or\s)?\s*|\s+or\s+)'
|
|
delims_re = re.compile(delims)
|
|
sub_targets = re.split(delims, target)
|
|
|
|
split_contnode = bool(contnode and contnode.astext() == target)
|
|
|
|
results = []
|
|
for sub_target in filter(None, sub_targets):
|
|
if split_contnode:
|
|
contnode = nodes.Text(sub_target)
|
|
|
|
if delims_re.match(sub_target):
|
|
results.append(contnode or innernode(sub_target, sub_target))
|
|
else:
|
|
results.append(self.make_xref(rolename, domain, sub_target,
|
|
innernode, contnode, env))
|
|
|
|
return results
|
|
|
|
|
|
class PyField(PyXrefMixin, Field):
|
|
def make_xref(self, rolename: str, domain: str, target: str,
|
|
innernode: "Type[TextlikeNode]" = nodes.emphasis,
|
|
contnode: Node = None, env: BuildEnvironment = None) -> Node:
|
|
if rolename == 'class' and target == 'None':
|
|
# None is not a type, so use obj role instead.
|
|
rolename = 'obj'
|
|
|
|
return super().make_xref(rolename, domain, target, innernode, contnode, env)
|
|
|
|
|
|
class PyGroupedField(PyXrefMixin, GroupedField):
|
|
pass
|
|
|
|
|
|
class PyTypedField(PyXrefMixin, TypedField):
|
|
def make_xref(self, rolename: str, domain: str, target: str,
|
|
innernode: "Type[TextlikeNode]" = nodes.emphasis,
|
|
contnode: Node = None, env: BuildEnvironment = None) -> Node:
|
|
if rolename == 'class' and target == 'None':
|
|
# None is not a type, so use obj role instead.
|
|
rolename = 'obj'
|
|
|
|
return super().make_xref(rolename, domain, target, innernode, contnode, env)
|
|
|
|
|
|
class PyObject(ObjectDescription):
|
|
"""
|
|
Description of a general Python object.
|
|
|
|
:cvar allow_nesting: Class is an object that allows for nested namespaces
|
|
:vartype allow_nesting: bool
|
|
"""
|
|
option_spec = {
|
|
'noindex': directives.flag,
|
|
'module': directives.unchanged,
|
|
'annotation': directives.unchanged,
|
|
}
|
|
|
|
doc_field_types = [
|
|
PyTypedField('parameter', label=_('Parameters'),
|
|
names=('param', 'parameter', 'arg', 'argument',
|
|
'keyword', 'kwarg', 'kwparam'),
|
|
typerolename='class', typenames=('paramtype', 'type'),
|
|
can_collapse=True),
|
|
PyTypedField('variable', label=_('Variables'), rolename='obj',
|
|
names=('var', 'ivar', 'cvar'),
|
|
typerolename='class', typenames=('vartype',),
|
|
can_collapse=True),
|
|
PyGroupedField('exceptions', label=_('Raises'), rolename='exc',
|
|
names=('raises', 'raise', 'exception', 'except'),
|
|
can_collapse=True),
|
|
Field('returnvalue', label=_('Returns'), has_arg=False,
|
|
names=('returns', 'return')),
|
|
PyField('returntype', label=_('Return type'), has_arg=False,
|
|
names=('rtype',), bodyrolename='class'),
|
|
]
|
|
|
|
allow_nesting = False
|
|
|
|
def get_signature_prefix(self, sig: str) -> str:
|
|
"""May return a prefix to put before the object name in the
|
|
signature.
|
|
"""
|
|
return ''
|
|
|
|
def needs_arglist(self) -> bool:
|
|
"""May return true if an empty argument list is to be generated even if
|
|
the document contains none.
|
|
"""
|
|
return False
|
|
|
|
def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]:
|
|
"""Transform a Python signature into RST nodes.
|
|
|
|
Return (fully qualified name of the thing, classname if any).
|
|
|
|
If inside a class, the current class name is handled intelligently:
|
|
* it is stripped from the displayed name if present
|
|
* it is added to the full name (return value) if not present
|
|
"""
|
|
m = py_sig_re.match(sig)
|
|
if m is None:
|
|
raise ValueError
|
|
prefix, name, arglist, retann = m.groups()
|
|
|
|
# determine module and class name (if applicable), as well as full name
|
|
modname = self.options.get('module', self.env.ref_context.get('py:module'))
|
|
classname = self.env.ref_context.get('py:class')
|
|
if classname:
|
|
add_module = False
|
|
if prefix and (prefix == classname or
|
|
prefix.startswith(classname + ".")):
|
|
fullname = prefix + name
|
|
# class name is given again in the signature
|
|
prefix = prefix[len(classname):].lstrip('.')
|
|
elif prefix:
|
|
# class name is given in the signature, but different
|
|
# (shouldn't happen)
|
|
fullname = classname + '.' + prefix + name
|
|
else:
|
|
# class name is not given in the signature
|
|
fullname = classname + '.' + name
|
|
else:
|
|
add_module = True
|
|
if prefix:
|
|
classname = prefix.rstrip('.')
|
|
fullname = prefix + name
|
|
else:
|
|
classname = ''
|
|
fullname = name
|
|
|
|
signode['module'] = modname
|
|
signode['class'] = classname
|
|
signode['fullname'] = fullname
|
|
|
|
sig_prefix = self.get_signature_prefix(sig)
|
|
if sig_prefix:
|
|
signode += addnodes.desc_annotation(sig_prefix, sig_prefix)
|
|
|
|
if prefix:
|
|
signode += addnodes.desc_addname(prefix, prefix)
|
|
elif add_module and self.env.config.add_module_names:
|
|
if modname and modname != 'exceptions':
|
|
# exceptions are a special case, since they are documented in the
|
|
# 'exceptions' module.
|
|
nodetext = modname + '.'
|
|
signode += addnodes.desc_addname(nodetext, nodetext)
|
|
|
|
signode += addnodes.desc_name(name, name)
|
|
if arglist:
|
|
try:
|
|
signode += _parse_arglist(arglist)
|
|
except SyntaxError:
|
|
# fallback to parse arglist original parser.
|
|
# it supports to represent optional arguments (ex. "func(foo [, bar])")
|
|
_pseudo_parse_arglist(signode, arglist)
|
|
except NotImplementedError as exc:
|
|
logger.warning("could not parse arglist (%r): %s", arglist, exc,
|
|
location=signode)
|
|
_pseudo_parse_arglist(signode, arglist)
|
|
else:
|
|
if self.needs_arglist():
|
|
# for callables, add an empty parameter list
|
|
signode += addnodes.desc_parameterlist()
|
|
|
|
if retann:
|
|
children = _parse_annotation(retann)
|
|
signode += addnodes.desc_returns(retann, '', *children)
|
|
|
|
anno = self.options.get('annotation')
|
|
if anno:
|
|
signode += addnodes.desc_annotation(' ' + anno, ' ' + anno)
|
|
|
|
return fullname, prefix
|
|
|
|
def get_index_text(self, modname: str, name: Tuple[str, str]) -> str:
|
|
"""Return the text for the index entry of the object."""
|
|
raise NotImplementedError('must be implemented in subclasses')
|
|
|
|
def add_target_and_index(self, name_cls: Tuple[str, str], sig: str,
|
|
signode: desc_signature) -> None:
|
|
modname = self.options.get('module', self.env.ref_context.get('py:module'))
|
|
fullname = (modname + '.' if modname else '') + name_cls[0]
|
|
node_id = make_id(self.env, self.state.document, '', fullname)
|
|
signode['ids'].append(node_id)
|
|
|
|
# Assign old styled node_id(fullname) not to break old hyperlinks (if possible)
|
|
# Note: Will removed in Sphinx-5.0 (RemovedInSphinx50Warning)
|
|
if node_id != fullname and fullname not in self.state.document.ids:
|
|
signode['ids'].append(fullname)
|
|
|
|
self.state.document.note_explicit_target(signode)
|
|
|
|
domain = cast(PythonDomain, self.env.get_domain('py'))
|
|
domain.note_object(fullname, self.objtype, node_id, location=signode)
|
|
|
|
indextext = self.get_index_text(modname, name_cls)
|
|
if indextext:
|
|
self.indexnode['entries'].append(('single', indextext, node_id, '', None))
|
|
|
|
def before_content(self) -> None:
|
|
"""Handle object nesting before content
|
|
|
|
:py:class:`PyObject` represents Python language constructs. For
|
|
constructs that are nestable, such as a Python classes, this method will
|
|
build up a stack of the nesting heirarchy so that it can be later
|
|
de-nested correctly, in :py:meth:`after_content`.
|
|
|
|
For constructs that aren't nestable, the stack is bypassed, and instead
|
|
only the most recent object is tracked. This object prefix name will be
|
|
removed with :py:meth:`after_content`.
|
|
"""
|
|
prefix = None
|
|
if self.names:
|
|
# fullname and name_prefix come from the `handle_signature` method.
|
|
# fullname represents the full object name that is constructed using
|
|
# object nesting and explicit prefixes. `name_prefix` is the
|
|
# explicit prefix given in a signature
|
|
(fullname, name_prefix) = self.names[-1]
|
|
if self.allow_nesting:
|
|
prefix = fullname
|
|
elif name_prefix:
|
|
prefix = name_prefix.strip('.')
|
|
if prefix:
|
|
self.env.ref_context['py:class'] = prefix
|
|
if self.allow_nesting:
|
|
classes = self.env.ref_context.setdefault('py:classes', [])
|
|
classes.append(prefix)
|
|
if 'module' in self.options:
|
|
modules = self.env.ref_context.setdefault('py:modules', [])
|
|
modules.append(self.env.ref_context.get('py:module'))
|
|
self.env.ref_context['py:module'] = self.options['module']
|
|
|
|
def after_content(self) -> None:
|
|
"""Handle object de-nesting after content
|
|
|
|
If this class is a nestable object, removing the last nested class prefix
|
|
ends further nesting in the object.
|
|
|
|
If this class is not a nestable object, the list of classes should not
|
|
be altered as we didn't affect the nesting levels in
|
|
:py:meth:`before_content`.
|
|
"""
|
|
classes = self.env.ref_context.setdefault('py:classes', [])
|
|
if self.allow_nesting:
|
|
try:
|
|
classes.pop()
|
|
except IndexError:
|
|
pass
|
|
self.env.ref_context['py:class'] = (classes[-1] if len(classes) > 0
|
|
else None)
|
|
if 'module' in self.options:
|
|
modules = self.env.ref_context.setdefault('py:modules', [])
|
|
if modules:
|
|
self.env.ref_context['py:module'] = modules.pop()
|
|
else:
|
|
self.env.ref_context.pop('py:module')
|
|
|
|
|
|
class PyModulelevel(PyObject):
|
|
"""
|
|
Description of an object on module level (functions, data).
|
|
"""
|
|
|
|
def run(self) -> List[Node]:
|
|
for cls in self.__class__.__mro__:
|
|
if cls.__name__ != 'DirectiveAdapter':
|
|
warnings.warn('PyModulelevel is deprecated. '
|
|
'Please check the implementation of %s' % cls,
|
|
RemovedInSphinx40Warning, stacklevel=2)
|
|
break
|
|
else:
|
|
warnings.warn('PyModulelevel is deprecated',
|
|
RemovedInSphinx40Warning, stacklevel=2)
|
|
|
|
return super().run()
|
|
|
|
def needs_arglist(self) -> bool:
|
|
return self.objtype == 'function'
|
|
|
|
def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
|
|
if self.objtype == 'function':
|
|
if not modname:
|
|
return _('%s() (built-in function)') % name_cls[0]
|
|
return _('%s() (in module %s)') % (name_cls[0], modname)
|
|
elif self.objtype == 'data':
|
|
if not modname:
|
|
return _('%s (built-in variable)') % name_cls[0]
|
|
return _('%s (in module %s)') % (name_cls[0], modname)
|
|
else:
|
|
return ''
|
|
|
|
|
|
class PyFunction(PyObject):
|
|
"""Description of a function."""
|
|
|
|
option_spec = PyObject.option_spec.copy()
|
|
option_spec.update({
|
|
'async': directives.flag,
|
|
})
|
|
|
|
def get_signature_prefix(self, sig: str) -> str:
|
|
if 'async' in self.options:
|
|
return 'async '
|
|
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)
|
|
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 = '%s; %s()' % (pairindextypes['builtin'], 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 None
|
|
|
|
|
|
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 = 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:
|
|
signode += addnodes.desc_annotation(typ, '', nodes.Text(': '), type_to_xref(typ))
|
|
|
|
value = self.options.get('value')
|
|
if value:
|
|
signode += addnodes.desc_annotation(value, ' = ' + 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 = PyObject.option_spec.copy()
|
|
option_spec.update({
|
|
'final': directives.flag,
|
|
})
|
|
|
|
allow_nesting = True
|
|
|
|
def get_signature_prefix(self, sig: str) -> str:
|
|
if 'final' in self.options:
|
|
return 'final %s ' % self.objtype
|
|
else:
|
|
return '%s ' % self.objtype
|
|
|
|
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 PyClassmember(PyObject):
|
|
"""
|
|
Description of a class member (methods, attributes).
|
|
"""
|
|
|
|
def run(self) -> List[Node]:
|
|
for cls in self.__class__.__mro__:
|
|
if cls.__name__ != 'DirectiveAdapter':
|
|
warnings.warn('PyClassmember is deprecated. '
|
|
'Please check the implementation of %s' % cls,
|
|
RemovedInSphinx40Warning, stacklevel=2)
|
|
break
|
|
else:
|
|
warnings.warn('PyClassmember is deprecated',
|
|
RemovedInSphinx40Warning, stacklevel=2)
|
|
|
|
return super().run()
|
|
|
|
def needs_arglist(self) -> bool:
|
|
return self.objtype.endswith('method')
|
|
|
|
def get_signature_prefix(self, sig: str) -> str:
|
|
if self.objtype == 'staticmethod':
|
|
return 'static '
|
|
elif self.objtype == 'classmethod':
|
|
return 'classmethod '
|
|
return ''
|
|
|
|
def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
|
|
name, cls = name_cls
|
|
add_modules = self.env.config.add_module_names
|
|
if self.objtype == 'method':
|
|
try:
|
|
clsname, methname = name.rsplit('.', 1)
|
|
except ValueError:
|
|
if modname:
|
|
return _('%s() (in module %s)') % (name, modname)
|
|
else:
|
|
return '%s()' % name
|
|
if modname and add_modules:
|
|
return _('%s() (%s.%s method)') % (methname, modname, clsname)
|
|
else:
|
|
return _('%s() (%s method)') % (methname, clsname)
|
|
elif self.objtype == 'staticmethod':
|
|
try:
|
|
clsname, methname = name.rsplit('.', 1)
|
|
except ValueError:
|
|
if modname:
|
|
return _('%s() (in module %s)') % (name, modname)
|
|
else:
|
|
return '%s()' % name
|
|
if modname and add_modules:
|
|
return _('%s() (%s.%s static method)') % (methname, modname,
|
|
clsname)
|
|
else:
|
|
return _('%s() (%s static method)') % (methname, clsname)
|
|
elif self.objtype == 'classmethod':
|
|
try:
|
|
clsname, methname = name.rsplit('.', 1)
|
|
except ValueError:
|
|
if modname:
|
|
return _('%s() (in module %s)') % (name, modname)
|
|
else:
|
|
return '%s()' % name
|
|
if modname:
|
|
return _('%s() (%s.%s class method)') % (methname, modname,
|
|
clsname)
|
|
else:
|
|
return _('%s() (%s class method)') % (methname, clsname)
|
|
elif self.objtype == 'attribute':
|
|
try:
|
|
clsname, attrname = name.rsplit('.', 1)
|
|
except ValueError:
|
|
if modname:
|
|
return _('%s (in module %s)') % (name, modname)
|
|
else:
|
|
return name
|
|
if modname and add_modules:
|
|
return _('%s (%s.%s attribute)') % (attrname, modname, clsname)
|
|
else:
|
|
return _('%s (%s attribute)') % (attrname, clsname)
|
|
else:
|
|
return ''
|
|
|
|
|
|
class PyMethod(PyObject):
|
|
"""Description of a method."""
|
|
|
|
option_spec = PyObject.option_spec.copy()
|
|
option_spec.update({
|
|
'abstractmethod': directives.flag,
|
|
'async': directives.flag,
|
|
'classmethod': directives.flag,
|
|
'final': directives.flag,
|
|
'property': directives.flag,
|
|
'staticmethod': directives.flag,
|
|
})
|
|
|
|
def needs_arglist(self) -> bool:
|
|
if 'property' in self.options:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
def get_signature_prefix(self, sig: str) -> str:
|
|
prefix = []
|
|
if 'final' in self.options:
|
|
prefix.append('final')
|
|
if 'abstractmethod' in self.options:
|
|
prefix.append('abstract')
|
|
if 'async' in self.options:
|
|
prefix.append('async')
|
|
if 'classmethod' in self.options:
|
|
prefix.append('classmethod')
|
|
if 'property' in self.options:
|
|
prefix.append('property')
|
|
if 'staticmethod' in self.options:
|
|
prefix.append('static')
|
|
|
|
if prefix:
|
|
return ' '.join(prefix) + ' '
|
|
else:
|
|
return ''
|
|
|
|
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.env.config.add_module_names:
|
|
clsname = '.'.join([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 'property' in self.options:
|
|
return _('%s() (%s property)') % (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 = 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 = 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 = 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:
|
|
signode += addnodes.desc_annotation(typ, '', nodes.Text(': '), type_to_xref(typ))
|
|
|
|
value = self.options.get('value')
|
|
if value:
|
|
signode += addnodes.desc_annotation(value, ' = ' + 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.env.config.add_module_names:
|
|
clsname = '.'.join([modname, clsname])
|
|
except ValueError:
|
|
if modname:
|
|
return _('%s (in module %s)') % (name, modname)
|
|
else:
|
|
return name
|
|
|
|
return _('%s (%s attribute)') % (attrname, clsname)
|
|
|
|
|
|
class PyDecoratorMixin:
|
|
"""
|
|
Mixin for decorator directives.
|
|
"""
|
|
def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]:
|
|
for cls in self.__class__.__mro__:
|
|
if cls.__name__ != 'DirectiveAdapter':
|
|
warnings.warn('PyDecoratorMixin is deprecated. '
|
|
'Please check the implementation of %s' % cls,
|
|
RemovedInSphinx50Warning, stacklevel=2)
|
|
break
|
|
else:
|
|
warnings.warn('PyDecoratorMixin is deprecated',
|
|
RemovedInSphinx50Warning, stacklevel=2)
|
|
|
|
ret = super().handle_signature(sig, signode) # type: ignore
|
|
signode.insert(0, addnodes.desc_addname('@', '@'))
|
|
return ret
|
|
|
|
def needs_arglist(self) -> bool:
|
|
return False
|
|
|
|
|
|
class PyModule(SphinxDirective):
|
|
"""
|
|
Directive to mark description of a new module.
|
|
"""
|
|
|
|
has_content = False
|
|
required_arguments = 1
|
|
optional_arguments = 0
|
|
final_argument_whitespace = False
|
|
option_spec = {
|
|
'platform': lambda x: x,
|
|
'synopsis': lambda x: x,
|
|
'noindex': directives.flag,
|
|
'deprecated': directives.flag,
|
|
}
|
|
|
|
def run(self) -> List[Node]:
|
|
domain = cast(PythonDomain, self.env.get_domain('py'))
|
|
|
|
modname = self.arguments[0].strip()
|
|
noindex = 'noindex' in self.options
|
|
self.env.ref_context['py:module'] = modname
|
|
ret = [] # type: List[Node]
|
|
if not noindex:
|
|
# 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)
|
|
|
|
# Assign old styled node_id not to break old hyperlinks (if possible)
|
|
# Note: Will removed in Sphinx-5.0 (RemovedInSphinx50Warning)
|
|
old_node_id = self.make_old_id(modname)
|
|
if node_id != old_node_id and old_node_id not in self.state.document.ids:
|
|
target['ids'].append(old_node_id)
|
|
|
|
self.state.document.note_explicit_target(target)
|
|
|
|
domain.note_module(modname,
|
|
node_id,
|
|
self.options.get('synopsis', ''),
|
|
self.options.get('platform', ''),
|
|
'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
|
|
ret.append(target)
|
|
indextext = '%s; %s' % (pairindextypes['module'], modname)
|
|
inode = addnodes.index(entries=[('pair', indextext, node_id, '', None)])
|
|
ret.append(inode)
|
|
return ret
|
|
|
|
def make_old_id(self, name: str) -> str:
|
|
"""Generate old styled node_id.
|
|
|
|
Old styled node_id is incompatible with docutils' node_id.
|
|
It can contain dots and hyphens.
|
|
|
|
.. note:: Old styled node_id was mainly used until Sphinx-3.0.
|
|
"""
|
|
return 'module-%s' % name
|
|
|
|
|
|
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 = {} # type: Dict
|
|
|
|
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)
|
|
for field in fields:
|
|
field_name = cast(nodes.field_body, field[0]).astext().strip()
|
|
if field_name == 'meta' or field_name.startswith('meta '):
|
|
node.remove(field)
|
|
break
|
|
|
|
|
|
class PythonModuleIndex(Index):
|
|
"""
|
|
Index subclass to provide the Python module index.
|
|
"""
|
|
|
|
name = 'modindex'
|
|
localname = _('Python Module Index')
|
|
shortname = _('modules')
|
|
|
|
def generate(self, docnames: Iterable[str] = None
|
|
) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
|
|
content = {} # type: Dict[str, List[IndexEntry]]
|
|
# list of prefixes to ignore
|
|
ignores = None # type: List[str]
|
|
ignores = self.domain.env.config['modindex_common_prefix'] # type: ignore
|
|
ignores = sorted(ignores, key=len, reverse=True)
|
|
# list of all modules, sorted by module name
|
|
modules = sorted(self.domain.data['modules'].items(),
|
|
key=lambda x: x[0].lower())
|
|
# sort out collapsable modules
|
|
prev_modname = ''
|
|
num_toplevels = 0
|
|
for modname, (docname, node_id, synopsis, platforms, deprecated) in modules:
|
|
if docnames and docname not in docnames:
|
|
continue
|
|
|
|
for ignore in ignores:
|
|
if modname.startswith(ignore):
|
|
modname = modname[len(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('.')[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(last[0], 1, last[2], last[3],
|
|
last[4], last[5], last[6])
|
|
elif not prev_modname.startswith(package):
|
|
# submodule without parent in list, add dummy entry
|
|
entries.append(IndexEntry(stripped + package, 1, '', '', '', '', ''))
|
|
subtype = 2
|
|
else:
|
|
num_toplevels += 1
|
|
subtype = 0
|
|
|
|
qualifier = _('Deprecated') if deprecated else ''
|
|
entries.append(IndexEntry(stripped + modname, subtype, docname,
|
|
node_id, platforms, qualifier, synopsis))
|
|
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_toplevels < num_toplevels
|
|
|
|
# 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'),
|
|
'module': ObjType(_('module'), 'mod', 'obj'),
|
|
} # type: Dict[str, ObjType]
|
|
|
|
directives = {
|
|
'function': PyFunction,
|
|
'data': PyVariable,
|
|
'class': PyClasslike,
|
|
'exception': PyClasslike,
|
|
'method': PyMethod,
|
|
'classmethod': PyClassMethod,
|
|
'staticmethod': PyStaticMethod,
|
|
'attribute': PyAttribute,
|
|
'module': PyModule,
|
|
'currentmodule': PyCurrentModule,
|
|
'decorator': PyDecoratorFunction,
|
|
'decoratormethod': PyDecoratorMethod,
|
|
}
|
|
roles = {
|
|
'data': PyXRefRole(),
|
|
'exc': PyXRefRole(),
|
|
'func': PyXRefRole(fix_parens=True),
|
|
'class': PyXRefRole(),
|
|
'const': PyXRefRole(),
|
|
'attr': PyXRefRole(),
|
|
'meth': PyXRefRole(fix_parens=True),
|
|
'mod': PyXRefRole(),
|
|
'obj': PyXRefRole(),
|
|
}
|
|
initial_data = {
|
|
'objects': {}, # fullname -> docname, objtype
|
|
'modules': {}, # modname -> docname, synopsis, platform, deprecated
|
|
} # type: Dict[str, Dict[str, Tuple[Any]]]
|
|
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, location: Any = None) -> None:
|
|
"""Note a python object for cross reference.
|
|
|
|
.. versionadded:: 2.1
|
|
"""
|
|
if name in self.objects:
|
|
other = self.objects[name]
|
|
logger.warning(__('duplicate object description of %s, '
|
|
'other instance in %s, use :noindex: for one of them'),
|
|
name, other.docname, location=location)
|
|
self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype)
|
|
|
|
@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(self.env.docname, node_id,
|
|
synopsis, platform, deprecated)
|
|
|
|
def clear_doc(self, docname: str) -> None:
|
|
for fullname, obj in list(self.objects.items()):
|
|
if obj.docname == docname:
|
|
del self.objects[fullname]
|
|
for modname, mod in list(self.modules.items()):
|
|
if mod.docname == docname:
|
|
del self.modules[modname]
|
|
|
|
def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> 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, 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
|
|
if name[-2:] == '()':
|
|
name = name[:-2]
|
|
|
|
if not name:
|
|
return []
|
|
|
|
matches = [] # type: List[Tuple[str, ObjectEntry]]
|
|
|
|
newname = None
|
|
if searchmode == 1:
|
|
if type is None:
|
|
objtypes = 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 modname + '.' + name in self.objects and \
|
|
self.objects[modname + '.' + name].objtype in objtypes:
|
|
newname = modname + '.' + name
|
|
elif name in self.objects and self.objects[name].objtype in objtypes:
|
|
newname = name
|
|
else:
|
|
# "fuzzy" searching mode
|
|
searchname = '.' + 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
|
|
) -> Element:
|
|
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)
|
|
matches = self.find_obj(env, modname, clsname, target, 'meth', searchmode)
|
|
|
|
if not matches:
|
|
return None
|
|
elif len(matches) > 1:
|
|
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:
|
|
return make_refnode(builder, fromdocname, obj[0], obj[1], contnode, name)
|
|
|
|
def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
|
|
target: str, node: pending_xref, contnode: Element
|
|
) -> List[Tuple[str, Element]]:
|
|
modname = node.get('py:module')
|
|
clsname = node.get('py:class')
|
|
results = [] # type: List[Tuple[str, Element]]
|
|
|
|
# always search in "refspecific" mode with the :any: role
|
|
matches = self.find_obj(env, modname, clsname, target, None, 1)
|
|
for name, obj in matches:
|
|
if obj[2] == 'module':
|
|
results.append(('py:mod',
|
|
self._make_module_refnode(builder, fromdocname,
|
|
name, contnode)))
|
|
else:
|
|
results.append(('py:' + self.role_for_objtype(obj[2]),
|
|
make_refnode(builder, fromdocname, obj[0], obj[1],
|
|
contnode, name)))
|
|
return results
|
|
|
|
def _make_module_refnode(self, builder: Builder, fromdocname: str, name: str,
|
|
contnode: Node) -> Element:
|
|
# get additional info for modules
|
|
module = self.modules[name]
|
|
title = name
|
|
if module.synopsis:
|
|
title += ': ' + module.synopsis
|
|
if module.deprecated:
|
|
title += _(' (deprecated)')
|
|
if module.platform:
|
|
title += ' (' + module.platform + ')'
|
|
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
|
|
yield (refname, refname, obj.objtype, obj.docname, obj.node_id, 1)
|
|
|
|
def get_full_qualified_name(self, node: Element) -> str:
|
|
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:
|
|
"""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__ # type: ignore
|
|
|
|
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', 'exc'):
|
|
reftarget = node.get('reftarget')
|
|
if inspect.isclass(getattr(builtins, reftarget, None)):
|
|
# built-in class
|
|
return contnode
|
|
elif istyping(reftarget):
|
|
# typing class
|
|
return contnode
|
|
|
|
return None
|
|
|
|
|
|
def setup(app: Sphinx) -> Dict[str, Any]:
|
|
app.setup_extension('sphinx.directives')
|
|
|
|
app.add_domain(PythonDomain)
|
|
app.connect('object-description-transform', filter_meta_fields)
|
|
app.connect('missing-reference', builtin_resolver, priority=900)
|
|
|
|
return {
|
|
'version': 'builtin',
|
|
'env_version': 2,
|
|
'parallel_read_safe': True,
|
|
'parallel_write_safe': True,
|
|
}
|