diff --git a/CHANGES.md b/CHANGES.md index 888824ee055..8f9fc37f53a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -125,6 +125,7 @@ wheels in a future release as soon as the mypyc bug is fixed. - Implicitly concatenated strings used as function args are no longer wrapped inside parentheses (#3640) - Remove blank lines between a class definition and its docstring (#3692) +- Stop wrapping unnecessarily when there are comments inside brackets (#3362). ### Configuration diff --git a/src/black/linegen.py b/src/black/linegen.py index bdc4ee54ab2..4b2421d3ac4 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -561,7 +561,7 @@ def transform_line( if ( not line.contains_uncollapsable_type_comments() and not line.should_split_rhs - and not line.magic_trailing_comma + and not line.bracket_after_magic_trailing_comma and ( is_line_short_enough(line, mode=mode, line_str=line_str) or line.contains_unsplittable_type_ignore() @@ -619,6 +619,24 @@ def _rhs( string_paren_wrap, rhs, ] + + # If there are comments at the beginning or end of the line, call + # standalone_comment_split before delimiter_split. Else, call + # delimiter_split first. This means that when there is a comment in the + # middle of an expression or list, it will perform a delimiter split. + # But when the comment is at the top or bottom, it won't perform a + # delimiter split (if it doesn't have to). + if ( + Preview.stop_some_unnecessary_wrapping_inside_brackets in mode + and line.contains_standalone_comments() + and len(line.comments) == 0 + and all( + leaf.type != STANDALONE_COMMENT for leaf in line.leaves[1:-1] + ) + and line.magic_trailing_comma is None + ): + transformers.remove(standalone_comment_split) + transformers.insert(0, standalone_comment_split) else: transformers = [ string_merge, @@ -800,6 +818,7 @@ def _first_right_hand_split( body = bracket_split_build_line( body_leaves, line, opening_bracket, component=_BracketSplitComponent.body ) + body.magic_trailing_comma = line.magic_trailing_comma tail = bracket_split_build_line( tail_leaves, line, opening_bracket, component=_BracketSplitComponent.tail ) @@ -848,7 +867,7 @@ def _maybe_split_omitting_optional_parens( ) # the left side of assignment won't explode further because of magic # trailing comma - and rhs.head.magic_trailing_comma is None + and rhs.head.bracket_after_magic_trailing_comma is None # the split by omitting optional parens isn't preferred by some other # reason and not _prefer_split_rhs_oop(rhs_oop, mode) @@ -1526,7 +1545,7 @@ def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[Leaf """ omit: Set[LeafID] = set() - if not line.magic_trailing_comma: + if not line.bracket_after_magic_trailing_comma: yield omit length = 4 * line.depth diff --git a/src/black/lines.py b/src/black/lines.py index 71b657a0654..f69657ea3a0 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -57,6 +57,7 @@ class Line: bracket_tracker: BracketTracker = field(default_factory=BracketTracker) inside_brackets: bool = False should_split_rhs: bool = False + bracket_after_magic_trailing_comma: Optional[Leaf] = None magic_trailing_comma: Optional[Leaf] = None def append( @@ -89,7 +90,8 @@ def append( self.bracket_tracker.mark(leaf) if self.mode.magic_trailing_comma: if self.has_magic_trailing_comma(leaf): - self.magic_trailing_comma = leaf + self.bracket_after_magic_trailing_comma = leaf + self.magic_trailing_comma = self.get_last_non_comment_leaf() elif self.has_magic_trailing_comma(leaf, ensure_removable=True): self.remove_trailing_comma() if not self.append_comment(leaf): @@ -306,6 +308,13 @@ def contains_unsplittable_type_ignore(self) -> bool: def contains_multiline_strings(self) -> bool: return any(is_multiline_string(leaf) for leaf in self.leaves) + def get_last_non_comment_leaf(self) -> Optional[Leaf]: + for leaf in reversed(self.leaves): + if leaf.type != STANDALONE_COMMENT: + return leaf + + return None + def has_magic_trailing_comma( self, closing: Leaf, ensure_removable: bool = False ) -> bool: @@ -317,10 +326,18 @@ def has_magic_trailing_comma( - it's not from square bracket indexing (specifically, single-element square bracket indexing) """ + if Preview.stop_some_unnecessary_wrapping_inside_brackets in self.mode: + leaf_to_check = self.get_last_non_comment_leaf() + elif len(self.leaves) > 0: + leaf_to_check = self.leaves[-1] + else: + return False + if not ( closing.type in CLOSING_BRACKETS and self.leaves - and self.leaves[-1].type == token.COMMA + and leaf_to_check is not None + and leaf_to_check.type == token.COMMA ): return False @@ -411,7 +428,16 @@ def comments_after(self, leaf: Leaf) -> List[Leaf]: def remove_trailing_comma(self) -> None: """Remove the trailing comma and moves the comments attached to it.""" - trailing_comma = self.leaves.pop() + if Preview.stop_some_unnecessary_wrapping_inside_brackets in self.mode: + # There might be comments after the magic trailing comma, so we must search + # backwards to find it. + for i in range(len(self.leaves) - 1, -1, -1): + if self.leaves[i].type == token.COMMA: + trailing_comma = self.leaves.pop(i) + break + else: + trailing_comma = self.leaves.pop() + trailing_comma_comments = self.comments.pop(id(trailing_comma), []) self.comments.setdefault(id(self.leaves[-1]), []).extend( trailing_comma_comments @@ -463,6 +489,7 @@ def clone(self) -> "Line": inside_brackets=self.inside_brackets, should_split_rhs=self.should_split_rhs, magic_trailing_comma=self.magic_trailing_comma, + bracket_after_magic_trailing_comma=self.bracket_after_magic_trailing_comma, ) def __str__(self) -> str: diff --git a/src/black/mode.py b/src/black/mode.py index 30c5d2f1b2f..0ec1b61c9ee 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -185,6 +185,7 @@ class Preview(Enum): skip_magic_trailing_comma_in_subscript = auto() wrap_long_dict_values_in_parens = auto() wrap_multiple_context_managers_in_parens = auto() + stop_some_unnecessary_wrapping_inside_brackets = auto() dummy_implementations = auto() walrus_subscript = auto() diff --git a/src/black/trans.py b/src/black/trans.py index c0cc92613ac..87e9d8b3624 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -2133,6 +2133,7 @@ def do_transform( inside_brackets=True, should_split_rhs=line.should_split_rhs, magic_trailing_comma=line.magic_trailing_comma, + bracket_after_magic_trailing_comma=line.bracket_after_magic_trailing_comma, ) string_leaf = Leaf(token.STRING, string_value) insert_str_child(string_leaf) diff --git a/tests/data/miscellaneous/skip_magic_trailing_comma.py b/tests/data/miscellaneous/skip_magic_trailing_comma.py new file mode 100644 index 00000000000..9b50adb70da --- /dev/null +++ b/tests/data/miscellaneous/skip_magic_trailing_comma.py @@ -0,0 +1,5 @@ +def func( + x, + # this comment shouldn't be deleted +): + pass diff --git a/tests/data/miscellaneous/trailing_comma_and_comment.pyi b/tests/data/miscellaneous/trailing_comma_and_comment.pyi new file mode 100644 index 00000000000..1b9eef1ae7a --- /dev/null +++ b/tests/data/miscellaneous/trailing_comma_and_comment.pyi @@ -0,0 +1,5 @@ +def func( + x, + y, + # comment +): ... diff --git a/tests/data/preview/trailing_comma.py b/tests/data/preview/trailing_comma.py index 5b09c664606..0155de6919f 100644 --- a/tests/data/preview/trailing_comma.py +++ b/tests/data/preview/trailing_comma.py @@ -7,19 +7,19 @@ f = [ arg1, arg2, - arg3, arg4 + arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18 # comment ] g = ( arg1, arg2, - arg3, arg4 + arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18 # comment ) h = { arg1, arg2, - arg3, arg4 + arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18 # comment } @@ -37,6 +37,20 @@ arg2, arg3, arg4, + arg5, + arg6, + arg7, + arg8, + arg9, + arg10, + arg11, + arg12, + arg13, + arg14, + arg15, + arg16, + arg17, + arg18, # comment ] g = ( @@ -44,6 +58,20 @@ arg2, arg3, arg4, + arg5, + arg6, + arg7, + arg8, + arg9, + arg10, + arg11, + arg12, + arg13, + arg14, + arg15, + arg16, + arg17, + arg18, # comment ) h = { @@ -51,5 +79,19 @@ arg2, arg3, arg4, + arg5, + arg6, + arg7, + arg8, + arg9, + arg10, + arg11, + arg12, + arg13, + arg14, + arg15, + arg16, + arg17, + arg18, # comment } diff --git a/tests/data/preview/trailing_comma_with_comment.py b/tests/data/preview/trailing_comma_with_comment.py new file mode 100644 index 00000000000..cac789b6396 --- /dev/null +++ b/tests/data/preview/trailing_comma_with_comment.py @@ -0,0 +1,6 @@ +li = [ + 1, + 2, + 3, + # comment +] \ No newline at end of file diff --git a/tests/data/preview/wrapping_with_comments_in_brackets.py b/tests/data/preview/wrapping_with_comments_in_brackets.py new file mode 100644 index 00000000000..8531296e646 --- /dev/null +++ b/tests/data/preview/wrapping_with_comments_in_brackets.py @@ -0,0 +1,19 @@ +def get_requires_for_build_sdist( + # pylint: disable-next=unused-argument + config_settings: dict[str, str | list[str]] | None = None, +) -> list[str]: + return ["pathspec", "pyproject_metadata"] + + +( + a + and b + # comment + and c + and d +) + +( + # comment + a and b and c and d +) \ No newline at end of file diff --git a/tests/test_format.py b/tests/test_format.py index f3db423b637..d89ad0aacf9 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -218,3 +218,24 @@ def test_type_comment_syntax_error() -> None: source, expected = read_data("type_comments", "type_comment_syntax_error") assert_format(source, expected) black.assert_equivalent(source, expected) + + +def test_skip_magic_trailing_comma() -> None: + """ + Test a case where the --skip-magic-trailing-comma option used + """ + source, expected = read_data("miscellaneous", "skip_magic_trailing_comma") + mode = replace(DEFAULT_MODE, magic_trailing_comma=False, preview=True) + assert_format(source, expected, mode) + + +def test_trailing_comma_and_comment_in_pyi() -> None: + source, expected = read_data("miscellaneous", "trailing_comma_and_comment.pyi") + mode = replace( + DEFAULT_MODE, + magic_trailing_comma=False, + preview=False, + line_length=130, + is_pyi=True, + ) + assert_format(source, expected, mode)