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

Improve long values in dict literals #3440

Merged
merged 10 commits into from
Dec 15, 2022
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
- Fix a crash in preview style with assert + parenthesized string (#3415)
- Do not put the closing quotes in a docstring on a separate line, even if the line is
too long (#3430)
- Long values in dict literals are now wrapped in parentheses; correspondingly
unnecessary parentheses around short values in dict literals are now removed; long
string lambda values are now wrapped in parentheses (#3440)

### Configuration

Expand Down
17 changes: 17 additions & 0 deletions src/black/linegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,23 @@ def visit_stmt(

yield from self.visit(child)

def visit_dictsetmaker(self, node: Node) -> Iterator[Line]:
if Preview.wrap_long_dict_values_in_parens in self.mode:
for i, child in enumerate(node.children):
if i == 0:
continue
if node.children[i - 1].type == token.COLON:
if child.type == syms.atom and child.children[0].type == token.LPAR:
if maybe_make_parens_invisible_in_atom(
child,
parent=node,
remove_brackets_around_comma=False,
):
wrap_in_parentheses(node, child, visible=False)
else:
wrap_in_parentheses(node, child, visible=False)
yield from self.visit_default(node)

def visit_funcdef(self, node: Node) -> Iterator[Line]:
"""Visit function definition."""
if Preview.annotation_parens not in self.mode:
Expand Down
3 changes: 3 additions & 0 deletions src/black/mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,11 @@ class Preview(Enum):
one_element_subscript = auto()
remove_block_trailing_newline = auto()
remove_redundant_parens = auto()
# NOTE: string_processing requires wrap_long_dict_values_in_parens
# for https://github.com/psf/black/issues/3117 to be fixed.
string_processing = auto()
skip_magic_trailing_comma_in_subscript = auto()
wrap_long_dict_values_in_parens = auto()


class Deprecated(UserWarning):
Expand Down
36 changes: 29 additions & 7 deletions src/black/trans.py
Original file line number Diff line number Diff line change
Expand Up @@ -1638,6 +1638,8 @@ class StringParenWrapper(BaseStringSplitter, CustomSplitMapMixin):
* The line is a dictionary key assignment where some valid key is being
assigned the value of some string.
OR
* The line is an lambda expression and the value is a string.
OR
* The line starts with an "atom" string that prefers to be wrapped in
parens. It's preferred to be wrapped when the string is surrounded by
commas (or is the first/last child).
Expand Down Expand Up @@ -1683,7 +1685,7 @@ def do_splitter_match(self, line: Line) -> TMatchResult:
or self._else_match(LL)
or self._assert_match(LL)
or self._assign_match(LL)
or self._dict_match(LL)
or self._dict_or_lambda_match(LL)
or self._prefer_paren_wrap_match(LL)
)

Expand Down Expand Up @@ -1841,22 +1843,23 @@ def _assign_match(LL: List[Leaf]) -> Optional[int]:
return None

@staticmethod
def _dict_match(LL: List[Leaf]) -> Optional[int]:
def _dict_or_lambda_match(LL: List[Leaf]) -> Optional[int]:
"""
Returns:
string_idx such that @LL[string_idx] is equal to our target (i.e.
matched) string, if this line matches the dictionary key assignment
statement requirements listed in the 'Requirements' section of this
classes' docstring.
statement or lambda expression requirements listed in the
'Requirements' section of this classes' docstring.
OR
None, otherwise.
"""
# If this line is apart of a dictionary key assignment...
if syms.dictsetmaker in [parent_type(LL[0]), parent_type(LL[0].parent)]:
# If this line is apart of a dictionary key assignment or lambda expression...
yilei marked this conversation as resolved.
Show resolved Hide resolved
parent_types = [parent_type(LL[0]), parent_type(LL[0].parent)]
if syms.dictsetmaker in parent_types or syms.lambdef in parent_types:
is_valid_index = is_valid_index_factory(LL)

for i, leaf in enumerate(LL):
# We MUST find a colon...
# We MUST find a colon, it can either be dict's or lambda's colon...
if leaf.type == token.COLON:
idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1

Expand Down Expand Up @@ -1951,6 +1954,25 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]:
f" (left_leaves={left_leaves}, right_leaves={right_leaves})"
)
old_rpar_leaf = right_leaves.pop()
elif right_leaves and right_leaves[-1].type == token.RPAR:
# Special case for lambda expressions as dict's value, e.g.:
# my_dict = {
# "key": lambda x: f"formatted: {x},
# }
# After wrapping the dict's value with parentheses, the string is
# followed by a RPAR but it's opening bracket is lambda's, not
yilei marked this conversation as resolved.
Show resolved Hide resolved
# the string's:
# "key": (lambda x: f"formatted: {x}),
opening_bracket = right_leaves[-1].opening_bracket
if opening_bracket is not None and opening_bracket in left_leaves:
index = left_leaves.index(opening_bracket)
if (
index > 0
and index < len(left_leaves) - 1
and left_leaves[index - 1].type == token.COLON
and left_leaves[index + 1].value == "lambda"
):
right_leaves.pop()

append_leaves(string_line, line, right_leaves)

Expand Down
53 changes: 53 additions & 0 deletions tests/data/preview/long_dict_values.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
my_dict = {
"something_something":
r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t"
r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t"
r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t",
}

my_dict = {
"a key in my dict": a_very_long_variable * and_a_very_long_function_call() / 100000.0
}

my_dict = {
"a key in my dict": a_very_long_variable * and_a_very_long_function_call() * and_another_long_func() / 100000.0
}

my_dict = {
"a key in my dict": MyClass.some_attribute.first_call().second_call().third_call(some_args="some value")
}


# output


my_dict = {
"something_something": (
r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t"
r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t"
r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t"
),
}

my_dict = {
"a key in my dict": (
a_very_long_variable * and_a_very_long_function_call() / 100000.0
)
}

my_dict = {
"a key in my dict": (
a_very_long_variable
* and_a_very_long_function_call()
* and_another_long_func()
/ 100000.0
)
}

my_dict = {
"a key in my dict": (
MyClass.some_attribute.first_call()
.second_call()
.third_call(some_args="some value")
)
}
28 changes: 25 additions & 3 deletions tests/data/preview/long_strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,15 @@ def foo():
"........................................................................... \\N{LAO KO LA}"
)

msg = lambda x: f"this is a very very very long lambda value {x} that doesn't fit on a single line"

dict_with_lambda_values = {
"join": lambda j: (
f"{j.__class__.__name__}({some_function_call(j.left)}, "
f"{some_function_call(j.right)})"
),
}


# output

Expand Down Expand Up @@ -362,9 +371,8 @@ def foo():
"A %s %s"
% ("formatted", "string"): (
"This is a really really really long string that has to go inside of a"
" dictionary. It is %s bad (#%d)."
)
% ("soooo", 2),
" dictionary. It is %s bad (#%d)." % ("soooo", 2)
),
}

D5 = { # Test for https://github.com/psf/black/issues/3261
Expand Down Expand Up @@ -806,3 +814,17 @@ def foo():
"..........................................................................."
" \\N{LAO KO LA}"
)

msg = (
lambda x: (
f"this is a very very very long lambda value {x} that doesn't fit on a single"
" line"
)
)

dict_with_lambda_values = {
"join": lambda j: (
f"{j.__class__.__name__}({some_function_call(j.left)}, "
f"{some_function_call(j.right)})"
),
}
15 changes: 15 additions & 0 deletions tests/data/preview/long_strings__regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,13 @@ async def foo(self):
},
)

# Regression test for https://github.com/psf/black/issues/3117.
some_dict = {
"something_something":
r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t"
r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t",
}


# output

Expand Down Expand Up @@ -1178,3 +1185,11 @@ async def foo(self):
),
},
)

# Regression test for https://github.com/psf/black/issues/3117.
some_dict = {
"something_something": (
r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t"
r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t"
),
}