mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Close #7341: py domain: type annotations are converted to cross refs
This commit is contained in:
parent
dd85cb6588
commit
b0a6b3f285
1
CHANGES
1
CHANGES
@ -83,6 +83,7 @@ Features added
|
|||||||
* #6417: py domain: Allow to make a style for arguments of functions and methods
|
* #6417: py domain: Allow to make a style for arguments of functions and methods
|
||||||
* #7238, #7239: py domain: Emit a warning on describing a python object if the
|
* #7238, #7239: py domain: Emit a warning on describing a python object if the
|
||||||
entry is already added as the same name
|
entry is already added as the same name
|
||||||
|
* #7341: py domain: type annotations in singature are converted to cross refs
|
||||||
* Support priority of event handlers. For more detail, see
|
* Support priority of event handlers. For more detail, see
|
||||||
:py:meth:`.Sphinx.connect()`
|
:py:meth:`.Sphinx.connect()`
|
||||||
* #3077: Implement the scoping for :rst:dir:`productionlist` as indicated
|
* #3077: Implement the scoping for :rst:dir:`productionlist` as indicated
|
||||||
|
@ -30,6 +30,7 @@ from sphinx.directives import ObjectDescription
|
|||||||
from sphinx.domains import Domain, ObjType, Index, IndexEntry
|
from sphinx.domains import Domain, ObjType, Index, IndexEntry
|
||||||
from sphinx.environment import BuildEnvironment
|
from sphinx.environment import BuildEnvironment
|
||||||
from sphinx.locale import _, __
|
from sphinx.locale import _, __
|
||||||
|
from sphinx.pycode.ast import ast, parse as ast_parse
|
||||||
from sphinx.roles import XRefRole
|
from sphinx.roles import XRefRole
|
||||||
from sphinx.util import logging
|
from sphinx.util import logging
|
||||||
from sphinx.util.docfields import Field, GroupedField, TypedField
|
from sphinx.util.docfields import Field, GroupedField, TypedField
|
||||||
@ -67,6 +68,58 @@ pairindextypes = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_annotation(annotation: str) -> List[Node]:
|
||||||
|
"""Parse type annotation."""
|
||||||
|
def make_xref(text: str) -> addnodes.pending_xref:
|
||||||
|
return pending_xref('', nodes.Text(text),
|
||||||
|
refdomain='py', reftype='class', reftarget=text)
|
||||||
|
|
||||||
|
def unparse(node: ast.AST) -> List[Node]:
|
||||||
|
if isinstance(node, ast.Attribute):
|
||||||
|
return [nodes.Text("%s.%s" % (unparse(node.value)[0], node.attr))]
|
||||||
|
elif isinstance(node, ast.Expr):
|
||||||
|
return unparse(node.value)
|
||||||
|
elif isinstance(node, ast.Index):
|
||||||
|
return unparse(node.value)
|
||||||
|
elif isinstance(node, ast.List):
|
||||||
|
result = [addnodes.desc_sig_punctuation('', '[')] # type: List[Node]
|
||||||
|
for elem in node.elts:
|
||||||
|
result.extend(unparse(elem))
|
||||||
|
result.append(addnodes.desc_sig_punctuation('', ', '))
|
||||||
|
result.pop()
|
||||||
|
result.append(addnodes.desc_sig_punctuation('', ']'))
|
||||||
|
return result
|
||||||
|
elif isinstance(node, ast.Module):
|
||||||
|
return sum((unparse(e) for e in node.body), [])
|
||||||
|
elif isinstance(node, ast.Name):
|
||||||
|
return [nodes.Text(node.id)]
|
||||||
|
elif isinstance(node, ast.Subscript):
|
||||||
|
result = unparse(node.value)
|
||||||
|
result.append(addnodes.desc_sig_punctuation('', '['))
|
||||||
|
result.extend(unparse(node.slice))
|
||||||
|
result.append(addnodes.desc_sig_punctuation('', ']'))
|
||||||
|
return result
|
||||||
|
elif isinstance(node, ast.Tuple):
|
||||||
|
result = []
|
||||||
|
for elem in node.elts:
|
||||||
|
result.extend(unparse(elem))
|
||||||
|
result.append(addnodes.desc_sig_punctuation('', ', '))
|
||||||
|
result.pop()
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
raise SyntaxError # unsupported syntax
|
||||||
|
|
||||||
|
try:
|
||||||
|
tree = ast_parse(annotation)
|
||||||
|
result = unparse(tree)
|
||||||
|
for i, node in enumerate(result):
|
||||||
|
if isinstance(node, nodes.Text):
|
||||||
|
result[i] = make_xref(str(node))
|
||||||
|
return result
|
||||||
|
except SyntaxError:
|
||||||
|
return [make_xref(annotation)]
|
||||||
|
|
||||||
|
|
||||||
def _parse_arglist(arglist: str) -> addnodes.desc_parameterlist:
|
def _parse_arglist(arglist: str) -> addnodes.desc_parameterlist:
|
||||||
"""Parse a list of arguments using AST parser"""
|
"""Parse a list of arguments using AST parser"""
|
||||||
params = addnodes.desc_parameterlist(arglist)
|
params = addnodes.desc_parameterlist(arglist)
|
||||||
@ -93,9 +146,10 @@ def _parse_arglist(arglist: str) -> addnodes.desc_parameterlist:
|
|||||||
node += addnodes.desc_sig_name('', param.name)
|
node += addnodes.desc_sig_name('', param.name)
|
||||||
|
|
||||||
if param.annotation is not param.empty:
|
if param.annotation is not param.empty:
|
||||||
|
children = _parse_annotation(param.annotation)
|
||||||
node += addnodes.desc_sig_punctuation('', ':')
|
node += addnodes.desc_sig_punctuation('', ':')
|
||||||
node += nodes.Text(' ')
|
node += nodes.Text(' ')
|
||||||
node += addnodes.desc_sig_name('', param.annotation)
|
node += addnodes.desc_sig_name('', '', *children) # type: ignore
|
||||||
if param.default is not param.empty:
|
if param.default is not param.empty:
|
||||||
if param.annotation is not param.empty:
|
if param.annotation is not param.empty:
|
||||||
node += nodes.Text(' ')
|
node += nodes.Text(' ')
|
||||||
@ -354,7 +408,8 @@ class PyObject(ObjectDescription):
|
|||||||
signode += addnodes.desc_parameterlist()
|
signode += addnodes.desc_parameterlist()
|
||||||
|
|
||||||
if retann:
|
if retann:
|
||||||
signode += addnodes.desc_returns(retann, retann)
|
children = _parse_annotation(retann)
|
||||||
|
signode += addnodes.desc_returns(retann, '', *children)
|
||||||
|
|
||||||
anno = self.options.get('annotation')
|
anno = self.options.get('annotation')
|
||||||
if anno:
|
if anno:
|
||||||
|
@ -63,7 +63,7 @@ def assert_node(node: Node, cls: Any = None, xpath: str = "", **kwargs: Any) ->
|
|||||||
'The node%s has %d child nodes, not one' % (xpath, len(node))
|
'The node%s has %d child nodes, not one' % (xpath, len(node))
|
||||||
assert_node(node[0], cls[1:], xpath=xpath + "[0]", **kwargs)
|
assert_node(node[0], cls[1:], xpath=xpath + "[0]", **kwargs)
|
||||||
elif isinstance(cls, tuple):
|
elif isinstance(cls, tuple):
|
||||||
assert isinstance(node, nodes.Element), \
|
assert isinstance(node, (list, nodes.Element)), \
|
||||||
'The node%s does not have any items' % xpath
|
'The node%s does not have any items' % xpath
|
||||||
assert len(node) == len(cls), \
|
assert len(node) == len(cls), \
|
||||||
'The node%s has %d child nodes, not %r' % (xpath, len(node), len(cls))
|
'The node%s has %d child nodes, not %r' % (xpath, len(node), len(cls))
|
||||||
|
@ -18,11 +18,11 @@ from sphinx import addnodes
|
|||||||
from sphinx.addnodes import (
|
from sphinx.addnodes import (
|
||||||
desc, desc_addname, desc_annotation, desc_content, desc_name, desc_optional,
|
desc, desc_addname, desc_annotation, desc_content, desc_name, desc_optional,
|
||||||
desc_parameter, desc_parameterlist, desc_returns, desc_signature,
|
desc_parameter, desc_parameterlist, desc_returns, desc_signature,
|
||||||
desc_sig_name, desc_sig_operator, desc_sig_punctuation,
|
desc_sig_name, desc_sig_operator, desc_sig_punctuation, pending_xref,
|
||||||
)
|
)
|
||||||
from sphinx.domains import IndexEntry
|
from sphinx.domains import IndexEntry
|
||||||
from sphinx.domains.python import (
|
from sphinx.domains.python import (
|
||||||
py_sig_re, _pseudo_parse_arglist, PythonDomain, PythonModuleIndex
|
py_sig_re, _parse_annotation, _pseudo_parse_arglist, PythonDomain, PythonModuleIndex
|
||||||
)
|
)
|
||||||
from sphinx.testing import restructuredtext
|
from sphinx.testing import restructuredtext
|
||||||
from sphinx.testing.util import assert_node
|
from sphinx.testing.util import assert_node
|
||||||
@ -78,7 +78,7 @@ def test_domain_py_xrefs(app, status, warning):
|
|||||||
assert_node(node, **attributes)
|
assert_node(node, **attributes)
|
||||||
|
|
||||||
doctree = app.env.get_doctree('roles')
|
doctree = app.env.get_doctree('roles')
|
||||||
refnodes = list(doctree.traverse(addnodes.pending_xref))
|
refnodes = list(doctree.traverse(pending_xref))
|
||||||
assert_refnode(refnodes[0], None, None, 'TopLevel', 'class')
|
assert_refnode(refnodes[0], None, None, 'TopLevel', 'class')
|
||||||
assert_refnode(refnodes[1], None, None, 'top_level', 'meth')
|
assert_refnode(refnodes[1], None, None, 'top_level', 'meth')
|
||||||
assert_refnode(refnodes[2], None, 'NestedParentA', 'child_1', 'meth')
|
assert_refnode(refnodes[2], None, 'NestedParentA', 'child_1', 'meth')
|
||||||
@ -96,7 +96,7 @@ def test_domain_py_xrefs(app, status, warning):
|
|||||||
assert len(refnodes) == 13
|
assert len(refnodes) == 13
|
||||||
|
|
||||||
doctree = app.env.get_doctree('module')
|
doctree = app.env.get_doctree('module')
|
||||||
refnodes = list(doctree.traverse(addnodes.pending_xref))
|
refnodes = list(doctree.traverse(pending_xref))
|
||||||
assert_refnode(refnodes[0], 'module_a.submodule', None,
|
assert_refnode(refnodes[0], 'module_a.submodule', None,
|
||||||
'ModTopLevel', 'class')
|
'ModTopLevel', 'class')
|
||||||
assert_refnode(refnodes[1], 'module_a.submodule', 'ModTopLevel',
|
assert_refnode(refnodes[1], 'module_a.submodule', 'ModTopLevel',
|
||||||
@ -125,7 +125,7 @@ def test_domain_py_xrefs(app, status, warning):
|
|||||||
assert len(refnodes) == 16
|
assert len(refnodes) == 16
|
||||||
|
|
||||||
doctree = app.env.get_doctree('module_option')
|
doctree = app.env.get_doctree('module_option')
|
||||||
refnodes = list(doctree.traverse(addnodes.pending_xref))
|
refnodes = list(doctree.traverse(pending_xref))
|
||||||
print(refnodes)
|
print(refnodes)
|
||||||
print(refnodes[0])
|
print(refnodes[0])
|
||||||
print(refnodes[1])
|
print(refnodes[1])
|
||||||
@ -236,13 +236,44 @@ def test_get_full_qualified_name():
|
|||||||
assert domain.get_full_qualified_name(node) == 'module1.Class.func'
|
assert domain.get_full_qualified_name(node) == 'module1.Class.func'
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_annotation():
|
||||||
|
doctree = _parse_annotation("int")
|
||||||
|
assert_node(doctree, ([pending_xref, "int"],))
|
||||||
|
|
||||||
|
doctree = _parse_annotation("List[int]")
|
||||||
|
assert_node(doctree, ([pending_xref, "List"],
|
||||||
|
[desc_sig_punctuation, "["],
|
||||||
|
[pending_xref, "int"],
|
||||||
|
[desc_sig_punctuation, "]"]))
|
||||||
|
|
||||||
|
doctree = _parse_annotation("Tuple[int, int]")
|
||||||
|
assert_node(doctree, ([pending_xref, "Tuple"],
|
||||||
|
[desc_sig_punctuation, "["],
|
||||||
|
[pending_xref, "int"],
|
||||||
|
[desc_sig_punctuation, ", "],
|
||||||
|
[pending_xref, "int"],
|
||||||
|
[desc_sig_punctuation, "]"]))
|
||||||
|
|
||||||
|
doctree = _parse_annotation("Callable[[int, int], int]")
|
||||||
|
assert_node(doctree, ([pending_xref, "Callable"],
|
||||||
|
[desc_sig_punctuation, "["],
|
||||||
|
[desc_sig_punctuation, "["],
|
||||||
|
[pending_xref, "int"],
|
||||||
|
[desc_sig_punctuation, ", "],
|
||||||
|
[pending_xref, "int"],
|
||||||
|
[desc_sig_punctuation, "]"],
|
||||||
|
[desc_sig_punctuation, ", "],
|
||||||
|
[pending_xref, "int"],
|
||||||
|
[desc_sig_punctuation, "]"]))
|
||||||
|
|
||||||
|
|
||||||
def test_pyfunction_signature(app):
|
def test_pyfunction_signature(app):
|
||||||
text = ".. py:function:: hello(name: str) -> str"
|
text = ".. py:function:: hello(name: str) -> str"
|
||||||
doctree = restructuredtext.parse(app, text)
|
doctree = restructuredtext.parse(app, text)
|
||||||
assert_node(doctree, (addnodes.index,
|
assert_node(doctree, (addnodes.index,
|
||||||
[desc, ([desc_signature, ([desc_name, "hello"],
|
[desc, ([desc_signature, ([desc_name, "hello"],
|
||||||
desc_parameterlist,
|
desc_parameterlist,
|
||||||
[desc_returns, "str"])],
|
[desc_returns, pending_xref, "str"])],
|
||||||
desc_content)]))
|
desc_content)]))
|
||||||
assert_node(doctree[1], addnodes.desc, desctype="function",
|
assert_node(doctree[1], addnodes.desc, desctype="function",
|
||||||
domain="py", objtype="function", noindex=False)
|
domain="py", objtype="function", noindex=False)
|
||||||
@ -250,7 +281,7 @@ def test_pyfunction_signature(app):
|
|||||||
[desc_parameterlist, desc_parameter, ([desc_sig_name, "name"],
|
[desc_parameterlist, desc_parameter, ([desc_sig_name, "name"],
|
||||||
[desc_sig_punctuation, ":"],
|
[desc_sig_punctuation, ":"],
|
||||||
" ",
|
" ",
|
||||||
[nodes.inline, "str"])])
|
[nodes.inline, pending_xref, "str"])])
|
||||||
|
|
||||||
|
|
||||||
def test_pyfunction_signature_full(app):
|
def test_pyfunction_signature_full(app):
|
||||||
@ -260,7 +291,7 @@ def test_pyfunction_signature_full(app):
|
|||||||
assert_node(doctree, (addnodes.index,
|
assert_node(doctree, (addnodes.index,
|
||||||
[desc, ([desc_signature, ([desc_name, "hello"],
|
[desc, ([desc_signature, ([desc_name, "hello"],
|
||||||
desc_parameterlist,
|
desc_parameterlist,
|
||||||
[desc_returns, "str"])],
|
[desc_returns, pending_xref, "str"])],
|
||||||
desc_content)]))
|
desc_content)]))
|
||||||
assert_node(doctree[1], addnodes.desc, desctype="function",
|
assert_node(doctree[1], addnodes.desc, desctype="function",
|
||||||
domain="py", objtype="function", noindex=False)
|
domain="py", objtype="function", noindex=False)
|
||||||
@ -268,7 +299,7 @@ def test_pyfunction_signature_full(app):
|
|||||||
[desc_parameterlist, ([desc_parameter, ([desc_sig_name, "a"],
|
[desc_parameterlist, ([desc_parameter, ([desc_sig_name, "a"],
|
||||||
[desc_sig_punctuation, ":"],
|
[desc_sig_punctuation, ":"],
|
||||||
" ",
|
" ",
|
||||||
[desc_sig_name, "str"])],
|
[desc_sig_name, pending_xref, "str"])],
|
||||||
[desc_parameter, ([desc_sig_name, "b"],
|
[desc_parameter, ([desc_sig_name, "b"],
|
||||||
[desc_sig_operator, "="],
|
[desc_sig_operator, "="],
|
||||||
[nodes.inline, "1"])],
|
[nodes.inline, "1"])],
|
||||||
@ -276,11 +307,11 @@ def test_pyfunction_signature_full(app):
|
|||||||
[desc_sig_name, "args"],
|
[desc_sig_name, "args"],
|
||||||
[desc_sig_punctuation, ":"],
|
[desc_sig_punctuation, ":"],
|
||||||
" ",
|
" ",
|
||||||
[desc_sig_name, "str"])],
|
[desc_sig_name, pending_xref, "str"])],
|
||||||
[desc_parameter, ([desc_sig_name, "c"],
|
[desc_parameter, ([desc_sig_name, "c"],
|
||||||
[desc_sig_punctuation, ":"],
|
[desc_sig_punctuation, ":"],
|
||||||
" ",
|
" ",
|
||||||
[desc_sig_name, "bool"],
|
[desc_sig_name, pending_xref, "bool"],
|
||||||
" ",
|
" ",
|
||||||
[desc_sig_operator, "="],
|
[desc_sig_operator, "="],
|
||||||
" ",
|
" ",
|
||||||
@ -289,7 +320,7 @@ def test_pyfunction_signature_full(app):
|
|||||||
[desc_sig_name, "kwargs"],
|
[desc_sig_name, "kwargs"],
|
||||||
[desc_sig_punctuation, ":"],
|
[desc_sig_punctuation, ":"],
|
||||||
" ",
|
" ",
|
||||||
[desc_sig_name, "str"])])])
|
[desc_sig_name, pending_xref, "str"])])])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.')
|
@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.')
|
||||||
@ -340,7 +371,7 @@ def test_optional_pyfunction_signature(app):
|
|||||||
assert_node(doctree, (addnodes.index,
|
assert_node(doctree, (addnodes.index,
|
||||||
[desc, ([desc_signature, ([desc_name, "compile"],
|
[desc, ([desc_signature, ([desc_name, "compile"],
|
||||||
desc_parameterlist,
|
desc_parameterlist,
|
||||||
[desc_returns, "ast object"])],
|
[desc_returns, pending_xref, "ast object"])],
|
||||||
desc_content)]))
|
desc_content)]))
|
||||||
assert_node(doctree[1], addnodes.desc, desctype="function",
|
assert_node(doctree[1], addnodes.desc, desctype="function",
|
||||||
domain="py", objtype="function", noindex=False)
|
domain="py", objtype="function", noindex=False)
|
||||||
|
Loading…
Reference in New Issue
Block a user