diff --git a/CHANGES b/CHANGES index e9c8ae8ff..39e62901e 100644 --- a/CHANGES +++ b/CHANGES @@ -50,7 +50,12 @@ Features added -------------- * Add a helper class ``sphinx.transforms.post_transforms.SphinxPostTransform`` -* Add a helper method ``SphinxDirective.set_source_info()`` +* Add helper methods + + - ``PythonDomain.note_module()`` + - ``PythonDomain.note_object()`` + - ``SphinxDirective.set_source_info()`` + * #6180: Support ``--keep-going`` with BuildDoc setup command * ``math`` directive now supports ``:class:`` option * todo: ``todo`` directive now supports ``:name:`` option diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index b92794ddc..29c0c2536 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -9,6 +9,7 @@ """ import re +from typing import cast from docutils import nodes from docutils.parsers.rst import directives @@ -331,15 +332,9 @@ class PyObject(ObjectDescription): signode['ids'].append(fullname) signode['first'] = (not self.names) self.state.document.note_explicit_target(signode) - objects = self.env.domaindata['py']['objects'] - if fullname in objects: - self.state_machine.reporter.warning( - 'duplicate object description of %s, ' % fullname + - 'other instance in ' + - self.env.doc2path(objects[fullname][0]) + - ', use :noindex: for one of them', - line=self.lineno) - objects[fullname] = (self.env.docname, self.objtype) + + domain = cast(PythonDomain, self.env.get_domain('py')) + domain.note_object(fullname, self.objtype) indextext = self.get_index_text(modname, name_cls) if indextext: @@ -582,18 +577,20 @@ class PyModule(SphinxDirective): def run(self): # type: () -> List[nodes.Node] + domain = cast(PythonDomain, self.env.get_domain('py')) + modname = self.arguments[0].strip() noindex = 'noindex' in self.options self.env.ref_context['py:module'] = modname ret = [] # type: List[nodes.Node] if not noindex: - self.env.domaindata['py']['modules'][modname] = (self.env.docname, - self.options.get('synopsis', ''), - self.options.get('platform', ''), - 'deprecated' in self.options) - # make a duplicate entry in 'objects' to facilitate searching for - # the module in PythonDomain.find_obj() - self.env.domaindata['py']['objects'][modname] = (self.env.docname, 'module') + # note module to the domain + domain.note_module(modname, + self.options.get('synopsis', ''), + self.options.get('platform', ''), + 'deprecated' in self.options) + domain.note_object(modname, 'module') + targetnode = nodes.target('', '', ids=['module-' + modname], ismod=True) self.state.document.note_explicit_target(targetnode) @@ -776,24 +773,55 @@ class PythonDomain(Domain): PythonModuleIndex, ] + @property + def objects(self): + # type: () -> Dict[str, Tuple[str, str]] + return self.data.setdefault('objects', {}) # fullname -> docname, objtype + + def note_object(self, name, objtype, location=None): + # type: (str, str, Any) -> None + """Note a python object for cross reference. + + .. versionadded:: 2.1 + """ + if name in self.objects: + docname = self.objects[name][0] + 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, objtype) + + @property + def modules(self): + # type: () -> Dict[str, Tuple[str, str, str, bool]] + return self.data.setdefault('modules', {}) # modname -> docname, synopsis, platform, deprecated # NOQA + + def note_module(self, name, synopsis, platform, deprecated): + # type: (str, str, str, bool) -> None + """Note a python module for cross reference. + + .. versionadded:: 2.1 + """ + self.modules[name] = (self.env.docname, synopsis, platform, deprecated) + def clear_doc(self, docname): # type: (str) -> None - for fullname, (fn, _l) in list(self.data['objects'].items()): + for fullname, (fn, _l) in list(self.objects.items()): if fn == docname: - del self.data['objects'][fullname] - for modname, (fn, _x, _x, _x) in list(self.data['modules'].items()): + del self.objects[fullname] + for modname, (fn, _x, _x, _y) in list(self.modules.items()): if fn == docname: - del self.data['modules'][modname] + del self.modules[modname] def merge_domaindata(self, docnames, otherdata): # type: (List[str], Dict) -> None # XXX check duplicates? for fullname, (fn, objtype) in otherdata['objects'].items(): if fn in docnames: - self.data['objects'][fullname] = (fn, objtype) + self.objects[fullname] = (fn, objtype) for modname, data in otherdata['modules'].items(): if data[0] in docnames: - self.data['modules'][modname] = data + self.modules[modname] = data def find_obj(self, env, modname, classname, name, type, searchmode=0): # type: (BuildEnvironment, str, str, str, str, int) -> List[Tuple[str, Any]] @@ -807,7 +835,6 @@ class PythonDomain(Domain): if not name: return [] - objects = self.data['objects'] matches = [] # type: List[Tuple[str, Any]] newname = None @@ -819,44 +846,44 @@ class PythonDomain(Domain): if objtypes is not None: if modname and classname: fullname = modname + '.' + classname + '.' + name - if fullname in objects and objects[fullname][1] in objtypes: + if fullname in self.objects and self.objects[fullname][1] in objtypes: newname = fullname if not newname: - if modname and modname + '.' + name in objects and \ - objects[modname + '.' + name][1] in objtypes: + if modname and modname + '.' + name in self.objects and \ + self.objects[modname + '.' + name][1] in objtypes: newname = modname + '.' + name - elif name in objects and objects[name][1] in objtypes: + elif name in self.objects and self.objects[name][1] in objtypes: newname = name else: # "fuzzy" searching mode searchname = '.' + name - matches = [(oname, objects[oname]) for oname in objects + matches = [(oname, self.objects[oname]) for oname in self.objects if oname.endswith(searchname) and - objects[oname][1] in objtypes] + self.objects[oname][1] in objtypes] else: # NOTE: searching for exact match, object type is not considered - if name in objects: + if name in self.objects: newname = name elif type == 'mod': # only exact matches allowed for modules return [] - elif classname and classname + '.' + name in objects: + elif classname and classname + '.' + name in self.objects: newname = classname + '.' + name - elif modname and modname + '.' + name in objects: + elif modname and modname + '.' + name in self.objects: newname = modname + '.' + name elif modname and classname and \ - modname + '.' + classname + '.' + name in objects: + modname + '.' + classname + '.' + name in self.objects: newname = modname + '.' + classname + '.' + name # special case: builtin exceptions have module "exceptions" set elif type == 'exc' and '.' not in name and \ - 'exceptions.' + name in objects: + 'exceptions.' + name in self.objects: newname = 'exceptions.' + name # special case: object methods elif type in ('func', 'meth') and '.' not in name and \ - 'object.' + name in objects: + 'object.' + name in self.objects: newname = 'object.' + name if newname is not None: - matches.append((newname, objects[newname])) + matches.append((newname, self.objects[newname])) return matches def resolve_xref(self, env, fromdocname, builder, @@ -903,7 +930,7 @@ class PythonDomain(Domain): def _make_module_refnode(self, builder, fromdocname, name, contnode): # type: (Builder, str, str, nodes.Node) -> nodes.Element # get additional info for modules - docname, synopsis, platform, deprecated = self.data['modules'][name] + docname, synopsis, platform, deprecated = self.modules[name] title = name if synopsis: title += ': ' + synopsis @@ -916,9 +943,9 @@ class PythonDomain(Domain): def get_objects(self): # type: () -> Iterator[Tuple[str, str, str, str, str, int]] - for modname, info in self.data['modules'].items(): + for modname, info in self.modules.items(): yield (modname, modname, 'module', info[0], 'module-' + modname, 0) - for refname, (docname, type) in self.data['objects'].items(): + for refname, (docname, type) in self.objects.items(): if type != 'module': # modules are already handled yield (refname, refname, type, docname, refname, 1)