Repository navigation
Fix unnecessary parentheses in indexed assignment RHS - #5095
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.
β¦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. |
|
Hey @ParamChordiya, are you still updating this PR? |
Hey @cobaltt7 sorry i got caught up with something else and lost track of this PR. let me circle back soon. |
β¦ens-indexed-assignment # Conflicts: # CHANGES.md # docs/the_black_code_style/future_style.md # src/black/resources/black.schema.json
The previous heuristic in can_omit_invisible_parens was too broad and caused 758 unwanted formatting changes across the diff-shades corpus: - It allowed omitting optional parens whenever the RHS contained any bracket, even when the RHS did not fit on one line, producing mid-expression splits inside function calls and ternaries. - Its LHS check matched any bracket before the '=', so it fired for parenthesized tuple targets like '(x,) = ...' and annotated assignments like 'x: list[str] = ...', which are not indexed assignments. The path now requires: a visible ']' immediately before the '=' (a genuine subscripted target), no annotation, a head too long to fit on one line (so the subscript split is forced regardless), and an RHS that fits on the '] = ' tail line. This preserves the intended psf#4349 fix while leaving every other assignment shape untouched. With the narrowed check, bad candidates no longer reach the rhs-oop-vs-rhs comparison, so the subscript-chain guard added to _prefer_split_rhs_oop_over_rhs is no longer needed and is removed. Verified: all format tests pass; every regression hunk from the diff-shades report now formats unchanged; black's own sources are stable and idempotent under stable, preview, and unstable modes.
for more information, see https://pre-commit.ci
|
Hey @cobaltt7, back on this β thanks for your patience. You were right that the earlier approach was still too aggressive, so I reworked the heuristic instead of patching around it. What was causing the bad shades changes The check in The new approach The paren-omission path now only fires when all of these hold:
In other words, it only removes the parens in the exact #4349 situation: the line has to split at the LHS anyway, and the parens on the tail are pure noise. When the LHS fits, paren-wrapping the RHS stays the preferred style, untouched. With the bad candidates eliminated at the source, the subscript-chain guard I'd added to How I verified
Also merged |
|
Thanks! |
This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [black](https://github.com/psf/black) ([changelog](https://github.com/psf/black/blob/main/CHANGES.md)) | `<26.5.2,>=26.5.1` β `<26.10.1,>=26.10.0` |  |  | --- ### Release Notes <details> <summary>psf/black (black)</summary> ### [`v26.10.0`](https://github.com/psf/black/blob/HEAD/CHANGES.md#Version-26100) [Compare Source](psf/black@26.5.1...26.10.0) ##### Stable style - `--line-ranges` no longer inserts an empty line after a docstring when the range covers only the docstring itself ([#​5312](psf/black#5312)) - Fix `# fmt: skip` on a bracketed ternary turning the surrounding tuple into a call ([#​5464](psf/black#5464)) - Fix crash when `# fmt: skip` is placed on a one-line function or class with PEP 695 type parameters ([#​5429](psf/black#5429)) - Fix an inline comment after the closing bracket of optional parentheses being moved inside the parentheses when the parenthesized expression contains own-line comments ([#​5395](psf/black#5395)) - Fix unparseable output when `# fmt: skip` is placed on a bracket of an `if`, `while`, `for`, or `with` header ([#​5401](psf/black#5401), [#​5405](psf/black#5405)) - Fix crash when formatting parenthesized expressions with multiple inline comments and `# fmt: skip` ([#​5414](psf/black#5414)) - Fix parsing Jupyter notebook assignment magics when non-ASCII characters appear earlier on the line ([#​5381](psf/black#5381)) - Preserve blank lines that come immediately before a `# fmt: on` comment ([#​5300](psf/black#5300)) - Keep the parentheses around the target of an annotated assignment (e.g. `(x): int = 5`), which prevent CPython from including the name in `__annotations__` ([#​5321](psf/black#5321)) - Stop treating a t-string in docstring position as a docstring (e.g. `t" spam "` as the first statement of a module, class or function) ([#​5287](psf/black#5287)) - Fix unparseable output for a t-string whose replacement field contains a quote (e.g. `t'\'{a["b"]}\''`) ([#​5265](psf/black#5265)) - Fix unparseable output for a triple-quoted string whose body ends in an already-escaped double quote (e.g. `'''\'''\"'''`) ([#​5262](psf/black#5262)) - Fix `--skip-magic-trailing-comma` dropping the trailing comma from a split single-element tuple used as a lambda parameter default ([#​5246](psf/black#5246)) - Fix unstable formatting when an inline comment sits on optional parentheses (e.g. a parenthesized assert message) ([#​5241](psf/black#5241)) - Fix `--skip-magic-trailing-comma` dropping the trailing comma of a one-element subscript (`a[x,]`) when the line is long enough to be split and contains a power operator ([#​5272](psf/black#5272)) - Fix crash when a standalone comment sits between tokens of a comprehension or lambda ([#​5144](psf/black#5144)) - Fix inline comments on a bracket inside a comprehension being dropped ([#​5330](psf/black#5330)) - Respect the magic trailing comma in a PEP 695 type parameter list containing a `*TypeVarTuple` or `**ParamSpec` ([#​5244](psf/black#5244)) - Fix crash when a comment-only `# fmt: off`/`# fmt: on` block is followed by a `with` statement after another standalone comment ([#​5189](psf/black#5189)) - Fix a crash when splitting `case case if ...` match patterns at very small line lengths ([#​5147](psf/black#5147)) - Fix multiline docstring indentation when leading tabs are used inside indented docstrings ([#​5148](psf/black#5148)) - Respect `# fmt: skip` on a line that opens a bracket (e.g. `from x import ( # fmt: skip`) when a standalone comment is among the bracket's contents ([#​5161](psf/black#5161)) - Fix an AST safety error when separate `# type: ignore` comments in a parenthesized attribute chain were merged onto one physical line ([#​5297](psf/black#5297)) - Preserve comments and blank lines outside requested ranges when formatting with `--line-ranges` ([#​5175](psf/black#5175)) - Fix crash when `# fmt: skip` is used on one-line `async def`, `async with`, and `async for` statements containing a semicolon ([#​5311](psf/black#5311)) - Stop converting form feeds or other similar characters in docstrings into newlines ([#​5461](psf/black#5461)) - Fix `--skip-source-first-line` turning the skipped line's CRLF ending into `\r\r\n` when reformatting a file with CRLF line endings ([#​5438](psf/black#5438)) ##### Preview style ##### New preview features - Add missing blank lines after classes whose last method has an ellipsis body ([#​5439](psf/black#5439)) - Split only the brackets holding a magic trailing comma when more trailers follow them (e.g. stop splitting inside the `[2]` of `[1,][2](3)`) ([#​5448](psf/black#5448)) - Keep dictionary keys containing operators together on one line when the value can be wrapped onto a new line instead ([#​5435](psf/black#5435)) - Remove redundant parentheses around individual variables in unpacking targets (e.g. `for (x), (y) in points:` becomes `for x, y in points:`) ([#​5416](psf/black#5416)) - Normalize uppercase `T` prefixes on t-strings to lowercase ([#​5425](psf/black#5425)) - Remove redundant parentheses around generator expressions ([#​5304](psf/black#5304), [#​5369](psf/black#5369)) - Preserve two blank lines before a top-level class starting inside a `# fmt: off` block after an import ([#​5238](psf/black#5238)) - Fix unnecessary parentheses around short right-hand expressions in indexed assignments (e.g. `x[key] = expr`) ([#​5095](psf/black#5095)) - Parenthesize tuple expressions in `yield` statements for consistency with function calls and returns ([#​5170](psf/black#5170)) - Stop splitting between a variable and its operator (`not in`, `==`, `is`, ...) when the right-hand side is a bracketed expression, and instead split inside the brackets ([#​5135](psf/black#5135)) - In `.pyi` stub files, enforce a blank line after a function or method that has a docstring-only body when another comment or statement follows it ([#​5158](psf/black#5158)) ##### Updates to existing preview features - Fix crash in stub files when `# fmt: skip` is placed on a function in a group of same-name decorated functions (e.g. `@overload`s or a property setter) ([#​5430](psf/black#5430)) - Keep the parentheses around a lambda used as the iterable of a comprehension (e.g. `[x for x in (lambda: 0) if x]`) ([#​5176](psf/black#5176), [#​5200](psf/black#5200)) ##### Updates to existing unstable features - Do not treat multi-line expressions with merged strings and trailing `# type: ignore` comments as single-line unsplittable expressions ([#​5466](psf/black#5466)) - Fix duplicated inline comment when stripping the parentheses around a string or merging a backslash-continued string on the same line ([#​5449](psf/black#5449)) - Split long stringified return annotations even when the function has parameters ([#​5427](psf/black#5427)) - Don't hug brackets when doing so would join two `# type: ignore` comments onto one line ([#​5271](psf/black#5271)) - Fix a crash when `# type: ignore` is lost during formatting of a long parenthesized string ([#​5329](psf/black#5329)) - Fix only the first part of an implicitly concatenated unmergeable string (e.g. `r"..." r"..."`) being wrapped in parentheses ([#​5434](psf/black#5434)) ##### Configuration - Add support for `NO_COLOR` environment variable to disable ANSI output ([#​5129](psf/black#5129)) - Remove spurious target version warning when runtime version is included in a `--target-version` flag ([#​5167](psf/black#5167)) - Fix `--force-exclude` not excluding files whose path contains `..` (e.g. `black ../generated/file.py` run from a subdirectory) ([#​5471](psf/black#5471)) - Fall back to the default configuration, with a warning, when the given sources share no common project root (e.g. they are on different drives on Windows) instead of crashing ([#​5386](psf/black#5386)) - Fix loading a stale cached `pyproject.toml` path when `--code` is used from different working directories in the same process ([#​5152](psf/black#5152)) - Add validation for `--line-ranges` values ([#​5107](psf/black#5107)) - Ignore empty cache files instead of raising an `EOFError` ([#​5192](psf/black#5192)) - Reject non-string `include` and `force-exclude` values in `pyproject.toml` ([#​5193](psf/black#5193)) - Validate `BLACK_NUM_WORKERS` values and report invalid values as usage errors instead of crashing ([#​5211](psf/black#5211)) - Ignore permission errors when reading cache ([#​5258](psf/black#5258)) ##### Packaging - Reduce the size of Linux standalone binaries by stripping debug symbols during the PyInstaller release build ([#​5223](psf/black#5223)) - Black is now released using GitHub [Immutable Releases](https://docs.github.com/code-security/concepts/supply-chain-security/immutable-releases) ([#​5296](psf/black#5296)) ##### Performance - Fix superlinear runtime growth with the number of input files ([#​5450](psf/black#5450)) - Improve performance on strings containing many consecutive backslashes ([#​5163](psf/black#5163)) - Improve performance on files with many `# fmt: skip`/`# fmt: off`/`# fmt: on` comments ([#​5169](psf/black#5169), [#​5190](psf/black#5190), [#​5232](psf/black#5232)) - Improve performance on long calls and collections ([#​5177](psf/black#5177)) - Improve performance on multiline strings inside large collections (e.g. a dict literal with triple-quoted strings as values) ([#​5188](psf/black#5188)) - Improve performance on files with many soft keywords (e.g. `match`/`case` blocks) ([#​5186](psf/black#5186)) - Improve performance on long semicolon-separated statements (in the stable style) and large dict literals (in `--preview`) ([#​5184](psf/black#5184)) - Improve performance of `--line-ranges` on files with many sibling blocks (a long `if`/`elif` chain, a `match` with many cases, or many top-level definitions) ([#​5213](psf/black#5213)) - Improve performance on deeply nested bracketed expressions ([#​5171](psf/black#5171), [#​5242](psf/black#5242)) - Improve performance on lists and subscripts holding one long expression without operators (e.g. a long run of implicitly concatenated strings inside `[]`) ([#​5239](psf/black#5239)) - Improve performance on deeply chained operations (e.g. a long `a ** b ** c ** ...` chain) ([#​5235](psf/black#5235)) - Improve performance on long `if`/`elif` chains and other compound statements with many clauses ([#​5322](psf/black#5322)) - Improve performance of `string_processing`: - when merging implicitly concatenated f-strings containing long string literals ([#​5165](psf/black#5165)) - when merging long implicitly concatenated strings ([#​5173](psf/black#5173), [#​5194](psf/black#5194)) - when rewriting large nodes (e.g. `"%s ..." % (a, b, c, ...)`) ([#​5178](psf/black#5178), [#​5199](psf/black#5199), [#​5220](psf/black#5220)) - when splitting long string literals ([#​5183](psf/black#5183)) ##### Output - Report parser failures using editor-friendly `path:line:column` locations ([#​5237](psf/black#5237)) - Fix crash when writing formatted code or diffs to a `sys.stdout` that has no `buffer` attribute (e.g. in Jupyter notebooks) ([#​5411](psf/black#5411)) - Report parse failures on Black's own output as internal errors ([#​5383](psf/black#5383)) ##### *Blackd* - Return HTTP 400 instead of 500 when the `X-Python-Variant` header is empty or has an empty entry (e.g. a trailing comma) ([#​5428](psf/black#5428)) - Allow optional whitespace around comma-separated versions and `pyi` in the `X-Python-Variant` header ([#​5441](psf/black#5441)) ##### Integrations - Remove unused `migrate-black` script ([#​5319](psf/black#5319)) - Support PEP 440 version specifiers in `tool.black.required-version` for the GitHub Action ([#​5399](psf/black#5399)) - Add outputs (`is_formatted`, `change_count`, `same_count`, `failure_count`) to GitHub Action runs ([#​5408](psf/black#5408)) ##### Documentation - Document `vim-python-pep8-indent`, which provides an `indentexpr` for Black-style insert-mode indentation ([#​5288](psf/black#5288)) </details> --- ### Configuration π **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) π¦ **Automerge**: Disabled by config. Please merge this manually once you are satisfied. β» **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. π **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMzIuNSIsInVwZGF0ZWRJblZlciI6IjQ0LjEzMi41IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJ0eXBlL2RlcGVuZGVuY2llcyJdfQ==--> Reviewed-on: https://git.tainton.uk/luke/instant-msg-api/pulls/290 Co-authored-by: renovate[bot] <renovate-bot@git.tainton.uk>



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