Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tweak collection literals to explode with trailing comma #826

Merged
merged 4 commits into from
Oct 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 82 additions & 47 deletions black.py
Original file line number Diff line number Diff line change
Expand Up @@ -1240,6 +1240,61 @@ def is_stub_class(self) -> bool:
Leaf(token.DOT, ".") for _ in range(3)
]

@property
def is_collection_with_optional_trailing_comma(self) -> bool:
"""Is this line a collection literal with a trailing comma that's optional?

Note that the trailing comma in a 1-tuple is not optional.
"""
if not self.leaves or len(self.leaves) < 4:
return False
# Look for and address a trailing colon.
if self.leaves[-1].type == token.COLON:
closer = self.leaves[-2]
close_index = -2
else:
closer = self.leaves[-1]
close_index = -1
if closer.type not in CLOSING_BRACKETS or self.inside_brackets:
return False
if closer.type == token.RPAR:
# Tuples require an extra check, because if there's only
# one element in the tuple removing the comma unmakes the
# tuple.
#
# We also check for parens before looking for the trailing
# comma because in some cases (eg assigning a dict
# literal) the literal gets wrapped in temporary parens
# during parsing. This case is covered by the
# collections.py test data.
opener = closer.opening_bracket
for _open_index, leaf in enumerate(self.leaves):
if leaf is opener:
break
else:
# Couldn't find the matching opening paren, play it safe.
return False
commas = 0
comma_depth = self.leaves[close_index - 1].bracket_depth
for leaf in self.leaves[_open_index + 1 : close_index]:
if leaf.bracket_depth == comma_depth and leaf.type == token.COMMA:
commas += 1
if commas > 1:
# We haven't looked yet for the trailing comma because
# we might also have caught noop parens.
return self.leaves[close_index - 1].type == token.COMMA
elif commas == 1:
return False # it's either a one-tuple or didn't have a trailing comma
if self.leaves[close_index - 1].type in CLOSING_BRACKETS:
close_index -= 1
closer = self.leaves[close_index]
if closer.type == token.RPAR:
# TODO: this is a gut feeling. Will we ever see this?
return False
if self.leaves[close_index - 1].type != token.COMMA:
return False
return True

@property
def is_def(self) -> bool:
"""Is this a function definition? (Also returns True for async defs.)"""
Expand Down Expand Up @@ -1356,60 +1411,39 @@ def contains_multiline_strings(self) -> bool:

def maybe_remove_trailing_comma(self, closing: Leaf) -> bool:
"""Remove trailing comma if there is one and it's safe."""
if not (self.leaves and self.leaves[-1].type == token.COMMA):
return False
# We remove trailing commas only in the case of importing a
# single name from a module.
if not (
self.leaves
and self.is_import
and len(self.leaves) > 4
and self.leaves[-1].type == token.COMMA
and closing.type in CLOSING_BRACKETS
and self.leaves[-4].type == token.NAME
and (
# regular `from foo import bar,`
self.leaves[-4].value == "import"
# `from foo import (bar as baz,)
or (
len(self.leaves) > 6
and self.leaves[-6].value == "import"
and self.leaves[-3].value == "as"
)
# `from foo import bar as baz,`
or (
len(self.leaves) > 5
and self.leaves[-5].value == "import"
and self.leaves[-3].value == "as"
)
)
and closing.type == token.RPAR
):
return False

if closing.type == token.RBRACE:
self.remove_trailing_comma()
return True

if closing.type == token.RSQB:
comma = self.leaves[-1]
if comma.parent and comma.parent.type == syms.listmaker:
self.remove_trailing_comma()
return True

# For parens let's check if it's safe to remove the comma.
# Imports are always safe.
if self.is_import:
self.remove_trailing_comma()
return True

# Otherwise, if the trailing one is the only one, we might mistakenly
# change a tuple into a different type by removing the comma.
depth = closing.bracket_depth + 1
commas = 0
opening = closing.opening_bracket
for _opening_index, leaf in enumerate(self.leaves):
if leaf is opening:
break

else:
return False

for leaf in self.leaves[_opening_index + 1 :]:
if leaf is closing:
break

bracket_depth = leaf.bracket_depth
if bracket_depth == depth and leaf.type == token.COMMA:
commas += 1
if leaf.parent and leaf.parent.type in {
syms.arglist,
syms.typedargslist,
}:
commas += 1
break

if commas > 1:
self.remove_trailing_comma()
return True

return False
self.remove_trailing_comma()
return True

def append_comment(self, comment: Leaf) -> bool:
"""Add an inline or standalone comment to the line."""
Expand Down Expand Up @@ -2354,6 +2388,7 @@ def split_line(
if (
not line.contains_uncollapsable_type_comments()
and not line.should_explode
and not line.is_collection_with_optional_trailing_comma
and (
is_line_short_enough(line, line_length=line_length, line_str=line_str)
or line.contains_unsplittable_type_ignore()
Expand Down
160 changes: 160 additions & 0 deletions tests/data/collections.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import core, time, a

from . import A, B, C

# unwraps
from foo import (
bar,
)

# stays wrapped
from foo import (
baz,
qux,
)

# as doesn't get confusing when unwrapped
from foo import (
xyzzy as magic,
)

a = {1,2,3,}
b = {
1,2,
3}
c = {
1,
2,
3,
}
x = 1,
y = narf(),
nested = {(1,2,3),(4,5,6),}
nested_no_trailing_comma = {(1,2,3),(4,5,6)}
nested_long_lines = ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "cccccccccccccccccccccccccccccccccccccccc", (1, 2, 3), "dddddddddddddddddddddddddddddddddddddddd"]
{"oneple": (1,),}
{"oneple": (1,)}
['ls', 'lsoneple/%s' % (foo,)]
x = {"oneple": (1,)}
y = {"oneple": (1,),}
assert False, ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa wraps %s" % bar)

# looping over a 1-tuple should also not get wrapped
for x in (1,):
pass
for (x,) in (1,), (2,), (3,):
pass

[1, 2, 3,]

division_result_tuple = (6/2,)
print("foo %r", (foo.bar,))

if True:
IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = (
Config.IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING
| {pylons.controllers.WSGIController}
)

if True:
ec2client.get_waiter('instance_stopped').wait(
InstanceIds=[instance.id],
WaiterConfig={
'Delay': 5,
})
ec2client.get_waiter("instance_stopped").wait(
InstanceIds=[instance.id],
WaiterConfig={"Delay": 5,},
)
ec2client.get_waiter("instance_stopped").wait(
InstanceIds=[instance.id], WaiterConfig={"Delay": 5,},
)

# output


import core, time, a

from . import A, B, C

# unwraps
from foo import bar

# stays wrapped
from foo import (
baz,
qux,
)

# as doesn't get confusing when unwrapped
from foo import xyzzy as magic

a = {
1,
2,
3,
}
b = {1, 2, 3}
c = {
1,
2,
3,
}
x = (1,)
y = (narf(),)
nested = {
(1, 2, 3),
(4, 5, 6),
}
nested_no_trailing_comma = {(1, 2, 3), (4, 5, 6)}
nested_long_lines = [
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"cccccccccccccccccccccccccccccccccccccccc",
(1, 2, 3),
"dddddddddddddddddddddddddddddddddddddddd",
]
{
"oneple": (1,),
}
{"oneple": (1,)}
["ls", "lsoneple/%s" % (foo,)]
x = {"oneple": (1,)}
y = {
"oneple": (1,),
}
assert False, (
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa wraps %s"
% bar
)

# looping over a 1-tuple should also not get wrapped
for x in (1,):
pass
for (x,) in (1,), (2,), (3,):
pass

[
1,
2,
3,
]

division_result_tuple = (6 / 2,)
print("foo %r", (foo.bar,))

if True:
IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = Config.IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING | {
pylons.controllers.WSGIController
}

if True:
ec2client.get_waiter("instance_stopped").wait(
InstanceIds=[instance.id], WaiterConfig={"Delay": 5,}
)
ec2client.get_waiter("instance_stopped").wait(
InstanceIds=[instance.id], WaiterConfig={"Delay": 5,},
)
ec2client.get_waiter("instance_stopped").wait(
InstanceIds=[instance.id], WaiterConfig={"Delay": 5,},
)
4 changes: 2 additions & 2 deletions tests/data/comments2.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def inline_comments_in_brackets_ruin_everything():
parameters.children = [
children[0], # (1
body,
children[-1], # )1
children[-1] # )1
]
parameters.children = [
children[0],
Expand Down Expand Up @@ -142,7 +142,7 @@ def inline_comments_in_brackets_ruin_everything():
syms.simple_stmt,
[
Node(statement, result),
Leaf(token.NEWLINE, '\n'), # FIXME: \r\n?
Leaf(token.NEWLINE, '\n') # FIXME: \r\n?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What caused this change? Black should put a trailing comma here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, but you're commenting on the pre-formatted part of the test case, not the result. The spirit of the test appeared to be that the comment moved correctly with the collapsed line, so I removed the trailing comma in the input.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Sorry for that, it's not always easy to tell from changes in the test suite what is actually changing.

],
)

Expand Down
2 changes: 1 addition & 1 deletion tests/data/comments7.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def func():

def func():
c = call(
0.0123, 0.0456, 0.0789, 0.0123, 0.0789, a[-1] # type: ignore
0.0123, 0.0456, 0.0789, 0.0123, 0.0789, a[-1], # type: ignore
)

# The type: ignore exception only applies to line length, not
Expand Down
Loading