Skip to content

fix: keep parentheses around top-level named expressions (walrus)#480

Open
chuenchen309 wants to merge 1 commit into
mkdocstrings:mainfrom
chuenchen309:fix/top-level-walrus-parentheses
Open

fix: keep parentheses around top-level named expressions (walrus)#480
chuenchen309 wants to merge 1 commit into
mkdocstrings:mainfrom
chuenchen309:fix/top-level-walrus-parentheses

Conversation

@chuenchen309

Copy link
Copy Markdown

The bug

A top-level named expression (walrus, :=) — extracted as a whole attribute value or parameter default — is rendered without its surrounding parentheses, producing invalid Python:

import griffe
# module: x = (n := 1); def f(a=(b := compute())): ...
m = griffe.load("mod")
str(m["x"].value)                    # 'n := 1'         -> `x = n := 1` is a SyntaxError
str(m["f"].parameters["a"].default)  # 'b := compute()' -> invalid as a default

ast.unparse (the authoritative "valid, faithful Python" renderer) always parenthesizes a NamedExpr here → '(n := 1)'. Griffe drops the parens only at the top level.

Root cause

Expr.__str__ (packages/griffelib/src/griffe/_internal/expressions.py) renders the top expression by iterating flatly, which bypasses the _yield/precedence logic that adds walrus parentheses in nested positions. ExprNamedExpr deliberately does not self-parenthesize (that would double-wrap when nested), and a top-level expression has no enclosing context to add them — so a standalone walrus never gets parenthesized.

This is exactly the gap left by commit eb85f0d ("Make stringified expressions valid and faithful Python"): its property test test_expressions_stay_valid_and_equivalent embeds (w := 1) in ~26 contexts, but every one wraps the expression — none renders it standalone.

The fix

Wrap a top-level ExprNamedExpr in parentheses in __str__, matching ast.unparse. Nested walruses are still parenthesized by their enclosing context (not by __str__), so there is no double-wrapping.

Verification

  • The repro now renders (n := 1) and (b := compute()); nested cases are unchanged ({'a': (w := 1)}, foo(k := 2) — single parens / valid).
  • Added "%s" (the standalone context) to the _expression_contexts property test — on main it fails on exactly [%s-(w := 1)] and nothing else — plus an explicit test_top_level_named_expression_keeps_parentheses.
  • tests/test_expressions.py → 910 passed / 16 skipped; tests/test_nodes.py → 141 passed. No regression.

Reachability: X = (n := compute()) (module/class attribute via walrus) and the shared-mutable-default idiom def f(cache=(_cache := {})) are ordinary user code; mkdocstrings renders these extracted strings straight into signatures/attribute docs, so the invalid output is user-visible.

Not a duplicate — the only related history, closed #152, is a different build-time KeyError for a walrus in a comprehension; no open issue/PR concerns top-level walrus rendering.


This PR was authored by an AI coding agent (Claude Code) running on this account: the AI found the bug, ran the repro, wrote the test, and wrote this description. The human account holder reviews every change and is accountable for it. The verification above is real and re-runnable from the diff. If this isn't the kind of contribution you want, say so and I'll close it.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unhandled case of a walrus operator in the output part of a list comprehension

1 participant