diff --git a/packages/griffelib/src/griffe/_internal/expressions.py b/packages/griffelib/src/griffe/_internal/expressions.py index 2916ba70..c5d33c66 100644 --- a/packages/griffelib/src/griffe/_internal/expressions.py +++ b/packages/griffelib/src/griffe/_internal/expressions.py @@ -1003,6 +1003,14 @@ class ExprTuple(Expr): """Whether the tuple is implicit (e.g. without parentheses in a subscript's slice).""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: + if self.implicit and not self.elements: + # An empty tuple is always written as `()` and cannot be implicit. + # This arises in annotations like `tuple[()]`, where the AST represents + # the subscript slice as an empty Tuple node, but the parentheses must + # be preserved to produce valid Python (`tuple[]` is a SyntaxError). + yield "(" + yield ")" + return if not self.implicit: yield "(" yield from _join(self.elements, ", ", flat=flat) diff --git a/packages/griffelib/tests/test_expressions.py b/packages/griffelib/tests/test_expressions.py index dab89b81..4db0b3fa 100644 --- a/packages/griffelib/tests/test_expressions.py +++ b/packages/griffelib/tests/test_expressions.py @@ -311,3 +311,22 @@ def test_render_dict_with_unpacking() -> None: assert str(module["a"].value) == "{**base, 'x': 1}" assert str(module["b"].value) == "{**d1, **d2}" assert str(module["c"].value) == "{None: 1, 'y': 2}" + + +def test_empty_tuple_annotation_str() -> None: + """Check that empty-tuple annotations round-trip correctly. + + ``tuple[()]`` is a valid annotation for a zero-element tuple. + Its string representation must remain ``tuple[()]``, not the + invalid ``tuple[]``. + """ + with temporary_visited_module( + """ + from typing import Tuple + + def f1() -> tuple[()]: ... + def f2() -> Tuple[()]: ... + """, + ) as module: + assert str(module["f1"].returns) == "tuple[()]" + assert str(module["f2"].returns) == "Tuple[()]"