Fix unnecessary parentheses in indexed assignment RHS#5095
Fix unnecessary parentheses in indexed assignment RHS#5095ParamChordiya wants to merge 12 commits intopsf:mainfrom
Conversation
When an assignment target contains brackets (e.g. indexed access like
`x[key] = expr`), Black would wrap the right-hand side expression in
unnecessary parentheses when the line was too long. For example:
dictionary["key"][idx] = (10 - 5)
This happened because `can_omit_invisible_parens` returned False for
simple expressions with operators (like `10 - 5`) that don't start or
end with brackets. The function was too conservative: it didn't consider
that the line could instead be split at the LHS brackets, producing:
dictionary["key"][
idx
] = 10 - 5
The fix allows omitting optional parens when the RHS body is short
enough and the LHS contains brackets that can absorb the split. The
downstream `_prefer_split_rhs_oop_over_rhs` still decides whether the
resulting layout is actually better.
Fixes psf#4349.
|
diff-shades results comparing this PR (3a86bcb) to main (866c350):
|
…S.md Move the fix for unnecessary parentheses in indexed assignment RHS behind a Preview flag to avoid unintentional stable style changes. Test cases moved to a preview-specific test file. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ck, add docs - Make `mode` parameter required in `can_omit_invisible_parens` to prevent accidental omission at call sites (fixes flake8 B008) - Replace `str_width(str(line))` with `enumerate_with_length()` for an AST-only body length calculation (avoids expensive string conversion) - Document `fix_unnecessary_parens_in_indexed_assignment` in `docs/the_black_code_style/future_style.md` (fixes test_feature_lists_are_up_to_date) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Didn't realize the changes to Black and in shades
…r_rhs When the OOP split lands mid-chain (tail starts with ] followed by more tokens like .attr, and = is already in head), the split is inside a subscript access on the RHS — not the LHS target. Guard against this by returning False so paren-wrapping is preferred instead. Adds regression tests for attribute-subscript chains on the RHS.
Black formats itself as part of CI (run_self). After the formatting rule change in _prefer_split_rhs_oop_over_rhs, 4 files needed reformatting to stay consistent with the updated formatter output.
The guard in _prefer_split_rhs_oop_over_rhs that prevents mid-chain subscript splits on the RHS was running unconditionally, affecting stable mode formatting across a large corpus (diff-shades failure). Gate it behind Preview.fix_unnecessary_parens_in_indexed_assignment, matching the flag used in can_omit_invisible_parens, so stable code style is unchanged.
In linegen.py: tighten the subscript-chain guard to only suppress OOP splitting when the token after `]` is a DOT (attribute access), not any token. Without this, list literal assignments with magic trailing commas were incorrectly paren-wrapped because the tail had `]` + NEWLINE (count=2). In lines.py: fix can_omit_invisible_parens to check only visible brackets in the LHS when deciding whether to allow the indexed-assignment OOP path. Invisible bracket tokens (empty LPAR/RPAR from tuple assignments like `a, b = expr`) were matching the condition, causing tuple-target assignments to incorrectly omit optional parens and produce unstable formatting.
|
Hey @cobaltt7 , thanks for catching that, really appreciate the thorough review. You were right, and it took a fair bit of digging to get to the bottom of it. What was happening The regression you spotted (middle.children[1].prefix = ... splitting mid-chain at the subscript instead of paren-wrapping the whole RHS) was happening because _prefer_split_rhs_oop_over_rhs in linegen.py was choosing the OOP (omit-optional-parens) split path even when the result was ugly. The OOP split would break at middle.children[ and put the index 1 on its own line, which is clearly wrong for a chained attribute access. The first round of issues The second bug, the trickier one The fix was a one-character addition: and leaf.value, only count a bracket if it has an actual visible value, which correctly excludes the implicit/invisible tokens that the parser inserts. How we verified
The net change across both files is two lines, but it took some real archaeology to get there, the invisible-bracket issue in particular was subtle since nothing in the usual formatting output hinted that those tokens even existed. |
|
Please check the shades changes - it's still splitting within function calls. |



Summary
Fixes #4349.
When an assignment target contains brackets (e.g. indexed access like
x[key] = expr), Black would wrap the right-hand side expression in unnecessary parentheses when the line was too long.Before:
After:
Details
The root cause is in
can_omit_invisible_parens()insrc/black/lines.py. This function decides whether optional parentheses around the RHS can be safely removed. It returnedFalsefor simple expressions with operators (like10 - 5) because they don't start or end with brackets — falling through to the defaultreturn False.The fix adds a targeted check: when the RHS head ends with
=and the LHS contains brackets (indicating an indexed/subscript assignment), and the body expression is short enough, allow omitting the optional parens. The downstream_prefer_split_rhs_oop_over_rhs()still makes the final decision on whether the resulting layout is better.The
line_length // 2threshold ensures we only omit parens for genuinely short RHS expressions — long expressions that need their own line splitting still get wrapped as before.Test plan
tests/data/cases/prefer_rhs_split.py(idempotent formatting)tests/data/cases/prefer_rhs_split_reformatted.py