From e1f726db08f7dd01d347d25ae4ab58e3bf10c156 Mon Sep 17 00:00:00 2001 From: Param Chordiya Date: Sun, 12 Apr 2026 15:24:53 -0500 Subject: [PATCH 01/10] Fix unnecessary parentheses in indexed assignment RHS (#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. 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 #4349. --- src/black/lines.py | 16 ++++++++++++++++ tests/data/cases/prefer_rhs_split.py | 6 ++++++ tests/data/cases/prefer_rhs_split_reformatted.py | 8 ++++++++ 3 files changed, 30 insertions(+) diff --git a/src/black/lines.py b/src/black/lines.py index 93d8036d9ca..2d99466e0f2 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -1479,6 +1479,22 @@ def can_omit_invisible_parens( if _can_omit_closing_paren(line, last=last, line_length=line_length): return True + # For assignment RHS where the LHS contains brackets (e.g. indexed + # assignments like `x[key] = expr`), allow omitting optional parens + # if the body is short enough. The split will happen at the LHS + # brackets instead, and _prefer_split_rhs_oop_over_rhs will decide + # whether it actually produces better output. + if ( + len(rhs.head.leaves) >= 2 + and rhs.head.leaves[-2].type == token.EQUAL + and any(leaf.type in BRACKETS for leaf in rhs.head.leaves[:-2]) + ): + # Only when the body is short enough to fit on the tail line + # after the LHS bracket split (e.g. `] = 10 - 5`). + body_length = str_width(str(line).strip("\n")) + if body_length <= line_length // 2: + return True + return False diff --git a/tests/data/cases/prefer_rhs_split.py b/tests/data/cases/prefer_rhs_split.py index f3d9fd67251..38fbc4b8d57 100644 --- a/tests/data/cases/prefer_rhs_split.py +++ b/tests/data/cases/prefer_rhs_split.py @@ -76,6 +76,12 @@ ] = 1 +# Indexed assignment with a short RHS expression should not get unnecessary parens. +dictionary_of_arrays["long_key_name_for_the_example"][ + very_long_index_name, index_zero +] = 10 - 5 + + # Right side of assignment contains un-nested pairs of inner parens. some_kind_of_instance.some_kind_of_map[a_key] = ( isinstance(some_var, SomeClass) diff --git a/tests/data/cases/prefer_rhs_split_reformatted.py b/tests/data/cases/prefer_rhs_split_reformatted.py index 2ec0728af82..0102652a8a1 100644 --- a/tests/data/cases/prefer_rhs_split_reformatted.py +++ b/tests/data/cases/prefer_rhs_split_reformatted.py @@ -11,6 +11,9 @@ # exactly line length limit + 1, it won't be split like that. xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)] = 1 +# Indexed assignment should not get unnecessary parens around the RHS (#4349). +dictionary_of_arrays["long_key_name_for_the_example"][very_long_index_name, index_zero] = 10 - 5 + # Regression test for #1187 print( dict( @@ -45,6 +48,11 @@ xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1) ] = 1 +# Indexed assignment should not get unnecessary parens around the RHS (#4349). +dictionary_of_arrays["long_key_name_for_the_example"][ + very_long_index_name, index_zero +] = 10 - 5 + # Regression test for #1187 print( dict( From d710761a69089c6e50d7d05ed128140696f75357 Mon Sep 17 00:00:00 2001 From: Param Chordiya Date: Sun, 12 Apr 2026 19:57:23 -0500 Subject: [PATCH 02/10] Gate indexed assignment parens fix behind preview mode and add CHANGES.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 --- CHANGES.md | 2 ++ src/black/linegen.py | 2 +- src/black/lines.py | 4 +++- src/black/mode.py | 1 + tests/data/cases/prefer_rhs_split.py | 6 ------ .../cases/prefer_rhs_split_reformatted.py | 8 ------- ...iew_prefer_rhs_split_indexed_assignment.py | 21 +++++++++++++++++++ 7 files changed, 28 insertions(+), 16 deletions(-) create mode 100644 tests/data/cases/preview_prefer_rhs_split_indexed_assignment.py diff --git a/CHANGES.md b/CHANGES.md index 90a99abe6d5..83492ae782c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,6 +20,8 @@ +- Fix unnecessary parentheses around short RHS expressions in indexed assignments like + `x[key] = expr` (#5095) - Improve heuristics around whether blank lines should appear before, within and after groups of same-name decorated functions (such as `@overload` groups) in `.pyi` stub files (#5021) diff --git a/src/black/linegen.py b/src/black/linegen.py index 06899029469..41bb38497cb 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1066,7 +1066,7 @@ def _maybe_split_omitting_optional_parens( # in this case; attempting a split without them is a waste of time) and not line.is_import # and we can actually remove the parens - and can_omit_invisible_parens(rhs, mode.line_length) + and can_omit_invisible_parens(rhs, mode.line_length, mode) ): omit = {id(rhs.closing_bracket), *omit} try: diff --git a/src/black/lines.py b/src/black/lines.py index 2d99466e0f2..64e8dd40ea3 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -1350,6 +1350,7 @@ def can_be_split(line: Line) -> bool: def can_omit_invisible_parens( rhs: RHSResult, line_length: int, + mode: Mode = Mode(), ) -> bool: """Does `rhs.body` have a shape safe to reformat without optional parens around it? @@ -1485,7 +1486,8 @@ def can_omit_invisible_parens( # brackets instead, and _prefer_split_rhs_oop_over_rhs will decide # whether it actually produces better output. if ( - len(rhs.head.leaves) >= 2 + Preview.fix_unnecessary_parens_in_indexed_assignment in mode + and len(rhs.head.leaves) >= 2 and rhs.head.leaves[-2].type == token.EQUAL and any(leaf.type in BRACKETS for leaf in rhs.head.leaves[:-2]) ): diff --git a/src/black/mode.py b/src/black/mode.py index 5f8814ce80a..c5064d7b24d 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -258,6 +258,7 @@ class Preview(Enum): wrap_long_dict_values_in_parens = auto() fix_if_guard_explosion_in_case_statement = auto() pyi_overload_group_blank_lines = auto() + fix_unnecessary_parens_in_indexed_assignment = auto() UNSTABLE_FEATURES: set[Preview] = { diff --git a/tests/data/cases/prefer_rhs_split.py b/tests/data/cases/prefer_rhs_split.py index 38fbc4b8d57..f3d9fd67251 100644 --- a/tests/data/cases/prefer_rhs_split.py +++ b/tests/data/cases/prefer_rhs_split.py @@ -76,12 +76,6 @@ ] = 1 -# Indexed assignment with a short RHS expression should not get unnecessary parens. -dictionary_of_arrays["long_key_name_for_the_example"][ - very_long_index_name, index_zero -] = 10 - 5 - - # Right side of assignment contains un-nested pairs of inner parens. some_kind_of_instance.some_kind_of_map[a_key] = ( isinstance(some_var, SomeClass) diff --git a/tests/data/cases/prefer_rhs_split_reformatted.py b/tests/data/cases/prefer_rhs_split_reformatted.py index 0102652a8a1..2ec0728af82 100644 --- a/tests/data/cases/prefer_rhs_split_reformatted.py +++ b/tests/data/cases/prefer_rhs_split_reformatted.py @@ -11,9 +11,6 @@ # exactly line length limit + 1, it won't be split like that. xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)] = 1 -# Indexed assignment should not get unnecessary parens around the RHS (#4349). -dictionary_of_arrays["long_key_name_for_the_example"][very_long_index_name, index_zero] = 10 - 5 - # Regression test for #1187 print( dict( @@ -48,11 +45,6 @@ xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1) ] = 1 -# Indexed assignment should not get unnecessary parens around the RHS (#4349). -dictionary_of_arrays["long_key_name_for_the_example"][ - very_long_index_name, index_zero -] = 10 - 5 - # Regression test for #1187 print( dict( diff --git a/tests/data/cases/preview_prefer_rhs_split_indexed_assignment.py b/tests/data/cases/preview_prefer_rhs_split_indexed_assignment.py new file mode 100644 index 00000000000..e1394c10d4f --- /dev/null +++ b/tests/data/cases/preview_prefer_rhs_split_indexed_assignment.py @@ -0,0 +1,21 @@ +# flags: --preview + +# Indexed assignment with a short RHS expression should not get unnecessary parens. +dictionary_of_arrays["long_key_name_for_the_example"][ + very_long_index_name, index_zero +] = 10 - 5 + +# Unformatted input: the unnecessary parens should be removed. +dictionary_of_arrays["long_key_name_for_the_example"][very_long_index_name, index_zero] = (10 - 5) + +# output + +# Indexed assignment with a short RHS expression should not get unnecessary parens. +dictionary_of_arrays["long_key_name_for_the_example"][ + very_long_index_name, index_zero +] = 10 - 5 + +# Unformatted input: the unnecessary parens should be removed. +dictionary_of_arrays["long_key_name_for_the_example"][ + very_long_index_name, index_zero +] = 10 - 5 From 7b592a2cb04088a84e477fc972223848c5210915 Mon Sep 17 00:00:00 2001 From: Param Chordiya Date: Sun, 12 Apr 2026 21:37:46 -0500 Subject: [PATCH 03/10] Address review feedback: make mode required, use AST-based length check, 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 --- docs/the_black_code_style/future_style.md | 30 +++++++++++++++++++++++ src/black/lines.py | 6 +++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 0963916bd30..8d62aea1fee 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -28,6 +28,9 @@ Currently, the following features are included in the preview style: - `pyi_overload_group_blank_lines`: In `.pyi` stub files, improve heuristics around when blank lines should appear before, after and within decorated function groups. ([see below](labels/pyi-overload-group)) +- `fix_unnecessary_parens_in_indexed_assignment`: Remove unnecessary parentheses around + the right-hand side of indexed assignments (e.g. `x[key] = expr`) when the expression + is short enough. ([see below](labels/fix-unnecessary-parens-indexed-assignment)) (labels/wrap-comprehension-in)= @@ -189,6 +192,33 @@ def foo(x: str) -> str: ... def bar(x): ... ``` +(labels/fix-unnecessary-parens-indexed-assignment)= + +### Unnecessary parentheses in indexed assignments + +When an assignment target contains brackets (e.g. indexed access like `x[key] = expr`), +Black would previously wrap the right-hand side expression in unnecessary parentheses +when the line was too long. With this feature enabled, Black removes the unnecessary +parentheses when the RHS expression is short enough. + +For example: + +```python +# Before +dictionary_of_arrays["long_key_name_for_the_example"][ + very_long_index_name, index_zero +] = (10 - 5) +``` + +will be formatted to: + +```python +# After (with --preview) +dictionary_of_arrays["long_key_name_for_the_example"][ + very_long_index_name, index_zero +] = 10 - 5 +``` + ## Unstable style (labels/unstable-style)= diff --git a/src/black/lines.py b/src/black/lines.py index 64e8dd40ea3..23a30aefaf5 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -1350,7 +1350,7 @@ def can_be_split(line: Line) -> bool: def can_omit_invisible_parens( rhs: RHSResult, line_length: int, - mode: Mode = Mode(), + mode: Mode, ) -> bool: """Does `rhs.body` have a shape safe to reformat without optional parens around it? @@ -1493,7 +1493,9 @@ def can_omit_invisible_parens( ): # Only when the body is short enough to fit on the tail line # after the LHS bracket split (e.g. `] = 10 - 5`). - body_length = str_width(str(line).strip("\n")) + body_length = 4 * line.depth + for _index, _leaf, leaf_length in line.enumerate_with_length(): + body_length += leaf_length if body_length <= line_length // 2: return True From 83db67deaf3fbf9affda11d00e58d6e2c1f25070 Mon Sep 17 00:00:00 2001 From: Param Chordiya Date: Sun, 12 Apr 2026 22:37:51 -0500 Subject: [PATCH 04/10] Regenerate schema to include fix_unnecessary_parens_in_indexed_assignment Co-Authored-By: Claude Opus 4.6 --- src/black/resources/black.schema.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/black/resources/black.schema.json b/src/black/resources/black.schema.json index 658de8d632e..68c7f9015da 100644 --- a/src/black/resources/black.schema.json +++ b/src/black/resources/black.schema.json @@ -87,7 +87,8 @@ "simplify_power_operator_hugging", "wrap_long_dict_values_in_parens", "fix_if_guard_explosion_in_case_statement", - "pyi_overload_group_blank_lines" + "pyi_overload_group_blank_lines", + "fix_unnecessary_parens_in_indexed_assignment" ] }, "description": "Enable specific features included in the `--unstable` style. Requires `--preview`. No compatibility guarantees are provided on the behavior or existence of any unstable features." From 4a7809a0ffc44a7d7c6f39c8813a10f54e169c25 Mon Sep 17 00:00:00 2001 From: Param Chordiya Date: Sun, 19 Apr 2026 22:01:46 -0500 Subject: [PATCH 05/10] Address review: improve heuristic and fix docs wording --- docs/the_black_code_style/future_style.md | 6 +-- src/black/lines.py | 41 ++++++++++--------- ...iew_prefer_rhs_split_indexed_assignment.py | 16 ++++++++ 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 8d62aea1fee..d5a20fadeb9 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -197,9 +197,9 @@ def bar(x): ... ### Unnecessary parentheses in indexed assignments When an assignment target contains brackets (e.g. indexed access like `x[key] = expr`), -Black would previously wrap the right-hand side expression in unnecessary parentheses -when the line was too long. With this feature enabled, Black removes the unnecessary -parentheses when the RHS expression is short enough. +previously, Black would incorrectly wrap the right-hand side expression in unnecessary +parentheses when the line was too long. With this feature enabled, Black removes the +unnecessary parentheses when the RHS expression fits on the tail line. For example: diff --git a/src/black/lines.py b/src/black/lines.py index 23a30aefaf5..cb0b23b10a8 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -1414,6 +1414,28 @@ def can_omit_invisible_parens( ): closing_bracket = leaf + # For assignment RHS where the LHS contains brackets (e.g. indexed + # assignments like `x[key] = expr`), allow omitting optional parens + # if the body fits on a single line or has brackets to further split on. + # This check must come before the delimiter_count > 1 early return below, + # which would otherwise reject expressions like `1 + 1 + 1 + ...`. + # The downstream _prefer_split_rhs_oop_over_rhs will make the final + # decision on whether the result is actually better. + if ( + Preview.fix_unnecessary_parens_in_indexed_assignment in mode + and len(rhs.head.leaves) >= 2 + and rhs.head.leaves[-2].type == token.EQUAL + and any(leaf.type in BRACKETS for leaf in rhs.head.leaves[:-2]) + ): + body_length = 4 * line.depth + has_brackets = False + for _index, _leaf, leaf_length in line.enumerate_with_length(): + body_length += leaf_length + if _leaf.type in OPENING_BRACKETS: + has_brackets = True + if has_brackets or body_length <= line_length: + return True + bt = line.bracket_tracker if not bt.delimiters: # Without delimiters the optional parentheses are useless. @@ -1480,25 +1502,6 @@ def can_omit_invisible_parens( if _can_omit_closing_paren(line, last=last, line_length=line_length): return True - # For assignment RHS where the LHS contains brackets (e.g. indexed - # assignments like `x[key] = expr`), allow omitting optional parens - # if the body is short enough. The split will happen at the LHS - # brackets instead, and _prefer_split_rhs_oop_over_rhs will decide - # whether it actually produces better output. - if ( - Preview.fix_unnecessary_parens_in_indexed_assignment in mode - and len(rhs.head.leaves) >= 2 - and rhs.head.leaves[-2].type == token.EQUAL - and any(leaf.type in BRACKETS for leaf in rhs.head.leaves[:-2]) - ): - # Only when the body is short enough to fit on the tail line - # after the LHS bracket split (e.g. `] = 10 - 5`). - body_length = 4 * line.depth - for _index, _leaf, leaf_length in line.enumerate_with_length(): - body_length += leaf_length - if body_length <= line_length // 2: - return True - return False diff --git a/tests/data/cases/preview_prefer_rhs_split_indexed_assignment.py b/tests/data/cases/preview_prefer_rhs_split_indexed_assignment.py index e1394c10d4f..03c14dd782a 100644 --- a/tests/data/cases/preview_prefer_rhs_split_indexed_assignment.py +++ b/tests/data/cases/preview_prefer_rhs_split_indexed_assignment.py @@ -8,6 +8,12 @@ # Unformatted input: the unnecessary parens should be removed. dictionary_of_arrays["long_key_name_for_the_example"][very_long_index_name, index_zero] = (10 - 5) +# Longer RHS expressions that fit on the tail line should also lose parens. +dictionary_of_arrays["long_key_name_for_the_example"][very_long_index_name, index_zero] = (1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1) + +# RHS with brackets (function call) should also lose unnecessary parens. +dictionary_of_arrays["long_key_name_for_the_example"][very_long_index_name, index_zero] = (some_function(arg1, arg2)) + # output # Indexed assignment with a short RHS expression should not get unnecessary parens. @@ -19,3 +25,13 @@ dictionary_of_arrays["long_key_name_for_the_example"][ very_long_index_name, index_zero ] = 10 - 5 + +# Longer RHS expressions that fit on the tail line should also lose parens. +dictionary_of_arrays["long_key_name_for_the_example"][ + very_long_index_name, index_zero +] = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + +# RHS with brackets (function call) should also lose unnecessary parens. +dictionary_of_arrays["long_key_name_for_the_example"][ + very_long_index_name, index_zero +] = some_function(arg1, arg2) From a34452cca4e340f2e404f2f9688673c34ca81cc0 Mon Sep 17 00:00:00 2001 From: Param Chordiya Date: Fri, 24 Apr 2026 23:57:28 -0500 Subject: [PATCH 06/10] Fix RHS subscript chain split regression in _prefer_split_rhs_oop_over_rhs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/black/linegen.py | 13 +++++++++++++ tests/data/cases/prefer_rhs_split.py | 12 ++++++++++++ tests/data/cases/prefer_rhs_split_reformatted.py | 14 ++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/src/black/linegen.py b/src/black/linegen.py index 52b0052bd8a..eed24e79058 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1180,6 +1180,19 @@ def _prefer_split_rhs_oop_over_rhs( if rhs_head_equal_count > 1 and rhs_head_equal_count > rhs_oop_head_equal_count: return False + # Don't prefer OOP if the split breaks inside a subscript access chain on the + # RHS. When rhs_oop.tail starts with `]` followed by more tokens (like `.attr` + # or `.method()`), and `=` is in rhs_oop.head (split is in the RHS, not the + # LHS), this means we split mid-chain — creating ugly output like + # `expr + obj[\n idx\n].attr`. Prefer paren-wrapping instead. + if ( + rhs_oop.tail.leaves + and rhs_oop.tail.leaves[0].type == token.RSQB + and len(rhs_oop.tail.leaves) > 1 + and any(leaf.type == token.EQUAL for leaf in rhs_oop.head.leaves) + ): + return False + has_closing_bracket_after_assign = False for leaf in reversed(rhs_oop.head.leaves): if leaf.type == token.EQUAL: diff --git a/tests/data/cases/prefer_rhs_split.py b/tests/data/cases/prefer_rhs_split.py index f3d9fd67251..0dde9a189ae 100644 --- a/tests/data/cases/prefer_rhs_split.py +++ b/tests/data/cases/prefer_rhs_split.py @@ -104,3 +104,15 @@ ) = ( cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc ) = ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd + + +# Make sure that when the RHS contains a subscript access chain (attribute +# access with subscript like obj.children[idx].attr), Black prefers wrapping +# the RHS in parentheses rather than splitting at the subscript. +some_node.children[1].prefix = ( + some_node.children[0].prefix + some_node.children[1].prefix +) + +another_node.children[idx].value = ( + another_node.children[idx - 1].value + another_node.children[idx + 1].value +) diff --git a/tests/data/cases/prefer_rhs_split_reformatted.py b/tests/data/cases/prefer_rhs_split_reformatted.py index 2ec0728af82..0c0076bfad8 100644 --- a/tests/data/cases/prefer_rhs_split_reformatted.py +++ b/tests/data/cases/prefer_rhs_split_reformatted.py @@ -20,6 +20,11 @@ ) ) +# Regression: subscript access chain on RHS should not be split mid-chain +some_node.children[1].prefix = some_node.children[0].prefix + some_node.children[1].prefix + +another_node.children[idx].value = another_node.children[idx - 1].value + another_node.children[idx + 1].value + # output @@ -55,3 +60,12 @@ c=3, ) ) + +# Regression: subscript access chain on RHS should not be split mid-chain +some_node.children[1].prefix = ( + some_node.children[0].prefix + some_node.children[1].prefix +) + +another_node.children[idx].value = ( + another_node.children[idx - 1].value + another_node.children[idx + 1].value +) From eba22d090e80d91bfe1bfbadd803b46c95f57237 Mon Sep 17 00:00:00 2001 From: Param Chordiya Date: Sat, 25 Apr 2026 00:06:13 -0500 Subject: [PATCH 07/10] Reformat black's own source files after linegen change 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. --- src/black/__init__.py | 5 +- src/black/_width_table.py | 256 +++++++++++++++++++------------------- src/black/trans.py | 24 ++-- tests/test_black.py | 14 ++- 4 files changed, 153 insertions(+), 146 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index f9f1b51c6fa..770a34176d1 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -596,8 +596,9 @@ def main( ) ctx.exit(1) - root, method = ( - find_project_root(src, stdin_filename) if code is None else (None, None) + root, method = find_project_root(src, stdin_filename) if code is None else ( + None, + None, ) ctx.obj["root"] = root diff --git a/src/black/_width_table.py b/src/black/_width_table.py index 517535701f7..2a3f44833a5 100644 --- a/src/black/_width_table.py +++ b/src/black/_width_table.py @@ -3,130 +3,132 @@ # Unicode 17.0.0 from typing import Final -WIDTH_TABLE: Final[list[tuple[int, int, int]]] = [ - (4352, 4447, 2), - (8986, 8987, 2), - (9001, 9002, 2), - (9193, 9196, 2), - (9200, 9200, 2), - (9203, 9203, 2), - (9725, 9726, 2), - (9748, 9749, 2), - (9776, 9783, 2), - (9800, 9811, 2), - (9855, 9855, 2), - (9866, 9871, 2), - (9875, 9875, 2), - (9889, 9889, 2), - (9898, 9899, 2), - (9917, 9918, 2), - (9924, 9925, 2), - (9934, 9934, 2), - (9940, 9940, 2), - (9962, 9962, 2), - (9970, 9971, 2), - (9973, 9973, 2), - (9978, 9978, 2), - (9981, 9981, 2), - (9989, 9989, 2), - (9994, 9995, 2), - (10024, 10024, 2), - (10060, 10060, 2), - (10062, 10062, 2), - (10067, 10069, 2), - (10071, 10071, 2), - (10133, 10135, 2), - (10160, 10160, 2), - (10175, 10175, 2), - (11035, 11036, 2), - (11088, 11088, 2), - (11093, 11093, 2), - (11904, 11929, 2), - (11931, 12019, 2), - (12032, 12245, 2), - (12272, 12329, 2), - (12336, 12350, 2), - (12353, 12438, 2), - (12443, 12543, 2), - (12549, 12591, 2), - (12593, 12686, 2), - (12688, 12773, 2), - (12783, 12830, 2), - (12832, 12871, 2), - (12880, 42124, 2), - (42128, 42182, 2), - (43360, 43388, 2), - (44032, 55203, 2), - (63744, 64255, 2), - (65040, 65049, 2), - (65072, 65106, 2), - (65108, 65126, 2), - (65128, 65131, 2), - (65281, 65376, 2), - (65504, 65510, 2), - (94176, 94179, 2), - (94194, 94198, 2), - (94208, 101589, 2), - (101631, 101662, 2), - (101760, 101874, 2), - (110576, 110579, 2), - (110581, 110587, 2), - (110589, 110590, 2), - (110592, 110882, 2), - (110898, 110898, 2), - (110928, 110930, 2), - (110933, 110933, 2), - (110948, 110951, 2), - (110960, 111355, 2), - (119552, 119638, 2), - (119648, 119670, 2), - (126980, 126980, 2), - (127183, 127183, 2), - (127374, 127374, 2), - (127377, 127386, 2), - (127488, 127490, 2), - (127504, 127547, 2), - (127552, 127560, 2), - (127568, 127569, 2), - (127584, 127589, 2), - (127744, 127776, 2), - (127789, 127797, 2), - (127799, 127868, 2), - (127870, 127891, 2), - (127904, 127946, 2), - (127951, 127955, 2), - (127968, 127984, 2), - (127988, 127988, 2), - (127992, 127994, 2), - (128000, 128062, 2), - (128064, 128064, 2), - (128066, 128252, 2), - (128255, 128317, 2), - (128331, 128334, 2), - (128336, 128359, 2), - (128378, 128378, 2), - (128405, 128406, 2), - (128420, 128420, 2), - (128507, 128591, 2), - (128640, 128709, 2), - (128716, 128716, 2), - (128720, 128722, 2), - (128725, 128728, 2), - (128732, 128735, 2), - (128747, 128748, 2), - (128756, 128764, 2), - (128992, 129003, 2), - (129008, 129008, 2), - (129292, 129338, 2), - (129340, 129349, 2), - (129351, 129535, 2), - (129648, 129660, 2), - (129664, 129674, 2), - (129678, 129734, 2), - (129736, 129736, 2), - (129741, 129756, 2), - (129759, 129770, 2), - (129775, 129784, 2), - (131072, 196605, 2), - (196608, 262141, 2), -] +WIDTH_TABLE: Final[list[tuple[int, int, int]]] = ( + [ + (4352, 4447, 2), + (8986, 8987, 2), + (9001, 9002, 2), + (9193, 9196, 2), + (9200, 9200, 2), + (9203, 9203, 2), + (9725, 9726, 2), + (9748, 9749, 2), + (9776, 9783, 2), + (9800, 9811, 2), + (9855, 9855, 2), + (9866, 9871, 2), + (9875, 9875, 2), + (9889, 9889, 2), + (9898, 9899, 2), + (9917, 9918, 2), + (9924, 9925, 2), + (9934, 9934, 2), + (9940, 9940, 2), + (9962, 9962, 2), + (9970, 9971, 2), + (9973, 9973, 2), + (9978, 9978, 2), + (9981, 9981, 2), + (9989, 9989, 2), + (9994, 9995, 2), + (10024, 10024, 2), + (10060, 10060, 2), + (10062, 10062, 2), + (10067, 10069, 2), + (10071, 10071, 2), + (10133, 10135, 2), + (10160, 10160, 2), + (10175, 10175, 2), + (11035, 11036, 2), + (11088, 11088, 2), + (11093, 11093, 2), + (11904, 11929, 2), + (11931, 12019, 2), + (12032, 12245, 2), + (12272, 12329, 2), + (12336, 12350, 2), + (12353, 12438, 2), + (12443, 12543, 2), + (12549, 12591, 2), + (12593, 12686, 2), + (12688, 12773, 2), + (12783, 12830, 2), + (12832, 12871, 2), + (12880, 42124, 2), + (42128, 42182, 2), + (43360, 43388, 2), + (44032, 55203, 2), + (63744, 64255, 2), + (65040, 65049, 2), + (65072, 65106, 2), + (65108, 65126, 2), + (65128, 65131, 2), + (65281, 65376, 2), + (65504, 65510, 2), + (94176, 94179, 2), + (94194, 94198, 2), + (94208, 101589, 2), + (101631, 101662, 2), + (101760, 101874, 2), + (110576, 110579, 2), + (110581, 110587, 2), + (110589, 110590, 2), + (110592, 110882, 2), + (110898, 110898, 2), + (110928, 110930, 2), + (110933, 110933, 2), + (110948, 110951, 2), + (110960, 111355, 2), + (119552, 119638, 2), + (119648, 119670, 2), + (126980, 126980, 2), + (127183, 127183, 2), + (127374, 127374, 2), + (127377, 127386, 2), + (127488, 127490, 2), + (127504, 127547, 2), + (127552, 127560, 2), + (127568, 127569, 2), + (127584, 127589, 2), + (127744, 127776, 2), + (127789, 127797, 2), + (127799, 127868, 2), + (127870, 127891, 2), + (127904, 127946, 2), + (127951, 127955, 2), + (127968, 127984, 2), + (127988, 127988, 2), + (127992, 127994, 2), + (128000, 128062, 2), + (128064, 128064, 2), + (128066, 128252, 2), + (128255, 128317, 2), + (128331, 128334, 2), + (128336, 128359, 2), + (128378, 128378, 2), + (128405, 128406, 2), + (128420, 128420, 2), + (128507, 128591, 2), + (128640, 128709, 2), + (128716, 128716, 2), + (128720, 128722, 2), + (128725, 128728, 2), + (128732, 128735, 2), + (128747, 128748, 2), + (128756, 128764, 2), + (128992, 129003, 2), + (129008, 129008, 2), + (129292, 129338, 2), + (129340, 129349, 2), + (129351, 129535, 2), + (129648, 129660, 2), + (129664, 129674, 2), + (129678, 129734, 2), + (129736, 129736, 2), + (129741, 129756, 2), + (129759, 129770, 2), + (129775, 129784, 2), + (131072, 196605, 2), + (196608, 262141, 2), + ] +) diff --git a/src/black/trans.py b/src/black/trans.py index 6f859358e0c..c6ccfea3150 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1090,17 +1090,19 @@ class BaseStringSplitter(StringTransformer): * The target string is not a multiline (i.e. triple-quote) string. """ - STRING_OPERATORS: Final = [ - token.EQEQUAL, - token.GREATER, - token.GREATEREQUAL, - token.LESS, - token.LESSEQUAL, - token.NOTEQUAL, - token.PERCENT, - token.PLUS, - token.STAR, - ] + STRING_OPERATORS: Final = ( + [ + token.EQEQUAL, + token.GREATER, + token.GREATEREQUAL, + token.LESS, + token.LESSEQUAL, + token.NOTEQUAL, + token.PERCENT, + token.PLUS, + token.STAR, + ] + ) @abstractmethod def do_splitter_match(self, line: Line) -> TMatchResult: diff --git a/tests/test_black.py b/tests/test_black.py index 8e61e8e866e..48ae4cb8379 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -2695,12 +2695,14 @@ def test_nested_gitignore(self) -> None: exclude = re.compile(r"") root_gitignore = black.files.get_gitignore(path) report = black.Report() - expected: list[Path] = [ - Path(path / "x.py"), - Path(path / "root/b.py"), - Path(path / "root/c.py"), - Path(path / "root/child/c.py"), - ] + expected: list[Path] = ( + [ + Path(path / "x.py"), + Path(path / "root/b.py"), + Path(path / "root/c.py"), + Path(path / "root/child/c.py"), + ] + ) this_abs = THIS_DIR.resolve() sources = list( black.gen_python_files( From 903e4c3a280ee8c9fc6f3fa476767a07858ffa62 Mon Sep 17 00:00:00 2001 From: Param Chordiya Date: Sat, 25 Apr 2026 00:35:44 -0500 Subject: [PATCH 08/10] Gate RHS subscript-chain guard behind preview flag 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. --- src/black/linegen.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index eed24e79058..b8916cb4310 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1185,8 +1185,12 @@ def _prefer_split_rhs_oop_over_rhs( # or `.method()`), and `=` is in rhs_oop.head (split is in the RHS, not the # LHS), this means we split mid-chain — creating ugly output like # `expr + obj[\n idx\n].attr`. Prefer paren-wrapping instead. + # Gated behind the same preview flag as the indexed assignment fix in + # can_omit_invisible_parens, since this guard only matters when that path + # is active (stable mode is unaffected). if ( - rhs_oop.tail.leaves + Preview.fix_unnecessary_parens_in_indexed_assignment in mode + and rhs_oop.tail.leaves and rhs_oop.tail.leaves[0].type == token.RSQB and len(rhs_oop.tail.leaves) > 1 and any(leaf.type == token.EQUAL for leaf in rhs_oop.head.leaves) From 4d2b46ab4d8da56ecdc9669e48e5bc9f9645fa77 Mon Sep 17 00:00:00 2001 From: Param Chordiya Date: Sat, 25 Apr 2026 16:31:45 -0500 Subject: [PATCH 09/10] Revert self-reformatted source files to pre-self-formatting state The unstable-mode self-format (eba22d0) broke test_piping which formats __init__.py in stable mode and expects an idempotent result. The two modes disagree on the ternary expression formatting, so the files need to stay in the stable-mode-idempotent form from the upstream merge (87d9925). --- src/black/__init__.py | 5 +- src/black/_width_table.py | 256 +++++++++++++++++++------------------- src/black/trans.py | 24 ++-- tests/test_black.py | 14 +-- 4 files changed, 146 insertions(+), 153 deletions(-) diff --git a/src/black/__init__.py b/src/black/__init__.py index 770a34176d1..f9f1b51c6fa 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -596,9 +596,8 @@ def main( ) ctx.exit(1) - root, method = find_project_root(src, stdin_filename) if code is None else ( - None, - None, + root, method = ( + find_project_root(src, stdin_filename) if code is None else (None, None) ) ctx.obj["root"] = root diff --git a/src/black/_width_table.py b/src/black/_width_table.py index 2a3f44833a5..517535701f7 100644 --- a/src/black/_width_table.py +++ b/src/black/_width_table.py @@ -3,132 +3,130 @@ # Unicode 17.0.0 from typing import Final -WIDTH_TABLE: Final[list[tuple[int, int, int]]] = ( - [ - (4352, 4447, 2), - (8986, 8987, 2), - (9001, 9002, 2), - (9193, 9196, 2), - (9200, 9200, 2), - (9203, 9203, 2), - (9725, 9726, 2), - (9748, 9749, 2), - (9776, 9783, 2), - (9800, 9811, 2), - (9855, 9855, 2), - (9866, 9871, 2), - (9875, 9875, 2), - (9889, 9889, 2), - (9898, 9899, 2), - (9917, 9918, 2), - (9924, 9925, 2), - (9934, 9934, 2), - (9940, 9940, 2), - (9962, 9962, 2), - (9970, 9971, 2), - (9973, 9973, 2), - (9978, 9978, 2), - (9981, 9981, 2), - (9989, 9989, 2), - (9994, 9995, 2), - (10024, 10024, 2), - (10060, 10060, 2), - (10062, 10062, 2), - (10067, 10069, 2), - (10071, 10071, 2), - (10133, 10135, 2), - (10160, 10160, 2), - (10175, 10175, 2), - (11035, 11036, 2), - (11088, 11088, 2), - (11093, 11093, 2), - (11904, 11929, 2), - (11931, 12019, 2), - (12032, 12245, 2), - (12272, 12329, 2), - (12336, 12350, 2), - (12353, 12438, 2), - (12443, 12543, 2), - (12549, 12591, 2), - (12593, 12686, 2), - (12688, 12773, 2), - (12783, 12830, 2), - (12832, 12871, 2), - (12880, 42124, 2), - (42128, 42182, 2), - (43360, 43388, 2), - (44032, 55203, 2), - (63744, 64255, 2), - (65040, 65049, 2), - (65072, 65106, 2), - (65108, 65126, 2), - (65128, 65131, 2), - (65281, 65376, 2), - (65504, 65510, 2), - (94176, 94179, 2), - (94194, 94198, 2), - (94208, 101589, 2), - (101631, 101662, 2), - (101760, 101874, 2), - (110576, 110579, 2), - (110581, 110587, 2), - (110589, 110590, 2), - (110592, 110882, 2), - (110898, 110898, 2), - (110928, 110930, 2), - (110933, 110933, 2), - (110948, 110951, 2), - (110960, 111355, 2), - (119552, 119638, 2), - (119648, 119670, 2), - (126980, 126980, 2), - (127183, 127183, 2), - (127374, 127374, 2), - (127377, 127386, 2), - (127488, 127490, 2), - (127504, 127547, 2), - (127552, 127560, 2), - (127568, 127569, 2), - (127584, 127589, 2), - (127744, 127776, 2), - (127789, 127797, 2), - (127799, 127868, 2), - (127870, 127891, 2), - (127904, 127946, 2), - (127951, 127955, 2), - (127968, 127984, 2), - (127988, 127988, 2), - (127992, 127994, 2), - (128000, 128062, 2), - (128064, 128064, 2), - (128066, 128252, 2), - (128255, 128317, 2), - (128331, 128334, 2), - (128336, 128359, 2), - (128378, 128378, 2), - (128405, 128406, 2), - (128420, 128420, 2), - (128507, 128591, 2), - (128640, 128709, 2), - (128716, 128716, 2), - (128720, 128722, 2), - (128725, 128728, 2), - (128732, 128735, 2), - (128747, 128748, 2), - (128756, 128764, 2), - (128992, 129003, 2), - (129008, 129008, 2), - (129292, 129338, 2), - (129340, 129349, 2), - (129351, 129535, 2), - (129648, 129660, 2), - (129664, 129674, 2), - (129678, 129734, 2), - (129736, 129736, 2), - (129741, 129756, 2), - (129759, 129770, 2), - (129775, 129784, 2), - (131072, 196605, 2), - (196608, 262141, 2), - ] -) +WIDTH_TABLE: Final[list[tuple[int, int, int]]] = [ + (4352, 4447, 2), + (8986, 8987, 2), + (9001, 9002, 2), + (9193, 9196, 2), + (9200, 9200, 2), + (9203, 9203, 2), + (9725, 9726, 2), + (9748, 9749, 2), + (9776, 9783, 2), + (9800, 9811, 2), + (9855, 9855, 2), + (9866, 9871, 2), + (9875, 9875, 2), + (9889, 9889, 2), + (9898, 9899, 2), + (9917, 9918, 2), + (9924, 9925, 2), + (9934, 9934, 2), + (9940, 9940, 2), + (9962, 9962, 2), + (9970, 9971, 2), + (9973, 9973, 2), + (9978, 9978, 2), + (9981, 9981, 2), + (9989, 9989, 2), + (9994, 9995, 2), + (10024, 10024, 2), + (10060, 10060, 2), + (10062, 10062, 2), + (10067, 10069, 2), + (10071, 10071, 2), + (10133, 10135, 2), + (10160, 10160, 2), + (10175, 10175, 2), + (11035, 11036, 2), + (11088, 11088, 2), + (11093, 11093, 2), + (11904, 11929, 2), + (11931, 12019, 2), + (12032, 12245, 2), + (12272, 12329, 2), + (12336, 12350, 2), + (12353, 12438, 2), + (12443, 12543, 2), + (12549, 12591, 2), + (12593, 12686, 2), + (12688, 12773, 2), + (12783, 12830, 2), + (12832, 12871, 2), + (12880, 42124, 2), + (42128, 42182, 2), + (43360, 43388, 2), + (44032, 55203, 2), + (63744, 64255, 2), + (65040, 65049, 2), + (65072, 65106, 2), + (65108, 65126, 2), + (65128, 65131, 2), + (65281, 65376, 2), + (65504, 65510, 2), + (94176, 94179, 2), + (94194, 94198, 2), + (94208, 101589, 2), + (101631, 101662, 2), + (101760, 101874, 2), + (110576, 110579, 2), + (110581, 110587, 2), + (110589, 110590, 2), + (110592, 110882, 2), + (110898, 110898, 2), + (110928, 110930, 2), + (110933, 110933, 2), + (110948, 110951, 2), + (110960, 111355, 2), + (119552, 119638, 2), + (119648, 119670, 2), + (126980, 126980, 2), + (127183, 127183, 2), + (127374, 127374, 2), + (127377, 127386, 2), + (127488, 127490, 2), + (127504, 127547, 2), + (127552, 127560, 2), + (127568, 127569, 2), + (127584, 127589, 2), + (127744, 127776, 2), + (127789, 127797, 2), + (127799, 127868, 2), + (127870, 127891, 2), + (127904, 127946, 2), + (127951, 127955, 2), + (127968, 127984, 2), + (127988, 127988, 2), + (127992, 127994, 2), + (128000, 128062, 2), + (128064, 128064, 2), + (128066, 128252, 2), + (128255, 128317, 2), + (128331, 128334, 2), + (128336, 128359, 2), + (128378, 128378, 2), + (128405, 128406, 2), + (128420, 128420, 2), + (128507, 128591, 2), + (128640, 128709, 2), + (128716, 128716, 2), + (128720, 128722, 2), + (128725, 128728, 2), + (128732, 128735, 2), + (128747, 128748, 2), + (128756, 128764, 2), + (128992, 129003, 2), + (129008, 129008, 2), + (129292, 129338, 2), + (129340, 129349, 2), + (129351, 129535, 2), + (129648, 129660, 2), + (129664, 129674, 2), + (129678, 129734, 2), + (129736, 129736, 2), + (129741, 129756, 2), + (129759, 129770, 2), + (129775, 129784, 2), + (131072, 196605, 2), + (196608, 262141, 2), +] diff --git a/src/black/trans.py b/src/black/trans.py index c6ccfea3150..6f859358e0c 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1090,19 +1090,17 @@ class BaseStringSplitter(StringTransformer): * The target string is not a multiline (i.e. triple-quote) string. """ - STRING_OPERATORS: Final = ( - [ - token.EQEQUAL, - token.GREATER, - token.GREATEREQUAL, - token.LESS, - token.LESSEQUAL, - token.NOTEQUAL, - token.PERCENT, - token.PLUS, - token.STAR, - ] - ) + STRING_OPERATORS: Final = [ + token.EQEQUAL, + token.GREATER, + token.GREATEREQUAL, + token.LESS, + token.LESSEQUAL, + token.NOTEQUAL, + token.PERCENT, + token.PLUS, + token.STAR, + ] @abstractmethod def do_splitter_match(self, line: Line) -> TMatchResult: diff --git a/tests/test_black.py b/tests/test_black.py index 48ae4cb8379..8e61e8e866e 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -2695,14 +2695,12 @@ def test_nested_gitignore(self) -> None: exclude = re.compile(r"") root_gitignore = black.files.get_gitignore(path) report = black.Report() - expected: list[Path] = ( - [ - Path(path / "x.py"), - Path(path / "root/b.py"), - Path(path / "root/c.py"), - Path(path / "root/child/c.py"), - ] - ) + expected: list[Path] = [ + Path(path / "x.py"), + Path(path / "root/b.py"), + Path(path / "root/c.py"), + Path(path / "root/child/c.py"), + ] this_abs = THIS_DIR.resolve() sources = list( black.gen_python_files( From 3a86bcb40f76cddb71c906bbce840569c626c27d Mon Sep 17 00:00:00 2001 From: Param Chordiya Date: Sat, 25 Apr 2026 17:03:28 -0500 Subject: [PATCH 10/10] Fix two precision bugs in indexed-assignment formatting 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. --- src/black/linegen.py | 1 + src/black/lines.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index b8916cb4310..46db783d372 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1193,6 +1193,7 @@ def _prefer_split_rhs_oop_over_rhs( and rhs_oop.tail.leaves and rhs_oop.tail.leaves[0].type == token.RSQB and len(rhs_oop.tail.leaves) > 1 + and rhs_oop.tail.leaves[1].type == token.DOT and any(leaf.type == token.EQUAL for leaf in rhs_oop.head.leaves) ): return False diff --git a/src/black/lines.py b/src/black/lines.py index a986318637c..f41bfd8f08f 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -1441,7 +1441,7 @@ def can_omit_invisible_parens( Preview.fix_unnecessary_parens_in_indexed_assignment in mode and len(rhs.head.leaves) >= 2 and rhs.head.leaves[-2].type == token.EQUAL - and any(leaf.type in BRACKETS for leaf in rhs.head.leaves[:-2]) + and any(leaf.type in BRACKETS and leaf.value for leaf in rhs.head.leaves[:-2]) ): body_length = 4 * line.depth has_brackets = False