Enable automatic formatting for `sphinx/domains/python/`

This commit is contained in:
Adam Turner 2025-01-01 13:17:50 +00:00
parent 917b74ab43
commit e48a88d05f
4 changed files with 404 additions and 215 deletions

View File

@ -394,8 +394,5 @@ preview = true
quote-style = "single" quote-style = "single"
exclude = [ exclude = [
"sphinx/builders/latex/constants.py", "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", "sphinx/domains/std/__init__.py",
] ]

View File

@ -87,16 +87,19 @@ class PyFunction(PyObject):
def get_signature_prefix(self, sig: str) -> list[nodes.Node]: def get_signature_prefix(self, sig: str) -> list[nodes.Node]:
if 'async' in self.options: if 'async' in self.options:
return [addnodes.desc_sig_keyword('', 'async'), return [
addnodes.desc_sig_space()] addnodes.desc_sig_keyword('', 'async'),
addnodes.desc_sig_space(),
]
else: else:
return [] return []
def needs_arglist(self) -> bool: def needs_arglist(self) -> bool:
return True return True
def add_target_and_index(self, name_cls: tuple[str, str], sig: str, def add_target_and_index(
signode: desc_signature) -> None: self, name_cls: tuple[str, str], sig: str, signode: desc_signature
) -> None:
super().add_target_and_index(name_cls, sig, signode) super().add_target_and_index(name_cls, sig, signode)
if 'no-index-entry' not in self.options: if 'no-index-entry' not in self.options:
modname = self.options.get('module', self.env.ref_context.get('py:module')) modname = self.options.get('module', self.env.ref_context.get('py:module'))
@ -147,17 +150,24 @@ class PyVariable(PyObject):
typ = self.options.get('type') typ = self.options.get('type')
if typ: if typ:
annotations = _parse_annotation(typ, self.env) annotations = _parse_annotation(typ, self.env)
signode += addnodes.desc_annotation(typ, '', signode += addnodes.desc_annotation(
addnodes.desc_sig_punctuation('', ':'), typ,
addnodes.desc_sig_space(), *annotations) '',
addnodes.desc_sig_punctuation('', ':'),
addnodes.desc_sig_space(),
*annotations,
)
value = self.options.get('value') value = self.options.get('value')
if value: if value:
signode += addnodes.desc_annotation(value, '', signode += addnodes.desc_annotation(
addnodes.desc_sig_space(), value,
addnodes.desc_sig_punctuation('', '='), '',
addnodes.desc_sig_space(), addnodes.desc_sig_space(),
nodes.Text(value)) addnodes.desc_sig_punctuation('', '='),
addnodes.desc_sig_space(),
nodes.Text(value),
)
return fullname, prefix return fullname, prefix
@ -183,8 +193,12 @@ class PyClasslike(PyObject):
def get_signature_prefix(self, sig: str) -> list[nodes.Node]: def get_signature_prefix(self, sig: str) -> list[nodes.Node]:
if 'final' in self.options: if 'final' in self.options:
return [nodes.Text('final'), addnodes.desc_sig_space(), return [
nodes.Text(self.objtype), addnodes.desc_sig_space()] nodes.Text('final'),
addnodes.desc_sig_space(),
nodes.Text(self.objtype),
addnodes.desc_sig_space(),
]
else: else:
return [nodes.Text(self.objtype), addnodes.desc_sig_space()] return [nodes.Text(self.objtype), addnodes.desc_sig_space()]
@ -308,18 +322,24 @@ class PyAttribute(PyObject):
typ = self.options.get('type') typ = self.options.get('type')
if typ: if typ:
annotations = _parse_annotation(typ, self.env) annotations = _parse_annotation(typ, self.env)
signode += addnodes.desc_annotation(typ, '', signode += addnodes.desc_annotation(
addnodes.desc_sig_punctuation('', ':'), typ,
addnodes.desc_sig_space(), '',
*annotations) addnodes.desc_sig_punctuation('', ':'),
addnodes.desc_sig_space(),
*annotations,
)
value = self.options.get('value') value = self.options.get('value')
if value: if value:
signode += addnodes.desc_annotation(value, '', signode += addnodes.desc_annotation(
addnodes.desc_sig_space(), value,
addnodes.desc_sig_punctuation('', '='), '',
addnodes.desc_sig_space(), addnodes.desc_sig_space(),
nodes.Text(value)) addnodes.desc_sig_punctuation('', '='),
addnodes.desc_sig_space(),
nodes.Text(value),
)
return fullname, prefix return fullname, prefix
@ -354,10 +374,13 @@ class PyProperty(PyObject):
typ = self.options.get('type') typ = self.options.get('type')
if typ: if typ:
annotations = _parse_annotation(typ, self.env) annotations = _parse_annotation(typ, self.env)
signode += addnodes.desc_annotation(typ, '', signode += addnodes.desc_annotation(
addnodes.desc_sig_punctuation('', ':'), typ,
addnodes.desc_sig_space(), '',
*annotations) addnodes.desc_sig_punctuation('', ':'),
addnodes.desc_sig_space(),
*annotations,
)
return fullname, prefix return fullname, prefix
@ -405,7 +428,8 @@ class PyTypeAlias(PyObject):
if canonical := self.options.get('canonical'): if canonical := self.options.get('canonical'):
canonical_annotations = _parse_annotation(canonical, self.env) canonical_annotations = _parse_annotation(canonical, self.env)
signode += addnodes.desc_annotation( signode += addnodes.desc_annotation(
canonical, '', canonical,
'',
addnodes.desc_sig_space(), addnodes.desc_sig_space(),
addnodes.desc_sig_punctuation('', '='), addnodes.desc_sig_punctuation('', '='),
addnodes.desc_sig_space(), addnodes.desc_sig_space(),
@ -513,12 +537,18 @@ class PyCurrentModule(SphinxDirective):
class PyXRefRole(XRefRole): class PyXRefRole(XRefRole):
def process_link(self, env: BuildEnvironment, refnode: Element, def process_link(
has_explicit_title: bool, title: str, target: str) -> tuple[str, str]: 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:module'] = env.ref_context.get('py:module')
refnode['py:class'] = env.ref_context.get('py:class') refnode['py:class'] = env.ref_context.get('py:class')
if not has_explicit_title: 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 target = target.lstrip('~') # only has a meaning for the title
# if the first character is a tilde, don't display the module/class # if the first character is a tilde, don't display the module/class
# parts of the contents # parts of the contents
@ -526,7 +556,7 @@ class PyXRefRole(XRefRole):
title = title[1:] title = title[1:]
dot = title.rfind('.') dot = title.rfind('.')
if dot != -1: if dot != -1:
title = title[dot + 1:] title = title[dot + 1 :]
# if the first character is a dot, search more specific namespaces first # if the first character is a dot, search more specific namespaces first
# else search builtins first # else search builtins first
if target[0:1] == '.': if target[0:1] == '.':
@ -535,7 +565,9 @@ class PyXRefRole(XRefRole):
return title, target 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.""" """Filter ``:meta:`` field from its docstring."""
if domain != 'py': if domain != 'py':
return return
@ -560,8 +592,9 @@ class PythonModuleIndex(Index):
shortname = _('modules') shortname = _('modules')
domain: PythonDomain domain: PythonDomain
def generate(self, docnames: Iterable[str] | None = None, def generate(
) -> tuple[list[tuple[str, list[IndexEntry]]], bool]: self, docnames: Iterable[str] | None = None
) -> tuple[list[tuple[str, list[IndexEntry]]], bool]:
doc_names = frozenset(docnames) if docnames is not None else None doc_names = frozenset(docnames) if docnames is not None else None
content: dict[str, list[IndexEntry]] = {} content: dict[str, list[IndexEntry]] = {}
@ -657,46 +690,46 @@ class PythonDomain(Domain):
name = 'py' name = 'py'
label = 'Python' label = 'Python'
object_types: dict[str, ObjType] = { object_types: dict[str, ObjType] = {
'function': ObjType(_('function'), 'func', 'obj'), 'function': ObjType(_('function'), 'func', 'obj'),
'data': ObjType(_('data'), 'data', 'obj'), 'data': ObjType(_('data'), 'data', 'obj'),
'class': ObjType(_('class'), 'class', 'exc', 'obj'), 'class': ObjType(_('class'), 'class', 'exc', 'obj'),
'exception': ObjType(_('exception'), 'exc', 'class', 'obj'), 'exception': ObjType(_('exception'), 'exc', 'class', 'obj'),
'method': ObjType(_('method'), 'meth', 'obj'), 'method': ObjType(_('method'), 'meth', 'obj'),
'classmethod': ObjType(_('class method'), 'meth', 'obj'), 'classmethod': ObjType(_('class method'), 'meth', 'obj'),
'staticmethod': ObjType(_('static method'), 'meth', 'obj'), 'staticmethod': ObjType(_('static method'), 'meth', 'obj'),
'attribute': ObjType(_('attribute'), 'attr', 'obj'), 'attribute': ObjType(_('attribute'), 'attr', 'obj'),
'property': ObjType(_('property'), 'attr', '_prop', 'obj'), 'property': ObjType(_('property'), 'attr', '_prop', 'obj'),
'type': ObjType(_('type alias'), 'type', 'obj'), 'type': ObjType(_('type alias'), 'type', 'obj'),
'module': ObjType(_('module'), 'mod', 'obj'), 'module': ObjType(_('module'), 'mod', 'obj'),
} }
directives = { directives = {
'function': PyFunction, 'function': PyFunction,
'data': PyVariable, 'data': PyVariable,
'class': PyClasslike, 'class': PyClasslike,
'exception': PyClasslike, 'exception': PyClasslike,
'method': PyMethod, 'method': PyMethod,
'classmethod': PyClassMethod, 'classmethod': PyClassMethod,
'staticmethod': PyStaticMethod, 'staticmethod': PyStaticMethod,
'attribute': PyAttribute, 'attribute': PyAttribute,
'property': PyProperty, 'property': PyProperty,
'type': PyTypeAlias, 'type': PyTypeAlias,
'module': PyModule, 'module': PyModule,
'currentmodule': PyCurrentModule, 'currentmodule': PyCurrentModule,
'decorator': PyDecoratorFunction, 'decorator': PyDecoratorFunction,
'decoratormethod': PyDecoratorMethod, 'decoratormethod': PyDecoratorMethod,
} }
roles = { roles = {
'data': PyXRefRole(), 'data': PyXRefRole(),
'exc': PyXRefRole(), 'exc': PyXRefRole(),
'func': PyXRefRole(fix_parens=True), 'func': PyXRefRole(fix_parens=True),
'class': PyXRefRole(), 'class': PyXRefRole(),
'const': PyXRefRole(), 'const': PyXRefRole(),
'attr': PyXRefRole(), 'attr': PyXRefRole(),
'type': PyXRefRole(), 'type': PyXRefRole(),
'meth': PyXRefRole(fix_parens=True), 'meth': PyXRefRole(fix_parens=True),
'mod': PyXRefRole(), 'mod': PyXRefRole(),
'obj': PyXRefRole(), 'obj': PyXRefRole(),
} }
initial_data: dict[str, dict[str, tuple[Any]]] = { initial_data: dict[str, dict[str, tuple[Any]]] = {
'objects': {}, # fullname -> docname, objtype 'objects': {}, # fullname -> docname, objtype
@ -710,8 +743,14 @@ class PythonDomain(Domain):
def objects(self) -> dict[str, ObjectEntry]: def objects(self) -> dict[str, ObjectEntry]:
return self.data.setdefault('objects', {}) # fullname -> ObjectEntry return self.data.setdefault('objects', {}) # fullname -> ObjectEntry
def note_object(self, name: str, objtype: str, node_id: str, def note_object(
aliased: bool = False, location: Any = None) -> None: self,
name: str,
objtype: str,
node_id: str,
aliased: bool = False,
location: Any = None,
) -> None:
"""Note a python object for cross reference. """Note a python object for cross reference.
.. versionadded:: 2.1 .. versionadded:: 2.1
@ -726,9 +765,15 @@ class PythonDomain(Domain):
return return
else: else:
# duplicated # duplicated
logger.warning(__('duplicate object description of %s, ' logger.warning(
'other instance in %s, use :no-index: for one of them'), __(
name, other.docname, location=location) '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) self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype, aliased)
@property @property
@ -771,9 +816,15 @@ class PythonDomain(Domain):
if mod.docname in docnames: if mod.docname in docnames:
self.modules[modname] = mod self.modules[modname] = mod
def find_obj(self, env: BuildEnvironment, modname: str, classname: str, def find_obj(
name: str, type: str | None, searchmode: int = 0, self,
) -> list[tuple[str, ObjectEntry]]: 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 """Find a Python object for "name", perhaps using the given module
and/or classname. Returns a list of (name, object entry) tuples. and/or classname. Returns a list of (name, object entry) tuples.
""" """
@ -794,20 +845,31 @@ class PythonDomain(Domain):
if objtypes is not None: if objtypes is not None:
if modname and classname: if modname and classname:
fullname = modname + '.' + classname + '.' + name 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 newname = fullname
if not newname: if not newname:
if modname and modname + '.' + name in self.objects and \ if (
self.objects[modname + '.' + name].objtype in objtypes: modname
newname = modname + '.' + name and f'{modname}.{name}' in self.objects
elif name in self.objects and self.objects[name].objtype in objtypes: 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 newname = name
else: else:
# "fuzzy" searching mode # "fuzzy" searching mode
searchname = '.' + name searchname = f'.{name}'
matches = [(oname, self.objects[oname]) for oname in self.objects matches = [
if oname.endswith(searchname) and (oname, self.objects[oname])
self.objects[oname].objtype in objtypes] for oname in self.objects
if oname.endswith(searchname)
and self.objects[oname].objtype in objtypes
]
else: else:
# NOTE: searching for exact match, object type is not considered # NOTE: searching for exact match, object type is not considered
if name in self.objects: if name in self.objects:
@ -819,21 +881,30 @@ class PythonDomain(Domain):
newname = classname + '.' + name newname = classname + '.' + name
elif modname and modname + '.' + name in self.objects: elif modname and modname + '.' + name in self.objects:
newname = modname + '.' + name newname = modname + '.' + name
elif modname and classname and \ elif (
modname + '.' + classname + '.' + name in self.objects: modname
and classname
and modname + '.' + classname + '.' + name in self.objects
):
newname = modname + '.' + classname + '.' + name newname = modname + '.' + classname + '.' + name
if newname is not None: if newname is not None:
matches.append((newname, self.objects[newname])) matches.append((newname, self.objects[newname]))
return matches return matches
def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, def resolve_xref(
type: str, target: str, node: pending_xref, contnode: Element, self,
) -> nodes.reference | None: env: BuildEnvironment,
fromdocname: str,
builder: Builder,
type: str,
target: str,
node: pending_xref,
contnode: Element,
) -> nodes.reference | None:
modname = node.get('py:module') modname = node.get('py:module')
clsname = node.get('py:class') clsname = node.get('py:class')
searchmode = 1 if node.hasattr('refspecific') else 0 searchmode = 1 if node.hasattr('refspecific') else 0
matches = self.find_obj(env, modname, clsname, target, matches = self.find_obj(env, modname, clsname, target, type, searchmode)
type, searchmode)
if not matches and type == 'attr': if not matches and type == 'attr':
# fallback to meth (for property; Sphinx 2.4.x) # fallback to meth (for property; Sphinx 2.4.x)
@ -855,9 +926,14 @@ class PythonDomain(Domain):
if len(canonicals) == 1: if len(canonicals) == 1:
matches = canonicals matches = canonicals
else: else:
logger.warning(__('more than one target found for cross-reference %r: %s'), logger.warning(
target, ', '.join(match[0] for match in matches), __('more than one target found for cross-reference %r: %s'),
type='ref', subtype='python', location=node) target,
', '.join(match[0] for match in matches),
type='ref',
subtype='python',
location=node,
)
name, obj = matches[0] name, obj = matches[0]
if obj[2] == 'module': if obj[2] == 'module':
@ -873,9 +949,15 @@ class PythonDomain(Domain):
return make_refnode(builder, fromdocname, obj[0], obj[1], children, name) return make_refnode(builder, fromdocname, obj[0], obj[1], children, name)
def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, def resolve_any_xref(
target: str, node: pending_xref, contnode: Element, self,
) -> list[tuple[str, nodes.reference]]: env: BuildEnvironment,
fromdocname: str,
builder: Builder,
target: str,
node: pending_xref,
contnode: Element,
) -> list[tuple[str, nodes.reference]]:
modname = node.get('py:module') modname = node.get('py:module')
clsname = node.get('py:class') clsname = node.get('py:class')
results: list[tuple[str, nodes.reference]] = [] results: list[tuple[str, nodes.reference]] = []
@ -885,7 +967,6 @@ class PythonDomain(Domain):
multiple_matches = len(matches) > 1 multiple_matches = len(matches) > 1
for name, obj in matches: for name, obj in matches:
if multiple_matches and obj.aliased: if multiple_matches and obj.aliased:
# Skip duplicated matches # Skip duplicated matches
continue continue
@ -893,7 +974,7 @@ class PythonDomain(Domain):
if obj[2] == 'module': if obj[2] == 'module':
results.append(( results.append((
'py:mod', 'py:mod',
self._make_module_refnode(builder, fromdocname, name, contnode) self._make_module_refnode(builder, fromdocname, name, contnode),
)) ))
else: else:
# determine the content of the reference by conditions # determine the content of the reference by conditions
@ -905,12 +986,15 @@ class PythonDomain(Domain):
children = [contnode] children = [contnode]
role = 'py:' + self.role_for_objtype(obj[2]) # type: ignore[operator] role = 'py:' + self.role_for_objtype(obj[2]) # type: ignore[operator]
results.append((role, make_refnode(builder, fromdocname, obj[0], obj[1], results.append((
children, name))) role,
make_refnode(builder, fromdocname, obj[0], obj[1], children, name),
))
return results return results
def _make_module_refnode(self, builder: Builder, fromdocname: str, name: str, def _make_module_refnode(
contnode: Node) -> nodes.reference: self, builder: Builder, fromdocname: str, name: str, contnode: Node
) -> nodes.reference:
# get additional info for modules # get additional info for modules
module: ModuleEntry = self.modules[name] module: ModuleEntry = self.modules[name]
title_parts = [name] title_parts = [name]
@ -946,9 +1030,11 @@ class PythonDomain(Domain):
return '.'.join(filter(None, [modname, clsname, target])) return '.'.join(filter(None, [modname, clsname, target]))
def builtin_resolver(app: Sphinx, env: BuildEnvironment, def builtin_resolver(
node: pending_xref, contnode: Element) -> Element | None: app: Sphinx, env: BuildEnvironment, node: pending_xref, contnode: Element
) -> Element | None:
"""Do not emit nitpicky warnings for built-in types.""" """Do not emit nitpicky warnings for built-in types."""
def istyping(s: str) -> bool: def istyping(s: str) -> bool:
if s.startswith('typing.'): if s.startswith('typing.'):
s = s.split('.', 1)[1] s = s.split('.', 1)[1]
@ -977,7 +1063,7 @@ def setup(app: Sphinx) -> ExtensionMetadata:
app.add_domain(PythonDomain) app.add_domain(PythonDomain)
app.add_config_value('python_use_unqualified_type_names', False, 'env') app.add_config_value('python_use_unqualified_type_names', False, 'env')
app.add_config_value( 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.add_config_value('python_display_short_literal_types', False, 'env')
app.connect('object-description-transform', filter_meta_fields) app.connect('object-description-transform', filter_meta_fields)

View File

@ -23,8 +23,9 @@ if TYPE_CHECKING:
from sphinx.environment import BuildEnvironment from sphinx.environment import BuildEnvironment
def parse_reftarget(reftarget: str, suppress_prefix: bool = False, def parse_reftarget(
) -> tuple[str, str, str, bool]: reftarget: str, suppress_prefix: bool = False
) -> tuple[str, str, str, bool]:
"""Parse a type string and return (reftype, reftarget, title, refspecific flag)""" """Parse a type string and return (reftype, reftarget, title, refspecific flag)"""
refspecific = False refspecific = False
if reftarget.startswith('.'): if reftarget.startswith('.'):
@ -50,12 +51,15 @@ def parse_reftarget(reftarget: str, suppress_prefix: bool = False,
return reftype, reftarget, title, refspecific return reftype, reftarget, title, refspecific
def type_to_xref(target: str, env: BuildEnvironment, *, def type_to_xref(
suppress_prefix: bool = False) -> addnodes.pending_xref: target: str, env: BuildEnvironment, *, suppress_prefix: bool = False
) -> addnodes.pending_xref:
"""Convert a type string to a cross reference node.""" """Convert a type string to a cross reference node."""
if env: if env:
kwargs = {'py:module': env.ref_context.get('py:module'), kwargs = {
'py:class': env.ref_context.get('py:class')} 'py:module': env.ref_context.get('py:module'),
'py:class': env.ref_context.get('py:class'),
}
else: else:
kwargs = {} 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 # nested classes. But python domain can't access the real python object because this
# module should work not-dynamically. # module should work not-dynamically.
shortname = title.split('.')[-1] shortname = title.split('.')[-1]
contnodes: list[Node] = [pending_xref_condition('', shortname, condition='resolved'), contnodes: list[Node] = [
pending_xref_condition('', title, condition='*')] pending_xref_condition('', shortname, condition='resolved'),
pending_xref_condition('', title, condition='*'),
]
else: else:
contnodes = [nodes.Text(title)] contnodes = [nodes.Text(title)]
return pending_xref('', *contnodes, return pending_xref(
refdomain='py', reftype=reftype, reftarget=target, '',
refspecific=refspecific, **kwargs) *contnodes,
refdomain='py',
reftype=reftype,
reftarget=target,
refspecific=refspecific,
**kwargs,
)
def _parse_annotation(annotation: str, env: BuildEnvironment) -> list[Node]: 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]: def unparse(node: ast.AST) -> list[Node]:
if isinstance(node, ast.Attribute): 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): if isinstance(node, ast.BinOp):
result: list[Node] = unparse(node.left) result: list[Node] = unparse(node.left)
result.extend(unparse(node.op)) result.extend(unparse(node.op))
result.extend(unparse(node.right)) result.extend(unparse(node.right))
return result return result
if isinstance(node, ast.BitOr): if isinstance(node, ast.BitOr):
return [addnodes.desc_sig_space(), return [
addnodes.desc_sig_punctuation('', '|'), addnodes.desc_sig_space(),
addnodes.desc_sig_space()] addnodes.desc_sig_punctuation('', '|'),
addnodes.desc_sig_space(),
]
if isinstance(node, ast.Constant): if isinstance(node, ast.Constant):
if node.value is Ellipsis: if node.value is Ellipsis:
return [addnodes.desc_sig_punctuation('', "...")] return [addnodes.desc_sig_punctuation('', '...')]
if isinstance(node.value, bool): if isinstance(node.value, bool):
return [addnodes.desc_sig_keyword('', repr(node.value))] return [addnodes.desc_sig_keyword('', repr(node.value))]
if isinstance(node.value, int): if isinstance(node.value, int):
@ -157,8 +171,10 @@ def _parse_annotation(annotation: str, env: BuildEnvironment) -> list[Node]:
result.pop() result.pop()
result.pop() result.pop()
else: else:
result = [addnodes.desc_sig_punctuation('', '('), result = [
addnodes.desc_sig_punctuation('', ')')] addnodes.desc_sig_punctuation('', '('),
addnodes.desc_sig_punctuation('', ')'),
]
return result return result
if isinstance(node, ast.Call): if isinstance(node, ast.Call):
@ -211,8 +227,11 @@ def _parse_annotation(annotation: str, env: BuildEnvironment) -> list[Node]:
if isinstance(node, nodes.literal): if isinstance(node, nodes.literal):
result.append(node[0]) result.append(node[0])
elif isinstance(node, nodes.Text) and node.strip(): elif isinstance(node, nodes.Text) and node.strip():
if (result and isinstance(result[-1], addnodes.desc_sig_punctuation) and if (
result[-1].astext() == '~'): result
and isinstance(result[-1], addnodes.desc_sig_punctuation)
and result[-1].astext() == '~'
):
result.pop() result.pop()
result.append(type_to_xref(str(node), env, suppress_prefix=True)) result.append(type_to_xref(str(node), env, suppress_prefix=True))
else: else:
@ -244,8 +263,7 @@ class _TypeParameterListParser(TokenProcessor):
else: else:
if current == token.INDENT: if current == token.INDENT:
tokens += self.fetch_until(token.DEDENT) tokens += self.fetch_until(token.DEDENT)
elif current.match( elif current.match([token.OP, ':'], [token.OP, '='], [token.OP, ',']):
[token.OP, ':'], [token.OP, '='], [token.OP, ',']):
tokens.pop() tokens.pop()
break break
return tokens return tokens
@ -254,7 +272,9 @@ class _TypeParameterListParser(TokenProcessor):
while current := self.fetch_token(): while current := self.fetch_token():
if current == token.NAME: if current == token.NAME:
tp_name = current.value.strip() 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, '*']: if self.previous == [token.OP, '*']:
tp_kind = Parameter.VAR_POSITIONAL tp_kind = Parameter.VAR_POSITIONAL
else: else:
@ -275,9 +295,14 @@ class _TypeParameterListParser(TokenProcessor):
tokens = self.fetch_type_param_spec() tokens = self.fetch_type_param_spec()
tp_default = self._build_identifier(tokens) tp_default = self._build_identifier(tokens)
if tp_kind != Parameter.POSITIONAL_OR_KEYWORD and tp_ann != Parameter.empty: if (
msg = ('type parameter bound or constraint is not allowed ' tp_kind != Parameter.POSITIONAL_OR_KEYWORD
f'for {tp_kind.description} parameters') and tp_ann != Parameter.empty
):
msg = (
'type parameter bound or constraint is not allowed '
f'for {tp_kind.description} parameters'
)
raise SyntaxError(msg) raise SyntaxError(msg)
type_param = (tp_name, tp_kind, tp_default, tp_ann) type_param = (tp_name, tp_kind, tp_default, tp_ann)
@ -315,12 +340,22 @@ class _TypeParameterListParser(TokenProcessor):
idents.append(ident) idents.append(ident)
# determine if the next token is an unpack operator depending # determine if the next token is an unpack operator depending
# on the left and right hand side of the operator symbol # on the left and right hand side of the operator symbol
is_unpack_operator = ( is_unpack_operator = op.match([token.OP, '*'], [token.OP, '**']) and not (
op.match([token.OP, '*'], [token.OP, '**']) and not ( tok.match(
tok.match(token.NAME, token.NUMBER, token.STRING, token.NAME,
[token.OP, ')'], [token.OP, ']'], [token.OP, '}']) token.NUMBER,
and after.match(token.NAME, token.NUMBER, token.STRING, token.STRING,
[token.OP, '('], [token.OP, '['], [token.OP, '{']) [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 @@ class _TypeParameterListParser(TokenProcessor):
[token.OP, '@'], [token.OP, '/'], [token.OP, '//'], [token.OP, '%'], [token.OP, '@'], [token.OP, '/'], [token.OP, '//'], [token.OP, '%'],
[token.OP, '<<'], [token.OP, '>>'], [token.OP, '>>>'], [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 f' {tok.value} '
return tok.value return tok.value
def _parse_type_list( def _parse_type_list(
tp_list: str, env: BuildEnvironment, tp_list: str, env: BuildEnvironment, multi_line_parameter_list: bool = False
multi_line_parameter_list: bool = False,
) -> addnodes.desc_type_parameter_list: ) -> addnodes.desc_type_parameter_list:
"""Parse a list of type parameters according to PEP 695.""" """Parse a list of type parameters according to PEP 695."""
type_params = addnodes.desc_type_parameter_list(tp_list) 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 # type annotations are interpreted as type parameter bound or constraints
parser = _TypeParameterListParser(tp_list) parser = _TypeParameterListParser(tp_list)
parser.parse() 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 # no positional-only or keyword-only allowed in a type parameters list
if tp_kind in {Parameter.POSITIONAL_ONLY, Parameter.KEYWORD_ONLY}: if tp_kind in {Parameter.POSITIONAL_ONLY, Parameter.KEYWORD_ONLY}:
msg = ('positional-only or keyword-only parameters ' msg = (
'are prohibited in type parameter lists') 'positional-only or keyword-only parameters '
'are prohibited in type parameter lists'
)
raise SyntaxError(msg) raise SyntaxError(msg)
node = addnodes.desc_type_parameter() node = addnodes.desc_type_parameter()
@ -395,8 +431,7 @@ def _parse_type_list(
node += addnodes.desc_sig_punctuation('', ':') node += addnodes.desc_sig_punctuation('', ':')
node += addnodes.desc_sig_space() node += addnodes.desc_sig_space()
type_ann_expr = addnodes.desc_sig_name('', '', type_ann_expr = addnodes.desc_sig_name('', '', *annotation) # type: ignore[arg-type]
*annotation) # type: ignore[arg-type]
# a type bound is ``T: U`` whereas type constraints # a type bound is ``T: U`` whereas type constraints
# must be enclosed with parentheses. ``T: (U, V)`` # must be enclosed with parentheses. ``T: (U, V)``
if tp_ann.startswith('(') and tp_ann.endswith(')'): 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_space()
node += addnodes.desc_sig_operator('', '=') node += addnodes.desc_sig_operator('', '=')
node += addnodes.desc_sig_space() node += addnodes.desc_sig_space()
node += nodes.inline('', tp_default, node += nodes.inline(
classes=['default_value'], '', tp_default, classes=['default_value'], support_smartquotes=False
support_smartquotes=False) )
type_params += node type_params += node
return type_params return type_params
def _parse_arglist( 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: ) -> addnodes.desc_parameterlist:
"""Parse a list of arguments using AST parser""" """Parse a list of arguments using AST parser"""
params = addnodes.desc_parameterlist(arglist) params = addnodes.desc_parameterlist(arglist)
@ -435,12 +470,18 @@ def _parse_arglist(
for param in sig.parameters.values(): for param in sig.parameters.values():
if param.kind != param.POSITIONAL_ONLY and last_kind == param.POSITIONAL_ONLY: if param.kind != param.POSITIONAL_ONLY and last_kind == param.POSITIONAL_ONLY:
# PEP-570: Separator for Positional Only Parameter: / # PEP-570: Separator for Positional Only Parameter: /
params += addnodes.desc_parameter('', '', addnodes.desc_sig_operator('', '/')) params += addnodes.desc_parameter(
if param.kind == param.KEYWORD_ONLY and last_kind in {param.POSITIONAL_OR_KEYWORD, '', '', addnodes.desc_sig_operator('', '/')
param.POSITIONAL_ONLY, )
None}: 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: * # 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() node = addnodes.desc_parameter()
if param.kind == param.VAR_POSITIONAL: if param.kind == param.VAR_POSITIONAL:
@ -464,8 +505,9 @@ def _parse_arglist(
node += addnodes.desc_sig_space() node += addnodes.desc_sig_space()
else: else:
node += addnodes.desc_sig_operator('', '=') node += addnodes.desc_sig_operator('', '=')
node += nodes.inline('', param.default, classes=['default_value'], node += nodes.inline(
support_smartquotes=False) '', param.default, classes=['default_value'], support_smartquotes=False
)
params += node params += node
last_kind = param.kind last_kind = param.kind
@ -478,9 +520,9 @@ def _parse_arglist(
def _pseudo_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: ) -> 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 Arguments can have "optional" annotations given by enclosing them in
brackets. Currently, this will split at any comma, even if it's inside a 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() argument = argument[:-1].strip()
if argument: if argument:
stack[-1] += addnodes.desc_parameter( stack[-1] += addnodes.desc_parameter(
'', '', addnodes.desc_sig_name(argument, argument)) '', '', addnodes.desc_sig_name(argument, argument)
)
while ends_open: while ends_open:
stack.append(addnodes.desc_optional()) stack.append(addnodes.desc_optional())
stack[-2] += stack[-1] stack[-2] += stack[-1]

View File

@ -35,13 +35,15 @@ logger = logging.getLogger(__name__)
# REs for Python signatures # REs for Python signatures
py_sig_re = re.compile( py_sig_re = re.compile(
r'''^ ([\w.]*\.)? # class name(s) r"""^ ([\w.]*\.)? # class name(s)
(\w+) \s* # thing name (\w+) \s* # thing name
(?: \[\s*(.*)\s*])? # optional: type parameters list (?: \[\s*(.*)\s*])? # optional: type parameters list
(?: \(\s*(.*)\s*\) # optional: arguments (?: \(\s*(.*)\s*\) # optional: arguments
(?:\s* -> \s* (.*))? # return annotation (?:\s* -> \s* (.*))? # return annotation
)? $ # and nothing more )? $ # and nothing more
''', re.VERBOSE) """,
re.VERBOSE,
)
# This override allows our inline type specifiers to behave like :class: link # This override allows our inline type specifiers to behave like :class: link
@ -60,9 +62,16 @@ class PyXrefMixin:
) -> Node: ) -> Node:
# we use inliner=None to make sure we get the old behaviour with a single # we use inliner=None to make sure we get the old behaviour with a single
# pending_xref node # pending_xref node
result = super().make_xref(rolename, domain, target, # type: ignore[misc] result = super().make_xref( # type: ignore[misc]
innernode, contnode, rolename,
env, inliner=None, location=None) domain,
target,
innernode,
contnode,
env,
inliner=None,
location=None,
)
if isinstance(result, pending_xref): if isinstance(result, pending_xref):
assert env is not None assert env is not None
result['refspecific'] = True result['refspecific'] = True
@ -82,8 +91,10 @@ class PyXrefMixin:
shortname = target.split('.')[-1] shortname = target.split('.')[-1]
textnode = innernode('', shortname) # type: ignore[call-arg] textnode = innernode('', shortname) # type: ignore[call-arg]
contnodes = [pending_xref_condition('', '', textnode, condition='resolved'), contnodes = [
pending_xref_condition('', '', *children, condition='*')] pending_xref_condition('', '', textnode, condition='resolved'),
pending_xref_condition('', '', *children, condition='*'),
]
result.extend(contnodes) result.extend(contnodes)
return result return result
@ -116,8 +127,18 @@ class PyXrefMixin:
if in_literal or self._delimiters_re.match(sub_target): if in_literal or self._delimiters_re.match(sub_target):
results.append(contnode or innernode(sub_target, sub_target)) # type: ignore[call-arg] results.append(contnode or innernode(sub_target, sub_target)) # type: ignore[call-arg]
else: else:
results.append(self.make_xref(rolename, domain, sub_target, results.append(
innernode, contnode, env, inliner, location)) self.make_xref(
rolename,
domain,
sub_target,
innernode,
contnode,
env,
inliner,
location,
)
)
if sub_target in {'Literal', 'typing.Literal', '~typing.Literal'}: if sub_target in {'Literal', 'typing.Literal', '~typing.Literal'}:
in_literal = True in_literal = True
@ -161,22 +182,50 @@ class PyObject(ObjectDescription[tuple[str, str]]):
} }
doc_field_types = [ doc_field_types = [
PyTypedField('parameter', label=_('Parameters'), PyTypedField(
names=('param', 'parameter', 'arg', 'argument', 'parameter',
'keyword', 'kwarg', 'kwparam'), label=_('Parameters'),
typerolename='class', typenames=('paramtype', 'type'), names=(
can_collapse=True), 'param',
PyTypedField('variable', label=_('Variables'), 'parameter',
names=('var', 'ivar', 'cvar'), 'arg',
typerolename='class', typenames=('vartype',), 'argument',
can_collapse=True), 'keyword',
PyGroupedField('exceptions', label=_('Raises'), rolename='exc', 'kwarg',
names=('raises', 'raise', 'exception', 'except'), 'kwparam',
can_collapse=True), ),
Field('returnvalue', label=_('Returns'), has_arg=False, typerolename='class',
names=('returns', 'return')), typenames=('paramtype', 'type'),
PyField('returntype', label=_('Return type'), has_arg=False, can_collapse=True,
names=('rtype',), bodyrolename='class'), ),
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 allow_nesting = False
@ -212,18 +261,17 @@ class PyObject(ObjectDescription[tuple[str, str]]):
classname = self.env.ref_context.get('py:class') classname = self.env.ref_context.get('py:class')
if classname: if classname:
add_module = False add_module = False
if prefix and (prefix == classname or if prefix and (prefix == classname or prefix.startswith(f'{classname}.')):
prefix.startswith(classname + ".")):
fullname = prefix + name fullname = prefix + name
# class name is given again in the signature # class name is given again in the signature
prefix = prefix[len(classname):].lstrip('.') prefix = prefix[len(classname) :].lstrip('.')
elif prefix: elif prefix:
# class name is given in the signature, but different # class name is given in the signature, but different
# (shouldn't happen) # (shouldn't happen)
fullname = classname + '.' + prefix + name fullname = f'{classname}.{prefix}{name}'
else: else:
# class name is not given in the signature # class name is not given in the signature
fullname = classname + '.' + name fullname = f'{classname}.{name}'
else: else:
add_module = True add_module = True
if prefix: if prefix:
@ -237,9 +285,11 @@ class PyObject(ObjectDescription[tuple[str, str]]):
signode['class'] = classname signode['class'] = classname
signode['fullname'] = fullname signode['fullname'] = fullname
max_len = (self.env.config.python_maximum_signature_line_length max_len = (
or self.env.config.maximum_signature_line_length self.env.config.python_maximum_signature_line_length
or 0) or self.env.config.maximum_signature_line_length
or 0
)
# determine if the function arguments (without its type parameters) # determine if the function arguments (without its type parameters)
# should be formatted on a multiline or not by removing the width of # should be formatted on a multiline or not by removing the width of
@ -261,26 +311,31 @@ class PyObject(ObjectDescription[tuple[str, str]]):
sig_prefix = self.get_signature_prefix(sig) sig_prefix = self.get_signature_prefix(sig)
if sig_prefix: if sig_prefix:
if type(sig_prefix) is str: if type(sig_prefix) is str:
msg = ("Python directive method get_signature_prefix()" msg = (
" must return a list of nodes." 'Python directive method get_signature_prefix()'
f" Return value was '{sig_prefix}'.") ' must return a list of nodes.'
f" Return value was '{sig_prefix}'."
)
raise TypeError(msg) raise TypeError(msg)
signode += addnodes.desc_annotation(str(sig_prefix), '', *sig_prefix) signode += addnodes.desc_annotation(str(sig_prefix), '', *sig_prefix)
if prefix: if prefix:
signode += addnodes.desc_addname(prefix, prefix) signode += addnodes.desc_addname(prefix, prefix)
elif modname and add_module and self.env.config.add_module_names: 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_addname(nodetext, nodetext)
signode += addnodes.desc_name(name, name) signode += addnodes.desc_name(name, name)
if tp_list: if tp_list:
try: 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: except Exception as exc:
logger.warning("could not parse tp_list (%r): %s", tp_list, exc, logger.warning(
location=signode) 'could not parse tp_list (%r): %s', tp_list, exc, location=signode
)
if arglist: if arglist:
try: try:
@ -293,8 +348,9 @@ class PyObject(ObjectDescription[tuple[str, str]]):
_pseudo_parse_arglist(signode, arglist, multi_line_parameter_list) _pseudo_parse_arglist(signode, arglist, multi_line_parameter_list)
except (NotImplementedError, ValueError) as exc: except (NotImplementedError, ValueError) as exc:
# duplicated parameter names raise ValueError and not a SyntaxError # duplicated parameter names raise ValueError and not a SyntaxError
logger.warning("could not parse arglist (%r): %s", arglist, exc, logger.warning(
location=signode) 'could not parse arglist (%r): %s', arglist, exc, location=signode
)
_pseudo_parse_arglist(signode, arglist, multi_line_parameter_list) _pseudo_parse_arglist(signode, arglist, multi_line_parameter_list)
else: else:
if self.needs_arglist(): if self.needs_arglist():
@ -307,9 +363,9 @@ class PyObject(ObjectDescription[tuple[str, str]]):
anno = self.options.get('annotation') anno = self.options.get('annotation')
if anno: if anno:
signode += addnodes.desc_annotation(' ' + anno, '', signode += addnodes.desc_annotation(
addnodes.desc_sig_space(), f' {anno}', '', addnodes.desc_sig_space(), nodes.Text(anno)
nodes.Text(anno)) )
return fullname, prefix return fullname, prefix
@ -329,10 +385,11 @@ class PyObject(ObjectDescription[tuple[str, str]]):
msg = 'must be implemented in subclasses' msg = 'must be implemented in subclasses'
raise NotImplementedError(msg) raise NotImplementedError(msg)
def add_target_and_index(self, name_cls: tuple[str, str], sig: str, def add_target_and_index(
signode: desc_signature) -> None: self, name_cls: tuple[str, str], sig: str, signode: desc_signature
) -> None:
modname = self.options.get('module', self.env.ref_context.get('py:module')) 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) node_id = make_id(self.env, self.state.document, '', fullname)
signode['ids'].append(node_id) signode['ids'].append(node_id)
self.state.document.note_explicit_target(signode) self.state.document.note_explicit_target(signode)
@ -342,13 +399,20 @@ class PyObject(ObjectDescription[tuple[str, str]]):
canonical_name = self.options.get('canonical') canonical_name = self.options.get('canonical')
if canonical_name: if canonical_name:
domain.note_object(canonical_name, self.objtype, node_id, aliased=True, domain.note_object(
location=signode) canonical_name, self.objtype, node_id, aliased=True, location=signode
)
if 'no-index-entry' not in self.options: if 'no-index-entry' not in self.options:
indextext = self.get_index_text(modname, name_cls) indextext = self.get_index_text(modname, name_cls)
if indextext: 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: def before_content(self) -> None:
"""Handle object nesting before content """Handle object nesting before content
@ -398,8 +462,7 @@ class PyObject(ObjectDescription[tuple[str, str]]):
with contextlib.suppress(IndexError): with contextlib.suppress(IndexError):
classes.pop() classes.pop()
self.env.ref_context['py:class'] = (classes[-1] if len(classes) > 0 self.env.ref_context['py:class'] = classes[-1] if len(classes) > 0 else None
else None)
if 'module' in self.options: if 'module' in self.options:
modules = self.env.ref_context.setdefault('py:modules', []) modules = self.env.ref_context.setdefault('py:modules', [])
if modules: if modules: