diff --git a/packages/griffelib/src/griffe/_internal/expressions.py b/packages/griffelib/src/griffe/_internal/expressions.py index b25cf5e2..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` @@ -953,10 +952,12 @@ class ExprNamedExpr(Expr): """Value.""" 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) @@ -1252,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 bed3941c..e2adf6fd 100644 --- a/packages/griffelib/tests/test_expressions.py +++ b/packages/griffelib/tests/test_expressions.py @@ -218,6 +218,23 @@ def test_nested_container_format_spec_is_roundtrip_safe() -> None: assert ast.dump(reparsed) == ast.dump(original), f"{code!r} rendered as {rendered!r}" +@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) + assert expression is not None + + 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}" + + @pytest.mark.parametrize( "code", [ @@ -253,7 +270,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)]"),