From cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 15 Apr 2024 19:25:40 +0200 Subject: Adding upstream version 7.2.6. Signed-off-by: Daniel Baumann --- sphinx/domains/python.py | 1769 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1769 insertions(+) create mode 100644 sphinx/domains/python.py (limited to 'sphinx/domains/python.py') diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py new file mode 100644 index 0000000..930f8e7 --- /dev/null +++ b/sphinx/domains/python.py @@ -0,0 +1,1769 @@ +"""The Python domain.""" + +from __future__ import annotations + +import ast +import builtins +import contextlib +import inspect +import re +import token +import typing +from inspect import Parameter +from typing import TYPE_CHECKING, Any, NamedTuple, cast + +from docutils import nodes +from docutils.parsers.rst import directives + +from sphinx import addnodes +from sphinx.addnodes import desc_signature, pending_xref, pending_xref_condition +from sphinx.directives import ObjectDescription +from sphinx.domains import Domain, Index, IndexEntry, ObjType +from sphinx.locale import _, __ +from sphinx.pycode.parser import Token, TokenProcessor +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 ( + find_pending_xref_condition, + make_id, + make_refnode, + nested_parse_with_titles, +) + +if TYPE_CHECKING: + from collections.abc import Iterable, Iterator + + from docutils.nodes import Element, Node + from docutils.parsers.rst.states import Inliner + + from sphinx.application import Sphinx + from sphinx.builders import Builder + from sphinx.environment import BuildEnvironment + from sphinx.util.typing import OptionSpec, TextlikeNode + +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: type parameters list + (?: \(\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', +} + + +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 + + +def parse_reftarget(reftarget: str, suppress_prefix: bool = False, + ) -> tuple[str, str, str, bool]: + """Parse a type string and return (reftype, reftarget, title, refspecific flag)""" + refspecific = False + if reftarget.startswith('.'): + reftarget = reftarget[1:] + title = reftarget + refspecific = True + elif reftarget.startswith('~'): + reftarget = reftarget[1:] + title = reftarget.split('.')[-1] + elif suppress_prefix: + title = reftarget.split('.')[-1] + elif reftarget.startswith('typing.'): + title = reftarget[7:] + else: + title = reftarget + + if reftarget == 'None' or reftarget.startswith('typing.'): + # typing module provides non-class types. Obj reference is good to refer them. + reftype = 'obj' + else: + reftype = 'class' + + return reftype, reftarget, title, refspecific + + +def type_to_xref(target: str, env: BuildEnvironment, *, + suppress_prefix: bool = False) -> addnodes.pending_xref: + """Convert a type string to a cross reference node.""" + if env: + kwargs = {'py:module': env.ref_context.get('py:module'), + 'py:class': env.ref_context.get('py:class')} + else: + kwargs = {} + + reftype, target, title, refspecific = parse_reftarget(target, suppress_prefix) + + if env.config.python_use_unqualified_type_names: + # Note: It would be better to use qualname to describe the object to support support + # nested classes. But python domain can't access the real python object because this + # module should work not-dynamically. + shortname = title.split('.')[-1] + contnodes: list[Node] = [pending_xref_condition('', shortname, condition='resolved'), + pending_xref_condition('', title, condition='*')] + else: + contnodes = [nodes.Text(title)] + + return pending_xref('', *contnodes, + refdomain='py', reftype=reftype, reftarget=target, + refspecific=refspecific, **kwargs) + + +def _parse_annotation(annotation: str, env: BuildEnvironment) -> list[Node]: + """Parse type annotation.""" + short_literals = env.config.python_display_short_literal_types + + def unparse(node: ast.AST) -> list[Node]: + if isinstance(node, ast.Attribute): + return [nodes.Text(f"{unparse(node.value)[0]}.{node.attr}")] + if isinstance(node, ast.BinOp): + result: list[Node] = unparse(node.left) + result.extend(unparse(node.op)) + result.extend(unparse(node.right)) + return result + if isinstance(node, ast.BitOr): + return [addnodes.desc_sig_space(), + addnodes.desc_sig_punctuation('', '|'), + addnodes.desc_sig_space()] + if isinstance(node, ast.Constant): + if node.value is Ellipsis: + return [addnodes.desc_sig_punctuation('', "...")] + if isinstance(node.value, bool): + return [addnodes.desc_sig_keyword('', repr(node.value))] + if isinstance(node.value, int): + return [addnodes.desc_sig_literal_number('', repr(node.value))] + if isinstance(node.value, str): + return [addnodes.desc_sig_literal_string('', repr(node.value))] + else: + # handles None, which is further handled by type_to_xref later + # and fallback for other types that should be converted + return [nodes.Text(repr(node.value))] + if isinstance(node, ast.Expr): + return unparse(node.value) + if isinstance(node, ast.Invert): + return [addnodes.desc_sig_punctuation('', '~')] + if isinstance(node, ast.List): + result = [addnodes.desc_sig_punctuation('', '[')] + if node.elts: + # check if there are elements in node.elts to only pop the + # last element of result if the for-loop was run at least + # once + for elem in node.elts: + result.extend(unparse(elem)) + result.append(addnodes.desc_sig_punctuation('', ',')) + result.append(addnodes.desc_sig_space()) + result.pop() + result.pop() + result.append(addnodes.desc_sig_punctuation('', ']')) + return result + if isinstance(node, ast.Module): + return sum((unparse(e) for e in node.body), []) + if isinstance(node, ast.Name): + return [nodes.Text(node.id)] + if isinstance(node, ast.Subscript): + if getattr(node.value, 'id', '') in {'Optional', 'Union'}: + return _unparse_pep_604_annotation(node) + if short_literals and getattr(node.value, 'id', '') == 'Literal': + return _unparse_pep_604_annotation(node) + result = unparse(node.value) + result.append(addnodes.desc_sig_punctuation('', '[')) + result.extend(unparse(node.slice)) + result.append(addnodes.desc_sig_punctuation('', ']')) + + # Wrap the Text nodes inside brackets by literal node if the subscript is a Literal + if result[0] in ('Literal', 'typing.Literal'): + for i, subnode in enumerate(result[1:], start=1): + if isinstance(subnode, nodes.Text): + result[i] = nodes.literal('', '', subnode) + return result + if isinstance(node, ast.UnaryOp): + return unparse(node.op) + unparse(node.operand) + if isinstance(node, ast.Tuple): + if node.elts: + result = [] + for elem in node.elts: + result.extend(unparse(elem)) + result.append(addnodes.desc_sig_punctuation('', ',')) + result.append(addnodes.desc_sig_space()) + result.pop() + result.pop() + else: + result = [addnodes.desc_sig_punctuation('', '('), + addnodes.desc_sig_punctuation('', ')')] + + return result + raise SyntaxError # unsupported syntax + + def _unparse_pep_604_annotation(node: ast.Subscript) -> list[Node]: + subscript = node.slice + + flattened: list[Node] = [] + if isinstance(subscript, ast.Tuple): + flattened.extend(unparse(subscript.elts[0])) + for elt in subscript.elts[1:]: + flattened.extend(unparse(ast.BitOr())) + flattened.extend(unparse(elt)) + else: + # e.g. a Union[] inside an Optional[] + flattened.extend(unparse(subscript)) + + if getattr(node.value, 'id', '') == 'Optional': + flattened.extend(unparse(ast.BitOr())) + flattened.append(nodes.Text('None')) + + return flattened + + try: + tree = ast.parse(annotation, type_comments=True) + result: list[Node] = [] + for node in unparse(tree): + if isinstance(node, nodes.literal): + result.append(node[0]) + elif isinstance(node, nodes.Text) and node.strip(): + if (result and isinstance(result[-1], addnodes.desc_sig_punctuation) and + result[-1].astext() == '~'): + result.pop() + result.append(type_to_xref(str(node), env, suppress_prefix=True)) + else: + result.append(type_to_xref(str(node), env)) + else: + result.append(node) + return result + except SyntaxError: + return [type_to_xref(annotation, env)] + + +class _TypeParameterListParser(TokenProcessor): + def __init__(self, sig: str) -> None: + signature = sig.replace('\n', '').strip() + super().__init__([signature]) + # Each item is a tuple (name, kind, default, annotation) mimicking + # ``inspect.Parameter`` to allow default values on VAR_POSITIONAL + # or VAR_KEYWORD parameters. + self.type_params: list[tuple[str, int, Any, Any]] = [] + + def fetch_type_param_spec(self) -> list[Token]: + tokens = [] + while current := self.fetch_token(): + tokens.append(current) + for ldelim, rdelim in ('(', ')'), ('{', '}'), ('[', ']'): + if current == [token.OP, ldelim]: + tokens += self.fetch_until([token.OP, rdelim]) + break + else: + if current == token.INDENT: + tokens += self.fetch_until(token.DEDENT) + elif current.match( + [token.OP, ':'], [token.OP, '='], [token.OP, ',']): + tokens.pop() + break + return tokens + + def parse(self) -> None: + while current := self.fetch_token(): + if current == token.NAME: + tp_name = current.value.strip() + if self.previous and self.previous.match([token.OP, '*'], [token.OP, '**']): + if self.previous == [token.OP, '*']: + tp_kind = Parameter.VAR_POSITIONAL + else: + tp_kind = Parameter.VAR_KEYWORD # type: ignore[assignment] + else: + tp_kind = Parameter.POSITIONAL_OR_KEYWORD # type: ignore[assignment] + + tp_ann: Any = Parameter.empty + tp_default: Any = Parameter.empty + + current = self.fetch_token() + if current and current.match([token.OP, ':'], [token.OP, '=']): + if current == [token.OP, ':']: + tokens = self.fetch_type_param_spec() + tp_ann = self._build_identifier(tokens) + + if self.current and self.current == [token.OP, '=']: + tokens = self.fetch_type_param_spec() + tp_default = self._build_identifier(tokens) + + if tp_kind != Parameter.POSITIONAL_OR_KEYWORD and tp_ann != Parameter.empty: + msg = ('type parameter bound or constraint is not allowed ' + f'for {tp_kind.description} parameters') + raise SyntaxError(msg) + + type_param = (tp_name, tp_kind, tp_default, tp_ann) + self.type_params.append(type_param) + + def _build_identifier(self, tokens: list[Token]) -> str: + from itertools import chain, tee + + def pairwise(iterable): + a, b = tee(iterable) + next(b, None) + return zip(a, b) + + def triplewise(iterable): + for (a, _z), (b, c) in pairwise(pairwise(iterable)): + yield a, b, c + + idents: list[str] = [] + tokens: Iterable[Token] = iter(tokens) # type: ignore[no-redef] + # do not format opening brackets + for tok in tokens: + if not tok.match([token.OP, '('], [token.OP, '['], [token.OP, '{']): + # check if the first non-delimiter character is an unpack operator + is_unpack_operator = tok.match([token.OP, '*'], [token.OP, ['**']]) + idents.append(self._pformat_token(tok, native=is_unpack_operator)) + break + idents.append(tok.value) + + # check the remaining tokens + stop = Token(token.ENDMARKER, '', (-1, -1), (-1, -1), '') + is_unpack_operator = False + for tok, op, after in triplewise(chain(tokens, [stop, stop])): + ident = self._pformat_token(tok, native=is_unpack_operator) + idents.append(ident) + # determine if the next token is an unpack operator depending + # on the left and right hand side of the operator symbol + is_unpack_operator = ( + op.match([token.OP, '*'], [token.OP, '**']) and not ( + tok.match(token.NAME, token.NUMBER, token.STRING, + [token.OP, ')'], [token.OP, ']'], [token.OP, '}']) + and after.match(token.NAME, token.NUMBER, token.STRING, + [token.OP, '('], [token.OP, '['], [token.OP, '{']) + ) + ) + + return ''.join(idents).strip() + + def _pformat_token(self, tok: Token, native: bool = False) -> str: + if native: + return tok.value + + if tok.match(token.NEWLINE, token.ENDMARKER): + return '' + + if tok.match([token.OP, ':'], [token.OP, ','], [token.OP, '#']): + return f'{tok.value} ' + + # Arithmetic operators are allowed because PEP 695 specifies the + # default type parameter to be *any* expression (so "T1 << T2" is + # allowed if it makes sense). The caller is responsible to ensure + # that a multiplication operator ("*") is not to be confused with + # an unpack operator (which will not be surrounded by spaces). + # + # The operators are ordered according to how likely they are to + # be used and for (possible) future implementations (e.g., "&" for + # an intersection type). + if tok.match( + # Most likely operators to appear + [token.OP, '='], [token.OP, '|'], + # Type composition (future compatibility) + [token.OP, '&'], [token.OP, '^'], [token.OP, '<'], [token.OP, '>'], + # Unlikely type composition + [token.OP, '+'], [token.OP, '-'], [token.OP, '*'], [token.OP, '**'], + # Unlikely operators but included for completeness + [token.OP, '@'], [token.OP, '/'], [token.OP, '//'], [token.OP, '%'], + [token.OP, '<<'], [token.OP, '>>'], [token.OP, '>>>'], + [token.OP, '<='], [token.OP, '>='], [token.OP, '=='], [token.OP, '!='], + ): + return f' {tok.value} ' + + return tok.value + + +def _parse_type_list( + tp_list: str, env: BuildEnvironment, + multi_line_parameter_list: bool = False, +) -> addnodes.desc_type_parameter_list: + """Parse a list of type parameters according to PEP 695.""" + type_params = addnodes.desc_type_parameter_list(tp_list) + type_params['multi_line_parameter_list'] = multi_line_parameter_list + # formal parameter names are interpreted as type parameter names and + # type annotations are interpreted as type parameter bound or constraints + parser = _TypeParameterListParser(tp_list) + parser.parse() + for (tp_name, tp_kind, tp_default, tp_ann) in parser.type_params: + # no positional-only or keyword-only allowed in a type parameters list + if tp_kind in {Parameter.POSITIONAL_ONLY, Parameter.KEYWORD_ONLY}: + msg = ('positional-only or keyword-only parameters ' + 'are prohibited in type parameter lists') + raise SyntaxError(msg) + + node = addnodes.desc_type_parameter() + if tp_kind == Parameter.VAR_POSITIONAL: + node += addnodes.desc_sig_operator('', '*') + elif tp_kind == Parameter.VAR_KEYWORD: + node += addnodes.desc_sig_operator('', '**') + node += addnodes.desc_sig_name('', tp_name) + + if tp_ann is not Parameter.empty: + annotation = _parse_annotation(tp_ann, env) + if not annotation: + continue + + node += addnodes.desc_sig_punctuation('', ':') + node += addnodes.desc_sig_space() + + type_ann_expr = addnodes.desc_sig_name('', '', + *annotation) # type: ignore[arg-type] + # a type bound is ``T: U`` whereas type constraints + # must be enclosed with parentheses. ``T: (U, V)`` + if tp_ann.startswith('(') and tp_ann.endswith(')'): + type_ann_text = type_ann_expr.astext() + if type_ann_text.startswith('(') and type_ann_text.endswith(')'): + node += type_ann_expr + else: + # surrounding braces are lost when using _parse_annotation() + node += addnodes.desc_sig_punctuation('', '(') + node += type_ann_expr # type constraint + node += addnodes.desc_sig_punctuation('', ')') + else: + node += type_ann_expr # type bound + + if tp_default is not Parameter.empty: + # Always surround '=' with spaces, even if there is no annotation + node += addnodes.desc_sig_space() + node += addnodes.desc_sig_operator('', '=') + node += addnodes.desc_sig_space() + node += nodes.inline('', tp_default, + classes=['default_value'], + support_smartquotes=False) + + type_params += node + return type_params + + +def _parse_arglist( + arglist: str, env: BuildEnvironment, multi_line_parameter_list: bool = False, +) -> addnodes.desc_parameterlist: + """Parse a list of arguments using AST parser""" + params = addnodes.desc_parameterlist(arglist) + params['multi_line_parameter_list'] = multi_line_parameter_list + 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, env) + node += addnodes.desc_sig_punctuation('', ':') + node += addnodes.desc_sig_space() + node += addnodes.desc_sig_name('', '', *children) # type: ignore[arg-type] + if param.default is not param.empty: + if param.annotation is not param.empty: + node += addnodes.desc_sig_space() + node += addnodes.desc_sig_operator('', '=') + node += addnodes.desc_sig_space() + 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, multi_line_parameter_list: bool = False, +) -> 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() + paramlist['multi_line_parameter_list'] = multi_line_parameter_list + stack: list[Element] = [paramlist] + 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( + '', '', addnodes.desc_sig_name(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 = None, + env: BuildEnvironment | None = None, + inliner: Inliner | None = None, + location: Node | None = None, + ) -> Node: + # we use inliner=None to make sure we get the old behaviour with a single + # pending_xref node + result = super().make_xref(rolename, domain, target, # type: ignore[misc] + innernode, contnode, + env, inliner=None, location=None) + if isinstance(result, pending_xref): + assert env is not None + result['refspecific'] = True + result['py:module'] = env.ref_context.get('py:module') + result['py:class'] = env.ref_context.get('py:class') + + reftype, reftarget, reftitle, _ = parse_reftarget(target) + if reftarget != reftitle: + result['reftype'] = reftype + result['reftarget'] = reftarget + + result.clear() + result += innernode(reftitle, reftitle) + elif env.config.python_use_unqualified_type_names: + children = result.children + result.clear() + + shortname = target.split('.')[-1] + textnode = innernode('', shortname) + contnodes = [pending_xref_condition('', '', textnode, condition='resolved'), + pending_xref_condition('', '', *children, condition='*')] + result.extend(contnodes) + + return result + + def make_xrefs( + self, + rolename: str, + domain: str, + target: str, + innernode: type[TextlikeNode] = nodes.emphasis, + contnode: Node | None = None, + env: BuildEnvironment | None = None, + inliner: Inliner | None = None, + location: Node | None = None, + ) -> list[Node]: + delims = r'(\s*[\[\]\(\),](?:\s*o[rf]\s)?\s*|\s+o[rf]\s+|\s*\|\s*|\.\.\.)' + delims_re = re.compile(delims) + sub_targets = re.split(delims, target) + + split_contnode = bool(contnode and contnode.astext() == target) + + in_literal = False + results = [] + for sub_target in filter(None, sub_targets): + if split_contnode: + contnode = nodes.Text(sub_target) + + if in_literal or 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, inliner, location)) + + if sub_target in ('Literal', 'typing.Literal', '~typing.Literal'): + in_literal = True + + return results + + +class PyField(PyXrefMixin, Field): + pass + + +class PyGroupedField(PyXrefMixin, GroupedField): + pass + + +class PyTypedField(PyXrefMixin, TypedField): + pass + + +class PyObject(ObjectDescription[tuple[str, str]]): + """ + Description of a general Python object. + + :cvar allow_nesting: Class is an object that allows for nested namespaces + :vartype allow_nesting: bool + """ + option_spec: OptionSpec = { + 'no-index': directives.flag, + 'no-index-entry': directives.flag, + 'no-contents-entry': directives.flag, + 'no-typesetting': directives.flag, + 'noindex': directives.flag, + 'noindexentry': directives.flag, + 'nocontentsentry': directives.flag, + 'single-line-parameter-list': directives.flag, + 'single-line-type-parameter-list': directives.flag, + 'module': directives.unchanged, + 'canonical': 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'), + 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) -> list[nodes.Node]: + """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, tp_list, 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 + + max_len = (self.env.config.python_maximum_signature_line_length + or self.env.config.maximum_signature_line_length + or 0) + + # determine if the function arguments (without its type parameters) + # should be formatted on a multiline or not by removing the width of + # the type parameters list (if any) + sig_len = len(sig) + tp_list_span = m.span(3) + multi_line_parameter_list = ( + 'single-line-parameter-list' not in self.options + and (sig_len - (tp_list_span[1] - tp_list_span[0])) > max_len > 0 + ) + + # determine whether the type parameter list must be wrapped or not + arglist_span = m.span(4) + multi_line_type_parameter_list = ( + 'single-line-type-parameter-list' not in self.options + and (sig_len - (arglist_span[1] - arglist_span[0])) > max_len > 0 + ) + + sig_prefix = self.get_signature_prefix(sig) + if sig_prefix: + if type(sig_prefix) is str: + msg = ("Python directive method get_signature_prefix()" + " must return a list of nodes." + f" Return value was '{sig_prefix}'.") + raise TypeError(msg) + signode += addnodes.desc_annotation(str(sig_prefix), '', *sig_prefix) + + if prefix: + signode += addnodes.desc_addname(prefix, prefix) + elif modname and add_module and self.env.config.add_module_names: + nodetext = modname + '.' + signode += addnodes.desc_addname(nodetext, nodetext) + + signode += addnodes.desc_name(name, name) + + if tp_list: + try: + signode += _parse_type_list(tp_list, self.env, multi_line_type_parameter_list) + except Exception as exc: + logger.warning("could not parse tp_list (%r): %s", tp_list, exc, + location=signode) + + if arglist: + try: + signode += _parse_arglist(arglist, self.env, multi_line_parameter_list) + except SyntaxError: + # fallback to parse arglist original parser + # (this may happen if the argument list is incorrectly used + # as a list of bases when documenting a class) + # it supports to represent optional arguments (ex. "func(foo [, bar])") + _pseudo_parse_arglist(signode, arglist, multi_line_parameter_list) + except (NotImplementedError, ValueError) as exc: + # duplicated parameter names raise ValueError and not a SyntaxError + logger.warning("could not parse arglist (%r): %s", arglist, exc, + location=signode) + _pseudo_parse_arglist(signode, arglist, multi_line_parameter_list) + else: + if self.needs_arglist(): + # for callables, add an empty parameter list + signode += addnodes.desc_parameterlist() + + if retann: + children = _parse_annotation(retann, self.env) + signode += addnodes.desc_returns(retann, '', *children) + + anno = self.options.get('annotation') + if anno: + signode += addnodes.desc_annotation(' ' + anno, '', + addnodes.desc_sig_space(), + nodes.Text(anno)) + + return fullname, prefix + + def _object_hierarchy_parts(self, sig_node: desc_signature) -> tuple[str, ...]: + if 'fullname' not in sig_node: + return () + modname = sig_node.get('module') + fullname = sig_node['fullname'] + + if modname: + return (modname, *fullname.split('.')) + else: + return tuple(fullname.split('.')) + + def get_index_text(self, modname: str, name: tuple[str, str]) -> str: + """Return the text for the index entry of the object.""" + msg = 'must be implemented in subclasses' + raise NotImplementedError(msg) + + 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) + 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) + + canonical_name = self.options.get('canonical') + if canonical_name: + domain.note_object(canonical_name, self.objtype, node_id, aliased=True, + location=signode) + + if 'no-index-entry' not in self.options: + 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 hierarchy 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: + with contextlib.suppress(IndexError): + classes.pop() + + 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') + + def _toc_entry_name(self, sig_node: desc_signature) -> str: + if not sig_node.get('_toc_parts'): + return '' + + config = self.env.app.config + objtype = sig_node.parent.get('objtype') + if config.add_function_parentheses and objtype in {'function', 'method'}: + parens = '()' + else: + parens = '' + *parents, name = sig_node['_toc_parts'] + if config.toc_object_entries_show_parents == 'domain': + return sig_node.get('fullname', name) + parens + if config.toc_object_entries_show_parents == 'hide': + return name + parens + if config.toc_object_entries_show_parents == 'all': + return '.'.join(parents + [name + parens]) + return '' + + +class PyFunction(PyObject): + """Description of a function.""" + + option_spec: 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: 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: 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: 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.append(nodes.Text('final')) + prefix.append(addnodes.desc_sig_space()) + if 'abstractmethod' in self.options: + prefix.append(nodes.Text('abstract')) + prefix.append(addnodes.desc_sig_space()) + if 'async' in self.options: + prefix.append(nodes.Text('async')) + prefix.append(addnodes.desc_sig_space()) + if 'classmethod' in self.options: + prefix.append(nodes.Text('classmethod')) + prefix.append(addnodes.desc_sig_space()) + if 'staticmethod' in self.options: + prefix.append(nodes.Text('static')) + prefix.append(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.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 '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: 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: 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: 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.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 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.append(nodes.Text('abstract')) + prefix.append(addnodes.desc_sig_space()) + if 'classmethod' in self.options: + prefix.append(nodes.Text('class')) + prefix.append(addnodes.desc_sig_space()) + + prefix.append(nodes.Text('property')) + prefix.append(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.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 property)') % (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: 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]: + domain = cast(PythonDomain, self.env.get_domain('py')) + + modname = self.arguments[0].strip() + no_index = 'no-index' in self.options or 'noindex' in self.options + self.env.ref_context['py:module'] = modname + + content_node: Element = nodes.section() + # necessary so that the child nodes get the right source/line set + content_node.document = self.state.document + nested_parse_with_titles(self.state, self.content, content_node, self.content_offset) + + 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(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 + indextext = f'module; {modname}' + inode = addnodes.index(entries=[('pair', indextext, node_id, '', None)]) + # The node order is: index node first, then target node. + ret.append(inode) + ret.append(target) + ret.extend(content_node.children) + 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: 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') + + def generate(self, docnames: Iterable[str] | None = None, + ) -> tuple[list[tuple[str, list[IndexEntry]]], bool]: + content: dict[str, list[IndexEntry]] = {} + # list of prefixes to ignore + ignores: list[str] = self.domain.env.config['modindex_common_prefix'] + 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 collapsible 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: 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'), + 'module': ObjType(_('module'), 'mod', 'obj'), + } + + directives = { + 'function': PyFunction, + 'data': PyVariable, + 'class': PyClasslike, + 'exception': PyClasslike, + 'method': PyMethod, + 'classmethod': PyClassMethod, + 'staticmethod': PyStaticMethod, + 'attribute': PyAttribute, + 'property': PyProperty, + '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: 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(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[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 + if name[-2:] == '()': + name = name[:-2] + + 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 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 | 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, Element]]: + modname = node.get('py:module') + clsname = node.get('py:class') + results: list[tuple[str, Element]] = [] + + # 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) -> 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 + 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) -> dict[str, Any]: + 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={int, None}) + 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, + } -- cgit v1.2.3