Skip to content

Commit

Permalink
py domain: Add :canonical: option
Browse files Browse the repository at this point in the history
  • Loading branch information
tk0miya committed May 3, 2020
1 parent 7520396 commit 5d9e50b
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 29 deletions.
2 changes: 2 additions & 0 deletions CHANGES
Expand Up @@ -101,6 +101,8 @@ Features added
* C++, parse trailing return types.
* #7143: py domain: Add ``:final:`` option to :rst:dir:`py:class:`,
:rst:dir:`py:exception:` and :rst:dir:`py:method:` directives
* #4826: py domain: Add ``:canonical:`` option to python directives to describe
the location where the object is defined
* #7582: napoleon: a type for attribute are represented like type annotation

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:: 3.1
.. 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:: 3.1
.. 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:: 3.1
.. 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:: 3.1
.. 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:: 3.1
.. 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)])
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

0 comments on commit 5d9e50b

Please sign in to comment.