Don't explode trailers that fit in a single line

This commit is contained in:
Łukasz Langa 2018-05-14 12:05:39 -07:00
parent a677713ebf
commit 3eea3aad86
9 changed files with 157 additions and 32 deletions

View File

@ -566,6 +566,9 @@ More details can be found in [CONTRIBUTING](CONTRIBUTING.md).
on Python 3.6+ only code and Python 2.7+ code with the `unicode_literals` on Python 3.6+ only code and Python 2.7+ code with the `unicode_literals`
future import (#188, #198, #199) future import (#188, #198, #199)
* fixed trailers (content with brackets) being unnecessarily exploded
into their own lines after a dedented closing bracket
* fixed an invalid trailing comma sometimes left in imports (#185) * fixed an invalid trailing comma sometimes left in imports (#185)
* fixed non-deterministic formatting when multiple pairs of removable parentheses * fixed non-deterministic formatting when multiple pairs of removable parentheses

111
black.py
View File

@ -24,6 +24,7 @@
List, List,
Optional, Optional,
Pattern, Pattern,
Sequence,
Set, Set,
Tuple, Tuple,
Type, Type,
@ -41,6 +42,7 @@
from blib2to3.pgen2 import driver, token from blib2to3.pgen2 import driver, token
from blib2to3.pgen2.parse import ParseError from blib2to3.pgen2.parse import ParseError
__version__ = "18.4a6" __version__ = "18.4a6"
DEFAULT_LINE_LENGTH = 88 DEFAULT_LINE_LENGTH = 88
@ -1828,11 +1830,7 @@ def split_line(
return return
line_str = str(line).strip("\n") line_str = str(line).strip("\n")
if ( if is_line_short_enough(line, line_length=line_length, line_str=line_str):
len(line_str) <= line_length
and "\n" not in line_str # multiline strings
and not line.contains_standalone_comments()
):
yield line yield line
return return
@ -1841,10 +1839,22 @@ def split_line(
split_funcs = [left_hand_split] split_funcs = [left_hand_split]
elif line.is_import: elif line.is_import:
split_funcs = [explode_split] split_funcs = [explode_split]
elif line.inside_brackets:
split_funcs = [delimiter_split, standalone_comment_split, right_hand_split]
else: else:
split_funcs = [right_hand_split]
def rhs(line: Line, py36: bool = False) -> Iterator[Line]:
for omit in generate_trailers_to_omit(line, line_length):
lines = list(right_hand_split(line, py36, omit=omit))
if is_line_short_enough(lines[0], line_length=line_length):
yield from lines
return
# All splits failed, best effort split with no omits.
yield from right_hand_split(line, py36)
if line.inside_brackets:
split_funcs = [delimiter_split, standalone_comment_split, rhs]
else:
split_funcs = [rhs]
for split_func in split_funcs: for split_func in split_funcs:
# We are accumulating lines in `result` because we might want to abort # We are accumulating lines in `result` because we might want to abort
# mission and return the original line in the end, or attempt a different # mission and return the original line in the end, or attempt a different
@ -1917,6 +1927,8 @@ def right_hand_split(
"""Split line into many lines, starting with the last matching bracket pair. """Split line into many lines, starting with the last matching bracket pair.
If the split was by optional parentheses, attempt splitting without them, too. If the split was by optional parentheses, attempt splitting without them, too.
`omit` is a collection of closing bracket IDs that shouldn't be considered for
this split.
""" """
head = Line(depth=line.depth) head = Line(depth=line.depth)
body = Line(depth=line.depth + 1, inside_brackets=True) body = Line(depth=line.depth + 1, inside_brackets=True)
@ -2446,6 +2458,67 @@ def is_python36(node: Node) -> bool:
return False return False
def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[LeafID]]:
"""Generate sets of closing bracket IDs that should be omitted in a RHS.
Brackets can be omitted if the entire trailer up to and including
a preceding closing bracket fits in one line.
Yielded sets are cumulative (contain results of previous yields, too). First
set is empty.
"""
omit: Set[LeafID] = set()
yield omit
length = 4 * line.depth
opening_bracket = None
closing_bracket = None
optional_brackets: Set[LeafID] = set()
inner_brackets: Set[LeafID] = set()
for index, leaf in enumerate_reversed(line.leaves):
length += len(leaf.prefix) + len(leaf.value)
if length > line_length:
break
comment: Optional[Leaf]
for comment in line.comments_after(leaf, index):
if "\n" in comment.prefix:
break # Oops, standalone comment!
length += len(comment.value)
else:
comment = None
if comment is not None:
break # There was a standalone comment, we can't continue.
optional_brackets.discard(id(leaf))
if opening_bracket:
if leaf is opening_bracket:
opening_bracket = None
elif leaf.type in CLOSING_BRACKETS:
inner_brackets.add(id(leaf))
elif leaf.type in CLOSING_BRACKETS:
if not leaf.value:
optional_brackets.add(id(opening_bracket))
continue
if index > 0 and line.leaves[index - 1].type in OPENING_BRACKETS:
# Empty brackets would fail a split so treat them as "inner"
# brackets (e.g. only add them to the `omit` set if another
# pair of brackets was good enough.
inner_brackets.add(id(leaf))
continue
opening_bracket = leaf.opening_bracket
if closing_bracket:
omit.add(id(closing_bracket))
omit.update(inner_brackets)
inner_brackets.clear()
yield omit
closing_bracket = leaf
def get_future_imports(node: Node) -> Set[str]: def get_future_imports(node: Node) -> Set[str]:
"""Return a set of __future__ imports in the file.""" """Return a set of __future__ imports in the file."""
imports = set() imports = set()
@ -2723,6 +2796,28 @@ def sub_twice(regex: Pattern[str], replacement: str, original: str) -> str:
return regex.sub(replacement, regex.sub(replacement, original)) return regex.sub(replacement, regex.sub(replacement, original))
def enumerate_reversed(sequence: Sequence[T]) -> Iterator[Tuple[Index, T]]:
"""Like `reversed(enumerate(sequence))` if that were possible."""
index = len(sequence) - 1
for element in reversed(sequence):
yield (index, element)
index -= 1
def is_line_short_enough(line: Line, *, line_length: int, line_str: str = "") -> bool:
"""Return True if `line` is no longer than `line_length`.
Uses the provided `line_str` rendering, if any, otherwise computes a new one.
"""
if not line_str:
line_str = str(line).strip("\n")
return (
len(line_str) <= line_length
and "\n" not in line_str # multiline strings
and not line.contains_standalone_comments()
)
CACHE_DIR = Path(user_cache_dir("black", version=__version__)) CACHE_DIR = Path(user_cache_dir("black", version=__version__))

View File

@ -20,6 +20,8 @@ Assertions and checks
.. autofunction:: black.is_import .. autofunction:: black.is_import
.. autofunction:: black.is_line_short_enough
.. autofunction:: black.is_one_tuple .. autofunction:: black.is_one_tuple
.. autofunction:: black.is_python36 .. autofunction:: black.is_python36
@ -94,6 +96,8 @@ Utilities
.. autofunction:: black.ensure_visible .. autofunction:: black.ensure_visible
.. autofunction:: black.enumerate_reversed
.. autofunction:: black.generate_comments .. autofunction:: black.generate_comments
.. autofunction:: black.make_comment .. autofunction:: black.make_comment

View File

@ -63,14 +63,26 @@ def foo(list_a, list_b):
results = ( results = (
User.query.filter(User.foo == "bar").filter( # Because foo. User.query.filter(User.foo == "bar").filter( # Because foo.
db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b))
).filter( ).filter(User.xyz.is_(None))
User.xyz.is_(None)
)
# Another comment about the filtering on is_quux goes here. # Another comment about the filtering on is_quux goes here.
.filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))).order_by( .filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))).order_by(
User.created_at.desc() User.created_at.desc()
).with_for_update( ).with_for_update(key_share=True).all()
key_share=True
).all()
) )
return results return results
def foo2(list_a, list_b):
# Standalone comment reasonably placed.
return User.query.filter(User.foo == "bar").filter(
db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b))
).filter(User.xyz.is_(None))
def foo3(list_a, list_b):
return (
# Standlone comment but weirdly placed.
User.query.filter(User.foo == "bar").filter(
db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b))
).filter(User.xyz.is_(None))
)

View File

@ -32,3 +32,14 @@ def test(self) -> None:
# Another # Another
): ):
print(i) print(i)
def omitting_trailers() -> None:
get_collection(
hey_this_is_a_very_long_call, it_has_funny_attributes, really=True
)[OneLevelIndex]
get_collection(
hey_this_is_a_very_long_call, it_has_funny_attributes, really=True
)[OneLevelIndex][TwoLevelIndex][ThreeLevelIndex][FourLevelIndex]
d[0][1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][
22
]

View File

@ -11,7 +11,7 @@
True True
False False
1 1
@@ -29,62 +29,84 @@ @@ -29,62 +29,82 @@
~great ~great
+value +value
-1 -1
@ -30,9 +30,7 @@
-foo = (lambda port_id, ignore_missing: {"port1": port1_resource, "port2": port2_resource}[port_id]) -foo = (lambda port_id, ignore_missing: {"port1": port1_resource, "port2": port2_resource}[port_id])
+foo = lambda port_id, ignore_missing: { +foo = lambda port_id, ignore_missing: {
+ "port1": port1_resource, "port2": port2_resource + "port1": port1_resource, "port2": port2_resource
+}[ +}[port_id]
+ port_id
+]
1 if True else 2 1 if True else 2
str or None if True else str or bytes or None str or None if True else str or bytes or None
(str or None) if True else (str or bytes or None) (str or None) if True else (str or bytes or None)
@ -117,7 +115,7 @@
call(**self.screen_kwargs) call(**self.screen_kwargs)
call(b, **self.screen_kwargs) call(b, **self.screen_kwargs)
lukasz.langa.pl lukasz.langa.pl
@@ -93,11 +115,11 @@ @@ -93,11 +113,11 @@
1.0 .real 1.0 .real
....__class__ ....__class__
list[str] list[str]
@ -130,7 +128,7 @@
] ]
slice[0] slice[0]
slice[0:1] slice[0:1]
@@ -124,103 +146,140 @@ @@ -124,103 +144,138 @@
numpy[-(c + 1) :, d] numpy[-(c + 1) :, d]
numpy[:, l[-2]] numpy[:, l[-2]]
numpy[:, ::-1] numpy[:, ::-1]
@ -177,9 +175,7 @@
+) +)
+result = session.query(models.Customer.id).filter( +result = session.query(models.Customer.id).filter(
+ models.Customer.account_id == account_id, models.Customer.email == email_address + models.Customer.account_id == account_id, models.Customer.email == email_address
+).order_by( +).order_by(models.Customer.id.asc()).all()
+ models.Customer.id.asc()
+).all()
Ø = set() Ø = set()
authors.łukasz.say_thanks() authors.łukasz.say_thanks()
mapping = { mapping = {

View File

@ -270,9 +270,7 @@ async def f():
manylambdas = lambda x=lambda y=lambda z=1: z: y(): x() manylambdas = lambda x=lambda y=lambda z=1: z: y(): x()
foo = lambda port_id, ignore_missing: { foo = lambda port_id, ignore_missing: {
"port1": port1_resource, "port2": port2_resource "port1": port1_resource, "port2": port2_resource
}[ }[port_id]
port_id
]
1 if True else 2 1 if True else 2
str or None if True else str or bytes or None str or None if True else str or bytes or None
(str or None) if True else (str or bytes or None) (str or None) if True else (str or bytes or None)
@ -411,9 +409,7 @@ async def f():
) )
result = session.query(models.Customer.id).filter( result = session.query(models.Customer.id).filter(
models.Customer.account_id == account_id, models.Customer.email == email_address models.Customer.account_id == account_id, models.Customer.email == email_address
).order_by( ).order_by(models.Customer.id.asc()).all()
models.Customer.id.asc()
).all()
Ø = set() Ø = set()
authors.łukasz.say_thanks() authors.łukasz.say_thanks()
mapping = { mapping = {

View File

@ -169,9 +169,7 @@ def spaces2(result=_core.Value(None)):
def example(session): def example(session):
result = session.query(models.Customer.id).filter( result = session.query(models.Customer.id).filter(
models.Customer.account_id == account_id, models.Customer.email == email_address models.Customer.account_id == account_id, models.Customer.email == email_address
).order_by( ).order_by(models.Customer.id.asc()).all()
models.Customer.id.asc()
).all()
def long_lines(): def long_lines():

View File

@ -2,6 +2,11 @@ def f(
a, a,
**kwargs, **kwargs,
) -> A: ) -> A:
with cache_dir():
if something:
result = (
CliRunner().invoke(black.main, [str(src1), str(src2), "--diff", "--check"])
)
return A( return A(
very_long_argument_name1=very_long_value_for_the_argument, very_long_argument_name1=very_long_value_for_the_argument,
very_long_argument_name2=very_long_value_for_the_argument, very_long_argument_name2=very_long_value_for_the_argument,
@ -11,6 +16,11 @@ def f(
# output # output
def f(a, **kwargs) -> A: def f(a, **kwargs) -> A:
with cache_dir():
if something:
result = CliRunner().invoke(
black.main, [str(src1), str(src2), "--diff", "--check"]
)
return A( return A(
very_long_argument_name1=very_long_value_for_the_argument, very_long_argument_name1=very_long_value_for_the_argument,
very_long_argument_name2=very_long_value_for_the_argument, very_long_argument_name2=very_long_value_for_the_argument,