From 6223f4806ad2d7bff7983586d7bb30614d152db9 Mon Sep 17 00:00:00 2001 From: chuenchen309 <48723787+chuenchen309@users.noreply.github.com> Date: Sun, 19 Jul 2026 18:00:38 +0800 Subject: [PATCH] fix: keep parentheses around top-level named expressions (walrus) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expr.__str__ rendered the top-level expression by iterating flatly, bypassing the precedence machinery that parenthesizes a named expression (walrus, :=) in nested positions. A named expression extracted as a whole value — an attribute value or a parameter default — was therefore rendered without its parentheses, producing invalid Python: `x = n := 1` is a SyntaxError, and `def f(a=b := 1)` likewise. Wrap a top-level ExprNamedExpr in parentheses, matching ast.unparse. Nested occurrences are still parenthesized by their enclosing context, so there is no double-wrapping. --- .../griffelib/src/griffe/_internal/expressions.py | 8 +++++++- packages/griffelib/tests/test_expressions.py | 13 +++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/griffelib/src/griffe/_internal/expressions.py b/packages/griffelib/src/griffe/_internal/expressions.py index 26a97d15..b3f833f7 100644 --- a/packages/griffelib/src/griffe/_internal/expressions.py +++ b/packages/griffelib/src/griffe/_internal/expressions.py @@ -164,7 +164,13 @@ class Expr: """Base class for expressions.""" def __str__(self) -> str: - return "".join(elem if isinstance(elem, str) else elem.name for elem in self.iterate(flat=True)) # ty:ignore[unresolved-attribute] + rendered = "".join(elem if isinstance(elem, str) else elem.name for elem in self.iterate(flat=True)) # ty:ignore[unresolved-attribute] + # A top-level named expression (walrus) is invalid Python without surrounding + # parentheses, e.g. as an attribute value or a parameter default. Nested occurrences + # are parenthesized by their enclosing context, but the top level has none. + if isinstance(self, ExprNamedExpr): + return f"({rendered})" + return rendered def __iter__(self) -> Iterator[str | Expr]: """Iterate on the expression syntax and elements.""" diff --git a/packages/griffelib/tests/test_expressions.py b/packages/griffelib/tests/test_expressions.py index 2f0d3bf5..6fba48db 100644 --- a/packages/griffelib/tests/test_expressions.py +++ b/packages/griffelib/tests/test_expressions.py @@ -142,6 +142,7 @@ def test_await_expression(source: str, expected: str) -> None: "f'{x:>{width}}'", ] _expression_contexts = [ + "%s", "f(%s)", "f(%s, 1)", "f(a=%s)", @@ -199,6 +200,18 @@ def test_length_one_tuple_as_string() -> None: assert str(module["x"].value) == "('a',)" +def test_top_level_named_expression_keeps_parentheses() -> None: + """A top-level named expression (walrus) must keep its surrounding parentheses. + + Without them, the rendered string is invalid Python as an attribute value + or a parameter default (e.g. `x = n := 1` is a syntax error). + """ + code = "x = (n := 1)\ndef f(a=(b := compute())): ...\n" + with temporary_visited_module(code) as module: + assert str(module["x"].value) == "(n := 1)" + assert str(module["f"].parameters["a"].default) == "(b := compute())" + + @pytest.mark.parametrize( ("annotation", "modernized"), [