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"), [