From 48e194760c8a7ab7c96d95e5db29e9f711181f29 Mon Sep 17 00:00:00 2001 From: ColumbusLabs <287001685+ColumbusLabs@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:32:03 -0400 Subject: [PATCH 1/5] Format symmetric list concatenations in preview --- CHANGES.md | 1 + docs/the_black_code_style/future_style.md | 25 +++++++ src/black/lines.py | 44 +++++++++++ src/black/mode.py | 1 + src/black/resources/black.schema.json | 3 +- src/blib2to3/pgen2/tokenize.py | 13 ++-- .../preview_symmetric_list_concatenation.py | 75 +++++++++++++++++++ ...ymmetric_list_concatenation_line_length.py | 12 +++ .../stable_symmetric_list_concatenation.py | 4 + 9 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 tests/data/cases/preview_symmetric_list_concatenation.py create mode 100644 tests/data/cases/preview_symmetric_list_concatenation_line_length.py create mode 100644 tests/data/cases/stable_symmetric_list_concatenation.py diff --git a/CHANGES.md b/CHANGES.md index 8343c4f61ea..00d506ce2d1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -58,6 +58,7 @@ +- Format long concatenations of two list displays symmetrically (#5259) - Preserve two blank lines before a top-level class starting inside a `# fmt: off` block after an import (#5238) - Fix unnecessary parentheses around short RHS expressions in indexed assignments like diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 4d098aa8888..d790af80cda 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -45,6 +45,31 @@ Currently, the following features are included in the preview style: statements. - `fmt_off_class_blank_lines`: Preserve two blank lines before a top-level class whose definition starts inside a `# fmt: off` block after an import. +- `symmetric_list_concatenation`: Keep optional parentheses around long concatenations + of two list displays so that each operand is formatted on its own line. + +(labels/symmetric-list-concatenation)= + +### Symmetric list concatenation + +When a concatenation of two list displays is too long for one line, Black keeps optional +parentheses around the expression so that the two operands can be split symmetrically. + +```python +# Before +names = ["Alice", "Bob", "Charlie", "Diana", "Edward"] + [ + "Fiona", + "George", + "Harriet", + "Isabelle", +] + +# After (with --preview) +names = ( + ["Alice", "Bob", "Charlie", "Diana", "Edward"] + + ["Fiona", "George", "Harriet", "Isabelle"] +) +``` (labels/wrap-comprehension-in)= diff --git a/src/black/lines.py b/src/black/lines.py index cab60fbcf9b..7b22ee717f5 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -1509,6 +1509,42 @@ def _is_annotated_assignment(head: Line) -> bool: return False +def _is_symmetric_list_concatenation(line: Line) -> bool: + """Is `line` exactly two list displays joined by a top-level `+`?""" + if len(line.bracket_tracker.delimiters) != 1: + return False + + delimiter_id = next(iter(line.bracket_tracker.delimiters)) + try: + left_closing_index = next( + index for index, leaf in enumerate(line.leaves) if id(leaf) == delimiter_id + ) + except StopIteration: + return False + + # Math operators are split *before* the delimiter, so BracketTracker keys + # them by the preceding leaf. + delimiter_index = left_closing_index + 1 + if delimiter_index == len(line.leaves) - 1: + return False + + first = line.leaves[0] + left_closing = line.leaves[left_closing_index] + delimiter = line.leaves[delimiter_index] + right_opening = line.leaves[delimiter_index + 1] + last = line.leaves[-1] + + return ( + delimiter.type == token.PLUS + and first.type == token.LSQB + and left_closing.type == token.RSQB + and left_closing.opening_bracket is first + and right_opening.type == token.LSQB + and last.type == token.RSQB + and last.opening_bracket is right_opening + ) + + def can_omit_invisible_parens( rhs: RHSResult, line_length: int, @@ -1634,6 +1670,14 @@ def can_omit_invisible_parens( # the with statement. `rhs.head` is the `with (` part on the previous # line. return False + if ( + Preview.symmetric_list_concatenation in mode + and not is_line_short_enough(line, mode=mode) + and _is_symmetric_list_concatenation(line) + ): + # Retaining the optional parentheses lets the delimiter splitter put + # each list operand on its own line instead of exploding just one list. + return False # Otherwise it may also read better, but we don't do it today and requires # careful considerations for all possible cases. See # https://github.com/psf/black/issues/2156. diff --git a/src/black/mode.py b/src/black/mode.py index 551e8f4cd68..155c0c19c3a 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -264,6 +264,7 @@ class Preview(Enum): hug_comparator = auto() parenthesize_tuple_in_yield = auto() fmt_off_class_blank_lines = auto() + symmetric_list_concatenation = auto() UNSTABLE_FEATURES: set[Preview] = { diff --git a/src/black/resources/black.schema.json b/src/black/resources/black.schema.json index 64db190152f..c0f9ca725de 100644 --- a/src/black/resources/black.schema.json +++ b/src/black/resources/black.schema.json @@ -93,7 +93,8 @@ "pyi_blank_line_after_function_docstring", "hug_comparator", "parenthesize_tuple_in_yield", - "fmt_off_class_blank_lines" + "fmt_off_class_blank_lines", + "symmetric_list_concatenation" ] }, "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." diff --git a/src/blib2to3/pgen2/tokenize.py b/src/blib2to3/pgen2/tokenize.py index 45423fcd8d6..34cc2247e97 100644 --- a/src/blib2to3/pgen2/tokenize.py +++ b/src/blib2to3/pgen2/tokenize.py @@ -62,11 +62,14 @@ from . import token as _token -__all__ = [x for x in dir(_token) if x[0] != "_"] + [ - "tokenize", - "generate_tokens", - "untokenize", -] +__all__ = ( + [x for x in dir(_token) if x[0] != "_"] + + [ + "tokenize", + "generate_tokens", + "untokenize", + ] +) del _token Coord = tuple[int, int] diff --git a/tests/data/cases/preview_symmetric_list_concatenation.py b/tests/data/cases/preview_symmetric_list_concatenation.py new file mode 100644 index 00000000000..66e36520760 --- /dev/null +++ b/tests/data/cases/preview_symmetric_list_concatenation.py @@ -0,0 +1,75 @@ +# flags: --preview + +# Regression test for https://github.com/psf/black/issues/260. +search_fields = (["file__%s" % field for field in FileAdmin.search_fields] + ["resource__%s" % field for field in ResourceAdmin.search_fields]) + +# Plain list displays receive the same symmetric treatment. +names = ["Alice", "Bob", "Charlie", "Diana", "Edward"] + ["Fiona", "George", "Harriet", "Isabelle"] + +# Comments on an operand stay attached and formatting remains stable. +commented = ( + ["first_long_value", "second_long_value", "third_long_value"] # first list + + ["fourth_long_value", "fifth_long_value", "sixth_long_value"] +) + +# A body that fits inside optional parentheses keeps the existing bracket split. +values = [first_value, second_value, third_value] + [fourth_value, fifth_value, sixth_value] + +# Chained concatenations already use the normal delimiter split. +chained = ["first_long_value", "second_long_value"] + ["third_long_value", "fourth_long_value"] + ["fifth_long_value", "sixth_long_value"] + +# Mixed operands are not symmetric list concatenations. +mixed_left = ["first_long_value", "second_long_value", "third_long_value"] + tuple_with_a_very_long_name +mixed_right = list_with_a_very_long_name + ["first_long_value", "second_long_value", "third_long_value"] + +# Short concatenations stay on one line. +small = [1, 2] + [3, 4] + +# output + +# Regression test for https://github.com/psf/black/issues/260. +search_fields = ( + ["file__%s" % field for field in FileAdmin.search_fields] + + ["resource__%s" % field for field in ResourceAdmin.search_fields] +) + +# Plain list displays receive the same symmetric treatment. +names = ( + ["Alice", "Bob", "Charlie", "Diana", "Edward"] + + ["Fiona", "George", "Harriet", "Isabelle"] +) + +# Comments on an operand stay attached and formatting remains stable. +commented = ( + ["first_long_value", "second_long_value", "third_long_value"] # first list + + ["fourth_long_value", "fifth_long_value", "sixth_long_value"] +) + +# A body that fits inside optional parentheses keeps the existing bracket split. +values = [first_value, second_value, third_value] + [ + fourth_value, + fifth_value, + sixth_value, +] + +# Chained concatenations already use the normal delimiter split. +chained = ( + ["first_long_value", "second_long_value"] + + ["third_long_value", "fourth_long_value"] + + ["fifth_long_value", "sixth_long_value"] +) + +# Mixed operands are not symmetric list concatenations. +mixed_left = [ + "first_long_value", + "second_long_value", + "third_long_value", +] + tuple_with_a_very_long_name +mixed_right = list_with_a_very_long_name + [ + "first_long_value", + "second_long_value", + "third_long_value", +] + +# Short concatenations stay on one line. +small = [1, 2] + [3, 4] diff --git a/tests/data/cases/preview_symmetric_list_concatenation_line_length.py b/tests/data/cases/preview_symmetric_list_concatenation_line_length.py new file mode 100644 index 00000000000..c847e737490 --- /dev/null +++ b/tests/data/cases/preview_symmetric_list_concatenation_line_length.py @@ -0,0 +1,12 @@ +# flags: --preview --line-length=60 + +values = [first_value, second_value] + [third_value, fourth_value] +short_values = [first_value] + [second_value] + +# output + +values = ( + [first_value, second_value] + + [third_value, fourth_value] +) +short_values = [first_value] + [second_value] diff --git a/tests/data/cases/stable_symmetric_list_concatenation.py b/tests/data/cases/stable_symmetric_list_concatenation.py new file mode 100644 index 00000000000..7fc4963b167 --- /dev/null +++ b/tests/data/cases/stable_symmetric_list_concatenation.py @@ -0,0 +1,4 @@ +# Stable style must keep the existing asymmetric bracket split. +search_fields = ["file__%s" % field for field in FileAdmin.search_fields] + [ + "resource__%s" % field for field in ResourceAdmin.search_fields +] From 678485f2c3f666730181c6b04dfd4216f6c8d510 Mon Sep 17 00:00:00 2001 From: ColumbusLabs <287001685+ColumbusLabs@users.noreply.github.com> Date: Tue, 28 Jul 2026 19:59:47 -0400 Subject: [PATCH 2/5] Preserve multiline list concatenation formatting --- src/black/lines.py | 37 +++++++++++++-- src/blib2to3/pgen2/tokenize.py | 13 ++---- .../preview_symmetric_list_concatenation.py | 46 +++++++++++++++++++ ...symmetric_list_concatenation_skip_magic.py | 15 ++++++ 4 files changed, 99 insertions(+), 12 deletions(-) create mode 100644 tests/data/cases/preview_symmetric_list_concatenation_skip_magic.py diff --git a/src/black/lines.py b/src/black/lines.py index 7b22ee717f5..7dd37c14ae4 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -1509,11 +1509,12 @@ def _is_annotated_assignment(head: Line) -> bool: return False -def _is_symmetric_list_concatenation(line: Line) -> bool: - """Is `line` exactly two list displays joined by a top-level `+`?""" +def _is_symmetric_list_concatenation(line: Line, line_length: int) -> bool: + """Is `line` exactly two single-line lists joined by a top-level `+`?""" if len(line.bracket_tracker.delimiters) != 1: return False + # Delimiters is keyed by leaf ID, not line position. delimiter_id = next(iter(line.bracket_tracker.delimiters)) try: left_closing_index = next( @@ -1534,7 +1535,7 @@ def _is_symmetric_list_concatenation(line: Line) -> bool: right_opening = line.leaves[delimiter_index + 1] last = line.leaves[-1] - return ( + if not ( delimiter.type == token.PLUS and first.type == token.LSQB and left_closing.type == token.RSQB @@ -1542,6 +1543,34 @@ def _is_symmetric_list_concatenation(line: Line) -> bool: and right_opening.type == token.LSQB and last.type == token.RSQB and last.opening_bracket is right_opening + ): + return False + + # Keep the existing asymmetric split when either list is already forced to + # split by a magic trailing comma. + if line.mode.magic_trailing_comma and ( + line.leaves[left_closing_index - 1].type == token.COMMA + or line.leaves[-2].type == token.COMMA + ): + return False + + def rendered_width(start: int, end: int) -> int | None: + leaves = line.leaves[start:end] + rendered = " " * line.depth + for index, leaf in enumerate(leaves): + rendered += leaf.value if index == 0 else str(leaf) + rendered += "".join(str(comment) for comment in line.comments_after(leaf)) + if "\n" in rendered: + return None + return str_width(rendered) + + left_width = rendered_width(0, delimiter_index) + right_width = rendered_width(delimiter_index, len(line.leaves)) + return ( + left_width is not None + and right_width is not None + and left_width <= line_length + and right_width <= line_length ) @@ -1673,7 +1702,7 @@ def can_omit_invisible_parens( if ( Preview.symmetric_list_concatenation in mode and not is_line_short_enough(line, mode=mode) - and _is_symmetric_list_concatenation(line) + and _is_symmetric_list_concatenation(line, line_length) ): # Retaining the optional parentheses lets the delimiter splitter put # each list operand on its own line instead of exploding just one list. diff --git a/src/blib2to3/pgen2/tokenize.py b/src/blib2to3/pgen2/tokenize.py index 34cc2247e97..45423fcd8d6 100644 --- a/src/blib2to3/pgen2/tokenize.py +++ b/src/blib2to3/pgen2/tokenize.py @@ -62,14 +62,11 @@ from . import token as _token -__all__ = ( - [x for x in dir(_token) if x[0] != "_"] - + [ - "tokenize", - "generate_tokens", - "untokenize", - ] -) +__all__ = [x for x in dir(_token) if x[0] != "_"] + [ + "tokenize", + "generate_tokens", + "untokenize", +] del _token Coord = tuple[int, int] diff --git a/tests/data/cases/preview_symmetric_list_concatenation.py b/tests/data/cases/preview_symmetric_list_concatenation.py index 66e36520760..edec6980fa9 100644 --- a/tests/data/cases/preview_symmetric_list_concatenation.py +++ b/tests/data/cases/preview_symmetric_list_concatenation.py @@ -25,6 +25,21 @@ # Short concatenations stay on one line. small = [1, 2] + [3, 4] +# Lists that already require bracket splitting keep the existing formatting. +long_left = ["first_value_with_an_extremely_long_name", "second_value_with_an_extremely_long_name", "third"] + ["short"] +long_right = ["short"] + ["first_value_with_an_extremely_long_name", "second_value_with_an_extremely_long_name", "third"] +both_long = ["first_value_with_an_extremely_long_name", "second_value_with_an_extremely_long_name", "third"] + ["fourth_value_with_an_extremely_long_name", "fifth_value_with_an_extremely_long_name", "sixth"] + +# Magic trailing commas also keep the existing formatting. +magic_left = [ + "first_long_value", + "second_long_value", +] + ["third_long_value", "fourth_long_value"] +magic_right = ["first_long_value", "second_long_value"] + [ + "third_long_value", + "fourth_long_value", +] + # output # Regression test for https://github.com/psf/black/issues/260. @@ -73,3 +88,34 @@ # Short concatenations stay on one line. small = [1, 2] + [3, 4] + +# Lists that already require bracket splitting keep the existing formatting. +long_left = [ + "first_value_with_an_extremely_long_name", + "second_value_with_an_extremely_long_name", + "third", +] + ["short"] +long_right = ["short"] + [ + "first_value_with_an_extremely_long_name", + "second_value_with_an_extremely_long_name", + "third", +] +both_long = [ + "first_value_with_an_extremely_long_name", + "second_value_with_an_extremely_long_name", + "third", +] + [ + "fourth_value_with_an_extremely_long_name", + "fifth_value_with_an_extremely_long_name", + "sixth", +] + +# Magic trailing commas also keep the existing formatting. +magic_left = [ + "first_long_value", + "second_long_value", +] + ["third_long_value", "fourth_long_value"] +magic_right = ["first_long_value", "second_long_value"] + [ + "third_long_value", + "fourth_long_value", +] diff --git a/tests/data/cases/preview_symmetric_list_concatenation_skip_magic.py b/tests/data/cases/preview_symmetric_list_concatenation_skip_magic.py new file mode 100644 index 00000000000..e2249ba52bb --- /dev/null +++ b/tests/data/cases/preview_symmetric_list_concatenation_skip_magic.py @@ -0,0 +1,15 @@ +# flags: --preview --skip-magic-trailing-comma + +# Ignored trailing commas do not prevent symmetric formatting. +values = [ + "first_long_value", + "second_long_value", +] + ["third_long_value", "fourth_long_value"] + +# output + +# Ignored trailing commas do not prevent symmetric formatting. +values = ( + ["first_long_value", "second_long_value"] + + ["third_long_value", "fourth_long_value"] +) From 79a44ca9e074232e2d0b8be3643f5f308240d608 Mon Sep 17 00:00:00 2001 From: ColumbusLabs <287001685+ColumbusLabs@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:08:03 -0400 Subject: [PATCH 3/5] Clarify symmetric list concatenation scope --- CHANGES.md | 4 +++- docs/the_black_code_style/future_style.md | 9 ++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 00d506ce2d1..ae8f73378d7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -58,7 +58,9 @@ -- Format long concatenations of two list displays symmetrically (#5259) +- Format long concatenations of two list displays symmetrically when both operands fit + on their own delimiter-split line; retain the existing layout when either operand + needs an internal split, including for an active magic trailing comma (#5259) - Preserve two blank lines before a top-level class starting inside a `# fmt: off` block after an import (#5238) - Fix unnecessary parentheses around short RHS expressions in indexed assignments like diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index d790af80cda..df32be0b14f 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -46,14 +46,17 @@ Currently, the following features are included in the preview style: - `fmt_off_class_blank_lines`: Preserve two blank lines before a top-level class whose definition starts inside a `# fmt: off` block after an import. - `symmetric_list_concatenation`: Keep optional parentheses around long concatenations - of two list displays so that each operand is formatted on its own line. + of two list displays when both operands fit on their own delimiter-split line. (labels/symmetric-list-concatenation)= ### Symmetric list concatenation -When a concatenation of two list displays is too long for one line, Black keeps optional -parentheses around the expression so that the two operands can be split symmetrically. +When a concatenation of two list displays is too long for one line and both operands fit +on their own delimiter-split line, Black keeps optional parentheses around the +expression so that the operands can be split symmetrically. If either list needs an +internal split, including when an active magic trailing comma forces it onto multiple +lines, Black retains the existing formatting. ```python # Before From 12b570bd1876969143ec50abb2c9b88d9adbdd17 Mon Sep 17 00:00:00 2001 From: ColumbusLabs <287001685+ColumbusLabs@users.noreply.github.com> Date: Fri, 7 Aug 2026 22:27:04 -0400 Subject: [PATCH 4/5] fix: refine symmetric list concatenation preview --- CHANGES.md | 5 ++- src/black/linegen.py | 11 +++++++ src/black/lines.py | 9 +++--- .../preview_symmetric_list_concatenation.py | 31 ++++++++++++++----- 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ae8f73378d7..14f88b87bdb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -58,9 +58,8 @@ -- Format long concatenations of two list displays symmetrically when both operands fit - on their own delimiter-split line; retain the existing layout when either operand - needs an internal split, including for an active magic trailing comma (#5259) +- Format long concatenations of two lists symmetrically when both operands fit on their + own delimiter-split line (#5259) - Preserve two blank lines before a top-level class starting inside a `# fmt: off` block after an import (#5238) - Fix unnecessary parentheses around short RHS expressions in indexed assignments like diff --git a/src/black/linegen.py b/src/black/linegen.py index 63cf73cffa9..f51ebc207ca 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -31,6 +31,7 @@ append_leaves, can_be_split, can_omit_invisible_parens, + is_symmetric_list_concatenation, is_line_short_enough, line_to_string, ) @@ -1089,6 +1090,14 @@ def _maybe_split_omitting_optional_parens( features: Collection[Feature] = (), omit: Collection[LeafID] = (), ) -> Iterator[Line]: + split_symmetric_lists = ( + Preview.symmetric_list_concatenation in mode + and rhs.opening_bracket.type == token.LPAR + and not rhs.opening_bracket.value + and rhs.closing_bracket.type == token.RPAR + and not rhs.closing_bracket.value + and is_symmetric_list_concatenation(rhs.body, mode.line_length) + ) if ( Feature.FORCE_OPTIONAL_PARENTHESES not in features # the opening bracket is an optional paren @@ -1150,6 +1159,8 @@ def _maybe_split_omitting_optional_parens( ensure_visible(rhs.opening_bracket) ensure_visible(rhs.closing_bracket) + if split_symmetric_lists: + rhs.body.should_split_rhs = True for result in (rhs.head, rhs.body, rhs.tail): if result: yield result diff --git a/src/black/lines.py b/src/black/lines.py index 7dd37c14ae4..200d251f3ea 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -1509,7 +1509,7 @@ def _is_annotated_assignment(head: Line) -> bool: return False -def _is_symmetric_list_concatenation(line: Line, line_length: int) -> bool: +def is_symmetric_list_concatenation(line: Line, line_length: int) -> bool: """Is `line` exactly two single-line lists joined by a top-level `+`?""" if len(line.bracket_tracker.delimiters) != 1: return False @@ -1526,7 +1526,7 @@ def _is_symmetric_list_concatenation(line: Line, line_length: int) -> bool: # Math operators are split *before* the delimiter, so BracketTracker keys # them by the preceding leaf. delimiter_index = left_closing_index + 1 - if delimiter_index == len(line.leaves) - 1: + if delimiter_index >= len(line.leaves) - 1: return False first = line.leaves[0] @@ -1557,9 +1557,9 @@ def _is_symmetric_list_concatenation(line: Line, line_length: int) -> bool: def rendered_width(start: int, end: int) -> int | None: leaves = line.leaves[start:end] rendered = " " * line.depth + # End-of-line comments do not determine whether the operand itself fits. for index, leaf in enumerate(leaves): rendered += leaf.value if index == 0 else str(leaf) - rendered += "".join(str(comment) for comment in line.comments_after(leaf)) if "\n" in rendered: return None return str_width(rendered) @@ -1701,8 +1701,7 @@ def can_omit_invisible_parens( return False if ( Preview.symmetric_list_concatenation in mode - and not is_line_short_enough(line, mode=mode) - and _is_symmetric_list_concatenation(line, line_length) + and is_symmetric_list_concatenation(line, line_length) ): # Retaining the optional parentheses lets the delimiter splitter put # each list operand on its own line instead of exploding just one list. diff --git a/tests/data/cases/preview_symmetric_list_concatenation.py b/tests/data/cases/preview_symmetric_list_concatenation.py index edec6980fa9..ac9f146e86c 100644 --- a/tests/data/cases/preview_symmetric_list_concatenation.py +++ b/tests/data/cases/preview_symmetric_list_concatenation.py @@ -12,7 +12,16 @@ + ["fourth_long_value", "fifth_long_value", "sixth_long_value"] ) -# A body that fits inside optional parentheses keeps the existing bracket split. +commented_left = ( + ["first_value", "second_value", "third_value"] # abc + + ["fourth_value", "fifth_value", "sixth_value"] +) +commented_right = ( + ["first_value", "second_value", "third_value"] + + ["fourth_value", "fifth_value", "sixth_value"] # abc +) + +# Split symmetrically even when the RHS alone fits inside optional parentheses. values = [first_value, second_value, third_value] + [fourth_value, fifth_value, sixth_value] # Chained concatenations already use the normal delimiter split. @@ -60,12 +69,20 @@ + ["fourth_long_value", "fifth_long_value", "sixth_long_value"] ) -# A body that fits inside optional parentheses keeps the existing bracket split. -values = [first_value, second_value, third_value] + [ - fourth_value, - fifth_value, - sixth_value, -] +commented_left = ( + ["first_value", "second_value", "third_value"] # abc + + ["fourth_value", "fifth_value", "sixth_value"] +) +commented_right = ( + ["first_value", "second_value", "third_value"] + + ["fourth_value", "fifth_value", "sixth_value"] # abc +) + +# Split symmetrically even when the RHS alone fits inside optional parentheses. +values = ( + [first_value, second_value, third_value] + + [fourth_value, fifth_value, sixth_value] +) # Chained concatenations already use the normal delimiter split. chained = ( From 339735ba2371c915f8e47e7495ccbca3f669ed23 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 02:27:27 +0000 Subject: [PATCH 5/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/black/linegen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index f51ebc207ca..f39049e73cf 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -31,8 +31,8 @@ append_leaves, can_be_split, can_omit_invisible_parens, - is_symmetric_list_concatenation, is_line_short_enough, + is_symmetric_list_concatenation, line_to_string, ) from black.mode import Feature, Mode, Preview