From 0c36faaee53884e95b9fc8371c75a7820bed5dc9 Mon Sep 17 00:00:00 2001 From: Yilei Yang Date: Tue, 13 Dec 2022 16:35:29 -0800 Subject: [PATCH 01/10] WIP: wrap long dict values in parens. --- src/black/linegen.py | 7 +++++ src/black/mode.py | 1 + tests/data/preview/long_dict_values.py | 38 ++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 tests/data/preview/long_dict_values.py diff --git a/src/black/linegen.py b/src/black/linegen.py index 644824a3c86..7c2a35f315d 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -179,6 +179,13 @@ 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: + normalize_invisible_parens( + node, parens_after={":"}, preview=self.mode.preview + ) + 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: diff --git a/src/black/mode.py b/src/black/mode.py index a3ce20b8619..2b8ea338695 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -159,6 +159,7 @@ class Preview(Enum): remove_redundant_parens = auto() string_processing = auto() skip_magic_trailing_comma_in_subscript = auto() + wrap_long_dict_values_in_parens = auto() class Deprecated(UserWarning): diff --git a/tests/data/preview/long_dict_values.py b/tests/data/preview/long_dict_values.py new file mode 100644 index 00000000000..0e317d9ff69 --- /dev/null +++ b/tests/data/preview/long_dict_values.py @@ -0,0 +1,38 @@ +some_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': deformation_rupture_mean * constraint_rupture_something_str / 100.0 +} + +new_cache = { + **cache, + **{str(src.resolve()): get_cache_info(src) for src in sources}, +} + + +# output + + +some_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": ( + deformation_rupture_mean * constraint_rupture_something_str / 100.0 + ) +} + +new_cache = { + **cache, + **{str(src.resolve()): get_cache_info(src) for src in sources}, +} From 5488fd7754ad9c561747780b5d91bd1558a2600a Mon Sep 17 00:00:00 2001 From: Yilei Yang Date: Tue, 13 Dec 2022 18:48:19 -0800 Subject: [PATCH 02/10] Manually wrap in parens, tests now pass. --- src/black/linegen.py | 16 +++++++-- src/black/mode.py | 2 ++ tests/data/preview/long_dict_values.py | 35 +++++++++++++------ tests/data/preview/long_strings.py | 5 ++- .../data/preview/long_strings__regression.py | 15 ++++++++ 5 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index 7c2a35f315d..f4b86deba90 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -181,9 +181,19 @@ def visit_stmt( def visit_dictsetmaker(self, node: Node) -> Iterator[Line]: if Preview.wrap_long_dict_values_in_parens in self.mode: - normalize_invisible_parens( - node, parens_after={":"}, preview=self.mode.preview - ) + 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]: diff --git a/src/black/mode.py b/src/black/mode.py index 2b8ea338695..bcd35b4d4be 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -157,6 +157,8 @@ 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() diff --git a/tests/data/preview/long_dict_values.py b/tests/data/preview/long_dict_values.py index 0e317d9ff69..f23c5d3dad1 100644 --- a/tests/data/preview/long_dict_values.py +++ b/tests/data/preview/long_dict_values.py @@ -1,4 +1,4 @@ -some_dict = { +my_dict = { "something_something": r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t" r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t" @@ -6,19 +6,22 @@ } my_dict = { - 'a key in my dict': deformation_rupture_mean * constraint_rupture_something_str / 100.0 + "a key in my dict": a_very_long_variable * and_a_very_long_function_call() / 100000.0 } -new_cache = { - **cache, - **{str(src.resolve()): get_cache_info(src) for src in sources}, +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 -some_dict = { +my_dict = { "something_something": ( r"Lorem ipsum dolor sit amet, an sed convenire eloquentiam \t" r"signiferumque, duo ea vocibus consetetur scriptorem. Facer \t" @@ -28,11 +31,23 @@ my_dict = { "a key in my dict": ( - deformation_rupture_mean * constraint_rupture_something_str / 100.0 + 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 ) } -new_cache = { - **cache, - **{str(src.resolve()): get_cache_info(src) for src in sources}, +my_dict = { + "a key in my dict": ( + MyClass.some_attribute.first_call() + .second_call() + .third_call(some_args="some value") + ) } diff --git a/tests/data/preview/long_strings.py b/tests/data/preview/long_strings.py index 9288b253b60..c0dcfe048a5 100644 --- a/tests/data/preview/long_strings.py +++ b/tests/data/preview/long_strings.py @@ -362,9 +362,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 diff --git a/tests/data/preview/long_strings__regression.py b/tests/data/preview/long_strings__regression.py index 8b00e76f40e..6d56dcc635d 100644 --- a/tests/data/preview/long_strings__regression.py +++ b/tests/data/preview/long_strings__regression.py @@ -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 @@ -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" + ), +} From 0ece4c6e848248389ff465e42d6ae5445512a6bc Mon Sep 17 00:00:00 2001 From: Yilei Yang Date: Tue, 13 Dec 2022 20:52:24 -0800 Subject: [PATCH 03/10] Fix an issue with lambda expression values in dicts. --- src/black/trans.py | 19 +++++++++++-------- tests/data/preview/long_dict_values.py | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/black/trans.py b/src/black/trans.py index 8893ab02aab..750d526a62b 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -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). @@ -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) ) @@ -1841,23 +1843,24 @@ 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... + 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... - if leaf.type == token.COLON: + # We MUST find a corresponding colon that matches the dict or lambda... + if leaf.type == token.COLON and parent_type(leaf) in parent_types: idx = i + 2 if is_empty_par(LL[i + 1]) else i + 1 # That colon MUST be followed by a string... diff --git a/tests/data/preview/long_dict_values.py b/tests/data/preview/long_dict_values.py index f23c5d3dad1..2ac7849b0d6 100644 --- a/tests/data/preview/long_dict_values.py +++ b/tests/data/preview/long_dict_values.py @@ -17,6 +17,13 @@ "a key in my dict": MyClass.some_attribute.first_call().second_call().third_call(some_args="some value") } +dict_with_lambda_values = { + "join": lambda j: ( + f"{j.__class__.__name__}({some_function_call(j.left)}, " + f"{some_function_call(j.right)})" + ), +} + # output @@ -51,3 +58,12 @@ .third_call(some_args="some value") ) } + +dict_with_lambda_values = { + "join": ( + lambda j: ( + f"{j.__class__.__name__}({some_function_call(j.left)}, " + f"{some_function_call(j.right)})" + ) + ), +} From ce5c4afd9032376c05923f154695b80178ee1ead Mon Sep 17 00:00:00 2001 From: Yilei Yang Date: Tue, 13 Dec 2022 21:31:41 -0800 Subject: [PATCH 04/10] Add a test case for long lambda string values. --- tests/data/preview/long_strings.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/data/preview/long_strings.py b/tests/data/preview/long_strings.py index c0dcfe048a5..e3729036a36 100644 --- a/tests/data/preview/long_strings.py +++ b/tests/data/preview/long_strings.py @@ -278,6 +278,8 @@ 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" + # output @@ -805,3 +807,10 @@ 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" + ) +) From a3b82a22c1649f8a943d3ffbfa8874959a85cbd0 Mon Sep 17 00:00:00 2001 From: Yilei Yang Date: Tue, 13 Dec 2022 22:55:20 -0800 Subject: [PATCH 05/10] Improve the formatting of dict values like "lambda x: f'{x}'". --- src/black/linegen.py | 2 +- src/black/trans.py | 22 ++++++++++++++++++++-- tests/data/preview/long_dict_values.py | 16 ---------------- tests/data/preview/long_strings.py | 14 ++++++++++++++ 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/black/linegen.py b/src/black/linegen.py index f4b86deba90..244dbe77eb5 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -184,7 +184,7 @@ def visit_dictsetmaker(self, node: Node) -> Iterator[Line]: for i, child in enumerate(node.children): if i == 0: continue - if node.children[i-1].type == token.COLON: + 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, diff --git a/src/black/trans.py b/src/black/trans.py index 750d526a62b..32c78c78bff 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1859,8 +1859,8 @@ def _dict_or_lambda_match(LL: List[Leaf]) -> Optional[int]: is_valid_index = is_valid_index_factory(LL) for i, leaf in enumerate(LL): - # We MUST find a corresponding colon that matches the dict or lambda... - if leaf.type == token.COLON and parent_type(leaf) in parent_types: + # 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 # That colon MUST be followed by a string... @@ -1954,6 +1954,24 @@ 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 + # the string's: + # "key": (lambda x: f"formatted: {x}), + opening_bracket = right_leaves[-1].opening_bracket + 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) diff --git a/tests/data/preview/long_dict_values.py b/tests/data/preview/long_dict_values.py index 2ac7849b0d6..f23c5d3dad1 100644 --- a/tests/data/preview/long_dict_values.py +++ b/tests/data/preview/long_dict_values.py @@ -17,13 +17,6 @@ "a key in my dict": MyClass.some_attribute.first_call().second_call().third_call(some_args="some value") } -dict_with_lambda_values = { - "join": lambda j: ( - f"{j.__class__.__name__}({some_function_call(j.left)}, " - f"{some_function_call(j.right)})" - ), -} - # output @@ -58,12 +51,3 @@ .third_call(some_args="some value") ) } - -dict_with_lambda_values = { - "join": ( - lambda j: ( - f"{j.__class__.__name__}({some_function_call(j.left)}, " - f"{some_function_call(j.right)})" - ) - ), -} diff --git a/tests/data/preview/long_strings.py b/tests/data/preview/long_strings.py index e3729036a36..9c78f675b8f 100644 --- a/tests/data/preview/long_strings.py +++ b/tests/data/preview/long_strings.py @@ -280,6 +280,13 @@ def foo(): 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 @@ -814,3 +821,10 @@ def foo(): " line" ) ) + +dict_with_lambda_values = { + "join": lambda j: ( + f"{j.__class__.__name__}({some_function_call(j.left)}, " + f"{some_function_call(j.right)})" + ), +} From 714e9fc1843f8508f8d4f9e44dd988b6589c3b69 Mon Sep 17 00:00:00 2001 From: Yilei Yang Date: Tue, 13 Dec 2022 23:02:05 -0800 Subject: [PATCH 06/10] Add change log. --- CHANGES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index f6040359623..de88e7edaf6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 From 1396bbb0cd5c11a9479484b9aa35cea6809a7f18 Mon Sep 17 00:00:00 2001 From: Yilei Yang Date: Tue, 13 Dec 2022 23:12:04 -0800 Subject: [PATCH 07/10] Fix mypy errors. --- src/black/trans.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/black/trans.py b/src/black/trans.py index 32c78c78bff..9d89ccd2e4e 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1964,14 +1964,15 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: # the string's: # "key": (lambda x: f"formatted: {x}), opening_bracket = right_leaves[-1].opening_bracket - 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() + if opening_bracket is not None: + 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) From de04c878806d04c18281886ffd835b23bd30c55d Mon Sep 17 00:00:00 2001 From: Yilei Yang Date: Tue, 13 Dec 2022 23:42:50 -0800 Subject: [PATCH 08/10] Fix a bug because list.index can raise ValueError :facepalm:. --- src/black/trans.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black/trans.py b/src/black/trans.py index 9d89ccd2e4e..4a4f9c60b7a 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1964,7 +1964,7 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: # the string's: # "key": (lambda x: f"formatted: {x}), opening_bracket = right_leaves[-1].opening_bracket - if opening_bracket is not None: + if opening_bracket is not None and opening_bracket in left_leaves: index = left_leaves.index(opening_bracket) if ( index > 0 From 6aa7d6eeda50bf33378d12553a0b3b513c2451b3 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Wed, 14 Dec 2022 22:10:10 -0800 Subject: [PATCH 09/10] Update src/black/trans.py Co-authored-by: Jelle Zijlstra --- src/black/trans.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black/trans.py b/src/black/trans.py index 4a4f9c60b7a..a95157be6c5 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1960,7 +1960,7 @@ def do_transform(self, line: Line, string_idx: int) -> Iterator[TResult[Line]]: # "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 + # followed by a RPAR but its opening bracket is lambda's, not # the string's: # "key": (lambda x: f"formatted: {x}), opening_bracket = right_leaves[-1].opening_bracket From ba1b968679ef70d539c9f89548cdabd093864c95 Mon Sep 17 00:00:00 2001 From: "Yilei \"Dolee\" Yang" Date: Wed, 14 Dec 2022 22:10:17 -0800 Subject: [PATCH 10/10] Update src/black/trans.py Co-authored-by: Jelle Zijlstra --- src/black/trans.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/black/trans.py b/src/black/trans.py index a95157be6c5..b08a6d243d8 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -1853,7 +1853,7 @@ def _dict_or_lambda_match(LL: List[Leaf]) -> Optional[int]: OR None, otherwise. """ - # If this line is apart of a dictionary key assignment or lambda expression... + # If this line is a part of a dictionary key assignment or lambda expression... 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)