Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support canonical name of python objects #7463

Merged
merged 1 commit into from May 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES
Expand Up @@ -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
----------

Expand All @@ -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
----------

Expand Down
40 changes: 40 additions & 0 deletions doc/usage/restructuredtext/domains.rst
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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``
Expand All @@ -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

Expand Down
22 changes: 17 additions & 5 deletions sphinx/domains/python.py
Expand Up @@ -65,7 +65,8 @@

ObjectEntry = NamedTuple('ObjectEntry', [('docname', str),
('node_id', str),
('objtype', str)])
('objtype', str),
('canonical', bool)])
tk0miya marked this conversation as resolved.
Show resolved Hide resolved
ModuleEntry = NamedTuple('ModuleEntry', [('docname', str),
('node_id', str),
('synopsis', str),
Expand Down Expand Up @@ -312,6 +313,7 @@ class PyObject(ObjectDescription):
option_spec = {
'noindex': directives.flag,
'module': directives.unchanged,
'canonical': directives.unchanged,
'annotation': directives.unchanged,
}

Expand Down Expand Up @@ -453,6 +455,11 @@ def add_target_and_index(self, name_cls: Tuple[str, str], sig: str,
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))
Expand Down Expand Up @@ -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
Expand All @@ -1047,7 +1055,7 @@ def note_object(self, name: str, objtype: str, node_id: str, location: Any = Non
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]:
Expand Down Expand Up @@ -1200,7 +1208,11 @@ def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]:
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')
Expand Down Expand Up @@ -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,
}
63 changes: 40 additions & 23 deletions tests/test_domain_py.py
Expand Up @@ -192,20 +192,22 @@ def find_obj(modname, prefix, obj_name, obj_type, searchmode=0):

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():
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_environment.py
Expand Up @@ -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
Expand Down