Skip to content

Commit

Permalink
Merge pull request sphinx-doc#7930 from tk0miya/7928_resolve_typehint…
Browse files Browse the repository at this point in the history
…s_for_attrs

Fix sphinx-doc#7928: py domain: failed to resolve a type annotation for the attribute
  • Loading branch information
tk0miya committed Jul 11, 2020
2 parents 1a31a7c + 4410668 commit d0416e7
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 24 deletions.
1 change: 1 addition & 0 deletions CHANGES
Expand Up @@ -35,6 +35,7 @@ Bugs fixed
* #7715: LaTeX: ``numfig_secnum_depth > 1`` leads to wrong figure links
* #7846: html theme: XML-invalid files were generated
* #7894: gettext: Wrong source info is shown when using rst_epilog
* #7928: py domain: failed to resolve a type annotation for the attribute
* #7869: :rst:role:`abbr` role without an explanation will show the explanation
from the previous abbr role
* C and C++, removed ``noindex`` directive option as it did
Expand Down
32 changes: 21 additions & 11 deletions sphinx/domains/python.py
Expand Up @@ -77,18 +77,24 @@
('deprecated', bool)])


def type_to_xref(text: str) -> addnodes.pending_xref:
def type_to_xref(text: str, env: BuildEnvironment = None) -> addnodes.pending_xref:
"""Convert a type string to a cross reference node."""
if text == 'None':
reftype = 'obj'
else:
reftype = 'class'

if env:
kwargs = {'py:module': env.ref_context.get('py:module'),
'py:class': env.ref_context.get('py:class')}
else:
kwargs = {}

return pending_xref('', nodes.Text(text),
refdomain='py', reftype=reftype, reftarget=text)
refdomain='py', reftype=reftype, reftarget=text, **kwargs)


def _parse_annotation(annotation: str) -> List[Node]:
def _parse_annotation(annotation: str, env: BuildEnvironment = None) -> List[Node]:
"""Parse type annotation."""
def unparse(node: ast.AST) -> List[Node]:
if isinstance(node, ast.Attribute):
Expand Down Expand Up @@ -130,18 +136,22 @@ def unparse(node: ast.AST) -> List[Node]:
else:
raise SyntaxError # unsupported syntax

if env is None:
warnings.warn("The env parameter for _parse_annotation becomes required now.",
RemovedInSphinx50Warning, stacklevel=2)

try:
tree = ast_parse(annotation)
result = unparse(tree)
for i, node in enumerate(result):
if isinstance(node, nodes.Text):
result[i] = type_to_xref(str(node))
result[i] = type_to_xref(str(node), env)
return result
except SyntaxError:
return [type_to_xref(annotation)]
return [type_to_xref(annotation, env)]


def _parse_arglist(arglist: str) -> addnodes.desc_parameterlist:
def _parse_arglist(arglist: str, env: BuildEnvironment = None) -> addnodes.desc_parameterlist:
"""Parse a list of arguments using AST parser"""
params = addnodes.desc_parameterlist(arglist)
sig = signature_from_str('(%s)' % arglist)
Expand All @@ -167,7 +177,7 @@ def _parse_arglist(arglist: str) -> addnodes.desc_parameterlist:
node += addnodes.desc_sig_name('', param.name)

if param.annotation is not param.empty:
children = _parse_annotation(param.annotation)
children = _parse_annotation(param.annotation, env)
node += addnodes.desc_sig_punctuation('', ':')
node += nodes.Text(' ')
node += addnodes.desc_sig_name('', '', *children) # type: ignore
Expand Down Expand Up @@ -415,7 +425,7 @@ def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]
signode += addnodes.desc_name(name, name)
if arglist:
try:
signode += _parse_arglist(arglist)
signode += _parse_arglist(arglist, self.env)
except SyntaxError:
# fallback to parse arglist original parser.
# it supports to represent optional arguments (ex. "func(foo [, bar])")
Expand All @@ -430,7 +440,7 @@ def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]
signode += addnodes.desc_parameterlist()

if retann:
children = _parse_annotation(retann)
children = _parse_annotation(retann, self.env)
signode += addnodes.desc_returns(retann, '', *children)

anno = self.options.get('annotation')
Expand Down Expand Up @@ -626,7 +636,7 @@ def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]

typ = self.options.get('type')
if typ:
annotations = _parse_annotation(typ)
annotations = _parse_annotation(typ, self.env)
signode += addnodes.desc_annotation(typ, '', nodes.Text(': '), *annotations)

value = self.options.get('value')
Expand Down Expand Up @@ -872,7 +882,7 @@ def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]

typ = self.options.get('type')
if typ:
annotations = _parse_annotation(typ)
annotations = _parse_annotation(typ, self.env)
signode += addnodes.desc_annotation(typ, '', nodes.Text(': '), *annotations)

value = self.options.get('value')
Expand Down
35 changes: 22 additions & 13 deletions tests/test_domain_py.py
Expand Up @@ -236,33 +236,33 @@ def test_get_full_qualified_name():
assert domain.get_full_qualified_name(node) == 'module1.Class.func'


def test_parse_annotation():
doctree = _parse_annotation("int")
def test_parse_annotation(app):
doctree = _parse_annotation("int", app.env)
assert_node(doctree, ([pending_xref, "int"],))
assert_node(doctree[0], pending_xref, refdomain="py", reftype="class", reftarget="int")

doctree = _parse_annotation("List[int]")
doctree = _parse_annotation("List[int]", app.env)
assert_node(doctree, ([pending_xref, "List"],
[desc_sig_punctuation, "["],
[pending_xref, "int"],
[desc_sig_punctuation, "]"]))

doctree = _parse_annotation("Tuple[int, int]")
doctree = _parse_annotation("Tuple[int, int]", app.env)
assert_node(doctree, ([pending_xref, "Tuple"],
[desc_sig_punctuation, "["],
[pending_xref, "int"],
[desc_sig_punctuation, ", "],
[pending_xref, "int"],
[desc_sig_punctuation, "]"]))

doctree = _parse_annotation("Tuple[()]")
doctree = _parse_annotation("Tuple[()]", app.env)
assert_node(doctree, ([pending_xref, "Tuple"],
[desc_sig_punctuation, "["],
[desc_sig_punctuation, "("],
[desc_sig_punctuation, ")"],
[desc_sig_punctuation, "]"]))

doctree = _parse_annotation("Callable[[int, int], int]")
doctree = _parse_annotation("Callable[[int, int], int]", app.env)
assert_node(doctree, ([pending_xref, "Callable"],
[desc_sig_punctuation, "["],
[desc_sig_punctuation, "["],
Expand All @@ -275,12 +275,11 @@ def test_parse_annotation():
[desc_sig_punctuation, "]"]))

# None type makes an object-reference (not a class reference)
doctree = _parse_annotation("None")
doctree = _parse_annotation("None", app.env)
assert_node(doctree, ([pending_xref, "None"],))
assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="None")



def test_pyfunction_signature(app):
text = ".. py:function:: hello(name: str) -> str"
doctree = restructuredtext.parse(app, text)
Expand Down Expand Up @@ -458,14 +457,22 @@ def test_pyobject_prefix(app):


def test_pydata(app):
text = ".. py:data:: var\n"
text = (".. py:module:: example\n"
".. py:data:: var\n"
" :type: int\n")
domain = app.env.get_domain('py')
doctree = restructuredtext.parse(app, text)
assert_node(doctree, (addnodes.index,
[desc, ([desc_signature, desc_name, "var"],
assert_node(doctree, (nodes.target,
addnodes.index,
addnodes.index,
[desc, ([desc_signature, ([desc_addname, "example."],
[desc_name, "var"],
[desc_annotation, (": ",
[pending_xref, "int"])])],
[desc_content, ()])]))
assert 'var' in domain.objects
assert domain.objects['var'] == ('index', 'var', 'data')
assert_node(doctree[3][0][2][1], pending_xref, **{"py:module": "example"})
assert 'example.var' in domain.objects
assert domain.objects['example.var'] == ('index', 'example.var', 'data')


def test_pyfunction(app):
Expand Down Expand Up @@ -698,6 +705,8 @@ def test_pyattribute(app):
[desc_sig_punctuation, "]"])],
[desc_annotation, " = ''"])],
[desc_content, ()]))
assert_node(doctree[1][1][1][0][1][1], pending_xref, **{"py:class": "Class"})
assert_node(doctree[1][1][1][0][1][3], pending_xref, **{"py:class": "Class"})
assert 'Class.attr' in domain.objects
assert domain.objects['Class.attr'] == ('index', 'Class.attr', 'attribute')

Expand Down

0 comments on commit d0416e7

Please sign in to comment.