From e48a88d05f6128cc4b535876aa15af55d2d1d8d2 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Wed, 1 Jan 2025 13:17:50 +0000 Subject: [PATCH] Enable automatic formatting for ``sphinx/domains/python/`` --- .ruff.toml | 3 - sphinx/domains/python/__init__.py | 296 +++++++++++++++++--------- sphinx/domains/python/_annotations.py | 151 ++++++++----- sphinx/domains/python/_object.py | 169 ++++++++++----- 4 files changed, 404 insertions(+), 215 deletions(-) diff --git a/.ruff.toml b/.ruff.toml index 661b7ab029c..4b5fa7913e3 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -394,8 +394,5 @@ preview = true quote-style = "single" exclude = [ "sphinx/builders/latex/constants.py", - "sphinx/domains/python/_annotations.py", - "sphinx/domains/python/__init__.py", - "sphinx/domains/python/_object.py", "sphinx/domains/std/__init__.py", ] diff --git a/sphinx/domains/python/__init__.py b/sphinx/domains/python/__init__.py index 27e4de4ebb8..a0ce2d8aa58 100644 --- a/sphinx/domains/python/__init__.py +++ b/sphinx/domains/python/__init__.py @@ -87,16 +87,19 @@ class PyFunction(PyObject): 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()] + 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: + 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')) @@ -147,17 +150,24 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str] 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) + 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)) + signode += addnodes.desc_annotation( + value, + '', + addnodes.desc_sig_space(), + addnodes.desc_sig_punctuation('', '='), + addnodes.desc_sig_space(), + nodes.Text(value), + ) return fullname, prefix @@ -183,8 +193,12 @@ class PyClasslike(PyObject): 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()] + 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()] @@ -308,18 +322,24 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str] 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) + 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)) + signode += addnodes.desc_annotation( + value, + '', + addnodes.desc_sig_space(), + addnodes.desc_sig_punctuation('', '='), + addnodes.desc_sig_space(), + nodes.Text(value), + ) return fullname, prefix @@ -354,10 +374,13 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str] 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) + signode += addnodes.desc_annotation( + typ, + '', + addnodes.desc_sig_punctuation('', ':'), + addnodes.desc_sig_space(), + *annotations, + ) return fullname, prefix @@ -405,7 +428,8 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str] if canonical := self.options.get('canonical'): canonical_annotations = _parse_annotation(canonical, self.env) signode += addnodes.desc_annotation( - canonical, '', + canonical, + '', addnodes.desc_sig_space(), addnodes.desc_sig_punctuation('', '='), addnodes.desc_sig_space(), @@ -513,12 +537,18 @@ def run(self) -> list[Node]: class PyXRefRole(XRefRole): - def process_link(self, env: BuildEnvironment, refnode: Element, - has_explicit_title: bool, title: str, target: str) -> tuple[str, str]: + 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 + 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 @@ -526,7 +556,7 @@ def process_link(self, env: BuildEnvironment, refnode: Element, title = title[1:] dot = title.rfind('.') if dot != -1: - title = title[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] == '.': @@ -535,7 +565,9 @@ def process_link(self, env: BuildEnvironment, refnode: Element, return title, target -def filter_meta_fields(app: Sphinx, domain: str, objtype: str, content: Element) -> None: +def filter_meta_fields( + app: Sphinx, domain: str, objtype: str, content: Element +) -> None: """Filter ``:meta:`` field from its docstring.""" if domain != 'py': return @@ -560,8 +592,9 @@ class PythonModuleIndex(Index): shortname = _('modules') domain: PythonDomain - def generate(self, docnames: Iterable[str] | None = None, - ) -> tuple[list[tuple[str, list[IndexEntry]]], bool]: + def generate( + self, docnames: Iterable[str] | None = None + ) -> tuple[list[tuple[str, list[IndexEntry]]], bool]: doc_names = frozenset(docnames) if docnames is not None else None content: dict[str, list[IndexEntry]] = {} @@ -657,46 +690,46 @@ class PythonDomain(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'), + 'function': ObjType(_('function'), 'func', 'obj'), + 'data': ObjType(_('data'), 'data', 'obj'), + 'class': ObjType(_('class'), 'class', 'exc', 'obj'), + 'exception': ObjType(_('exception'), 'exc', 'class', 'obj'), + 'method': ObjType(_('method'), 'meth', 'obj'), + 'classmethod': ObjType(_('class method'), 'meth', 'obj'), 'staticmethod': ObjType(_('static method'), 'meth', 'obj'), - 'attribute': ObjType(_('attribute'), 'attr', 'obj'), - 'property': ObjType(_('property'), 'attr', '_prop', 'obj'), - 'type': ObjType(_('type alias'), 'type', 'obj'), - 'module': ObjType(_('module'), 'mod', 'obj'), + 'attribute': ObjType(_('attribute'), 'attr', 'obj'), + 'property': ObjType(_('property'), 'attr', '_prop', 'obj'), + 'type': ObjType(_('type alias'), 'type', 'obj'), + 'module': ObjType(_('module'), 'mod', 'obj'), } directives = { - 'function': PyFunction, - 'data': PyVariable, - 'class': PyClasslike, - 'exception': PyClasslike, - 'method': PyMethod, - 'classmethod': PyClassMethod, - 'staticmethod': PyStaticMethod, - 'attribute': PyAttribute, - 'property': PyProperty, - 'type': PyTypeAlias, - 'module': PyModule, - 'currentmodule': PyCurrentModule, - 'decorator': PyDecoratorFunction, + 'function': PyFunction, + 'data': PyVariable, + 'class': PyClasslike, + 'exception': PyClasslike, + 'method': PyMethod, + 'classmethod': PyClassMethod, + 'staticmethod': PyStaticMethod, + 'attribute': PyAttribute, + 'property': PyProperty, + 'type': PyTypeAlias, + 'module': PyModule, + 'currentmodule': PyCurrentModule, + 'decorator': PyDecoratorFunction, 'decoratormethod': PyDecoratorMethod, } roles = { - 'data': PyXRefRole(), - 'exc': PyXRefRole(), - 'func': PyXRefRole(fix_parens=True), + 'data': PyXRefRole(), + 'exc': PyXRefRole(), + 'func': PyXRefRole(fix_parens=True), 'class': PyXRefRole(), 'const': PyXRefRole(), - 'attr': PyXRefRole(), - 'type': PyXRefRole(), - 'meth': PyXRefRole(fix_parens=True), - 'mod': PyXRefRole(), - 'obj': PyXRefRole(), + 'attr': PyXRefRole(), + 'type': PyXRefRole(), + 'meth': PyXRefRole(fix_parens=True), + 'mod': PyXRefRole(), + 'obj': PyXRefRole(), } initial_data: dict[str, dict[str, tuple[Any]]] = { 'objects': {}, # fullname -> docname, objtype @@ -710,8 +743,14 @@ class PythonDomain(Domain): 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: + 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 @@ -726,9 +765,15 @@ def note_object(self, name: str, objtype: str, node_id: str, 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) + 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 @@ -771,9 +816,15 @@ def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> Non 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]]: + 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. """ @@ -794,20 +845,31 @@ def find_obj(self, env: BuildEnvironment, modname: str, classname: str, 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: + 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: + if ( + modname + and f'{modname}.{name}' in self.objects + and self.objects[f'{modname}.{name}'].objtype in objtypes + ): + newname = f'{modname}.{name}' + elif ( + name in self.objects and self.objects[name].objtype in objtypes + ): newname = name else: # "fuzzy" searching mode - searchname = '.' + name - matches = [(oname, self.objects[oname]) for oname in self.objects - if oname.endswith(searchname) and - self.objects[oname].objtype in objtypes] + searchname = f'.{name}' + matches = [ + (oname, self.objects[oname]) + for oname in self.objects + if oname.endswith(searchname) + and self.objects[oname].objtype in objtypes + ] else: # NOTE: searching for exact match, object type is not considered if name in self.objects: @@ -819,21 +881,30 @@ def find_obj(self, env: BuildEnvironment, modname: str, classname: str, 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: + elif ( + modname + and classname + and modname + '.' + classname + '.' + name in self.objects + ): newname = modname + '.' + classname + '.' + name if newname is not None: matches.append((newname, self.objects[newname])) return matches - def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, - type: str, target: str, node: pending_xref, contnode: Element, - ) -> nodes.reference | None: + def resolve_xref( + self, + env: BuildEnvironment, + fromdocname: str, + builder: Builder, + type: str, + target: str, + node: pending_xref, + contnode: Element, + ) -> nodes.reference | None: modname = node.get('py:module') clsname = node.get('py:class') searchmode = 1 if node.hasattr('refspecific') else 0 - matches = self.find_obj(env, modname, clsname, target, - type, searchmode) + 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) @@ -855,9 +926,14 @@ def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder 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) + 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': @@ -873,9 +949,15 @@ def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder return make_refnode(builder, fromdocname, obj[0], obj[1], children, name) - def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, - target: str, node: pending_xref, contnode: Element, - ) -> list[tuple[str, nodes.reference]]: + def resolve_any_xref( + self, + env: BuildEnvironment, + fromdocname: str, + builder: Builder, + target: str, + node: pending_xref, + contnode: Element, + ) -> list[tuple[str, nodes.reference]]: modname = node.get('py:module') clsname = node.get('py:class') results: list[tuple[str, nodes.reference]] = [] @@ -885,7 +967,6 @@ def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Bui multiple_matches = len(matches) > 1 for name, obj in matches: - if multiple_matches and obj.aliased: # Skip duplicated matches continue @@ -893,7 +974,7 @@ def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Bui if obj[2] == 'module': results.append(( 'py:mod', - self._make_module_refnode(builder, fromdocname, name, contnode) + self._make_module_refnode(builder, fromdocname, name, contnode), )) else: # determine the content of the reference by conditions @@ -905,12 +986,15 @@ def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Bui 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))) + results.append(( + role, + make_refnode(builder, fromdocname, obj[0], obj[1], children, name), + )) return results - def _make_module_refnode(self, builder: Builder, fromdocname: str, name: str, - contnode: Node) -> nodes.reference: + def _make_module_refnode( + self, builder: Builder, fromdocname: str, name: str, contnode: Node + ) -> nodes.reference: # get additional info for modules module: ModuleEntry = self.modules[name] title_parts = [name] @@ -946,9 +1030,11 @@ def get_full_qualified_name(self, node: Element) -> str | None: return '.'.join(filter(None, [modname, clsname, target])) -def builtin_resolver(app: Sphinx, env: BuildEnvironment, - node: pending_xref, contnode: Element) -> Element | None: +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] @@ -977,7 +1063,7 @@ def setup(app: Sphinx) -> ExtensionMetadata: 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', {int, type(None)}, + 'python_maximum_signature_line_length', None, 'env', {int, type(None)} ) app.add_config_value('python_display_short_literal_types', False, 'env') app.connect('object-description-transform', filter_meta_fields) diff --git a/sphinx/domains/python/_annotations.py b/sphinx/domains/python/_annotations.py index 0c211ade672..767bb920b5f 100644 --- a/sphinx/domains/python/_annotations.py +++ b/sphinx/domains/python/_annotations.py @@ -23,8 +23,9 @@ from sphinx.environment import BuildEnvironment -def parse_reftarget(reftarget: str, suppress_prefix: bool = False, - ) -> tuple[str, str, str, 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('.'): @@ -50,12 +51,15 @@ def parse_reftarget(reftarget: str, suppress_prefix: bool = False, return reftype, reftarget, title, refspecific -def type_to_xref(target: str, env: BuildEnvironment, *, - suppress_prefix: bool = False) -> addnodes.pending_xref: +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')} + kwargs = { + 'py:module': env.ref_context.get('py:module'), + 'py:class': env.ref_context.get('py:class'), + } else: kwargs = {} @@ -66,14 +70,22 @@ def type_to_xref(target: str, env: BuildEnvironment, *, # 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='*')] + 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) + return pending_xref( + '', + *contnodes, + refdomain='py', + reftype=reftype, + reftarget=target, + refspecific=refspecific, + **kwargs, + ) def _parse_annotation(annotation: str, env: BuildEnvironment) -> list[Node]: @@ -82,19 +94,21 @@ def _parse_annotation(annotation: str, env: BuildEnvironment) -> list[Node]: def unparse(node: ast.AST) -> list[Node]: if isinstance(node, ast.Attribute): - return [nodes.Text(f"{unparse(node.value)[0]}.{node.attr}")] + 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()] + 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('', "...")] + return [addnodes.desc_sig_punctuation('', '...')] if isinstance(node.value, bool): return [addnodes.desc_sig_keyword('', repr(node.value))] if isinstance(node.value, int): @@ -157,8 +171,10 @@ def unparse(node: ast.AST) -> list[Node]: result.pop() result.pop() else: - result = [addnodes.desc_sig_punctuation('', '('), - addnodes.desc_sig_punctuation('', ')')] + result = [ + addnodes.desc_sig_punctuation('', '('), + addnodes.desc_sig_punctuation('', ')'), + ] return result if isinstance(node, ast.Call): @@ -211,8 +227,11 @@ def _unparse_pep_604_annotation(node: ast.Subscript) -> list[Node]: 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() == '~'): + 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: @@ -244,8 +263,7 @@ def fetch_type_param_spec(self) -> list[Token]: else: if current == token.INDENT: tokens += self.fetch_until(token.DEDENT) - elif current.match( - [token.OP, ':'], [token.OP, '='], [token.OP, ',']): + elif current.match([token.OP, ':'], [token.OP, '='], [token.OP, ',']): tokens.pop() break return tokens @@ -254,7 +272,9 @@ 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 and self.previous.match( + [token.OP, '*'], [token.OP, '**'] + ): if self.previous == [token.OP, '*']: tp_kind = Parameter.VAR_POSITIONAL else: @@ -275,9 +295,14 @@ def parse(self) -> None: 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') + 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) @@ -315,12 +340,22 @@ def triplewise(iterable: Iterable[Token]) -> Iterator[tuple[Token, ...]]: 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, '{']) + 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, '{'], ) ) @@ -356,15 +391,14 @@ def _pformat_token(self, tok: Token, native: bool = False) -> str: [token.OP, '@'], [token.OP, '/'], [token.OP, '//'], [token.OP, '%'], [token.OP, '<<'], [token.OP, '>>'], [token.OP, '>>>'], [token.OP, '<='], [token.OP, '>='], [token.OP, '=='], [token.OP, '!='], - ): + ): # fmt: skip return f' {tok.value} ' return tok.value def _parse_type_list( - tp_list: str, env: BuildEnvironment, - multi_line_parameter_list: bool = False, + 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) @@ -373,11 +407,13 @@ def _parse_type_list( # 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: + 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') + msg = ( + 'positional-only or keyword-only parameters ' + 'are prohibited in type parameter lists' + ) raise SyntaxError(msg) node = addnodes.desc_type_parameter() @@ -395,8 +431,7 @@ def _parse_type_list( node += addnodes.desc_sig_punctuation('', ':') node += addnodes.desc_sig_space() - type_ann_expr = addnodes.desc_sig_name('', '', - *annotation) # type: ignore[arg-type] + 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(')'): @@ -416,16 +451,16 @@ def _parse_type_list( 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) + 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, + 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) @@ -435,12 +470,18 @@ def _parse_arglist( 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}: + 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('', '*')) + params += addnodes.desc_parameter( + '', '', addnodes.desc_sig_operator('', '*') + ) node = addnodes.desc_parameter() if param.kind == param.VAR_POSITIONAL: @@ -464,8 +505,9 @@ def _parse_arglist( node += addnodes.desc_sig_space() else: node += addnodes.desc_sig_operator('', '=') - node += nodes.inline('', param.default, classes=['default_value'], - support_smartquotes=False) + node += nodes.inline( + '', param.default, classes=['default_value'], support_smartquotes=False + ) params += node last_kind = param.kind @@ -478,9 +520,9 @@ def _parse_arglist( def _pseudo_parse_arglist( - signode: desc_signature, arglist: str, multi_line_parameter_list: bool = False, + signode: desc_signature, arglist: str, multi_line_parameter_list: bool = False ) -> None: - """"Parse" a list of arguments separated by commas. + """'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 @@ -508,7 +550,8 @@ def _pseudo_parse_arglist( argument = argument[:-1].strip() if argument: stack[-1] += addnodes.desc_parameter( - '', '', addnodes.desc_sig_name(argument, argument)) + '', '', addnodes.desc_sig_name(argument, argument) + ) while ends_open: stack.append(addnodes.desc_optional()) stack[-2] += stack[-1] diff --git a/sphinx/domains/python/_object.py b/sphinx/domains/python/_object.py index 3e9049a1a27..f8472823657 100644 --- a/sphinx/domains/python/_object.py +++ b/sphinx/domains/python/_object.py @@ -35,13 +35,15 @@ # REs for Python signatures py_sig_re = re.compile( - r'''^ ([\w.]*\.)? # class name(s) + 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) + """, + re.VERBOSE, +) # This override allows our inline type specifiers to behave like :class: link @@ -60,9 +62,16 @@ def make_xref( ) -> 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) + result = super().make_xref( # type: ignore[misc] + rolename, + domain, + target, + innernode, + contnode, + env, + inliner=None, + location=None, + ) if isinstance(result, pending_xref): assert env is not None result['refspecific'] = True @@ -82,8 +91,10 @@ def make_xref( shortname = target.split('.')[-1] textnode = innernode('', shortname) # type: ignore[call-arg] - contnodes = [pending_xref_condition('', '', textnode, condition='resolved'), - pending_xref_condition('', '', *children, condition='*')] + contnodes = [ + pending_xref_condition('', '', textnode, condition='resolved'), + pending_xref_condition('', '', *children, condition='*'), + ] result.extend(contnodes) return result @@ -116,8 +127,18 @@ def make_xrefs( if in_literal or self._delimiters_re.match(sub_target): results.append(contnode or innernode(sub_target, sub_target)) # type: ignore[call-arg] else: - results.append(self.make_xref(rolename, domain, sub_target, - innernode, contnode, env, inliner, location)) + 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 @@ -161,22 +182,50 @@ class PyObject(ObjectDescription[tuple[str, str]]): } 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'), + 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 @@ -212,18 +261,17 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str] classname = self.env.ref_context.get('py:class') if classname: add_module = False - if prefix and (prefix == classname or - prefix.startswith(classname + ".")): + if prefix and (prefix == classname or prefix.startswith(f'{classname}.')): fullname = prefix + name # class name is given again in the signature - prefix = prefix[len(classname):].lstrip('.') + prefix = prefix[len(classname) :].lstrip('.') elif prefix: # class name is given in the signature, but different # (shouldn't happen) - fullname = classname + '.' + prefix + name + fullname = f'{classname}.{prefix}{name}' else: # class name is not given in the signature - fullname = classname + '.' + name + fullname = f'{classname}.{name}' else: add_module = True if prefix: @@ -237,9 +285,11 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str] 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) + 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 @@ -261,26 +311,31 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str] 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}'.") + 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 + '.' + nodetext = f'{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) + 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) + logger.warning( + 'could not parse tp_list (%r): %s', tp_list, exc, location=signode + ) if arglist: try: @@ -293,8 +348,9 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str] _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) + 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(): @@ -307,9 +363,9 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str] anno = self.options.get('annotation') if anno: - signode += addnodes.desc_annotation(' ' + anno, '', - addnodes.desc_sig_space(), - nodes.Text(anno)) + signode += addnodes.desc_annotation( + f' {anno}', '', addnodes.desc_sig_space(), nodes.Text(anno) + ) return fullname, prefix @@ -329,10 +385,11 @@ def get_index_text(self, modname: str, name: tuple[str, str]) -> str: 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: + 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] + fullname = (f'{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) @@ -342,13 +399,20 @@ def add_target_and_index(self, name_cls: tuple[str, str], sig: str, canonical_name = self.options.get('canonical') if canonical_name: - domain.note_object(canonical_name, self.objtype, node_id, aliased=True, - location=signode) + 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)) + self.indexnode['entries'].append(( + 'single', + indextext, + node_id, + '', + None, + )) def before_content(self) -> None: """Handle object nesting before content @@ -398,8 +462,7 @@ def after_content(self) -> None: with contextlib.suppress(IndexError): classes.pop() - self.env.ref_context['py:class'] = (classes[-1] if len(classes) > 0 - else None) + 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: