| """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), '<sentinel>') |
| 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, |
| } |