From e326ad6f67440c087b1cebf24487d035a51627fc Mon Sep 17 00:00:00 2001 From: Alicia Garcia-Raboso Date: Wed, 19 Feb 2025 08:59:54 +0100 Subject: [PATCH] Correctly parse and cross-reference unpacked type annotations --- sphinx/domains/python/_annotations.py | 4 ++++ sphinx/pycode/ast.py | 3 +++ tests/test_domains/test_domain_py.py | 22 ++++++++++++++++++++++ tests/test_pycode/test_pycode_ast.py | 1 + 4 files changed, 30 insertions(+) diff --git a/sphinx/domains/python/_annotations.py b/sphinx/domains/python/_annotations.py index 823aac01316..29e47fa7151 100644 --- a/sphinx/domains/python/_annotations.py +++ b/sphinx/domains/python/_annotations.py @@ -124,6 +124,10 @@ def unparse(node: ast.AST) -> list[Node]: return [nodes.Text(repr(node.value))] if isinstance(node, ast.Expr): return unparse(node.value) + if isinstance(node, ast.Starred): + result = [addnodes.desc_sig_operator('', '*')] + result.extend(unparse(node.value)) + return result if isinstance(node, ast.Invert): return [addnodes.desc_sig_punctuation('', '~')] if isinstance(node, ast.USub): diff --git a/sphinx/pycode/ast.py b/sphinx/pycode/ast.py index b1521595b49..640864e467a 100644 --- a/sphinx/pycode/ast.py +++ b/sphinx/pycode/ast.py @@ -202,5 +202,8 @@ def visit_Tuple(self, node: ast.Tuple) -> str: else: return '(' + ', '.join(self.visit(e) for e in node.elts) + ')' + def visit_Starred(self, node: ast.Starred) -> str: + return f'*{self.visit(node.value)}' + def generic_visit(self, node: ast.AST) -> NoReturn: raise NotImplementedError('Unable to parse %s object' % type(node).__name__) diff --git a/tests/test_domains/test_domain_py.py b/tests/test_domains/test_domain_py.py index 151fb4494f7..b9a2f1c85a4 100644 --- a/tests/test_domains/test_domain_py.py +++ b/tests/test_domains/test_domain_py.py @@ -508,6 +508,28 @@ def test_parse_annotation(app): ), ) + doctree = _parse_annotation('*tuple[str, int]', app.env) + assert_node( + doctree, + ( + [desc_sig_operator, '*'], + [pending_xref, 'tuple'], + [desc_sig_punctuation, '['], + [pending_xref, 'str'], + [desc_sig_punctuation, ','], + desc_sig_space, + [pending_xref, 'int'], + [desc_sig_punctuation, ']'], + ), + ) + assert_node( + doctree[1], + pending_xref, + refdomain='py', + reftype='class', + reftarget='tuple', + ) + @pytest.mark.sphinx('html', testroot='_blank') def test_parse_annotation_suppress(app): diff --git a/tests/test_pycode/test_pycode_ast.py b/tests/test_pycode/test_pycode_ast.py index 6ebc1a91099..409e5806d1b 100644 --- a/tests/test_pycode/test_pycode_ast.py +++ b/tests/test_pycode/test_pycode_ast.py @@ -62,6 +62,7 @@ 'x[:, np.newaxis, :, :]'), # Index, Subscript, numpy extended syntax ('y[:, 1:3][np.array([0, 2, 4]), :]', 'y[:, 1:3][np.array([0, 2, 4]), :]'), # Index, 2x Subscript, numpy extended syntax + ('*tuple[str, int]', '*tuple[str, int]'), # Starred ], ) # fmt: skip def test_unparse(source, expected):