Prefer splitting right hand side of assignment statements. (#3368)
This commit is contained in:
parent
658c8d8d96
commit
aafc21aa77
@ -73,6 +73,8 @@
|
|||||||
present) or as a single newline character (if a newline is present) (#3348)
|
present) or as a single newline character (if a newline is present) (#3348)
|
||||||
- Implicitly concatenated strings used as function args are now wrapped inside
|
- Implicitly concatenated strings used as function args are now wrapped inside
|
||||||
parentheses (#3307)
|
parentheses (#3307)
|
||||||
|
- For assignment statements, prefer splitting the right hand side if the left hand side
|
||||||
|
fits on a single line (#3368)
|
||||||
- Correctly handle trailing commas that are inside a line's leading non-nested parens
|
- Correctly handle trailing commas that are inside a line's leading non-nested parens
|
||||||
(#3370)
|
(#3370)
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
Generating lines of code.
|
Generating lines of code.
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from functools import partial, wraps
|
from functools import partial, wraps
|
||||||
from typing import Collection, Iterator, List, Optional, Set, Union, cast
|
from typing import Collection, Iterator, List, Optional, Set, Union, cast
|
||||||
@ -24,6 +25,7 @@
|
|||||||
from black.mode import Feature, Mode, Preview
|
from black.mode import Feature, Mode, Preview
|
||||||
from black.nodes import (
|
from black.nodes import (
|
||||||
ASSIGNMENTS,
|
ASSIGNMENTS,
|
||||||
|
BRACKETS,
|
||||||
CLOSING_BRACKETS,
|
CLOSING_BRACKETS,
|
||||||
OPENING_BRACKETS,
|
OPENING_BRACKETS,
|
||||||
RARROW,
|
RARROW,
|
||||||
@ -634,6 +636,17 @@ def left_hand_split(line: Line, _features: Collection[Feature] = ()) -> Iterator
|
|||||||
yield result
|
yield result
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _RHSResult:
|
||||||
|
"""Intermediate split result from a right hand split."""
|
||||||
|
|
||||||
|
head: Line
|
||||||
|
body: Line
|
||||||
|
tail: Line
|
||||||
|
opening_bracket: Leaf
|
||||||
|
closing_bracket: Leaf
|
||||||
|
|
||||||
|
|
||||||
def right_hand_split(
|
def right_hand_split(
|
||||||
line: Line,
|
line: Line,
|
||||||
line_length: int,
|
line_length: int,
|
||||||
@ -648,6 +661,22 @@ def right_hand_split(
|
|||||||
|
|
||||||
Note: running this function modifies `bracket_depth` on the leaves of `line`.
|
Note: running this function modifies `bracket_depth` on the leaves of `line`.
|
||||||
"""
|
"""
|
||||||
|
rhs_result = _first_right_hand_split(line, omit=omit)
|
||||||
|
yield from _maybe_split_omitting_optional_parens(
|
||||||
|
rhs_result, line, line_length, features=features, omit=omit
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _first_right_hand_split(
|
||||||
|
line: Line,
|
||||||
|
omit: Collection[LeafID] = (),
|
||||||
|
) -> _RHSResult:
|
||||||
|
"""Split the line into head, body, tail starting with the last bracket pair.
|
||||||
|
|
||||||
|
Note: this function should not have side effects. It's relied upon by
|
||||||
|
_maybe_split_omitting_optional_parens to get an opinion whether to prefer
|
||||||
|
splitting on the right side of an assignment statement.
|
||||||
|
"""
|
||||||
tail_leaves: List[Leaf] = []
|
tail_leaves: List[Leaf] = []
|
||||||
body_leaves: List[Leaf] = []
|
body_leaves: List[Leaf] = []
|
||||||
head_leaves: List[Leaf] = []
|
head_leaves: List[Leaf] = []
|
||||||
@ -683,37 +712,71 @@ def right_hand_split(
|
|||||||
tail_leaves, line, opening_bracket, component=_BracketSplitComponent.tail
|
tail_leaves, line, opening_bracket, component=_BracketSplitComponent.tail
|
||||||
)
|
)
|
||||||
bracket_split_succeeded_or_raise(head, body, tail)
|
bracket_split_succeeded_or_raise(head, body, tail)
|
||||||
|
return _RHSResult(head, body, tail, opening_bracket, closing_bracket)
|
||||||
|
|
||||||
|
|
||||||
|
def _maybe_split_omitting_optional_parens(
|
||||||
|
rhs: _RHSResult,
|
||||||
|
line: Line,
|
||||||
|
line_length: int,
|
||||||
|
features: Collection[Feature] = (),
|
||||||
|
omit: Collection[LeafID] = (),
|
||||||
|
) -> Iterator[Line]:
|
||||||
if (
|
if (
|
||||||
Feature.FORCE_OPTIONAL_PARENTHESES not in features
|
Feature.FORCE_OPTIONAL_PARENTHESES not in features
|
||||||
# the opening bracket is an optional paren
|
# the opening bracket is an optional paren
|
||||||
and opening_bracket.type == token.LPAR
|
and rhs.opening_bracket.type == token.LPAR
|
||||||
and not opening_bracket.value
|
and not rhs.opening_bracket.value
|
||||||
# the closing bracket is an optional paren
|
# the closing bracket is an optional paren
|
||||||
and closing_bracket.type == token.RPAR
|
and rhs.closing_bracket.type == token.RPAR
|
||||||
and not closing_bracket.value
|
and not rhs.closing_bracket.value
|
||||||
# it's not an import (optional parens are the only thing we can split on
|
# it's not an import (optional parens are the only thing we can split on
|
||||||
# in this case; attempting a split without them is a waste of time)
|
# in this case; attempting a split without them is a waste of time)
|
||||||
and not line.is_import
|
and not line.is_import
|
||||||
# there are no standalone comments in the body
|
# there are no standalone comments in the body
|
||||||
and not body.contains_standalone_comments(0)
|
and not rhs.body.contains_standalone_comments(0)
|
||||||
# and we can actually remove the parens
|
# and we can actually remove the parens
|
||||||
and can_omit_invisible_parens(body, line_length)
|
and can_omit_invisible_parens(rhs.body, line_length)
|
||||||
):
|
):
|
||||||
omit = {id(closing_bracket), *omit}
|
omit = {id(rhs.closing_bracket), *omit}
|
||||||
try:
|
try:
|
||||||
yield from right_hand_split(line, line_length, features=features, omit=omit)
|
# The _RHSResult Omitting Optional Parens.
|
||||||
return
|
rhs_oop = _first_right_hand_split(line, omit=omit)
|
||||||
|
if not (
|
||||||
|
Preview.prefer_splitting_right_hand_side_of_assignments in line.mode
|
||||||
|
# the split is right after `=`
|
||||||
|
and len(rhs.head.leaves) >= 2
|
||||||
|
and rhs.head.leaves[-2].type == token.EQUAL
|
||||||
|
# the left side of assignement contains brackets
|
||||||
|
and any(leaf.type in BRACKETS for leaf in rhs.head.leaves[:-1])
|
||||||
|
# the left side of assignment is short enough (the -1 is for the ending
|
||||||
|
# optional paren)
|
||||||
|
and is_line_short_enough(rhs.head, line_length=line_length - 1)
|
||||||
|
# the left side of assignment won't explode further because of magic
|
||||||
|
# trailing comma
|
||||||
|
and rhs.head.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, line_length=line_length)
|
||||||
|
):
|
||||||
|
yield from _maybe_split_omitting_optional_parens(
|
||||||
|
rhs_oop, line, line_length, features=features, omit=omit
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
except CannotSplit as e:
|
except CannotSplit as e:
|
||||||
if not (
|
if not (
|
||||||
can_be_split(body)
|
can_be_split(rhs.body)
|
||||||
or is_line_short_enough(body, line_length=line_length)
|
or is_line_short_enough(rhs.body, line_length=line_length)
|
||||||
):
|
):
|
||||||
raise CannotSplit(
|
raise CannotSplit(
|
||||||
"Splitting failed, body is still too long and can't be split."
|
"Splitting failed, body is still too long and can't be split."
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
elif head.contains_multiline_strings() or tail.contains_multiline_strings():
|
elif (
|
||||||
|
rhs.head.contains_multiline_strings()
|
||||||
|
or rhs.tail.contains_multiline_strings()
|
||||||
|
):
|
||||||
raise CannotSplit(
|
raise CannotSplit(
|
||||||
"The current optional pair of parentheses is bound to fail to"
|
"The current optional pair of parentheses is bound to fail to"
|
||||||
" satisfy the splitting algorithm because the head or the tail"
|
" satisfy the splitting algorithm because the head or the tail"
|
||||||
@ -721,13 +784,42 @@ def right_hand_split(
|
|||||||
" line."
|
" line."
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
ensure_visible(opening_bracket)
|
ensure_visible(rhs.opening_bracket)
|
||||||
ensure_visible(closing_bracket)
|
ensure_visible(rhs.closing_bracket)
|
||||||
for result in (head, body, tail):
|
for result in (rhs.head, rhs.body, rhs.tail):
|
||||||
if result:
|
if result:
|
||||||
yield result
|
yield result
|
||||||
|
|
||||||
|
|
||||||
|
def _prefer_split_rhs_oop(rhs_oop: _RHSResult, line_length: int) -> bool:
|
||||||
|
"""
|
||||||
|
Returns whether we should prefer the result from a split omitting optional parens.
|
||||||
|
"""
|
||||||
|
has_closing_bracket_after_assign = False
|
||||||
|
for leaf in reversed(rhs_oop.head.leaves):
|
||||||
|
if leaf.type == token.EQUAL:
|
||||||
|
break
|
||||||
|
if leaf.type in CLOSING_BRACKETS:
|
||||||
|
has_closing_bracket_after_assign = True
|
||||||
|
break
|
||||||
|
return (
|
||||||
|
# contains matching brackets after the `=` (done by checking there is a
|
||||||
|
# closing bracket)
|
||||||
|
has_closing_bracket_after_assign
|
||||||
|
or (
|
||||||
|
# the split is actually from inside the optional parens (done by checking
|
||||||
|
# the first line still contains the `=`)
|
||||||
|
any(leaf.type == token.EQUAL for leaf in rhs_oop.head.leaves)
|
||||||
|
# the first line is short enough
|
||||||
|
and is_line_short_enough(rhs_oop.head, line_length=line_length)
|
||||||
|
)
|
||||||
|
# contains unsplittable type ignore
|
||||||
|
or rhs_oop.head.contains_unsplittable_type_ignore()
|
||||||
|
or rhs_oop.body.contains_unsplittable_type_ignore()
|
||||||
|
or rhs_oop.tail.contains_unsplittable_type_ignore()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def bracket_split_succeeded_or_raise(head: Line, body: Line, tail: Line) -> None:
|
def bracket_split_succeeded_or_raise(head: Line, body: Line, tail: Line) -> None:
|
||||||
"""Raise :exc:`CannotSplit` if the last left- or right-hand split failed.
|
"""Raise :exc:`CannotSplit` if the last left- or right-hand split failed.
|
||||||
|
|
||||||
|
@ -155,6 +155,7 @@ class Preview(Enum):
|
|||||||
long_docstring_quotes_on_newline = auto()
|
long_docstring_quotes_on_newline = auto()
|
||||||
normalize_docstring_quotes_and_prefixes_properly = auto()
|
normalize_docstring_quotes_and_prefixes_properly = auto()
|
||||||
one_element_subscript = auto()
|
one_element_subscript = auto()
|
||||||
|
prefer_splitting_right_hand_side_of_assignments = auto()
|
||||||
remove_block_trailing_newline = auto()
|
remove_block_trailing_newline = auto()
|
||||||
remove_redundant_parens = auto()
|
remove_redundant_parens = auto()
|
||||||
# NOTE: string_processing requires wrap_long_dict_values_in_parens
|
# NOTE: string_processing requires wrap_long_dict_values_in_parens
|
||||||
|
@ -983,9 +983,9 @@ def xxxxxxx_xxxxxx(xxxx):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
value.__dict__[
|
value.__dict__[key] = (
|
||||||
key
|
"test" # set some Thrift field to non-None in the struct aa bb cc dd ee
|
||||||
] = "test" # set some Thrift field to non-None in the struct aa bb cc dd ee
|
)
|
||||||
|
|
||||||
RE_ONE_BACKSLASH = {
|
RE_ONE_BACKSLASH = {
|
||||||
"asdf_hjkl_jkl": re.compile(
|
"asdf_hjkl_jkl": re.compile(
|
||||||
|
85
tests/data/preview/prefer_rhs_split.py
Normal file
85
tests/data/preview/prefer_rhs_split.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
first_item, second_item = (
|
||||||
|
some_looooooooong_module.some_looooooooooooooong_function_name(
|
||||||
|
first_argument, second_argument, third_argument
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
some_dict["with_a_long_key"] = (
|
||||||
|
some_looooooooong_module.some_looooooooooooooong_function_name(
|
||||||
|
first_argument, second_argument, third_argument
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure it works when the RHS only has one pair of (optional) parens.
|
||||||
|
first_item, second_item = (
|
||||||
|
some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name
|
||||||
|
)
|
||||||
|
|
||||||
|
some_dict["with_a_long_key"] = (
|
||||||
|
some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure chaining assignments work.
|
||||||
|
first_item, second_item, third_item, forth_item = m["everything"] = (
|
||||||
|
some_looooooooong_module.some_looooooooooooooong_function_name(
|
||||||
|
first_argument, second_argument, third_argument
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure when the RHS's first split at the non-optional paren fits,
|
||||||
|
# we split there instead of the outer RHS optional paren.
|
||||||
|
first_item, second_item = some_looooooooong_module.some_loooooog_function_name(
|
||||||
|
first_argument, second_argument, third_argument
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
|
first_item,
|
||||||
|
second_item,
|
||||||
|
third_item,
|
||||||
|
forth_item,
|
||||||
|
fifth_item,
|
||||||
|
last_item_very_loooooong,
|
||||||
|
) = some_looooooooong_module.some_looooooooooooooong_function_name(
|
||||||
|
first_argument, second_argument, third_argument
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
|
first_item,
|
||||||
|
second_item,
|
||||||
|
third_item,
|
||||||
|
forth_item,
|
||||||
|
fifth_item,
|
||||||
|
last_item_very_loooooong,
|
||||||
|
) = everyting = some_loooooog_function_name(
|
||||||
|
first_argument, second_argument, third_argument
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Make sure unsplittable type ignore won't be moved.
|
||||||
|
some_kind_of_table[some_key] = util.some_function( # type: ignore # noqa: E501
|
||||||
|
some_arg
|
||||||
|
).intersection(pk_cols)
|
||||||
|
|
||||||
|
some_kind_of_table[
|
||||||
|
some_key
|
||||||
|
] = lambda obj: obj.some_long_named_method() # type: ignore # noqa: E501
|
||||||
|
|
||||||
|
some_kind_of_table[
|
||||||
|
some_key # type: ignore # noqa: E501
|
||||||
|
] = lambda obj: obj.some_long_named_method()
|
||||||
|
|
||||||
|
|
||||||
|
# Make when when the left side of assignement plus the opening paren "... = (" is
|
||||||
|
# exactly line length limit + 1, it won't be split like that.
|
||||||
|
xxxxxxxxx_yyy_zzzzzzzz[
|
||||||
|
xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)
|
||||||
|
] = 1
|
||||||
|
|
||||||
|
|
||||||
|
# Right side of assignment contains un-nested pairs of inner parens.
|
||||||
|
some_kind_of_instance.some_kind_of_map[a_key] = (
|
||||||
|
isinstance(some_var, SomeClass)
|
||||||
|
and table.something_and_something != table.something_else
|
||||||
|
) or (
|
||||||
|
isinstance(some_other_var, BaseClass) and table.something != table.some_other_thing
|
||||||
|
)
|
38
tests/data/preview/prefer_rhs_split_reformatted.py
Normal file
38
tests/data/preview/prefer_rhs_split_reformatted.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Test cases separate from `prefer_rhs_split.py` that contains unformatted source.
|
||||||
|
|
||||||
|
# Left hand side fits in a single line but will still be exploded by the
|
||||||
|
# magic trailing comma.
|
||||||
|
first_value, (m1, m2,), third_value = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv(
|
||||||
|
arg1,
|
||||||
|
arg2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make when when the left side of assignement plus the opening paren "... = (" is
|
||||||
|
# exactly line length limit + 1, it won't be split like that.
|
||||||
|
xxxxxxxxx_yyy_zzzzzzzz[xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)] = 1
|
||||||
|
|
||||||
|
|
||||||
|
# output
|
||||||
|
|
||||||
|
|
||||||
|
# Test cases separate from `prefer_rhs_split.py` that contains unformatted source.
|
||||||
|
|
||||||
|
# Left hand side fits in a single line but will still be exploded by the
|
||||||
|
# magic trailing comma.
|
||||||
|
(
|
||||||
|
first_value,
|
||||||
|
(
|
||||||
|
m1,
|
||||||
|
m2,
|
||||||
|
),
|
||||||
|
third_value,
|
||||||
|
) = xxxxxx_yyyyyy_zzzzzz_wwwwww_uuuuuuu_vvvvvvvvvvv(
|
||||||
|
arg1,
|
||||||
|
arg2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make when when the left side of assignement plus the opening paren "... = (" is
|
||||||
|
# exactly line length limit + 1, it won't be split like that.
|
||||||
|
xxxxxxxxx_yyy_zzzzzzzz[
|
||||||
|
xx.xxxxxx(x_yyy_zzzzzz.xxxxx[0]), x_yyy_zzzzzz.xxxxxx(xxxx=1)
|
||||||
|
] = 1
|
Loading…
Reference in New Issue
Block a user