From d55cade6bd6302da22c0747371157839a43c4231 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Thu, 25 Jun 2026 10:58:29 +0200 Subject: [PATCH] fix: render empty-tuple subscript annotation as tuple[()] not tuple[] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An empty `ExprTuple` with `implicit=True` (produced when parsing a subscript like `tuple[()]`) previously yielded nothing in `iterate()`, resulting in the invalid string `tuple[]`. An empty tuple has no implicit form—it must always be written as `()`—so treat an empty implicit tuple the same as an explicit one. Before: `str(returns)` of `def f() -> tuple[()]` gave `"tuple[]"`, which is a `SyntaxError` when re-parsed. After: correctly yields `"tuple[()]"`. --- .../src/griffe/_internal/expressions.py | 8 ++++++++ packages/griffelib/tests/test_expressions.py | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+) 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[()]"