From f1485f5c1c0d994a595c5e2cea89f4d726f48e01 Mon Sep 17 00:00:00 2001 From: lprnmns Date: Thu, 3 Sep 2026 00:24:09 +0300 Subject: [PATCH 1/3] fix: Preserve standalone named expression parentheses --- packages/griffelib/src/griffe/_internal/expressions.py | 3 +++ packages/griffelib/tests/test_expressions.py | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/packages/griffelib/src/griffe/_internal/expressions.py b/packages/griffelib/src/griffe/_internal/expressions.py index b25cf5e2..4b7e323a 100644 --- a/packages/griffelib/src/griffe/_internal/expressions.py +++ b/packages/griffelib/src/griffe/_internal/expressions.py @@ -952,6 +952,9 @@ class ExprNamedExpr(Expr): value: str | Expr """Value.""" + def __str__(self) -> str: + return f"({Expr.__str__(self)})" + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield from _yield(self.target, flat=flat, outer_precedence=_OperatorPrecedence.NONE) yield " := " diff --git a/packages/griffelib/tests/test_expressions.py b/packages/griffelib/tests/test_expressions.py index bed3941c..7be126bd 100644 --- a/packages/griffelib/tests/test_expressions.py +++ b/packages/griffelib/tests/test_expressions.py @@ -218,6 +218,16 @@ def test_nested_container_format_spec_is_roundtrip_safe() -> None: assert ast.dump(reparsed) == ast.dump(original), f"{code!r} rendered as {rendered!r}" +def test_standalone_named_expression_is_roundtrip_safe() -> None: + """Standalone named expressions must retain their required parentheses.""" + code = "(value := 1)" + original = ast.parse(code, mode="eval") + expression = get_expression(original.body, parent=Module("module"), parse_strings=False) + rendered = str(expression) + reparsed = ast.parse(rendered, mode="eval") + assert ast.dump(reparsed) == ast.dump(original), f"{code!r} rendered as {rendered!r}" + + @pytest.mark.parametrize( "code", [ From 01bb34a89f715703c26a8d498b71057398bccbe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 16 Oct 2026 17:51:38 +0200 Subject: [PATCH 2/3] fixup! fix: Preserve standalone named expression parentheses --- .../griffelib/src/griffe/_internal/expressions.py | 11 +++++------ packages/griffelib/tests/test_expressions.py | 10 ++++++++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/griffelib/src/griffe/_internal/expressions.py b/packages/griffelib/src/griffe/_internal/expressions.py index 4b7e323a..a28d90b2 100644 --- a/packages/griffelib/src/griffe/_internal/expressions.py +++ b/packages/griffelib/src/griffe/_internal/expressions.py @@ -58,7 +58,6 @@ class _OperatorPrecedence(IntEnum): NONE = auto() YIELD = auto() # `yield`, `yield from` - ASSIGN = auto() # `target := expr` STARRED = auto() # `*expr` (omitted by Python docs, see ruff impl) LAMBDA = auto() IF_ELSE = auto() # `expr if cond else expr` @@ -952,14 +951,13 @@ class ExprNamedExpr(Expr): value: str | Expr """Value.""" - def __str__(self) -> str: - return f"({Expr.__str__(self)})" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: + yield "(" yield from _yield(self.target, flat=flat, outer_precedence=_OperatorPrecedence.NONE) yield " := " - # Nested walrus assignments and yields need parentheses, e.g. `a := (b := c)`. + # Yield expressions need parentheses on the right. yield from _yield(self.value, flat=flat, outer_precedence=_OperatorPrecedence.STARRED) + yield ")" @dataclass(eq=True, slots=True) @@ -1255,7 +1253,8 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: ExprBoolOp: lambda e: {"and": _OperatorPrecedence.AND, "or": _OperatorPrecedence.OR}[e.operator], ExprCompare: lambda _: _OperatorPrecedence.COMP_MEMB_ID, ExprIfExp: lambda _: _OperatorPrecedence.IF_ELSE, - ExprNamedExpr: lambda _: _OperatorPrecedence.ASSIGN, + # Named expressions emit their own parentheses. + ExprNamedExpr: lambda _: _OperatorPrecedence.ATOMIC, ExprLambda: lambda _: _OperatorPrecedence.LAMBDA, ExprGeneratorExp: lambda _: _OperatorPrecedence.ATOMIC, ExprVarPositional: lambda _: _OperatorPrecedence.STARRED, diff --git a/packages/griffelib/tests/test_expressions.py b/packages/griffelib/tests/test_expressions.py index 7be126bd..93a303bc 100644 --- a/packages/griffelib/tests/test_expressions.py +++ b/packages/griffelib/tests/test_expressions.py @@ -218,12 +218,18 @@ def test_nested_container_format_spec_is_roundtrip_safe() -> None: assert ast.dump(reparsed) == ast.dump(original), f"{code!r} rendered as {rendered!r}" -def test_standalone_named_expression_is_roundtrip_safe() -> None: +@pytest.mark.parametrize("flat", [True, False]) +def test_standalone_named_expression_is_roundtrip_safe(flat: bool) -> None: """Standalone named expressions must retain their required parentheses.""" code = "(value := 1)" original = ast.parse(code, mode="eval") expression = get_expression(original.body, parent=Module("module"), parse_strings=False) + rendered = str(expression) + + # Both iteration modes must preserve the parentheses required by standalone walrus expressions. + assert "".join(str(element) for element in expression.iterate(flat=flat)) == rendered + reparsed = ast.parse(rendered, mode="eval") assert ast.dump(reparsed) == ast.dump(original), f"{code!r} rendered as {rendered!r}" @@ -263,7 +269,7 @@ def test_nested_tuples_keep_parentheses_in_implicit_contexts(code: str) -> None: ("o[(x, y) == z]", "o[(x, y) == z]"), ("o[(x, y) if condition else z]", "o[(x, y) if condition else z]"), ("o[lambda: (x, y)]", "o[lambda: (x, y)]"), - ("o[(item := (x, y))]", "o[item := (x, y)]"), + ("o[(item := (x, y))]", "o[(item := (x, y))]"), ("o[f'{(x, y)}']", "o[f'{(x, y)}']"), ("o[((x, y) for value in values)]", "o[((x, y) for value in values)]"), ("o[await (x, y)]", "o[await (x, y)]"), From 0f75dbbb6da0e965cd9e119b83ef9ceb907a62c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 16 Oct 2026 18:20:36 +0200 Subject: [PATCH 3/3] fixup! fix: Preserve standalone named expression parentheses --- packages/griffelib/tests/test_expressions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/griffelib/tests/test_expressions.py b/packages/griffelib/tests/test_expressions.py index 93a303bc..e2adf6fd 100644 --- a/packages/griffelib/tests/test_expressions.py +++ b/packages/griffelib/tests/test_expressions.py @@ -224,6 +224,7 @@ def test_standalone_named_expression_is_roundtrip_safe(flat: bool) -> None: code = "(value := 1)" original = ast.parse(code, mode="eval") expression = get_expression(original.body, parent=Module("module"), parse_strings=False) + assert expression is not None rendered = str(expression)