From 1c2b8d92225826c62c0ad21dff039d214c6deb78 Mon Sep 17 00:00:00 2001 From: cobalt <61329810+cobaltt7@users.noreply.github.com> Date: Sun, 11 May 2025 21:55:32 -0500 Subject: [PATCH 01/17] CI: Remove now-uneeded workarounds The issues mentioned in the comments have all been fixed Signed-off-by: cobalt <61329810+cobaltt7@users.noreply.github.com> --- scripts/fuzz.py | 21 +-------------------- tox.ini | 24 ++++++------------------ 2 files changed, 7 insertions(+), 38 deletions(-) diff --git a/scripts/fuzz.py b/scripts/fuzz.py index 0c507381d92..915a036b4ae 100644 --- a/scripts/fuzz.py +++ b/scripts/fuzz.py @@ -5,14 +5,11 @@ a coverage-guided fuzzer I'm working on. """ -import re - import hypothesmith from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st import black -from blib2to3.pgen2.tokenize import TokenError # This test uses the Hypothesis and Hypothesmith libraries to generate random @@ -45,23 +42,7 @@ def test_idempotent_any_syntatically_valid_python( compile(src_contents, "", "exec") # else the bug is in hypothesmith # Then format the code... - try: - dst_contents = black.format_str(src_contents, mode=mode) - except black.InvalidInput: - # This is a bug - if it's valid Python code, as above, Black should be - # able to cope with it. See issues #970, #1012 - # TODO: remove this try-except block when issues are resolved. - return - except TokenError as e: - if ( # Special-case logic for backslashes followed by newlines or end-of-input - e.args[0] == "EOF in multi-line statement" - and re.search(r"\\($|\r?\n)", src_contents) is not None - ): - # This is a bug - if it's valid Python code, as above, Black should be - # able to cope with it. See issue #1012. - # TODO: remove this block when the issue is resolved. - return - raise + dst_contents = black.format_str(src_contents, mode=mode) # And check that we got equivalent and stable output. black.assert_equivalent(src_contents, dst_contents) diff --git a/tox.ini b/tox.ini index d64fe7f2210..d4450219dc0 100644 --- a/tox.ini +++ b/tox.ini @@ -13,18 +13,16 @@ skip_install = True recreate = True deps = -r{toxinidir}/test_requirements.txt -; parallelization is disabled on CI because pytest-dev/pytest-xdist#620 occurs too frequently -; local runs can stay parallelized since they aren't rolling the dice so many times as like on CI commands = pip install -e .[d] coverage erase pytest tests --run-optional no_jupyter \ - !ci: --numprocesses auto \ + --numprocesses auto \ --cov {posargs} pip install -e .[jupyter] pytest tests --run-optional jupyter \ -m jupyter \ - !ci: --numprocesses auto \ + --numprocesses auto \ --cov --cov-append {posargs} coverage report @@ -34,20 +32,15 @@ skip_install = True recreate = True deps = -r{toxinidir}/test_requirements.txt -; a separate worker is required in ci due to https://foss.heptapod.net/pypy/pypy/-/issues/3317 -; this seems to cause tox to wait forever -; remove this when pypy releases the bugfix commands = pip install -e .[d] pytest tests \ --run-optional no_jupyter \ - !ci: --numprocesses auto \ - ci: --numprocesses 1 + --numprocesses auto pip install -e .[jupyter] pytest tests --run-optional jupyter \ -m jupyter \ - !ci: --numprocesses auto \ - ci: --numprocesses 1 + --numprocesses auto [testenv:{,ci-}311] setenv = @@ -59,22 +52,17 @@ deps = ; We currently need > aiohttp 3.8.1 that is on PyPI for 3.11 git+https://github.com/aio-libs/aiohttp -r{toxinidir}/test_requirements.txt -; a separate worker is required in ci due to https://foss.heptapod.net/pypy/pypy/-/issues/3317 -; this seems to cause tox to wait forever -; remove this when pypy releases the bugfix commands = pip install -e .[d] coverage erase pytest tests \ --run-optional no_jupyter \ - !ci: --numprocesses auto \ - ci: --numprocesses 1 \ + --numprocesses auto \ --cov {posargs} pip install -e .[jupyter] pytest tests --run-optional jupyter \ -m jupyter \ - !ci: --numprocesses auto \ - ci: --numprocesses 1 \ + --numprocesses auto \ --cov --cov-append {posargs} coverage report From 8285a35c362e3f032af0f79dcc0fbeca7485fee4 Mon Sep 17 00:00:00 2001 From: cobalt <61329810+cobaltt7@users.noreply.github.com> Date: Sun, 11 May 2025 22:22:02 -0500 Subject: [PATCH 02/17] Fix Click typing issue in run self --- tests/test_black.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_black.py b/tests/test_black.py index acafb521619..70f76c032bf 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -119,7 +119,7 @@ def __init__(self) -> None: if Version(imp_version("click")) >= Version("8.2.0"): super().__init__() else: - super().__init__(mix_stderr=False) + super().__init__(mix_stderr=False) # type: ignore def invokeBlack( From 7755e49f5148a9f50bef231bc8d03b238534b534 Mon Sep 17 00:00:00 2001 From: cobalt <61329810+cobaltt7@users.noreply.github.com> Date: Thu, 15 May 2025 07:06:08 -0500 Subject: [PATCH 03/17] oops, run self --- tests/test_black.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_black.py b/tests/test_black.py index 242d8aa6e8c..ee026f312de 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -119,7 +119,7 @@ def __init__(self) -> None: if Version(imp_version("click")) >= Version("8.2.0"): super().__init__() else: - super().__init__(mix_stderr=False) # type: ignore + super().__init__(mix_stderr=False) # type: ignore def invokeBlack( From 3d02a9346efb6aa3b3aadab05cbe4bb4b73dccff Mon Sep 17 00:00:00 2001 From: cobalt <61329810+cobaltt7@users.noreply.github.com> Date: Thu, 15 May 2025 07:12:06 -0500 Subject: [PATCH 04/17] i swear this was failing earlier ig it's not that related to this pr anyway --- tests/test_black.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_black.py b/tests/test_black.py index ee026f312de..f5c950244ef 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -119,7 +119,7 @@ def __init__(self) -> None: if Version(imp_version("click")) >= Version("8.2.0"): super().__init__() else: - super().__init__(mix_stderr=False) # type: ignore + super().__init__(mix_stderr=False) def invokeBlack( From 7e0e55f7e205e04f80a9bf3f68f26cad940287e4 Mon Sep 17 00:00:00 2001 From: cobalt <61329810+cobaltt7@users.noreply.github.com> Date: Fri, 16 May 2025 06:57:59 -0500 Subject: [PATCH 05/17] has click updated itself yet on pre-commit.ci --- tests/test_black.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_black.py b/tests/test_black.py index f5c950244ef..ee026f312de 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -119,7 +119,7 @@ def __init__(self) -> None: if Version(imp_version("click")) >= Version("8.2.0"): super().__init__() else: - super().__init__(mix_stderr=False) + super().__init__(mix_stderr=False) # type: ignore def invokeBlack( From 68732db7813a6d1655b70c1bc1885b7dbb749bc0 Mon Sep 17 00:00:00 2001 From: cobalt <61329810+cobaltt7@users.noreply.github.com> Date: Sun, 25 May 2025 18:11:47 -0500 Subject: [PATCH 06/17] Bump click Signed-off-by: cobalt <61329810+cobaltt7@users.noreply.github.com> --- .pre-commit-config.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dda279db36b..79fc8708129 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,9 @@ repos: - types-PyYAML - types-atheris - tomli >= 0.2.6, < 2.0.0 - - click >= 8.1.0, != 8.1.4, != 8.1.5 + - click >= 8.2.0 + # Click is intentionally out-of-sync with pyproject.toml + # v8.2 has breaking changes. We work around them at runtime, but we need the newer stubs. - packaging >= 22.0 - platformdirs >= 2.1.0 - pytokens >= 0.1.10 From 538ae23ebdb3b2045362ac29cffe4856713d345f Mon Sep 17 00:00:00 2001 From: cobalt <61329810+cobaltt7@users.noreply.github.com> Date: Fri, 20 Jun 2025 17:28:26 -0500 Subject: [PATCH 07/17] Split the `in` clause of comprehensions onto its own line if necessary Fixes #3498 Signed-off-by: cobalt <61329810+cobaltt7@users.noreply.github.com> --- CHANGES.md | 1 + docs/the_black_code_style/future_style.md | 1 + src/black/brackets.py | 9 ++ src/black/mode.py | 1 + src/black/resources/black.schema.json | 3 +- .../cases/preview_split_comprehension_in.py | 117 ++++++++++++++++++ 6 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 tests/data/cases/preview_split_comprehension_in.py diff --git a/CHANGES.md b/CHANGES.md index cf415f15fc3..9464fb69006 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,7 @@ - Fix a bug where one-liner functions/conditionals marked with `# fmt: skip` would still be formatted (#4552) +- Split the `in` clause of comprehensions onto its own line if necessary (#) ### Configuration diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index e801874a4f0..27e4e580165 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -29,6 +29,7 @@ Currently, the following features are included in the preview style: - `fix_fmt_skip_in_one_liners`: Fix `# fmt: skip` behaviour on one-liner declarations, such as `def foo(): return "mock" # fmt: skip`, where previously the declaration would have been incorrectly collapsed. +- `split_comprehension_in`: Split the `in` clause of list and dictionary comprehensions onto its own line if it would otherwise exceed the maximum line length. (labels/unstable-features)= diff --git a/src/black/brackets.py b/src/black/brackets.py index c2e8be4348e..1f2301af975 100644 --- a/src/black/brackets.py +++ b/src/black/brackets.py @@ -30,6 +30,7 @@ COMPREHENSION_PRIORITY: Final = 20 COMMA_PRIORITY: Final = 18 TERNARY_PRIORITY: Final = 16 +COMP_IN_PRIORITY: Final = 15 LOGIC_PRIORITY: Final = 14 STRING_PRIORITY: Final = 12 COMPARATOR_PRIORITY: Final = 10 @@ -290,6 +291,14 @@ def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Pr ): return COMPREHENSION_PRIORITY + if ( + # Preview.split_comprehension_in in mode and + leaf.value == "in" + and leaf.parent + and leaf.parent.type in {syms.comp_for, syms.old_comp_for} + ): + return COMP_IN_PRIORITY + if leaf.value in {"if", "else"} and leaf.parent and leaf.parent.type == syms.test: return TERNARY_PRIORITY diff --git a/src/black/mode.py b/src/black/mode.py index 362607efc86..f7397444968 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -204,6 +204,7 @@ class Preview(Enum): multiline_string_handling = auto() always_one_newline_after_import = auto() fix_fmt_skip_in_one_liners = auto() + split_comprehension_in = auto() UNSTABLE_FEATURES: set[Preview] = { diff --git a/src/black/resources/black.schema.json b/src/black/resources/black.schema.json index 572e5bbfa1e..0e127045889 100644 --- a/src/black/resources/black.schema.json +++ b/src/black/resources/black.schema.json @@ -84,7 +84,8 @@ "wrap_long_dict_values_in_parens", "multiline_string_handling", "always_one_newline_after_import", - "fix_fmt_skip_in_one_liners" + "fix_fmt_skip_in_one_liners", + "split_comprehension_in" ] }, "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/tests/data/cases/preview_split_comprehension_in.py b/tests/data/cases/preview_split_comprehension_in.py new file mode 100644 index 00000000000..a886a4bbc6f --- /dev/null +++ b/tests/data/cases/preview_split_comprehension_in.py @@ -0,0 +1,117 @@ +# flags: --unstable --line-length=79 + +[a for graph_path_expression in refined_constraint.condition_as_predicate.variables] +[ + (foobar_very_long_key, foobar_very_long_value) + for foobar_very_long_key, foobar_very_long_value in foobar_very_long_dictionary.items() +] +[ + a + for graph_path_expression + in refined_constraint.condition_as_predicate.variables +] + +# Don't split the `in` if it's not too long +lcomp3 = [ + element.split("\n", 1)[0] + for element in collection.select_elements() + # right + if element is not None +] + +# Nested arrays +[[ + x + for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + for y in xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxdxxxxxxxxxxxxxxxxxxxxxxxx +]] + +# Multiple comprehensions, only split the second `in` +graph_path_expressions_in_local_constraint_refinements = [ + graph_path_expression + for refined_constraint in self._local_constraint_refinements.values() + if refined_constraint is not None + for graph_path_expression in refined_constraint.condition_as_predicate.variables +] + +# Split long `if`s +graph_path_expressions_in_local_constraint_refinements = [ + foobar + for foobar in bar + if foobar not in {very_long_value, other_long_value, foobarbaz, one_more_value} +] + +# Dictionary comprehensions +dict_with_really_long_names = { + really_really_long_key_name: an_even_longer_really_really_long_key_value + for really_really_long_key_name, an_even_longer_really_really_long_key_value in really_really_really_long_dict_name.items() +} +{ + key_with_super_really_long_name: key_with_super_really_long_name + for key_with_super_really_long_name + in dictionary_with_super_really_long_name +} + +# output + +[ + a + for graph_path_expression + in refined_constraint.condition_as_predicate.variables +] +[ + (foobar_very_long_key, foobar_very_long_value) + for foobar_very_long_key, foobar_very_long_value + in foobar_very_long_dictionary.items() +] +[ + a + for graph_path_expression + in refined_constraint.condition_as_predicate.variables +] + +# Don't split the `in` if it's not too long +lcomp3 = [ + element.split("\n", 1)[0] + for element in collection.select_elements() + # right + if element is not None +] + +# Nested arrays +[[ + x + for x + in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + for y + in xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxdxxxxxxxxxxxxxxxxxxxxxxxx +]] + +# Multiple comprehensions, only split the second `in` +graph_path_expressions_in_local_constraint_refinements = [ + graph_path_expression + for refined_constraint in self._local_constraint_refinements.values() + if refined_constraint is not None + for graph_path_expression + in refined_constraint.condition_as_predicate.variables +] + +# Split long `if`s +graph_path_expressions_in_local_constraint_refinements = [ + foobar + for foobar in bar + if foobar + not in {very_long_value, other_long_value, foobarbaz, one_more_value} +] + +# Dictionary comprehensions +dict_with_really_long_names = { + really_really_long_key_name: an_even_longer_really_really_long_key_value + for really_really_long_key_name, an_even_longer_really_really_long_key_value + in really_really_really_long_dict_name.items() +} +{ + key_with_super_really_long_name: key_with_super_really_long_name + for key_with_super_really_long_name + in dictionary_with_super_really_long_name +} From e1078879e2e668bb0bc3dfe3ed16d4a7ea071b45 Mon Sep 17 00:00:00 2001 From: cobalt <61329810+cobaltt7@users.noreply.github.com> Date: Fri, 20 Jun 2025 18:27:40 -0500 Subject: [PATCH 08/17] Lint issues Signed-off-by: cobalt <61329810+cobaltt7@users.noreply.github.com> --- CHANGES.md | 2 +- docs/the_black_code_style/future_style.md | 7 ++++--- src/black/__init__.py | 5 +---- src/black/comments.py | 5 ++--- .../preview_hug_parens_with_braces_and_square_brackets.py | 3 ++- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0bb05c19116..ae5f03f1d5a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -27,7 +27,7 @@ - Fix a bug where one-liner functions/conditionals marked with `# fmt: skip` would still be formatted (#4552) -- Split the `in` clause of comprehensions onto its own line if necessary (#) +- Split the `in` clause of comprehensions onto its own line if necessary (#4699) ### Configuration diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 27e4e580165..8dd9c7d5990 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -27,9 +27,10 @@ Currently, the following features are included in the preview style: - `wrap_long_dict_values_in_parens`: Add parentheses around long values in dictionaries ([see below](labels/wrap-long-dict-values)) - `fix_fmt_skip_in_one_liners`: Fix `# fmt: skip` behaviour on one-liner declarations, - such as `def foo(): return "mock" # fmt: skip`, where previously the declaration - would have been incorrectly collapsed. -- `split_comprehension_in`: Split the `in` clause of list and dictionary comprehensions onto its own line if it would otherwise exceed the maximum line length. + such as `def foo(): return "mock" # fmt: skip`, where previously the declaration would + have been incorrectly collapsed. +- `split_comprehension_in`: Split the `in` clause of list and dictionary comprehensions + onto its own line if it would otherwise exceed the maximum line length. (labels/unstable-features)= diff --git a/src/black/__init__.py b/src/black/__init__.py index 93a08a8d88a..5f885c2cf6f 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1241,10 +1241,7 @@ def _format_str_once( elt = EmptyLineTracker(mode=mode) split_line_features = { feature - for feature in { - Feature.TRAILING_COMMA_IN_CALL, - Feature.TRAILING_COMMA_IN_DEF, - } + for feature in {Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF} if supports_feature(versions, feature) } block: Optional[LinesBlock] = None diff --git a/src/black/comments.py b/src/black/comments.py index 81d3cfd4a35..86125716919 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -446,9 +446,8 @@ def _contains_fmt_skip_comment(comment_line: str, mode: Mode) -> bool: ], *[ _COMMENT_PREFIX + comment.strip() - for comment in comment_line.strip(_COMMENT_PREFIX).split( - _COMMENT_LIST_SEPARATOR - ) + for comment + in comment_line.strip(_COMMENT_PREFIX).split(_COMMENT_LIST_SEPARATOR) ], ] diff --git a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py index cbbcf16d3bd..900ea8487ce 100644 --- a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py +++ b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py @@ -284,7 +284,8 @@ def foo_square_brackets(request): ) func([ x - for x in [ + for x + in [ x for x in "long line long line long line long line long line long line long line" ] From 602ca78cb464e6babefd2e5d0f7d2ba483cadd85 Mon Sep 17 00:00:00 2001 From: cobalt <61329810+cobaltt7@users.noreply.github.com> Date: Fri, 20 Jun 2025 18:41:59 -0500 Subject: [PATCH 09/17] Move to preview style Signed-off-by: cobalt <61329810+cobaltt7@users.noreply.github.com> --- src/black/brackets.py | 19 +++++++++------- src/black/linegen.py | 52 ++++++++++++++++++++----------------------- src/black/lines.py | 2 +- 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/src/black/brackets.py b/src/black/brackets.py index 1f2301af975..6291bfe13bb 100644 --- a/src/black/brackets.py +++ b/src/black/brackets.py @@ -4,6 +4,7 @@ from dataclasses import dataclass, field from typing import Final, Optional, Union +from black.mode import Mode, Preview from black.nodes import ( BRACKET, CLOSING_BRACKETS, @@ -69,7 +70,7 @@ class BracketTracker: _lambda_argument_depths: list[int] = field(default_factory=list) invisible: list[Leaf] = field(default_factory=list) - def mark(self, leaf: Leaf) -> None: + def mark(self, leaf: Leaf, mode: Mode) -> None: """Mark `leaf` with bracket-related metadata. Keep track of delimiters. All leaves receive an int `bracket_depth` field that stores how deep @@ -113,7 +114,7 @@ def mark(self, leaf: Leaf) -> None: self.invisible.append(leaf) leaf.bracket_depth = self.depth if self.depth == 0: - delim = is_split_before_delimiter(leaf, self.previous) + delim = is_split_before_delimiter(leaf, mode, self.previous) if delim and self.previous is not None: self.delimiters[id(self.previous)] = delim else: @@ -231,7 +232,9 @@ def is_split_after_delimiter(leaf: Leaf) -> Priority: return 0 -def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority: +def is_split_before_delimiter( + leaf: Leaf, mode: Mode, previous: Optional[Leaf] = None +) -> Priority: """Return the priority of the `leaf` delimiter, given a line break before it. The delimiter priorities returned here are from those delimiters that would @@ -292,8 +295,8 @@ def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Pr return COMPREHENSION_PRIORITY if ( - # Preview.split_comprehension_in in mode and - leaf.value == "in" + Preview.split_comprehension_in in mode + and leaf.value == "in" and leaf.parent and leaf.parent.type in {syms.comp_for, syms.old_comp_for} ): @@ -335,7 +338,7 @@ def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Pr return 0 -def max_delimiter_priority_in_atom(node: LN) -> Priority: +def max_delimiter_priority_in_atom(node: LN, mode: Mode) -> Priority: """Return maximum delimiter priority inside `node`. This is specific to atoms with contents contained in a pair of parentheses. @@ -352,10 +355,10 @@ def max_delimiter_priority_in_atom(node: LN) -> Priority: bt = BracketTracker() for c in node.children[1:-1]: if isinstance(c, Leaf): - bt.mark(c) + bt.mark(c, mode) else: for leaf in c.leaves(): - bt.mark(leaf) + bt.mark(leaf, mode) try: return bt.max_delimiter_priority() diff --git a/src/black/linegen.py b/src/black/linegen.py index fa574ca215e..62695f58449 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -250,8 +250,8 @@ def visit_dictsetmaker(self, node: Node) -> Iterator[Line]: maybe_make_parens_invisible_in_atom( child, parent=node, - remove_brackets_around_comma=False, - ) + mode=self.mode, + remove_brackets_around_comma=False ) else: wrap_in_parentheses(node, child, visible=False) yield from self.visit_default(node) @@ -270,7 +270,8 @@ def visit_funcdef(self, node: Node) -> Iterator[Line]: if maybe_make_parens_invisible_in_atom( child, parent=node, - remove_brackets_around_comma=False, + mode=self.mode, + remove_brackets_around_comma=False ): wrap_in_parentheses(node, child, visible=False) else: @@ -363,7 +364,7 @@ def visit_power(self, node: Node) -> Iterator[Line]: ): wrap_in_parentheses(node, leaf) - remove_await_parens(node) + remove_await_parens(node, self.mode) yield from self.visit_default(node) @@ -410,7 +411,9 @@ def foo(a: int, b: float = 7): ... def foo(a: (int), b: (float) = 7): ... """ assert len(node.children) == 3 - if maybe_make_parens_invisible_in_atom(node.children[2], parent=node): + if maybe_make_parens_invisible_in_atom( + node.children[2], parent=node, mode=self.mode + ): wrap_in_parentheses(node, node.children[2], visible=False) yield from self.visit_default(node) @@ -516,7 +519,9 @@ def visit_atom(self, node: Node) -> Iterator[Line]: first.type == token.LBRACE and last.type == token.RBRACE ): # Lists or sets of one item - maybe_make_parens_invisible_in_atom(node.children[1], parent=node) + maybe_make_parens_invisible_in_atom( + node.children[1], parent=node, mode=self.mode + ) yield from self.visit_default(node) @@ -1432,18 +1437,13 @@ def normalize_invisible_parens( # noqa: C901 and child.prev_sibling.value == "for" ): if maybe_make_parens_invisible_in_atom( - child, - parent=node, - remove_brackets_around_comma=True, + child, parent=node, mode=mode, remove_brackets_around_comma=True ): wrap_in_parentheses(node, child, visible=False) elif isinstance(child, Node) and node.type == syms.with_stmt: - remove_with_parens(child, node) + remove_with_parens(child, node, mode) elif child.type == syms.atom: - if maybe_make_parens_invisible_in_atom( - child, - parent=node, - ): + if maybe_make_parens_invisible_in_atom(child, parent=node, mode=mode): wrap_in_parentheses(node, child, visible=False) elif is_one_tuple(child): wrap_in_parentheses(node, child, visible=True) @@ -1494,7 +1494,7 @@ def _normalize_import_from(parent: Node, child: LN, index: int) -> None: parent.append_child(Leaf(token.RPAR, "")) -def remove_await_parens(node: Node) -> None: +def remove_await_parens(node: Node, mode: Mode) -> None: if node.children[0].type == token.AWAIT and len(node.children) > 1: if ( node.children[1].type == syms.atom @@ -1503,6 +1503,7 @@ def remove_await_parens(node: Node) -> None: if maybe_make_parens_invisible_in_atom( node.children[1], parent=node, + mode=mode, remove_brackets_around_comma=True, ): wrap_in_parentheses(node, node.children[1], visible=False) @@ -1571,7 +1572,7 @@ def _maybe_wrap_cms_in_parens( node.insert_child(1, new_child) -def remove_with_parens(node: Node, parent: Node) -> None: +def remove_with_parens(node: Node, parent: Node, mode: Mode) -> None: """Recursively hide optional parens in `with` statements.""" # Removing all unnecessary parentheses in with statements in one pass is a tad # complex as different variations of bracketed statements result in pretty @@ -1591,32 +1592,26 @@ def remove_with_parens(node: Node, parent: Node) -> None: # # contains multiple asexpr_test(s) if node.type == syms.atom: if maybe_make_parens_invisible_in_atom( - node, - parent=parent, - remove_brackets_around_comma=True, + node, parent=parent, mode=mode, remove_brackets_around_comma=True ): wrap_in_parentheses(parent, node, visible=False) if isinstance(node.children[1], Node): - remove_with_parens(node.children[1], node) + remove_with_parens(node.children[1], node, mode) elif node.type == syms.testlist_gexp: for child in node.children: if isinstance(child, Node): - remove_with_parens(child, node) + remove_with_parens(child, node, mode) elif node.type == syms.asexpr_test and not any( leaf.type == token.COLONEQUAL for leaf in node.leaves() ): if maybe_make_parens_invisible_in_atom( - node.children[0], - parent=node, - remove_brackets_around_comma=True, + node.children[0], parent=node, mode=mode, remove_brackets_around_comma=True ): wrap_in_parentheses(node, node.children[0], visible=False) def maybe_make_parens_invisible_in_atom( - node: LN, - parent: LN, - remove_brackets_around_comma: bool = False, + node: LN, parent: LN, mode: Mode, remove_brackets_around_comma: bool = False ) -> bool: """If it's safe, make the parens in the atom `node` invisible, recursively. Additionally, remove repeated, adjacent invisible parens from the atom `node` @@ -1640,7 +1635,7 @@ def maybe_make_parens_invisible_in_atom( # around a tuple, however, can be a bit overzealous so we provide # and option to skip this check for `for` and `with` statements. not remove_brackets_around_comma - and max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY + and max_delimiter_priority_in_atom(node, mode) >= COMMA_PRIORITY ) or is_tuple_containing_walrus(node) or is_tuple_containing_star(node) @@ -1681,6 +1676,7 @@ def maybe_make_parens_invisible_in_atom( maybe_make_parens_invisible_in_atom( middle, parent=parent, + mode=mode, remove_brackets_around_comma=remove_brackets_around_comma, ) diff --git a/src/black/lines.py b/src/black/lines.py index 2a719def3c9..ade0977e093 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -82,7 +82,7 @@ def append( mode=self.mode, ) if self.inside_brackets or not preformatted or track_bracket: - self.bracket_tracker.mark(leaf) + self.bracket_tracker.mark(leaf, self.mode) if self.mode.magic_trailing_comma: if self.has_magic_trailing_comma(leaf): self.magic_trailing_comma = leaf From b6cfff1f59013c84321eb8635277ada3e944ecbc Mon Sep 17 00:00:00 2001 From: cobalt <61329810+cobaltt7@users.noreply.github.com> Date: Fri, 20 Jun 2025 18:45:34 -0500 Subject: [PATCH 10/17] run self Signed-off-by: cobalt <61329810+cobaltt7@users.noreply.github.com> --- src/black/linegen.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index 62695f58449..95a58170bd4 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -251,7 +251,8 @@ def visit_dictsetmaker(self, node: Node) -> Iterator[Line]: child, parent=node, mode=self.mode, - remove_brackets_around_comma=False ) + remove_brackets_around_comma=False, + ) else: wrap_in_parentheses(node, child, visible=False) yield from self.visit_default(node) @@ -271,7 +272,7 @@ def visit_funcdef(self, node: Node) -> Iterator[Line]: child, parent=node, mode=self.mode, - remove_brackets_around_comma=False + remove_brackets_around_comma=False, ): wrap_in_parentheses(node, child, visible=False) else: From 388499b36752454a605fddd0c4188820ff5b28a6 Mon Sep 17 00:00:00 2001 From: cobalt <61329810+cobaltt7@users.noreply.github.com> Date: Sun, 3 Aug 2025 18:19:10 -0500 Subject: [PATCH 11/17] Alternative approach Signed-off-by: cobalt <61329810+cobaltt7@users.noreply.github.com> --- CHANGES.md | 2 +- docs/the_black_code_style/future_style.md | 4 +- src/black/__init__.py | 5 +- src/black/brackets.py | 25 +--- src/black/comments.py | 5 +- src/black/linegen.py | 57 +++++---- src/black/lines.py | 2 +- src/black/mode.py | 2 +- src/black/resources/black.schema.json | 2 +- ..._parens_with_braces_and_square_brackets.py | 3 +- .../cases/preview_split_comprehension_in.py | 116 ++++++++++++------ 11 files changed, 132 insertions(+), 91 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 67c546f43e3..1c17ee82e1d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -30,7 +30,7 @@ be formatted (#4552) - Fix a bug where `string_processing` would not split f-strings directly after expressions (#4680) -- Split the `in` clause of comprehensions onto its own line if necessary (#4699) +- Wrap the `in` clause of comprehensions across lines if necessary (#4699) ### Configuration diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 8dd9c7d5990..6babf046eed 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -29,8 +29,8 @@ Currently, the following features are included in the preview style: - `fix_fmt_skip_in_one_liners`: Fix `# fmt: skip` behaviour on one-liner declarations, such as `def foo(): return "mock" # fmt: skip`, where previously the declaration would have been incorrectly collapsed. -- `split_comprehension_in`: Split the `in` clause of list and dictionary comprehensions - onto its own line if it would otherwise exceed the maximum line length. +- `wrap_comprehension_in`: Wrap the `in` clause of list and dictionary comprehensions + across lines if it would otherwise exceed the maximum line length. (labels/unstable-features)= diff --git a/src/black/__init__.py b/src/black/__init__.py index 5f885c2cf6f..93a08a8d88a 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -1241,7 +1241,10 @@ def _format_str_once( elt = EmptyLineTracker(mode=mode) split_line_features = { feature - for feature in {Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF} + for feature in { + Feature.TRAILING_COMMA_IN_CALL, + Feature.TRAILING_COMMA_IN_DEF, + } if supports_feature(versions, feature) } block: Optional[LinesBlock] = None diff --git a/src/black/brackets.py b/src/black/brackets.py index 6291bfe13bb..b703c8e983d 100644 --- a/src/black/brackets.py +++ b/src/black/brackets.py @@ -4,7 +4,6 @@ from dataclasses import dataclass, field from typing import Final, Optional, Union -from black.mode import Mode, Preview from black.nodes import ( BRACKET, CLOSING_BRACKETS, @@ -30,8 +29,6 @@ COMPREHENSION_PRIORITY: Final = 20 COMMA_PRIORITY: Final = 18 -TERNARY_PRIORITY: Final = 16 -COMP_IN_PRIORITY: Final = 15 LOGIC_PRIORITY: Final = 14 STRING_PRIORITY: Final = 12 COMPARATOR_PRIORITY: Final = 10 @@ -70,7 +67,7 @@ class BracketTracker: _lambda_argument_depths: list[int] = field(default_factory=list) invisible: list[Leaf] = field(default_factory=list) - def mark(self, leaf: Leaf, mode: Mode) -> None: + def mark(self, leaf: Leaf) -> None: """Mark `leaf` with bracket-related metadata. Keep track of delimiters. All leaves receive an int `bracket_depth` field that stores how deep @@ -114,7 +111,7 @@ def mark(self, leaf: Leaf, mode: Mode) -> None: self.invisible.append(leaf) leaf.bracket_depth = self.depth if self.depth == 0: - delim = is_split_before_delimiter(leaf, mode, self.previous) + delim = is_split_before_delimiter(leaf, self.previous) if delim and self.previous is not None: self.delimiters[id(self.previous)] = delim else: @@ -232,9 +229,7 @@ def is_split_after_delimiter(leaf: Leaf) -> Priority: return 0 -def is_split_before_delimiter( - leaf: Leaf, mode: Mode, previous: Optional[Leaf] = None -) -> Priority: +def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority: """Return the priority of the `leaf` delimiter, given a line break before it. The delimiter priorities returned here are from those delimiters that would @@ -294,14 +289,6 @@ def is_split_before_delimiter( ): return COMPREHENSION_PRIORITY - if ( - Preview.split_comprehension_in in mode - and leaf.value == "in" - and leaf.parent - and leaf.parent.type in {syms.comp_for, syms.old_comp_for} - ): - return COMP_IN_PRIORITY - if leaf.value in {"if", "else"} and leaf.parent and leaf.parent.type == syms.test: return TERNARY_PRIORITY @@ -338,7 +325,7 @@ def is_split_before_delimiter( return 0 -def max_delimiter_priority_in_atom(node: LN, mode: Mode) -> Priority: +def max_delimiter_priority_in_atom(node: LN) -> Priority: """Return maximum delimiter priority inside `node`. This is specific to atoms with contents contained in a pair of parentheses. @@ -355,10 +342,10 @@ def max_delimiter_priority_in_atom(node: LN, mode: Mode) -> Priority: bt = BracketTracker() for c in node.children[1:-1]: if isinstance(c, Leaf): - bt.mark(c, mode) + bt.mark(c) else: for leaf in c.leaves(): - bt.mark(leaf, mode) + bt.mark(leaf) try: return bt.max_delimiter_priority() diff --git a/src/black/comments.py b/src/black/comments.py index 360f115e6b7..2b530f2b910 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -446,8 +446,9 @@ def _contains_fmt_skip_comment(comment_line: str, mode: Mode) -> bool: ], *[ _COMMENT_PREFIX + comment.strip() - for comment - in comment_line.strip(_COMMENT_PREFIX).split(_COMMENT_LIST_SEPARATOR) + for comment in comment_line.strip(_COMMENT_PREFIX).split( + _COMMENT_LIST_SEPARATOR + ) ], ] diff --git a/src/black/linegen.py b/src/black/linegen.py index 95a58170bd4..f4eae59c9b7 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -250,7 +250,6 @@ def visit_dictsetmaker(self, node: Node) -> Iterator[Line]: maybe_make_parens_invisible_in_atom( child, parent=node, - mode=self.mode, remove_brackets_around_comma=False, ) else: @@ -271,7 +270,6 @@ def visit_funcdef(self, node: Node) -> Iterator[Line]: if maybe_make_parens_invisible_in_atom( child, parent=node, - mode=self.mode, remove_brackets_around_comma=False, ): wrap_in_parentheses(node, child, visible=False) @@ -365,7 +363,7 @@ def visit_power(self, node: Node) -> Iterator[Line]: ): wrap_in_parentheses(node, leaf) - remove_await_parens(node, self.mode) + remove_await_parens(node) yield from self.visit_default(node) @@ -412,9 +410,7 @@ def foo(a: int, b: float = 7): ... def foo(a: (int), b: (float) = 7): ... """ assert len(node.children) == 3 - if maybe_make_parens_invisible_in_atom( - node.children[2], parent=node, mode=self.mode - ): + if maybe_make_parens_invisible_in_atom(node.children[2], parent=node): wrap_in_parentheses(node, node.children[2], visible=False) yield from self.visit_default(node) @@ -520,9 +516,7 @@ def visit_atom(self, node: Node) -> Iterator[Line]: first.type == token.LBRACE and last.type == token.RBRACE ): # Lists or sets of one item - maybe_make_parens_invisible_in_atom( - node.children[1], parent=node, mode=self.mode - ) + maybe_make_parens_invisible_in_atom(node.children[1], parent=node) yield from self.visit_default(node) @@ -574,6 +568,16 @@ def visit_fstring(self, node: Node) -> Iterator[Line]: # yield from self.visit_default(node) + def visit_comp_for(self, node: Node) -> Iterator[Line]: + if Preview.wrap_comprehension_in in self.mode: + normalize_invisible_parens( + node, parens_after={"in"}, mode=self.mode, features=self.features + ) + yield from self.visit_default(node) + + def visit_old_comp_for(self, node: Node) -> Iterator[Line]: + yield from self.visit_comp_for(node) + def __post_init__(self) -> None: """You are in a twisty little maze of passages.""" self.current_line = Line(mode=self.mode) @@ -1438,13 +1442,18 @@ def normalize_invisible_parens( # noqa: C901 and child.prev_sibling.value == "for" ): if maybe_make_parens_invisible_in_atom( - child, parent=node, mode=mode, remove_brackets_around_comma=True + child, + parent=node, + remove_brackets_around_comma=True, ): wrap_in_parentheses(node, child, visible=False) elif isinstance(child, Node) and node.type == syms.with_stmt: - remove_with_parens(child, node, mode) + remove_with_parens(child, node) elif child.type == syms.atom: - if maybe_make_parens_invisible_in_atom(child, parent=node, mode=mode): + if maybe_make_parens_invisible_in_atom( + child, + parent=node, + ): wrap_in_parentheses(node, child, visible=False) elif is_one_tuple(child): wrap_in_parentheses(node, child, visible=True) @@ -1495,7 +1504,7 @@ def _normalize_import_from(parent: Node, child: LN, index: int) -> None: parent.append_child(Leaf(token.RPAR, "")) -def remove_await_parens(node: Node, mode: Mode) -> None: +def remove_await_parens(node: Node) -> None: if node.children[0].type == token.AWAIT and len(node.children) > 1: if ( node.children[1].type == syms.atom @@ -1504,7 +1513,6 @@ def remove_await_parens(node: Node, mode: Mode) -> None: if maybe_make_parens_invisible_in_atom( node.children[1], parent=node, - mode=mode, remove_brackets_around_comma=True, ): wrap_in_parentheses(node, node.children[1], visible=False) @@ -1573,7 +1581,7 @@ def _maybe_wrap_cms_in_parens( node.insert_child(1, new_child) -def remove_with_parens(node: Node, parent: Node, mode: Mode) -> None: +def remove_with_parens(node: Node, parent: Node) -> None: """Recursively hide optional parens in `with` statements.""" # Removing all unnecessary parentheses in with statements in one pass is a tad # complex as different variations of bracketed statements result in pretty @@ -1593,26 +1601,32 @@ def remove_with_parens(node: Node, parent: Node, mode: Mode) -> None: # # contains multiple asexpr_test(s) if node.type == syms.atom: if maybe_make_parens_invisible_in_atom( - node, parent=parent, mode=mode, remove_brackets_around_comma=True + node, + parent=parent, + remove_brackets_around_comma=True, ): wrap_in_parentheses(parent, node, visible=False) if isinstance(node.children[1], Node): - remove_with_parens(node.children[1], node, mode) + remove_with_parens(node.children[1], node) elif node.type == syms.testlist_gexp: for child in node.children: if isinstance(child, Node): - remove_with_parens(child, node, mode) + remove_with_parens(child, node) elif node.type == syms.asexpr_test and not any( leaf.type == token.COLONEQUAL for leaf in node.leaves() ): if maybe_make_parens_invisible_in_atom( - node.children[0], parent=node, mode=mode, remove_brackets_around_comma=True + node.children[0], + parent=node, + remove_brackets_around_comma=True, ): wrap_in_parentheses(node, node.children[0], visible=False) def maybe_make_parens_invisible_in_atom( - node: LN, parent: LN, mode: Mode, remove_brackets_around_comma: bool = False + node: LN, + parent: LN, + remove_brackets_around_comma: bool = False, ) -> bool: """If it's safe, make the parens in the atom `node` invisible, recursively. Additionally, remove repeated, adjacent invisible parens from the atom `node` @@ -1636,7 +1650,7 @@ def maybe_make_parens_invisible_in_atom( # around a tuple, however, can be a bit overzealous so we provide # and option to skip this check for `for` and `with` statements. not remove_brackets_around_comma - and max_delimiter_priority_in_atom(node, mode) >= COMMA_PRIORITY + and max_delimiter_priority_in_atom(node) >= COMMA_PRIORITY ) or is_tuple_containing_walrus(node) or is_tuple_containing_star(node) @@ -1677,7 +1691,6 @@ def maybe_make_parens_invisible_in_atom( maybe_make_parens_invisible_in_atom( middle, parent=parent, - mode=mode, remove_brackets_around_comma=remove_brackets_around_comma, ) diff --git a/src/black/lines.py b/src/black/lines.py index ade0977e093..2a719def3c9 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -82,7 +82,7 @@ def append( mode=self.mode, ) if self.inside_brackets or not preformatted or track_bracket: - self.bracket_tracker.mark(leaf, self.mode) + self.bracket_tracker.mark(leaf) if self.mode.magic_trailing_comma: if self.has_magic_trailing_comma(leaf): self.magic_trailing_comma = leaf diff --git a/src/black/mode.py b/src/black/mode.py index f7397444968..bd49828d96c 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -204,7 +204,7 @@ class Preview(Enum): multiline_string_handling = auto() always_one_newline_after_import = auto() fix_fmt_skip_in_one_liners = auto() - split_comprehension_in = auto() + wrap_comprehension_in = auto() UNSTABLE_FEATURES: set[Preview] = { diff --git a/src/black/resources/black.schema.json b/src/black/resources/black.schema.json index 0e127045889..b354b853b5a 100644 --- a/src/black/resources/black.schema.json +++ b/src/black/resources/black.schema.json @@ -85,7 +85,7 @@ "multiline_string_handling", "always_one_newline_after_import", "fix_fmt_skip_in_one_liners", - "split_comprehension_in" + "wrap_comprehension_in" ] }, "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/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py index 900ea8487ce..cbbcf16d3bd 100644 --- a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py +++ b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py @@ -284,8 +284,7 @@ def foo_square_brackets(request): ) func([ x - for x - in [ + for x in [ x for x in "long line long line long line long line long line long line long line" ] diff --git a/tests/data/cases/preview_split_comprehension_in.py b/tests/data/cases/preview_split_comprehension_in.py index a886a4bbc6f..f74cb5b48ca 100644 --- a/tests/data/cases/preview_split_comprehension_in.py +++ b/tests/data/cases/preview_split_comprehension_in.py @@ -2,14 +2,25 @@ [a for graph_path_expression in refined_constraint.condition_as_predicate.variables] [ - (foobar_very_long_key, foobar_very_long_value) - for foobar_very_long_key, foobar_very_long_value in foobar_very_long_dictionary.items() + a + for graph_path_expression in refined_constraint.condition_as_predicate.variables ] [ a for graph_path_expression in refined_constraint.condition_as_predicate.variables ] +[ + a + for graph_path_expression in ( + refined_constraint.condition_as_predicate.variables + ) +] + +[ + (foobar_very_long_key, foobar_very_long_value) + for foobar_very_long_key, foobar_very_long_value in foobar_very_long_dictionary.items() +] # Don't split the `in` if it's not too long lcomp3 = [ @@ -20,10 +31,11 @@ ] # Nested arrays +# First in will not be split because it would still be too long [[ x for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb - for y in xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxdxxxxxxxxxxxxxxxxxxxxxxxx + for y in xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ]] # Multiple comprehensions, only split the second `in` @@ -34,40 +46,58 @@ for graph_path_expression in refined_constraint.condition_as_predicate.variables ] -# Split long `if`s -graph_path_expressions_in_local_constraint_refinements = [ - foobar - for foobar in bar - if foobar not in {very_long_value, other_long_value, foobarbaz, one_more_value} -] - # Dictionary comprehensions dict_with_really_long_names = { really_really_long_key_name: an_even_longer_really_really_long_key_value for really_really_long_key_name, an_even_longer_really_really_long_key_value in really_really_really_long_dict_name.items() } +{ + key_with_super_really_long_name: key_with_super_really_long_name + for key_with_super_really_long_name in dictionary_with_super_really_long_name +} { key_with_super_really_long_name: key_with_super_really_long_name for key_with_super_really_long_name in dictionary_with_super_really_long_name } +{ + key_with_super_really_long_name: key_with_super_really_long_name + for key in ( + dictionary + ) +} # output - [ a - for graph_path_expression - in refined_constraint.condition_as_predicate.variables + for graph_path_expression in ( + refined_constraint.condition_as_predicate.variables + ) ] [ - (foobar_very_long_key, foobar_very_long_value) - for foobar_very_long_key, foobar_very_long_value - in foobar_very_long_dictionary.items() + a + for graph_path_expression in ( + refined_constraint.condition_as_predicate.variables + ) ] [ a - for graph_path_expression - in refined_constraint.condition_as_predicate.variables + for graph_path_expression in ( + refined_constraint.condition_as_predicate.variables + ) +] +[ + a + for graph_path_expression in ( + refined_constraint.condition_as_predicate.variables + ) +] + +[ + (foobar_very_long_key, foobar_very_long_value) + for foobar_very_long_key, foobar_very_long_value in ( + foobar_very_long_dictionary.items() + ) ] # Don't split the `in` if it's not too long @@ -79,39 +109,47 @@ ] # Nested arrays -[[ - x - for x - in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb - for y - in xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxdxxxxxxxxxxxxxxxxxxxxxxxx -]] +# First in will not be split because it would still be too long +[ + [ + x + for x in bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + for y in ( + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + ) + ] +] # Multiple comprehensions, only split the second `in` graph_path_expressions_in_local_constraint_refinements = [ graph_path_expression for refined_constraint in self._local_constraint_refinements.values() if refined_constraint is not None - for graph_path_expression - in refined_constraint.condition_as_predicate.variables -] - -# Split long `if`s -graph_path_expressions_in_local_constraint_refinements = [ - foobar - for foobar in bar - if foobar - not in {very_long_value, other_long_value, foobarbaz, one_more_value} + for graph_path_expression in ( + refined_constraint.condition_as_predicate.variables + ) ] # Dictionary comprehensions dict_with_really_long_names = { really_really_long_key_name: an_even_longer_really_really_long_key_value - for really_really_long_key_name, an_even_longer_really_really_long_key_value - in really_really_really_long_dict_name.items() + for really_really_long_key_name, an_even_longer_really_really_long_key_value in ( + really_really_really_long_dict_name.items() + ) } { key_with_super_really_long_name: key_with_super_really_long_name - for key_with_super_really_long_name - in dictionary_with_super_really_long_name + for key_with_super_really_long_name in ( + dictionary_with_super_really_long_name + ) +} +{ + key_with_super_really_long_name: key_with_super_really_long_name + for key_with_super_really_long_name in ( + dictionary_with_super_really_long_name + ) +} +{ + key_with_super_really_long_name: key_with_super_really_long_name + for key in dictionary } From 69b2e3ae1815c8ebc074acdc901870ff35a807c3 Mon Sep 17 00:00:00 2001 From: cobalt <61329810+cobaltt7@users.noreply.github.com> Date: Sun, 3 Aug 2025 18:20:34 -0500 Subject: [PATCH 12/17] oops! Signed-off-by: cobalt <61329810+cobaltt7@users.noreply.github.com> --- src/black/brackets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/black/brackets.py b/src/black/brackets.py index b703c8e983d..c2e8be4348e 100644 --- a/src/black/brackets.py +++ b/src/black/brackets.py @@ -29,6 +29,7 @@ COMPREHENSION_PRIORITY: Final = 20 COMMA_PRIORITY: Final = 18 +TERNARY_PRIORITY: Final = 16 LOGIC_PRIORITY: Final = 14 STRING_PRIORITY: Final = 12 COMPARATOR_PRIORITY: Final = 10 From c6484e4ce119692cf0b3b50b30cbee9d47f9a160 Mon Sep 17 00:00:00 2001 From: cobalt <61329810+cobaltt7@users.noreply.github.com> Date: Sun, 3 Aug 2025 18:23:08 -0500 Subject: [PATCH 13/17] run test on preview, not unstable Signed-off-by: cobalt <61329810+cobaltt7@users.noreply.github.com> --- tests/data/cases/preview_split_comprehension_in.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/cases/preview_split_comprehension_in.py b/tests/data/cases/preview_split_comprehension_in.py index f74cb5b48ca..6eba2b7e5db 100644 --- a/tests/data/cases/preview_split_comprehension_in.py +++ b/tests/data/cases/preview_split_comprehension_in.py @@ -1,4 +1,4 @@ -# flags: --unstable --line-length=79 +# flags: --preview --line-length=79 [a for graph_path_expression in refined_constraint.condition_as_predicate.variables] [ From 27a0cc9683db7788d2e362007070c27d28e2077c Mon Sep 17 00:00:00 2001 From: cobalt <61329810+cobaltt7@users.noreply.github.com> Date: Sun, 10 Aug 2025 18:27:48 -0500 Subject: [PATCH 14/17] Don't remove parens around ternaries Signed-off-by: cobalt <61329810+cobaltt7@users.noreply.github.com> --- src/black/linegen.py | 8 +++++++- ...mprehension_in.py => preview_wrap_comprehension_in.py} | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) rename tests/data/cases/{preview_split_comprehension_in.py => preview_wrap_comprehension_in.py} (96%) diff --git a/src/black/linegen.py b/src/black/linegen.py index f4eae59c9b7..f1951eab89c 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1449,7 +1449,13 @@ def normalize_invisible_parens( # noqa: C901 wrap_in_parentheses(node, child, visible=False) elif isinstance(child, Node) and node.type == syms.with_stmt: remove_with_parens(child, node) - elif child.type == syms.atom: + elif child.type == syms.atom and not ( + "in" in parens_after + and len(child.children) == 3 + and is_lpar_token(child.children[0]) + and is_rpar_token(child.children[-1]) + and child.children[1].type == syms.test + ): if maybe_make_parens_invisible_in_atom( child, parent=node, diff --git a/tests/data/cases/preview_split_comprehension_in.py b/tests/data/cases/preview_wrap_comprehension_in.py similarity index 96% rename from tests/data/cases/preview_split_comprehension_in.py rename to tests/data/cases/preview_wrap_comprehension_in.py index 6eba2b7e5db..e457f0e772f 100644 --- a/tests/data/cases/preview_split_comprehension_in.py +++ b/tests/data/cases/preview_wrap_comprehension_in.py @@ -30,6 +30,9 @@ if element is not None ] +# Don't remove parens around ternaries +expected = [i for i in (a if b else c)] + # Nested arrays # First in will not be split because it would still be too long [[ @@ -108,6 +111,9 @@ if element is not None ] +# Don't remove parens around ternaries +expected = [i for i in (a if b else c)] + # Nested arrays # First in will not be split because it would still be too long [ From f350d71b55d707a32423dd7455c3c2112359d62d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 7 Sep 2025 03:35:30 +0000 Subject: [PATCH 15/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/the_black_code_style/future_style.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 3891d72d192..dee3212c334 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -30,9 +30,9 @@ Currently, the following features are included in the preview style: such as `def foo(): return "mock" # fmt: skip`, where previously the declaration would have been incorrectly collapsed. - `wrap_comprehension_in`: Wrap the `in` clause of list and dictionary comprehensions - across lines if it would otherwise exceed the maximum line length. - such as `def foo(): return "mock" # fmt: skip`, where previously the declaration - would have been incorrectly collapsed. + across lines if it would otherwise exceed the maximum line length. such as + `def foo(): return "mock" # fmt: skip`, where previously the declaration would have + been incorrectly collapsed. - `remove_parens_around_except_types`: Remove parentheses around multiple exception types in `except` and `except*` without `as`. See PEP 758 for details. From 7b2c2ce9e62bfbb51d5702ad630977a166fcdcb2 Mon Sep 17 00:00:00 2001 From: cobalt <61329810+cobaltt7@users.noreply.github.com> Date: Sat, 6 Sep 2025 22:38:07 -0500 Subject: [PATCH 16/17] Update src/black/linegen.py --- 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 7a63d257c71..27c2c92d9b2 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1482,7 +1482,7 @@ def normalize_invisible_parens( # noqa: C901 and is_lpar_token(child.children[0]) and is_rpar_token(child.children[-1]) and child.children[1].type == syms.test - ):= + ): if maybe_make_parens_invisible_in_atom( child, parent=node, mode=mode, features=features ): From 8feab8230f202400ad5261ce18adb52f34452d2a Mon Sep 17 00:00:00 2001 From: cobalt <61329810+cobaltt7@users.noreply.github.com> Date: Mon, 8 Sep 2025 07:51:16 -0500 Subject: [PATCH 17/17] Update docs/the_black_code_style/future_style.md --- docs/the_black_code_style/future_style.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index dee3212c334..13bcaa94e5d 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -30,9 +30,7 @@ Currently, the following features are included in the preview style: such as `def foo(): return "mock" # fmt: skip`, where previously the declaration would have been incorrectly collapsed. - `wrap_comprehension_in`: Wrap the `in` clause of list and dictionary comprehensions - across lines if it would otherwise exceed the maximum line length. such as - `def foo(): return "mock" # fmt: skip`, where previously the declaration would have - been incorrectly collapsed. + across lines if it would otherwise exceed the maximum line length. - `remove_parens_around_except_types`: Remove parentheses around multiple exception types in `except` and `except*` without `as`. See PEP 758 for details.