diff --git a/CHANGES b/CHANGES index 562bc8006b5..1935d6d0902 100644 --- a/CHANGES +++ b/CHANGES @@ -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 diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 4ca60624849..eb7d79f81dd 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -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): @@ -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) @@ -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 @@ -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])") @@ -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') @@ -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') @@ -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') diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 61cc5145b61..ccf539b6d0b 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -236,18 +236,18 @@ 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"], @@ -255,14 +255,14 @@ def test_parse_annotation(): [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, "["], @@ -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) @@ -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): @@ -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')