From 729efd28b043da163059e14f2122c1e88a11b64b Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Mon, 2 Mar 2020 00:45:50 +0900 Subject: [PATCH] py domain: Generate node_id for objects in the right way --- CHANGES | 6 ++- sphinx/domains/python.py | 63 +++++++++++++------------- tests/test_build_html.py | 18 ++++---- tests/test_domain_py.py | 93 ++++++++++++++++++++------------------- tests/test_environment.py | 2 +- tests/test_intl.py | 2 +- 6 files changed, 96 insertions(+), 88 deletions(-) diff --git a/CHANGES b/CHANGES index 027065f29..14f2f6c5f 100644 --- a/CHANGES +++ b/CHANGES @@ -31,8 +31,10 @@ Incompatible changes node_id for cross reference * #7229: rst domain: Non intended behavior is removed such as ``numref_`` links to ``.. rst:role:: numref`` -* #6903: py domain: Internal data structure has changed. Now modules have - node_id for cross reference +* #6903: py domain: Internal data structure has changed. Both objects and + modules have node_id for cross reference +* #6903: py domain: Non intended behavior is removed such as ``say_hello_`` + links to ``.. py:function:: say_hello()`` Deprecated ---------- diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index d0b4a9b7f..e627c7045 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -358,19 +358,22 @@ class PyObject(ObjectDescription): signode: desc_signature) -> None: modname = self.options.get('module', self.env.ref_context.get('py:module')) fullname = (modname + '.' if modname else '') + name_cls[0] - # note target - if fullname not in self.state.document.ids: - signode['names'].append(fullname) - signode['ids'].append(fullname) - self.state.document.note_explicit_target(signode) + node_id = make_id(self.env, self.state.document, modname or '', name_cls[0]) + signode['ids'].append(node_id) - domain = cast(PythonDomain, self.env.get_domain('py')) - domain.note_object(fullname, self.objtype, location=signode) + # Assign old styled node_id(fullname) not to break old hyperlinks (if possible) + # Note: Will removed in Sphinx-5.0 (RemovedInSphinx50Warning) + if node_id != fullname and fullname not in self.state.document.ids: + signode['ids'].append(fullname) + + self.state.document.note_explicit_target(signode) + + domain = cast(PythonDomain, self.env.get_domain('py')) + domain.note_object(fullname, self.objtype, node_id, location=signode) indextext = self.get_index_text(modname, name_cls) if indextext: - self.indexnode['entries'].append(('single', indextext, - fullname, '', None)) + self.indexnode['entries'].append(('single', indextext, node_id, '', None)) def before_content(self) -> None: """Handle object nesting before content @@ -805,7 +808,7 @@ class PyModule(SphinxDirective): self.options.get('synopsis', ''), self.options.get('platform', ''), 'deprecated' in self.options) - domain.note_object(modname, 'module', location=target) + domain.note_object(modname, 'module', node_id, location=target) # the platform and synopsis aren't printed; in fact, they are only # used in the modindex currently @@ -1008,10 +1011,10 @@ class PythonDomain(Domain): ] @property - def objects(self) -> Dict[str, Tuple[str, str]]: - return self.data.setdefault('objects', {}) # fullname -> docname, objtype + def objects(self) -> Dict[str, Tuple[str, str, str]]: + return self.data.setdefault('objects', {}) # fullname -> docname, node_id, objtype - def note_object(self, name: str, objtype: str, location: Any = None) -> None: + def note_object(self, name: str, objtype: str, node_id: str, location: Any = None) -> None: """Note a python object for cross reference. .. versionadded:: 2.1 @@ -1021,7 +1024,7 @@ class PythonDomain(Domain): 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) + self.objects[name] = (self.env.docname, node_id, objtype) @property def modules(self) -> Dict[str, Tuple[str, str, str, str, bool]]: @@ -1036,7 +1039,7 @@ class PythonDomain(Domain): self.modules[name] = (self.env.docname, node_id, synopsis, platform, deprecated) def clear_doc(self, docname: str) -> None: - for fullname, (fn, _l) in list(self.objects.items()): + for fullname, (fn, _x, _x) in list(self.objects.items()): if fn == docname: del self.objects[fullname] for modname, (fn, _x, _x, _x, _y) in list(self.modules.items()): @@ -1045,16 +1048,16 @@ class PythonDomain(Domain): def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None: # XXX check duplicates? - for fullname, (fn, objtype) in otherdata['objects'].items(): + for fullname, (fn, node_id, objtype) in otherdata['objects'].items(): if fn in docnames: - self.objects[fullname] = (fn, objtype) + self.objects[fullname] = (fn, node_id, objtype) for modname, data in otherdata['modules'].items(): if data[0] in docnames: self.modules[modname] = data def find_obj(self, env: BuildEnvironment, modname: str, classname: str, name: str, type: str, searchmode: int = 0 - ) -> List[Tuple[str, Tuple[str, str]]]: + ) -> List[Tuple[str, Tuple[str, str, str]]]: """Find a Python object for "name", perhaps using the given module and/or classname. Returns a list of (name, object entry) tuples. """ @@ -1065,7 +1068,7 @@ class PythonDomain(Domain): if not name: return [] - matches = [] # type: List[Tuple[str, Tuple[str, str]]] + matches = [] # type: List[Tuple[str, Tuple[str, str, str]]] newname = None if searchmode == 1: @@ -1076,20 +1079,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][1] in objtypes: + if fullname in self.objects and self.objects[fullname][2] in objtypes: newname = fullname if not newname: if modname and modname + '.' + name in self.objects and \ - self.objects[modname + '.' + name][1] in objtypes: + self.objects[modname + '.' + name][2] in objtypes: newname = modname + '.' + name - elif name in self.objects and self.objects[name][1] in objtypes: + elif name in self.objects and self.objects[name][2] 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][1] in objtypes] + self.objects[oname][2] in objtypes] else: # NOTE: searching for exact match, object type is not considered if name in self.objects: @@ -1137,10 +1140,10 @@ class PythonDomain(Domain): type='ref', subtype='python', location=node) name, obj = matches[0] - if obj[1] == 'module': + if obj[2] == 'module': return self._make_module_refnode(builder, fromdocname, name, contnode) else: - return make_refnode(builder, fromdocname, obj[0], name, contnode, name) + return make_refnode(builder, fromdocname, obj[0], obj[1], contnode, name) def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, target: str, node: pending_xref, contnode: Element @@ -1152,13 +1155,13 @@ class PythonDomain(Domain): # always search in "refspecific" mode with the :any: role matches = self.find_obj(env, modname, clsname, target, None, 1) for name, obj in matches: - if obj[1] == 'module': + if obj[2] == 'module': results.append(('py:mod', self._make_module_refnode(builder, fromdocname, name, contnode))) else: - results.append(('py:' + self.role_for_objtype(obj[1]), - make_refnode(builder, fromdocname, obj[0], name, + results.append(('py:' + self.role_for_objtype(obj[2]), + make_refnode(builder, fromdocname, obj[0], obj[1], contnode, name))) return results @@ -1178,9 +1181,9 @@ class PythonDomain(Domain): 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, type) in self.objects.items(): + for refname, (docname, node_id, type) in self.objects.items(): if type != 'module': # modules are already handled - yield (refname, refname, type, docname, refname, 1) + yield (refname, refname, type, docname, node_id, 1) def get_full_qualified_name(self, node: Element) -> str: modname = node.get('py:module') diff --git a/tests/test_build_html.py b/tests/test_build_html.py index adb7462df..39cb3bf71 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -176,8 +176,8 @@ def test_html4_output(app, status, warning): r'-| |-'), ], 'autodoc.html': [ - (".//dl[@class='py class']/dt[@id='autodoc_target.Class']", ''), - (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em", r'\*\*kwds'), + (".//dl[@class='py class']/dt[@id='autodoc-target-class']", ''), + (".//dl[@class='py function']/dt[@id='autodoc-target-function']/em", r'\*\*kwds'), (".//dd/p", r'Return spam\.'), ], 'extapi.html': [ @@ -262,7 +262,7 @@ def test_html4_output(app, status, warning): (".//p", 'Always present'), # tests for ``any`` role (".//a[@href='#with']/span", 'headings'), - (".//a[@href='objects.html#func_without_body']/code/span", 'objects'), + (".//a[@href='objects.html#func-without-body']/code/span", 'objects'), # tests for numeric labels (".//a[@href='#id1'][@class='reference internal']/span", 'Testing various markup'), # tests for smartypants @@ -274,18 +274,18 @@ def test_html4_output(app, status, warning): (".//p", 'Il dit : « C’est “super” ! »'), ], 'objects.html': [ - (".//dt[@id='mod.Cls.meth1']", ''), - (".//dt[@id='errmod.Error']", ''), + (".//dt[@id='mod-cls-meth1']", ''), + (".//dt[@id='errmod-error']", ''), (".//dt/code", r'long\(parameter,\s* list\)'), (".//dt/code", 'another one'), - (".//a[@href='#mod.Cls'][@class='reference internal']", ''), + (".//a[@href='#mod-cls'][@class='reference internal']", ''), (".//dl[@class='std userdesc']", ''), (".//dt[@id='userdesc-myobj']", ''), (".//a[@href='#userdesc-myobj'][@class='reference internal']", ''), # docfields - (".//a[@class='reference internal'][@href='#TimeInt']/em", 'TimeInt'), - (".//a[@class='reference internal'][@href='#Time']", 'Time'), - (".//a[@class='reference internal'][@href='#errmod.Error']/strong", 'Error'), + (".//a[@class='reference internal'][@href='#timeint']/em", 'TimeInt'), + (".//a[@class='reference internal'][@href='#time']", 'Time'), + (".//a[@class='reference internal'][@href='#errmod-error']/strong", 'Error'), # C references (".//span[@class='pre']", 'CFunction()'), (".//a[@href='#c.Sphinx_DoSomething']", ''), diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 6d41a0ae7..218ded510 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -145,24 +145,24 @@ def test_domain_py_objects(app, status, warning): assert 'module_b.submodule' in modules assert 'module_b.submodule' in objects - assert objects['module_a.submodule.ModTopLevel'] == ('module', 'class') - assert objects['module_a.submodule.ModTopLevel.mod_child_1'] == ('module', 'method') - assert objects['module_a.submodule.ModTopLevel.mod_child_2'] == ('module', 'method') + assert objects['module_a.submodule.ModTopLevel'][2] == 'class' + assert objects['module_a.submodule.ModTopLevel.mod_child_1'][2] == 'method' + assert objects['module_a.submodule.ModTopLevel.mod_child_2'][2] == 'method' assert 'ModTopLevel.ModNoModule' not in objects - assert objects['ModNoModule'] == ('module', 'class') - assert objects['module_b.submodule.ModTopLevel'] == ('module', 'class') + assert objects['ModNoModule'][2] == 'class' + assert objects['module_b.submodule.ModTopLevel'][2] == 'class' - assert objects['TopLevel'] == ('roles', 'class') - assert objects['top_level'] == ('roles', 'method') - assert objects['NestedParentA'] == ('roles', 'class') - assert objects['NestedParentA.child_1'] == ('roles', 'method') - assert objects['NestedParentA.any_child'] == ('roles', 'method') - assert objects['NestedParentA.NestedChildA'] == ('roles', 'class') - assert objects['NestedParentA.NestedChildA.subchild_1'] == ('roles', 'method') - assert objects['NestedParentA.NestedChildA.subchild_2'] == ('roles', 'method') - assert objects['NestedParentA.child_2'] == ('roles', 'method') - assert objects['NestedParentB'] == ('roles', 'class') - assert objects['NestedParentB.child_1'] == ('roles', 'method') + assert objects['TopLevel'][2] == 'class' + assert objects['top_level'][2] == 'method' + assert objects['NestedParentA'][2] == 'class' + assert objects['NestedParentA.child_1'][2] == 'method' + assert objects['NestedParentA.any_child'][2] == 'method' + assert objects['NestedParentA.NestedChildA'][2] == 'class' + assert objects['NestedParentA.NestedChildA.subchild_1'][2] == 'method' + assert objects['NestedParentA.NestedChildA.subchild_2'][2] == 'method' + assert objects['NestedParentA.child_2'][2] == 'method' + assert objects['NestedParentB'][2] == 'class' + assert objects['NestedParentB.child_1'][2] == 'method' @pytest.mark.sphinx('html', testroot='domain-py') @@ -170,11 +170,11 @@ def test_resolve_xref_for_properties(app, status, warning): app.builder.build_all() content = (app.outdir / 'module.html').read_text() - assert ('Link to ' '' 'prop attribute' in content) - assert ('Link to ' '' 'prop method' in content) @@ -191,17 +191,20 @@ def test_domain_py_find_obj(app, status, warning): assert (find_obj(None, None, 'NONEXISTANT', 'class') == []) assert (find_obj(None, None, 'NestedParentA', 'class') == - [('NestedParentA', ('roles', 'class'))]) + [('NestedParentA', ('roles', 'nestedparenta', 'class'))]) assert (find_obj(None, None, 'NestedParentA.NestedChildA', 'class') == - [('NestedParentA.NestedChildA', ('roles', 'class'))]) + [('NestedParentA.NestedChildA', ('roles', 'nestedparenta-nestedchilda', 'class'))]) assert (find_obj(None, 'NestedParentA', 'NestedChildA', 'class') == - [('NestedParentA.NestedChildA', ('roles', 'class'))]) + [('NestedParentA.NestedChildA', ('roles', 'nestedparenta-nestedchilda', 'class'))]) assert (find_obj(None, None, 'NestedParentA.NestedChildA.subchild_1', 'meth') == - [('NestedParentA.NestedChildA.subchild_1', ('roles', 'method'))]) + [('NestedParentA.NestedChildA.subchild_1', + ('roles', 'nestedparenta-nestedchilda-subchild-1', 'method'))]) assert (find_obj(None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth') == - [('NestedParentA.NestedChildA.subchild_1', ('roles', 'method'))]) + [('NestedParentA.NestedChildA.subchild_1', + ('roles', 'nestedparenta-nestedchilda-subchild-1', 'method'))]) assert (find_obj(None, 'NestedParentA.NestedChildA', 'subchild_1', 'meth') == - [('NestedParentA.NestedChildA.subchild_1', ('roles', 'method'))]) + [('NestedParentA.NestedChildA.subchild_1', + ('roles', 'nestedparenta-nestedchilda-subchild-1', 'method'))]) def test_get_full_qualified_name(): @@ -402,7 +405,7 @@ def test_pydata(app): [desc, ([desc_signature, desc_name, "var"], [desc_content, ()])])) assert 'var' in domain.objects - assert domain.objects['var'] == ('index', 'data') + assert domain.objects['var'] == ('index', 'var', 'data') def test_pyfunction(app): @@ -421,9 +424,9 @@ def test_pyfunction(app): [desc_parameterlist, ()])], [desc_content, ()])])) assert 'func1' in domain.objects - assert domain.objects['func1'] == ('index', 'function') + assert domain.objects['func1'] == ('index', 'func1', 'function') assert 'func2' in domain.objects - assert domain.objects['func2'] == ('index', 'function') + assert domain.objects['func2'] == ('index', 'func2', 'function') def test_pymethod_options(app): @@ -460,61 +463,61 @@ def test_pymethod_options(app): # method assert_node(doctree[1][1][0], addnodes.index, - entries=[('single', 'meth1() (Class method)', 'Class.meth1', '', None)]) + entries=[('single', 'meth1() (Class method)', 'class-meth1', '', None)]) assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "meth1"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth1' in domain.objects - assert domain.objects['Class.meth1'] == ('index', 'method') + assert domain.objects['Class.meth1'] == ('index', 'class-meth1', 'method') # :classmethod: assert_node(doctree[1][1][2], addnodes.index, - entries=[('single', 'meth2() (Class class method)', 'Class.meth2', '', None)]) + entries=[('single', 'meth2() (Class class method)', 'class-meth2', '', None)]) assert_node(doctree[1][1][3], ([desc_signature, ([desc_annotation, "classmethod "], [desc_name, "meth2"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth2' in domain.objects - assert domain.objects['Class.meth2'] == ('index', 'method') + assert domain.objects['Class.meth2'] == ('index', 'class-meth2', 'method') # :staticmethod: assert_node(doctree[1][1][4], addnodes.index, - entries=[('single', 'meth3() (Class static method)', 'Class.meth3', '', None)]) + entries=[('single', 'meth3() (Class static method)', 'class-meth3', '', None)]) assert_node(doctree[1][1][5], ([desc_signature, ([desc_annotation, "static "], [desc_name, "meth3"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth3' in domain.objects - assert domain.objects['Class.meth3'] == ('index', 'method') + assert domain.objects['Class.meth3'] == ('index', 'class-meth3', 'method') # :async: assert_node(doctree[1][1][6], addnodes.index, - entries=[('single', 'meth4() (Class method)', 'Class.meth4', '', None)]) + entries=[('single', 'meth4() (Class method)', 'class-meth4', '', None)]) assert_node(doctree[1][1][7], ([desc_signature, ([desc_annotation, "async "], [desc_name, "meth4"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth4' in domain.objects - assert domain.objects['Class.meth4'] == ('index', 'method') + assert domain.objects['Class.meth4'] == ('index', 'class-meth4', 'method') # :property: assert_node(doctree[1][1][8], addnodes.index, - entries=[('single', 'meth5() (Class property)', 'Class.meth5', '', None)]) + entries=[('single', 'meth5() (Class property)', 'class-meth5', '', None)]) assert_node(doctree[1][1][9], ([desc_signature, ([desc_annotation, "property "], [desc_name, "meth5"])], [desc_content, ()])) assert 'Class.meth5' in domain.objects - assert domain.objects['Class.meth5'] == ('index', 'method') + assert domain.objects['Class.meth5'] == ('index', 'class-meth5', 'method') # :abstractmethod: assert_node(doctree[1][1][10], addnodes.index, - entries=[('single', 'meth6() (Class method)', 'Class.meth6', '', None)]) + entries=[('single', 'meth6() (Class method)', 'class-meth6', '', None)]) assert_node(doctree[1][1][11], ([desc_signature, ([desc_annotation, "abstract "], [desc_name, "meth6"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth6' in domain.objects - assert domain.objects['Class.meth6'] == ('index', 'method') + assert domain.objects['Class.meth6'] == ('index', 'class-meth6', 'method') def test_pyclassmethod(app): @@ -529,13 +532,13 @@ def test_pyclassmethod(app): [desc_content, (addnodes.index, desc)])])) assert_node(doctree[1][1][0], addnodes.index, - entries=[('single', 'meth() (Class class method)', 'Class.meth', '', None)]) + entries=[('single', 'meth() (Class class method)', 'class-meth', '', None)]) assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, "classmethod "], [desc_name, "meth"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth' in domain.objects - assert domain.objects['Class.meth'] == ('index', 'method') + assert domain.objects['Class.meth'] == ('index', 'class-meth', 'method') def test_pystaticmethod(app): @@ -550,13 +553,13 @@ def test_pystaticmethod(app): [desc_content, (addnodes.index, desc)])])) assert_node(doctree[1][1][0], addnodes.index, - entries=[('single', 'meth() (Class static method)', 'Class.meth', '', None)]) + entries=[('single', 'meth() (Class static method)', 'class-meth', '', None)]) assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, "static "], [desc_name, "meth"], [desc_parameterlist, ()])], [desc_content, ()])) assert 'Class.meth' in domain.objects - assert domain.objects['Class.meth'] == ('index', 'method') + assert domain.objects['Class.meth'] == ('index', 'class-meth', 'method') def test_pyattribute(app): @@ -573,13 +576,13 @@ def test_pyattribute(app): [desc_content, (addnodes.index, desc)])])) assert_node(doctree[1][1][0], addnodes.index, - entries=[('single', 'attr (Class attribute)', 'Class.attr', '', None)]) + entries=[('single', 'attr (Class attribute)', 'class-attr', '', None)]) assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "attr"], [desc_annotation, ": str"], [desc_annotation, " = ''"])], [desc_content, ()])) assert 'Class.attr' in domain.objects - assert domain.objects['Class.attr'] == ('index', 'attribute') + assert domain.objects['Class.attr'] == ('index', 'class-attr', 'attribute') @pytest.mark.sphinx(freshenv=True) diff --git a/tests/test_environment.py b/tests/test_environment.py index 9324f36f0..4b1f8e77e 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -84,7 +84,7 @@ def test_object_inventory(app): refs = app.env.domaindata['py']['objects'] assert 'func_without_module' in refs - assert refs['func_without_module'] == ('objects', 'function') + assert refs['func_without_module'] == ('objects', 'func-without-module', 'function') assert 'func_without_module2' in refs assert 'mod.func_in_module' in refs assert 'mod.Cls' in refs diff --git a/tests/test_intl.py b/tests/test_intl.py index 0e7dd4f62..ee96490a4 100644 --- a/tests/test_intl.py +++ b/tests/test_intl.py @@ -870,7 +870,7 @@ def test_xml_refs_in_python_domain(app): assert_elem( para0[0], ['SEE THIS DECORATOR:', 'sensitive_variables()', '.'], - ['sensitive.sensitive_variables']) + ['sensitive-sensitive-variables']) @sphinx_intl