Merge pull request #7463 from tk0miya/4944_canonical_name

Support canonical name of python objects
This commit is contained in:
Takeshi KOMIYA 2020-05-04 15:05:44 +09:00 committed by GitHub
commit 4ad466c7a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 104 additions and 29 deletions

View File

@ -10,6 +10,9 @@ Dependencies
Incompatible changes
--------------------
* #4826: py domain: The structure of python objects is changed. A boolean value
is added to indicate that the python object is canonical one
Deprecated
----------
@ -23,6 +26,9 @@ Deprecated
Features added
--------------
* #4826: py domain: Add ``:canonical:`` option to python directives to describe
the location where the object is defined
Bugs fixed
----------

View File

@ -189,6 +189,14 @@ The following directives are provided for module and class contents:
.. versionadded:: 2.1
.. rst:directive:option:: canonical
:type: full qualified name including module name
Describe the location where the object is defined if the object is
imported from other modules
.. versionadded:: 4.0
.. rst:directive:: .. py:data:: name
Describes global data in a module, including both variables and values used
@ -207,6 +215,14 @@ The following directives are provided for module and class contents:
.. versionadded:: 2.4
.. rst:directive:option:: canonical
:type: full qualified name including module name
Describe the location where the object is defined if the object is
imported from other modules
.. versionadded:: 4.0
.. rst:directive:: .. py:exception:: name
Describes an exception class. The signature can, but need not include
@ -246,6 +262,14 @@ The following directives are provided for module and class contents:
.. rubric:: options
.. rst:directive:option:: canonical
:type: full qualified name including module name
Describe the location where the object is defined if the object is
imported from other modules
.. versionadded:: 4.0
.. rst:directive:option:: final
:type: no value
@ -271,6 +295,14 @@ The following directives are provided for module and class contents:
.. versionadded:: 2.4
.. rst:directive:option:: canonical
:type: full qualified name including module name
Describe the location where the object is defined if the object is
imported from other modules
.. versionadded:: 4.0
.. rst:directive:: .. py:method:: name(parameters)
Describes an object method. The parameters should not include the ``self``
@ -294,6 +326,14 @@ The following directives are provided for module and class contents:
.. versionadded:: 2.1
.. rst:directive:option:: canonical
:type: full qualified name including module name
Describe the location where the object is defined if the object is
imported from other modules
.. versionadded:: 4.0
.. rst:directive:option:: classmethod
:type: no value

View File

@ -65,7 +65,8 @@ pairindextypes = {
ObjectEntry = NamedTuple('ObjectEntry', [('docname', str),
('node_id', str),
('objtype', str)])
('objtype', str),
('canonical', bool)])
ModuleEntry = NamedTuple('ModuleEntry', [('docname', str),
('node_id', str),
('synopsis', str),
@ -312,6 +313,7 @@ class PyObject(ObjectDescription):
option_spec = {
'noindex': directives.flag,
'module': directives.unchanged,
'canonical': directives.unchanged,
'annotation': directives.unchanged,
}
@ -453,6 +455,11 @@ class PyObject(ObjectDescription):
domain = cast(PythonDomain, self.env.get_domain('py'))
domain.note_object(fullname, self.objtype, node_id, location=signode)
canonical_name = self.options.get('canonical')
if canonical_name:
domain.note_object(canonical_name, self.objtype, node_id, canonical=True,
location=signode)
indextext = self.get_index_text(modname, name_cls)
if indextext:
self.indexnode['entries'].append(('single', indextext, node_id, '', None))
@ -1037,7 +1044,8 @@ 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, location: Any = None) -> None:
def note_object(self, name: str, objtype: str, node_id: str,
canonical: bool = False, location: Any = None) -> None:
"""Note a python object for cross reference.
.. versionadded:: 2.1
@ -1047,7 +1055,7 @@ class PythonDomain(Domain):
logger.warning(__('duplicate object description of %s, '
'other instance in %s, use :noindex: for one of them'),
name, other.docname, location=location)
self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype)
self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype, canonical)
@property
def modules(self) -> Dict[str, ModuleEntry]:
@ -1200,7 +1208,11 @@ class PythonDomain(Domain):
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)
if obj.canonical:
# canonical names are not full-text searchable.
yield (refname, refname, obj.objtype, obj.docname, obj.node_id, -1)
else:
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')
@ -1246,7 +1258,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
return {
'version': 'builtin',
'env_version': 2,
'env_version': 3,
'parallel_read_safe': True,
'parallel_write_safe': True,
}

View File

@ -192,20 +192,22 @@ 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', 'NestedParentA', 'class'))])
[('NestedParentA', ('roles', 'NestedParentA', 'class', False))])
assert (find_obj(None, None, 'NestedParentA.NestedChildA', 'class') ==
[('NestedParentA.NestedChildA', ('roles', 'NestedParentA.NestedChildA', 'class'))])
[('NestedParentA.NestedChildA',
('roles', 'NestedParentA.NestedChildA', 'class', False))])
assert (find_obj(None, 'NestedParentA', 'NestedChildA', 'class') ==
[('NestedParentA.NestedChildA', ('roles', 'NestedParentA.NestedChildA', 'class'))])
[('NestedParentA.NestedChildA',
('roles', 'NestedParentA.NestedChildA', 'class', False))])
assert (find_obj(None, None, 'NestedParentA.NestedChildA.subchild_1', 'meth') ==
[('NestedParentA.NestedChildA.subchild_1',
('roles', 'NestedParentA.NestedChildA.subchild_1', 'method'))])
('roles', 'NestedParentA.NestedChildA.subchild_1', 'method', False))])
assert (find_obj(None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth') ==
[('NestedParentA.NestedChildA.subchild_1',
('roles', 'NestedParentA.NestedChildA.subchild_1', 'method'))])
('roles', 'NestedParentA.NestedChildA.subchild_1', 'method', False))])
assert (find_obj(None, 'NestedParentA.NestedChildA', 'subchild_1', 'meth') ==
[('NestedParentA.NestedChildA.subchild_1',
('roles', 'NestedParentA.NestedChildA.subchild_1', 'method'))])
('roles', 'NestedParentA.NestedChildA.subchild_1', 'method', False))])
def test_get_full_qualified_name():
@ -464,7 +466,7 @@ def test_pydata(app):
[desc, ([desc_signature, desc_name, "var"],
[desc_content, ()])]))
assert 'var' in domain.objects
assert domain.objects['var'] == ('index', 'var', 'data')
assert domain.objects['var'] == ('index', 'var', 'data', False)
def test_pyfunction(app):
@ -494,9 +496,9 @@ def test_pyfunction(app):
entries=[('single', 'func2() (in module example)', 'example.func2', '', None)])
assert 'func1' in domain.objects
assert domain.objects['func1'] == ('index', 'func1', 'function')
assert domain.objects['func1'] == ('index', 'func1', 'function', False)
assert 'example.func2' in domain.objects
assert domain.objects['example.func2'] == ('index', 'example.func2', 'function')
assert domain.objects['example.func2'] == ('index', 'example.func2', 'function', False)
def test_pyclass_options(app):
@ -518,13 +520,13 @@ def test_pyclass_options(app):
assert_node(doctree[0], addnodes.index,
entries=[('single', 'Class1 (built-in class)', 'Class1', '', None)])
assert 'Class1' in domain.objects
assert domain.objects['Class1'] == ('index', 'Class1', 'class')
assert domain.objects['Class1'] == ('index', 'Class1', 'class', False)
# :final:
assert_node(doctree[2], addnodes.index,
entries=[('single', 'Class2 (built-in class)', 'Class2', '', None)])
assert 'Class2' in domain.objects
assert domain.objects['Class2'] == ('index', 'Class2', 'class')
assert domain.objects['Class2'] == ('index', 'Class2', 'class', False)
def test_pymethod_options(app):
@ -570,7 +572,7 @@ def test_pymethod_options(app):
[desc_parameterlist, ()])],
[desc_content, ()]))
assert 'Class.meth1' in domain.objects
assert domain.objects['Class.meth1'] == ('index', 'Class.meth1', 'method')
assert domain.objects['Class.meth1'] == ('index', 'Class.meth1', 'method', False)
# :classmethod:
assert_node(doctree[1][1][2], addnodes.index,
@ -580,7 +582,7 @@ def test_pymethod_options(app):
[desc_parameterlist, ()])],
[desc_content, ()]))
assert 'Class.meth2' in domain.objects
assert domain.objects['Class.meth2'] == ('index', 'Class.meth2', 'method')
assert domain.objects['Class.meth2'] == ('index', 'Class.meth2', 'method', False)
# :staticmethod:
assert_node(doctree[1][1][4], addnodes.index,
@ -590,7 +592,7 @@ def test_pymethod_options(app):
[desc_parameterlist, ()])],
[desc_content, ()]))
assert 'Class.meth3' in domain.objects
assert domain.objects['Class.meth3'] == ('index', 'Class.meth3', 'method')
assert domain.objects['Class.meth3'] == ('index', 'Class.meth3', 'method', False)
# :async:
assert_node(doctree[1][1][6], addnodes.index,
@ -600,7 +602,7 @@ def test_pymethod_options(app):
[desc_parameterlist, ()])],
[desc_content, ()]))
assert 'Class.meth4' in domain.objects
assert domain.objects['Class.meth4'] == ('index', 'Class.meth4', 'method')
assert domain.objects['Class.meth4'] == ('index', 'Class.meth4', 'method', False)
# :property:
assert_node(doctree[1][1][8], addnodes.index,
@ -609,7 +611,7 @@ def test_pymethod_options(app):
[desc_name, "meth5"])],
[desc_content, ()]))
assert 'Class.meth5' in domain.objects
assert domain.objects['Class.meth5'] == ('index', 'Class.meth5', 'method')
assert domain.objects['Class.meth5'] == ('index', 'Class.meth5', 'method', False)
# :abstractmethod:
assert_node(doctree[1][1][10], addnodes.index,
@ -619,7 +621,7 @@ def test_pymethod_options(app):
[desc_parameterlist, ()])],
[desc_content, ()]))
assert 'Class.meth6' in domain.objects
assert domain.objects['Class.meth6'] == ('index', 'Class.meth6', 'method')
assert domain.objects['Class.meth6'] == ('index', 'Class.meth6', 'method', False)
# :final:
assert_node(doctree[1][1][12], addnodes.index,
@ -629,7 +631,7 @@ def test_pymethod_options(app):
[desc_parameterlist, ()])],
[desc_content, ()]))
assert 'Class.meth7' in domain.objects
assert domain.objects['Class.meth7'] == ('index', 'Class.meth7', 'method')
assert domain.objects['Class.meth7'] == ('index', 'Class.meth7', 'method', False)
def test_pyclassmethod(app):
@ -650,7 +652,7 @@ def test_pyclassmethod(app):
[desc_parameterlist, ()])],
[desc_content, ()]))
assert 'Class.meth' in domain.objects
assert domain.objects['Class.meth'] == ('index', 'Class.meth', 'method')
assert domain.objects['Class.meth'] == ('index', 'Class.meth', 'method', False)
def test_pystaticmethod(app):
@ -671,7 +673,7 @@ def test_pystaticmethod(app):
[desc_parameterlist, ()])],
[desc_content, ()]))
assert 'Class.meth' in domain.objects
assert domain.objects['Class.meth'] == ('index', 'Class.meth', 'method')
assert domain.objects['Class.meth'] == ('index', 'Class.meth', 'method', False)
def test_pyattribute(app):
@ -694,7 +696,7 @@ def test_pyattribute(app):
[desc_annotation, " = ''"])],
[desc_content, ()]))
assert 'Class.attr' in domain.objects
assert domain.objects['Class.attr'] == ('index', 'Class.attr', 'attribute')
assert domain.objects['Class.attr'] == ('index', 'Class.attr', 'attribute', False)
def test_pydecorator_signature(app):
@ -709,7 +711,7 @@ def test_pydecorator_signature(app):
domain="py", objtype="function", noindex=False)
assert 'deco' in domain.objects
assert domain.objects['deco'] == ('index', 'deco', 'function')
assert domain.objects['deco'] == ('index', 'deco', 'function', False)
def test_pydecoratormethod_signature(app):
@ -724,7 +726,22 @@ def test_pydecoratormethod_signature(app):
domain="py", objtype="method", noindex=False)
assert 'deco' in domain.objects
assert domain.objects['deco'] == ('index', 'deco', 'method')
assert domain.objects['deco'] == ('index', 'deco', 'method', False)
def test_canonical(app):
text = (".. py:class:: io.StringIO\n"
" :canonical: _io.StringIO")
domain = app.env.get_domain('py')
doctree = restructuredtext.parse(app, text)
assert_node(doctree, (addnodes.index,
[desc, ([desc_signature, ([desc_annotation, "class "],
[desc_addname, "io."],
[desc_name, "StringIO"])],
desc_content)]))
assert 'io.StringIO' in domain.objects
assert domain.objects['io.StringIO'] == ('index', 'io.StringIO', 'class', False)
assert domain.objects['_io.StringIO'] == ('index', 'io.StringIO', 'class', True)
@pytest.mark.sphinx(freshenv=True)

View File

@ -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', 'func_without_module', 'function')
assert refs['func_without_module'] == ('objects', 'func_without_module', 'function', False)
assert 'func_without_module2' in refs
assert 'mod.func_in_module' in refs
assert 'mod.Cls' in refs