diff options
Diffstat (limited to 'sphinx/domains/python.py')
-rw-r--r-- | sphinx/domains/python.py | 141 |
1 files changed, 84 insertions, 57 deletions
diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 19da49ac6..fc1136ae2 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -14,7 +14,7 @@ import re import typing import warnings from inspect import Parameter -from typing import Any, Dict, Iterable, Iterator, List, Tuple +from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Tuple from typing import cast from docutils import nodes @@ -67,18 +67,29 @@ pairindextypes = { 'builtin': _('built-in function'), } +ObjectEntry = NamedTuple('ObjectEntry', [('docname', str), + ('node_id', str), + ('objtype', str)]) +ModuleEntry = NamedTuple('ModuleEntry', [('docname', str), + ('node_id', str), + ('synopsis', str), + ('platform', str), + ('deprecated', bool)]) -def _parse_annotation(annotation: str) -> List[Node]: - """Parse type annotation.""" - def make_xref(text: str) -> addnodes.pending_xref: - if text == 'None': - reftype = 'obj' - else: - reftype = 'class' - return pending_xref('', nodes.Text(text), - refdomain='py', reftype=reftype, reftarget=text) +def type_to_xref(text: str) -> addnodes.pending_xref: + """Convert a type string to a cross reference node.""" + if text == 'None': + reftype = 'obj' + else: + reftype = 'class' + return pending_xref('', nodes.Text(text), + refdomain='py', reftype=reftype, reftarget=text) + + +def _parse_annotation(annotation: str) -> List[Node]: + """Parse type annotation.""" def unparse(node: ast.AST) -> List[Node]: if isinstance(node, ast.Attribute): return [nodes.Text("%s.%s" % (unparse(node.value)[0], node.attr))] @@ -124,10 +135,10 @@ def _parse_annotation(annotation: str) -> List[Node]: result = unparse(tree) for i, node in enumerate(result): if isinstance(node, nodes.Text): - result[i] = make_xref(str(node)) + result[i] = type_to_xref(str(node)) return result except SyntaxError: - return [make_xref(annotation)] + return [type_to_xref(annotation)] def _parse_arglist(arglist: str) -> addnodes.desc_parameterlist: @@ -521,10 +532,11 @@ class PyModulelevel(PyObject): if cls.__name__ != 'DirectiveAdapter': warnings.warn('PyModulelevel is deprecated. ' 'Please check the implementation of %s' % cls, - RemovedInSphinx40Warning) + RemovedInSphinx40Warning, stacklevel=2) break else: - warnings.warn('PyModulelevel is deprecated', RemovedInSphinx40Warning) + warnings.warn('PyModulelevel is deprecated', + RemovedInSphinx40Warning, stacklevel=2) return super().run() @@ -611,7 +623,7 @@ class PyVariable(PyObject): typ = self.options.get('type') if typ: - signode += addnodes.desc_annotation(typ, ': ' + typ) + signode += addnodes.desc_annotation(typ, '', nodes.Text(': '), type_to_xref(typ)) value = self.options.get('value') if value: @@ -632,10 +644,18 @@ class PyClasslike(PyObject): Description of a class-like object (classes, interfaces, exceptions). """ + option_spec = PyObject.option_spec.copy() + option_spec.update({ + 'final': directives.flag, + }) + allow_nesting = True def get_signature_prefix(self, sig: str) -> str: - return self.objtype + ' ' + if 'final' in self.options: + return 'final %s ' % self.objtype + else: + return '%s ' % self.objtype def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str: if self.objtype == 'class': @@ -658,10 +678,11 @@ class PyClassmember(PyObject): if cls.__name__ != 'DirectiveAdapter': warnings.warn('PyClassmember is deprecated. ' 'Please check the implementation of %s' % cls, - RemovedInSphinx40Warning) + RemovedInSphinx40Warning, stacklevel=2) break else: - warnings.warn('PyClassmember is deprecated', RemovedInSphinx40Warning) + warnings.warn('PyClassmember is deprecated', + RemovedInSphinx40Warning, stacklevel=2) return super().run() @@ -740,6 +761,7 @@ class PyMethod(PyObject): 'abstractmethod': directives.flag, 'async': directives.flag, 'classmethod': directives.flag, + 'final': directives.flag, 'property': directives.flag, 'staticmethod': directives.flag, }) @@ -752,6 +774,8 @@ class PyMethod(PyObject): def get_signature_prefix(self, sig: str) -> str: prefix = [] + if 'final' in self.options: + prefix.append('final') if 'abstractmethod' in self.options: prefix.append('abstract') if 'async' in self.options: @@ -844,7 +868,7 @@ class PyAttribute(PyObject): typ = self.options.get('type') if typ: - signode += addnodes.desc_annotation(typ, ': ' + typ) + signode += addnodes.desc_annotation(typ, '', nodes.Text(': '), type_to_xref(typ)) value = self.options.get('value') if value: @@ -876,10 +900,11 @@ class PyDecoratorMixin: if cls.__name__ != 'DirectiveAdapter': warnings.warn('PyDecoratorMixin is deprecated. ' 'Please check the implementation of %s' % cls, - RemovedInSphinx50Warning) + RemovedInSphinx50Warning, stacklevel=2) break else: - warnings.warn('PyDecoratorMixin is deprecated', RemovedInSphinx50Warning) + warnings.warn('PyDecoratorMixin is deprecated', + RemovedInSphinx50Warning, stacklevel=2) ret = super().handle_signature(sig, signode) # type: ignore signode.insert(0, addnodes.desc_addname('@', '@')) @@ -1134,8 +1159,8 @@ class PythonDomain(Domain): ] @property - def objects(self) -> Dict[str, Tuple[str, str, str]]: - return self.data.setdefault('objects', {}) # fullname -> docname, node_id, objtype + def objects(self) -> Dict[str, ObjectEntry]: + return self.data.setdefault('objects', {}) # fullname -> ObjectEntry def note_object(self, name: str, objtype: str, node_id: str, location: Any = None) -> None: """Note a python object for cross reference. @@ -1143,15 +1168,15 @@ class PythonDomain(Domain): .. versionadded:: 2.1 """ if name in self.objects: - docname = self.objects[name][0] + other = self.objects[name] logger.warning(__('duplicate object description of %s, ' 'other instance in %s, use :noindex: for one of them'), - name, docname, location=location) - self.objects[name] = (self.env.docname, node_id, objtype) + name, other.docname, location=location) + self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype) @property - def modules(self) -> Dict[str, Tuple[str, str, str, str, bool]]: - return self.data.setdefault('modules', {}) # modname -> docname, node_id, synopsis, platform, deprecated # NOQA + 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: @@ -1159,28 +1184,29 @@ class PythonDomain(Domain): .. versionadded:: 2.1 """ - self.modules[name] = (self.env.docname, node_id, synopsis, platform, deprecated) + self.modules[name] = ModuleEntry(self.env.docname, node_id, + synopsis, platform, deprecated) def clear_doc(self, docname: str) -> None: - for fullname, (fn, _x, _x) in list(self.objects.items()): - if fn == docname: + for fullname, obj in list(self.objects.items()): + if obj.docname == docname: del self.objects[fullname] - for modname, (fn, _x, _x, _x, _y) in list(self.modules.items()): - if fn == docname: + for modname, mod in list(self.modules.items()): + if mod.docname == docname: del self.modules[modname] def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None: # XXX check duplicates? - for fullname, (fn, node_id, objtype) in otherdata['objects'].items(): - if fn in docnames: - self.objects[fullname] = (fn, node_id, objtype) - for modname, data in otherdata['modules'].items(): - if data[0] in docnames: - self.modules[modname] = data + for fullname, obj in otherdata['objects'].items(): + if obj.docname in docnames: + self.objects[fullname] = obj + for modname, mod in otherdata['modules'].items(): + if mod.docname in docnames: + self.modules[modname] = mod def find_obj(self, env: BuildEnvironment, modname: str, classname: str, name: str, type: str, searchmode: int = 0 - ) -> List[Tuple[str, Tuple[str, str, str]]]: + ) -> 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. """ @@ -1191,7 +1217,7 @@ class PythonDomain(Domain): if not name: return [] - matches = [] # type: List[Tuple[str, Tuple[str, str, str]]] + matches = [] # type: List[Tuple[str, ObjectEntry]] newname = None if searchmode == 1: @@ -1202,20 +1228,20 @@ class PythonDomain(Domain): if objtypes is not None: if modname and classname: fullname = modname + '.' + classname + '.' + name - if fullname in self.objects and self.objects[fullname][2] 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][2] in objtypes: + self.objects[modname + '.' + name].objtype in objtypes: newname = modname + '.' + name - elif name in self.objects and self.objects[name][2] in objtypes: + 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][2] in objtypes] + self.objects[oname].objtype in objtypes] else: # NOTE: searching for exact match, object type is not considered if name in self.objects: @@ -1283,22 +1309,23 @@ class PythonDomain(Domain): def _make_module_refnode(self, builder: Builder, fromdocname: str, name: str, contnode: Node) -> Element: # get additional info for modules - docname, node_id, synopsis, platform, deprecated = self.modules[name] + module = self.modules[name] title = name - if synopsis: - title += ': ' + synopsis - if deprecated: + if module.synopsis: + title += ': ' + module.synopsis + if module.deprecated: title += _(' (deprecated)') - if platform: - title += ' (' + platform + ')' - return make_refnode(builder, fromdocname, docname, node_id, contnode, title) + 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, info in self.modules.items(): - yield (modname, modname, 'module', info[0], info[1], 0) - for refname, (docname, node_id, type) in self.objects.items(): - if type != 'module': # modules are already handled - yield (refname, refname, type, docname, node_id, 1) + for modname, mod in self.modules.items(): + yield (modname, modname, 'module', mod.docname, mod.node_id, 0) + for refname, obj in self.objects.items(): + if obj.objtype != 'module': # modules are already handled + yield (refname, refname, obj.objtype, obj.docname, obj.node_id, 1) def get_full_qualified_name(self, node: Element) -> str: modname = node.get('py:module') |