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

Fix #7928: py domain: failed to resolve a type annotation for the attribute #7930

Merged
merged 1 commit into from
Jul 11, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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