Skip to content

Commit aafc21a

Browse files
authored
Prefer splitting right hand side of assignment statements. (#3368)
1 parent 658c8d8 commit aafc21a

File tree

6 files changed

+236
-18
lines changed

6 files changed

+236
-18
lines changed

CHANGES.md

+2
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@
7373
present) or as a single newline character (if a newline is present) (#3348)
7474
- Implicitly concatenated strings used as function args are now wrapped inside
7575
parentheses (#3307)
76+
- For assignment statements, prefer splitting the right hand side if the left hand side
77+
fits on a single line (#3368)
7678
- Correctly handle trailing commas that are inside a line's leading non-nested parens
7779
(#3370)
7880

src/black/linegen.py

+107-15
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Generating lines of code.
33
"""
44
import sys
5+
from dataclasses import dataclass
56
from enum import Enum, auto
67
from functools import partial, wraps
78
from typing import Collection, Iterator, List, Optional, Set, Union, cast
@@ -24,6 +25,7 @@
2425
from black.mode import Feature, Mode, Preview
2526
from black.nodes import (
2627
ASSIGNMENTS,
28+
BRACKETS,
2729
CLOSING_BRACKETS,
2830
OPENING_BRACKETS,
2931
RARROW,
@@ -634,6 +636,17 @@ def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> Iterator
634636
yield result
635637

636638

639+
@dataclass
640+
class _RHSResult:
641+
"""Intermediate split result from a right hand split."""
642+
643+
head: Line
644+
body: Line
645+
tail: Line
646+
opening_bracket: Leaf
647+
closing_bracket: Leaf
648+
649+
637650
def right_hand_split(
638651
line: Line,
639652
line_length: int,
@@ -648,6 +661,22 @@ def right_hand_split(
648661
649662
Note: running this function modifies `bracket_depth` on the leaves of `line`.
650663
"""
664+
rhs_result = _first_right_hand_split(line, omit=omit)
665+
yield from _maybe_split_omitting_optional_parens(
666+
rhs_result, line, line_length, features=features, omit=omit
667+
)
668+
669+
670+
def _first_right_hand_split(
671+
line: Line,
672+
omit: Collection[LeafID] = (),
673+
) -> _RHSResult:
674+
"""Split the line into head, body, tail starting with the last bracket pair.
675+
676+
Note: this function should not have side effects. It's relied upon by
677+
_maybe_split_omitting_optional_parens to get an opinion whether to prefer
678+
splitting on the right side of an assignment statement.
679+
"""
651680
tail_leaves: List[Leaf] = []
652681
body_leaves: List[Leaf] = []
653682
head_leaves: List[Leaf] = []
@@ -683,51 +712,114 @@ def right_hand_split(
683712
tail_leaves, line, opening_bracket, component=_BracketSplitComponent.tail
684713
)
685714
bracket_split_succeeded_or_raise(head, body, tail)
715+
return _RHSResult(head, body, tail, opening_bracket, closing_bracket)
716+
717+
718+
def _maybe_split_omitting_optional_parens(
719+
rhs: _RHSResult,
720+
line: Line,
721+
line_length: int,
722+
features: Collection[Feature] = (),
723+
omit: Collection[LeafID] = (),
724+
) -> Iterator[Line]:
686725
if (
687726
Feature.FORCE_OPTIONAL_PARENTHESES not in features
688727
# the opening bracket is an optional paren
689-
and opening_bracket.type == token.LPAR
690-
and not opening_bracket.value
728+
and rhs.opening_bracket.type == token.LPAR
729+
and not rhs.opening_bracket.value
691730
# the closing bracket is an optional paren
692-
and closing_bracket.type == token.RPAR
693-
and not closing_bracket.value
731+
and rhs.closing_bracket.type == token.RPAR
732+
and not rhs.closing_bracket.value
694733
# it's not an import (optional parens are the only thing we can split on
695734
# in this case; attempting a split without them is a waste of time)
696735
and not line.is_import
697736
# there are no standalone comments in the body
698-
and not body.contains_standalone_comments(0)
737+
and not rhs.body.contains_standalone_comments(0)
699738
# and we can actually remove the parens
700-
and can_omit_invisible_parens(body, line_length)
739+
and can_omit_invisible_parens(rhs.body, line_length)
701740
):
702-
omit = {id(closing_bracket), *omit}
741+
omit = {id(rhs.closing_bracket), *omit}
703742
try:
704-
yield from right_hand_split(line, line_length, features=features, omit=omit)
705-
return
743+
# The _RHSResult Omitting Optional Parens.
744+
rhs_oop = _first_right_hand_split(line, omit=omit)
745+
if not (
746+
Preview.prefer_splitting_right_hand_side_of_assignments in line.mode
747+
# the split is right after `=`
748+
and len(rhs.head.leaves) >= 2
749+
and rhs.head.leaves[-2].type == token.EQUAL
750+
# the left side of assignement contains brackets
751+
and any(leaf.type in BRACKETS for leaf in rhs.head.leaves[:-1])
752+
# the left side of assignment is short enough (the -1 is for the ending
753+
# optional paren)
754+
and is_line_short_enough(rhs.head, line_length=line_length - 1)
755+
# the left side of assignment won't explode further because of magic
756+
# trailing comma
757+
and rhs.head.magic_trailing_comma is None
758+
# the split by omitting optional parens isn't preferred by some other
759+
# reason
760+
and not _prefer_split_rhs_oop(rhs_oop, line_length=line_length)
761+
):
762+
yield from _maybe_split_omitting_optional_parens(
763+
rhs_oop, line, line_length, features=features, omit=omit
764+
)
765+
return
706766

707767
except CannotSplit as e:
708768
if not (
709-
can_be_split(body)
710-
or is_line_short_enough(body, line_length=line_length)
769+
can_be_split(rhs.body)
770+
or is_line_short_enough(rhs.body, line_length=line_length)
711771
):
712772
raise CannotSplit(
713773
"Splitting failed, body is still too long and can't be split."
714774
) from e
715775

716-
elif head.contains_multiline_strings() or tail.contains_multiline_strings():
776+
elif (
777+
rhs.head.contains_multiline_strings()
778+
or rhs.tail.contains_multiline_strings()
779+
):
717780
raise CannotSplit(
718781
"The current optional pair of parentheses is bound to fail to"
719782
" satisfy the splitting algorithm because the head or the tail"
720783
" contains multiline strings which by definition never fit one"
721784
" line."
722785
) from e
723786

724-
ensure_visible(opening_bracket)
725-
ensure_visible(closing_bracket)
726-
for result in (head, body, tail):
787+
ensure_visible(rhs.opening_bracket)
788+
ensure_visible(rhs.closing_bracket)
789+
for result in (rhs.head, rhs.body, rhs.tail):
727790
if result:
728791
yield result
729792

730793

794+
def _prefer_split_rhs_oop(rhs_oop: _RHSResult, line_length: int) -> bool:
795+
"""
796+
Returns whether we should prefer the result from a split omitting optional parens.
797+
"""
798+
has_closing_bracket_after_assign = False
799+
for leaf in reversed(rhs_oop.head.leaves):
800+
if leaf.type == token.EQUAL:
801+
break
802+
if leaf.type in CLOSING_BRACKETS:
803+
has_closing_bracket_after_assign = True
804+
break
805+
return (
806+
# contains matching brackets after the `=` (done by checking there is a
807+
# closing bracket)
808+
has_closing_bracket_after_assign
809+
or (
810+
# the split is actually from inside the optional parens (done by checking
811+
# the first line still contains the `=`)
812+
any(leaf.type == token.EQUAL for leaf in rhs_oop.head.leaves)
813+
# the first line is short enough
814+
and is_line_short_enough(rhs_oop.head, line_length=line_length)
815+
)
816+
# contains unsplittable type ignore
817+
or rhs_oop.head.contains_unsplittable_type_ignore()
818+
or rhs_oop.body.contains_unsplittable_type_ignore()
819+
or rhs_oop.tail.contains_unsplittable_type_ignore()
820+
)
821+
822+
731823
def bracket_split_succeeded_or_raise(head: Line, body: Line, tail: Line) -> None:
732824
"""Raise :exc:`CannotSplit` if the last left- or right-hand split failed.
733825

src/black/mode.py

+1
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ class Preview(Enum):
155155
long_docstring_quotes_on_newline = auto()
156156
normalize_docstring_quotes_and_prefixes_properly = auto()
157157
one_element_subscript = auto()
158+
prefer_splitting_right_hand_side_of_assignments = auto()
158159
remove_block_trailing_newline = auto()
159160
remove_redundant_parens = auto()
160161
# NOTE: string_processing requires wrap_long_dict_values_in_parens

tests/data/preview/long_strings__regression.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -983,9 +983,9 @@ def xxxxxxx_xxxxxx(xxxx):
983983
)
984984

985985

986-
value.__dict__[
987-
key
988-
] = "test" # set some Thrift field to non-None in the struct aa bb cc dd ee
986+
value.__dict__[key] = (
987+
"test" # set some Thrift field to non-None in the struct aa bb cc dd ee
988+
)
989989

990990
RE_ONE_BACKSLASH = {
991991
"asdf_hjkl_jkl": re.compile(
+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
first_item, second_item = (
2+
some_looooooooong_module.some_looooooooooooooong_function_name(
3+
first_argument, second_argument, third_argument
4+
)
5+
)
6+
7+
some_dict["with_a_long_key"] = (
8+
some_looooooooong_module.some_looooooooooooooong_function_name(
9+
first_argument, second_argument, third_argument
10+
)
11+
)
12+
13+
# Make sure it works when the RHS only has one pair of (optional) parens.
14+
first_item, second_item = (
15+
some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name
16+
)
17+
18+
some_dict["with_a_long_key"] = (
19+
some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name
20+
)
21+
22+
# Make sure chaining assignments work.
23+
first_item, second_item, third_item, forth_item = m["everything"] = (
24+
some_looooooooong_module.some_looooooooooooooong_function_name(
25+
first_argument, second_argument, third_argument
26+
)
27+
)
28+
29+
# Make sure when the RHS's first split at the non-optional paren fits,
30+
# we split there instead of the outer RHS optional paren.
31+
first_item, second_item = some_looooooooong_module.some_loooooog_function_name(
32+
first_argument, second_argument, third_argument
33+
)
34+
35+
(
36+
first_item,
37+
second_item,
38+
third_item,
39+
forth_item,
40+
fifth_item,
41+
last_item_very_loooooong,
42+
) = some_looooooooong_module.some_looooooooooooooong_function_name(
43+
first_argument, second_argument, third_argument
44+
)
45+
46+
(
47+
first_item,
48+
second_item,
49+
third_item,
50+
forth_item,
51+
fifth_item,
52+
last_item_very_loooooong,
53+
) = everyting = some_loooooog_function_name(
54+
first_argument, second_argument, third_argument
55+
)
56+
57+
58+
# Make sure unsplittable type ignore won't be moved.
59+
some_kind_of_table[some_key] = util.some_function( # type: ignore # noqa: E501
60+
some_arg
61+
).intersection(pk_cols)
62+
63+
some_kind_of_table[
64+
some_key
65+
] = lambda obj: obj.some_long_named_method() # type: ignore # noqa: E501
66+
67+
some_kind_of_table[
68+
some_key # type: ignore # noqa: E501
69+
] = lambda obj: obj.some_long_named_method()
70+
71+
72+
# Make when when the left side of assignement plus the opening paren "... = (" is
73+
# exactly line length limit + 1, it won't be split like that.
74+
xxxxxxxxx_yyy_zzzzzzzz[
75+
xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)
76+
] = 1
77+
78+
79+
# Right side of assignment contains un-nested pairs of inner parens.
80+
some_kind_of_instance.some_kind_of_map[a_key] = (
81+
isinstance(some_var, SomeClass)
82+
and table.something_and_something != table.something_else
83+
) or (
84+
isinstance(some_other_var, BaseClass) and table.something != table.some_other_thing
85+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Test cases separate from `prefer_rhs_split.py` that contains unformatted source.
2+
3+
# Left hand side fits in a single line but will still be exploded by the
4+
# magic trailing comma.
5+
first_value, (m1, m2,), third_value = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv(
6+
arg1,
7+
arg2,
8+
)
9+
10+
# Make when when the left side of assignement plus the opening paren "... = (" is
11+
# exactly line length limit + 1, it won't be split like that.
12+
xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)] = 1
13+
14+
15+
# output
16+
17+
18+
# Test cases separate from `prefer_rhs_split.py` that contains unformatted source.
19+
20+
# Left hand side fits in a single line but will still be exploded by the
21+
# magic trailing comma.
22+
(
23+
first_value,
24+
(
25+
m1,
26+
m2,
27+
),
28+
third_value,
29+
) = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv(
30+
arg1,
31+
arg2,
32+
)
33+
34+
# Make when when the left side of assignement plus the opening paren "... = (" is
35+
# exactly line length limit + 1, it won't be split like that.
36+
xxxxxxxxx_yyy_zzzzzzzz[
37+
xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)
38+
] = 1

0 commit comments

Comments
 (0)