Wrap concatenated strings used as function args in parens (#3307)

Fixes #3292
This commit is contained in:
Yilei "Dolee" Yang 2022-10-26 18:03:10 -07:00 committed by GitHub
parent 09d4acdcb7
commit b73b77a9b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 169 additions and 110 deletions

View File

@ -15,6 +15,8 @@
<!-- Changes that affect Black's preview style --> <!-- Changes that affect Black's preview style -->
- Enforce empty lines before classes and functions with sticky leading comments (#3302) - Enforce empty lines before classes and functions with sticky leading comments (#3302)
- Implicitly concatenated strings used as function args are now wrapped inside
parentheses (#3307)
### Configuration ### Configuration

View File

@ -497,8 +497,10 @@ def main( # noqa: C901
user_level_config = str(find_user_pyproject_toml()) user_level_config = str(find_user_pyproject_toml())
if config == user_level_config: if config == user_level_config:
out( out(
"Using configuration from user-level config at " (
f"'{user_level_config}'.", "Using configuration from user-level config at "
f"'{user_level_config}'."
),
fg="blue", fg="blue",
) )
elif config_source in ( elif config_source in (

View File

@ -180,8 +180,10 @@ class Mode:
def __post_init__(self) -> None: def __post_init__(self) -> None:
if self.experimental_string_processing: if self.experimental_string_processing:
warn( warn(
"`experimental string processing` has been included in `preview`" (
" and deprecated. Use `preview` instead.", "`experimental string processing` has been included in `preview`"
" and deprecated. Use `preview` instead."
),
Deprecated, Deprecated,
) )

View File

@ -29,9 +29,11 @@
except ImportError: except ImportError:
if sys.version_info < (3, 8) and not _IS_PYPY: if sys.version_info < (3, 8) and not _IS_PYPY:
print( print(
"The typed_ast package is required but not installed.\n" (
"You can upgrade to Python 3.8+ or install typed_ast with\n" "The typed_ast package is required but not installed.\n"
"`python3 -m pip install typed-ast`.", "You can upgrade to Python 3.8+ or install typed_ast with\n"
"`python3 -m pip install typed-ast`."
),
file=sys.stderr, file=sys.stderr,
) )
sys.exit(1) sys.exit(1)

View File

@ -1058,33 +1058,19 @@ def _prefer_paren_wrap_match(LL: List[Leaf]) -> Optional[int]:
if LL[0].type != token.STRING: if LL[0].type != token.STRING:
return None return None
matching_nodes = [ # If the string is surrounded by commas (or is the first/last child)...
syms.listmaker, prev_sibling = LL[0].prev_sibling
syms.dictsetmaker, next_sibling = LL[0].next_sibling
syms.testlist_gexp, if not prev_sibling and not next_sibling and parent_type(LL[0]) == syms.atom:
] # If it's an atom string, we need to check the parent atom's siblings.
# If the string is an immediate child of a list/set/tuple literal... parent = LL[0].parent
if ( assert parent is not None # For type checkers.
parent_type(LL[0]) in matching_nodes prev_sibling = parent.prev_sibling
or parent_type(LL[0].parent) in matching_nodes next_sibling = parent.next_sibling
if (not prev_sibling or prev_sibling.type == token.COMMA) and (
not next_sibling or next_sibling.type == token.COMMA
): ):
# And the string is surrounded by commas (or is the first/last child)... return 0
prev_sibling = LL[0].prev_sibling
next_sibling = LL[0].next_sibling
if (
not prev_sibling
and not next_sibling
and parent_type(LL[0]) == syms.atom
):
# If it's an atom string, we need to check the parent atom's siblings.
parent = LL[0].parent
assert parent is not None # For type checkers.
prev_sibling = parent.prev_sibling
next_sibling = parent.next_sibling
if (not prev_sibling or prev_sibling.type == token.COMMA) and (
not next_sibling or next_sibling.type == token.COMMA
):
return 0
return None return None
@ -1653,9 +1639,8 @@ class StringParenWrapper(BaseStringSplitter, CustomSplitMapMixin):
assigned the value of some string. assigned the value of some string.
OR OR
* The line starts with an "atom" string that prefers to be wrapped in * The line starts with an "atom" string that prefers to be wrapped in
parens. It's preferred to be wrapped when it's is an immediate child of parens. It's preferred to be wrapped when the string is surrounded by
a list/set/tuple literal, AND the string is surrounded by commas (or is commas (or is the first/last child).
the first/last child).
Transformations: Transformations:
The chosen string is wrapped in parentheses and then split at the LPAR. The chosen string is wrapped in parentheses and then split at the LPAR.

View File

@ -79,10 +79,14 @@
) )
# long arguments # long arguments
normal_name = normal_function_name( normal_name = normal_function_name(
"but with super long string arguments that on their own exceed the line limit so" (
" there's no way it can ever fit", "but with super long string arguments that on their own exceed the line limit"
"eggs with spam and eggs and spam with eggs with spam and eggs and spam with eggs" " so there's no way it can ever fit"
" with spam and eggs and spam with eggs", ),
(
"eggs with spam and eggs and spam with eggs with spam and eggs and spam with"
" eggs with spam and eggs and spam with eggs"
),
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it=0, this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it=0,
) )
string_variable_name = "a string that is waaaaaaaayyyyyyyy too long, even in parens, there's nothing you can do" # noqa string_variable_name = "a string that is waaaaaaaayyyyyyyy too long, even in parens, there's nothing you can do" # noqa

View File

@ -297,8 +297,10 @@ def foo():
y = "Short string" y = "Short string"
print( print(
"This is a really long string inside of a print statement with extra arguments" (
" attached at the end of it.", "This is a really long string inside of a print statement with extra arguments"
" attached at the end of it."
),
x, x,
y, y,
z, z,
@ -474,13 +476,15 @@ def foo():
) )
bad_split_func1( bad_split_func1(
"But what should happen when code has already " (
"been formatted but in the wrong way? Like " "But what should happen when code has already "
"with a space at the end instead of the " "been formatted but in the wrong way? Like "
"beginning. Or what about when it is split too " "with a space at the end instead of the "
"soon? In the case of a split that is too " "beginning. Or what about when it is split too "
"short, black will try to honer the custom " "soon? In the case of a split that is too "
"split.", "short, black will try to honer the custom "
"split."
),
xxx, xxx,
yyy, yyy,
zzz, zzz,
@ -583,9 +587,11 @@ def foo():
) )
arg_comment_string = print( arg_comment_string = print(
"Long lines with inline comments which are apart of (and not the only member of) an" ( # This comment gets thrown to the top.
" argument list should have their comments appended to the reformatted string's" "Long lines with inline comments which are apart of (and not the only member"
" enclosing left parentheses.", # This comment gets thrown to the top. " of) an argument list should have their comments appended to the reformatted"
" string's enclosing left parentheses."
),
"Arg #2", "Arg #2",
"Arg #3", "Arg #3",
"Arg #4", "Arg #4",
@ -645,23 +651,31 @@ def foo():
) )
func_with_bad_comma( func_with_bad_comma(
"This is a really long string argument to a function that has a trailing comma" (
" which should NOT be there.", "This is a really long string argument to a function that has a trailing comma"
" which should NOT be there."
),
) )
func_with_bad_comma( func_with_bad_comma(
"This is a really long string argument to a function that has a trailing comma" ( # comment after comma
" which should NOT be there.", # comment after comma "This is a really long string argument to a function that has a trailing comma"
" which should NOT be there."
),
) )
func_with_bad_comma( func_with_bad_comma(
"This is a really long string argument to a function that has a trailing comma" (
" which should NOT be there.", "This is a really long string argument to a function that has a trailing comma"
" which should NOT be there."
),
) )
func_with_bad_comma( func_with_bad_comma(
"This is a really long string argument to a function that has a trailing comma" ( # comment after comma
" which should NOT be there.", # comment after comma "This is a really long string argument to a function that has a trailing comma"
" which should NOT be there."
),
) )
func_with_bad_parens_that_wont_fit_in_one_line( func_with_bad_parens_that_wont_fit_in_one_line(

View File

@ -679,9 +679,11 @@ class A:
def foo(): def foo():
some_func_call( some_func_call(
"xxxxxxxxxx", "xxxxxxxxxx",
"xx {xxxxxxxxxxx}/xxxxxxxxxxx.xxx xxxx.xxx && xxxxxx -x " (
'"xxxx xxxxxxx xxxxxx xxxx; xxxx xxxxxx_xxxxx xxxxxx xxxx; ' "xx {xxxxxxxxxxx}/xxxxxxxxxxx.xxx xxxx.xxx && xxxxxx -x "
"xxxx.xxxx_xxxxxx(['xxxx.xxx'], xxxx.xxxxxxx().xxxxxxxxxx)\" ", '"xxxx xxxxxxx xxxxxx xxxx; xxxx xxxxxx_xxxxx xxxxxx xxxx; '
"xxxx.xxxx_xxxxxx(['xxxx.xxx'], xxxx.xxxxxxx().xxxxxxxxxx)\" "
),
None, None,
("xxxxxxxxxxx",), ("xxxxxxxxxxx",),
), ),
@ -690,9 +692,11 @@ def foo():
class A: class A:
def foo(): def foo():
some_func_call( some_func_call(
"xx {xxxxxxxxxxx}/xxxxxxxxxxx.xxx xxxx.xxx && xxxxxx -x " (
"xxxx, ('xxxxxxx xxxxxx xxxx, xxxx') xxxxxx_xxxxx xxxxxx xxxx; " "xx {xxxxxxxxxxx}/xxxxxxxxxxx.xxx xxxx.xxx && xxxxxx -x "
"xxxx.xxxx_xxxxxx(['xxxx.xxx'], xxxx.xxxxxxx().xxxxxxxxxx)\" ", "xxxx, ('xxxxxxx xxxxxx xxxx, xxxx') xxxxxx_xxxxx xxxxxx xxxx; "
"xxxx.xxxx_xxxxxx(['xxxx.xxx'], xxxx.xxxxxxx().xxxxxxxxxx)\" "
),
None, None,
("xxxxxxxxxxx",), ("xxxxxxxxxxx",),
), ),
@ -810,8 +814,10 @@ def foo():
) )
lpar_and_rpar_have_comments = func_call( # LPAR Comment lpar_and_rpar_have_comments = func_call( # LPAR Comment
"Long really ridiculous type of string that shouldn't really even exist at all. I" ( # Comma Comment
" mean commmme onnn!!!", # Comma Comment "Long really ridiculous type of string that shouldn't really even exist at all."
" I mean commmme onnn!!!"
),
) # RPAR Comment ) # RPAR Comment
cmd_fstring = ( cmd_fstring = (

View File

@ -490,8 +490,10 @@ def err(msg: str, **kwargs: Any) -> None:
self.assertEqual(err_lines[-1], "error: cannot format e1: boom") self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"1 file reformatted, 2 files left unchanged, 1 file failed to" (
" reformat.", "1 file reformatted, 2 files left unchanged, 1 file failed to"
" reformat."
),
) )
self.assertEqual(report.return_code, 123) self.assertEqual(report.return_code, 123)
report.done(Path("f3"), black.Changed.YES) report.done(Path("f3"), black.Changed.YES)
@ -500,8 +502,10 @@ def err(msg: str, **kwargs: Any) -> None:
self.assertEqual(out_lines[-1], "reformatted f3") self.assertEqual(out_lines[-1], "reformatted f3")
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"2 files reformatted, 2 files left unchanged, 1 file failed to" (
" reformat.", "2 files reformatted, 2 files left unchanged, 1 file failed to"
" reformat."
),
) )
self.assertEqual(report.return_code, 123) self.assertEqual(report.return_code, 123)
report.failed(Path("e2"), "boom") report.failed(Path("e2"), "boom")
@ -510,8 +514,10 @@ def err(msg: str, **kwargs: Any) -> None:
self.assertEqual(err_lines[-1], "error: cannot format e2: boom") self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"2 files reformatted, 2 files left unchanged, 2 files failed to" (
" reformat.", "2 files reformatted, 2 files left unchanged, 2 files failed to"
" reformat."
),
) )
self.assertEqual(report.return_code, 123) self.assertEqual(report.return_code, 123)
report.path_ignored(Path("wat"), "no match") report.path_ignored(Path("wat"), "no match")
@ -520,8 +526,10 @@ def err(msg: str, **kwargs: Any) -> None:
self.assertEqual(out_lines[-1], "wat ignored: no match") self.assertEqual(out_lines[-1], "wat ignored: no match")
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"2 files reformatted, 2 files left unchanged, 2 files failed to" (
" reformat.", "2 files reformatted, 2 files left unchanged, 2 files failed to"
" reformat."
),
) )
self.assertEqual(report.return_code, 123) self.assertEqual(report.return_code, 123)
report.done(Path("f4"), black.Changed.NO) report.done(Path("f4"), black.Changed.NO)
@ -530,22 +538,28 @@ def err(msg: str, **kwargs: Any) -> None:
self.assertEqual(out_lines[-1], "f4 already well formatted, good job.") self.assertEqual(out_lines[-1], "f4 already well formatted, good job.")
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"2 files reformatted, 3 files left unchanged, 2 files failed to" (
" reformat.", "2 files reformatted, 3 files left unchanged, 2 files failed to"
" reformat."
),
) )
self.assertEqual(report.return_code, 123) self.assertEqual(report.return_code, 123)
report.check = True report.check = True
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"2 files would be reformatted, 3 files would be left unchanged, 2" (
" files would fail to reformat.", "2 files would be reformatted, 3 files would be left unchanged, 2"
" files would fail to reformat."
),
) )
report.check = False report.check = False
report.diff = True report.diff = True
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"2 files would be reformatted, 3 files would be left unchanged, 2" (
" files would fail to reformat.", "2 files would be reformatted, 3 files would be left unchanged, 2"
" files would fail to reformat."
),
) )
def test_report_quiet(self) -> None: def test_report_quiet(self) -> None:
@ -587,8 +601,10 @@ def err(msg: str, **kwargs: Any) -> None:
self.assertEqual(err_lines[-1], "error: cannot format e1: boom") self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"1 file reformatted, 2 files left unchanged, 1 file failed to" (
" reformat.", "1 file reformatted, 2 files left unchanged, 1 file failed to"
" reformat."
),
) )
self.assertEqual(report.return_code, 123) self.assertEqual(report.return_code, 123)
report.done(Path("f3"), black.Changed.YES) report.done(Path("f3"), black.Changed.YES)
@ -596,8 +612,10 @@ def err(msg: str, **kwargs: Any) -> None:
self.assertEqual(len(err_lines), 1) self.assertEqual(len(err_lines), 1)
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"2 files reformatted, 2 files left unchanged, 1 file failed to" (
" reformat.", "2 files reformatted, 2 files left unchanged, 1 file failed to"
" reformat."
),
) )
self.assertEqual(report.return_code, 123) self.assertEqual(report.return_code, 123)
report.failed(Path("e2"), "boom") report.failed(Path("e2"), "boom")
@ -606,8 +624,10 @@ def err(msg: str, **kwargs: Any) -> None:
self.assertEqual(err_lines[-1], "error: cannot format e2: boom") self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"2 files reformatted, 2 files left unchanged, 2 files failed to" (
" reformat.", "2 files reformatted, 2 files left unchanged, 2 files failed to"
" reformat."
),
) )
self.assertEqual(report.return_code, 123) self.assertEqual(report.return_code, 123)
report.path_ignored(Path("wat"), "no match") report.path_ignored(Path("wat"), "no match")
@ -615,8 +635,10 @@ def err(msg: str, **kwargs: Any) -> None:
self.assertEqual(len(err_lines), 2) self.assertEqual(len(err_lines), 2)
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"2 files reformatted, 2 files left unchanged, 2 files failed to" (
" reformat.", "2 files reformatted, 2 files left unchanged, 2 files failed to"
" reformat."
),
) )
self.assertEqual(report.return_code, 123) self.assertEqual(report.return_code, 123)
report.done(Path("f4"), black.Changed.NO) report.done(Path("f4"), black.Changed.NO)
@ -624,22 +646,28 @@ def err(msg: str, **kwargs: Any) -> None:
self.assertEqual(len(err_lines), 2) self.assertEqual(len(err_lines), 2)
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"2 files reformatted, 3 files left unchanged, 2 files failed to" (
" reformat.", "2 files reformatted, 3 files left unchanged, 2 files failed to"
" reformat."
),
) )
self.assertEqual(report.return_code, 123) self.assertEqual(report.return_code, 123)
report.check = True report.check = True
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"2 files would be reformatted, 3 files would be left unchanged, 2" (
" files would fail to reformat.", "2 files would be reformatted, 3 files would be left unchanged, 2"
" files would fail to reformat."
),
) )
report.check = False report.check = False
report.diff = True report.diff = True
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"2 files would be reformatted, 3 files would be left unchanged, 2" (
" files would fail to reformat.", "2 files would be reformatted, 3 files would be left unchanged, 2"
" files would fail to reformat."
),
) )
def test_report_normal(self) -> None: def test_report_normal(self) -> None:
@ -683,8 +711,10 @@ def err(msg: str, **kwargs: Any) -> None:
self.assertEqual(err_lines[-1], "error: cannot format e1: boom") self.assertEqual(err_lines[-1], "error: cannot format e1: boom")
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"1 file reformatted, 2 files left unchanged, 1 file failed to" (
" reformat.", "1 file reformatted, 2 files left unchanged, 1 file failed to"
" reformat."
),
) )
self.assertEqual(report.return_code, 123) self.assertEqual(report.return_code, 123)
report.done(Path("f3"), black.Changed.YES) report.done(Path("f3"), black.Changed.YES)
@ -693,8 +723,10 @@ def err(msg: str, **kwargs: Any) -> None:
self.assertEqual(out_lines[-1], "reformatted f3") self.assertEqual(out_lines[-1], "reformatted f3")
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"2 files reformatted, 2 files left unchanged, 1 file failed to" (
" reformat.", "2 files reformatted, 2 files left unchanged, 1 file failed to"
" reformat."
),
) )
self.assertEqual(report.return_code, 123) self.assertEqual(report.return_code, 123)
report.failed(Path("e2"), "boom") report.failed(Path("e2"), "boom")
@ -703,8 +735,10 @@ def err(msg: str, **kwargs: Any) -> None:
self.assertEqual(err_lines[-1], "error: cannot format e2: boom") self.assertEqual(err_lines[-1], "error: cannot format e2: boom")
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"2 files reformatted, 2 files left unchanged, 2 files failed to" (
" reformat.", "2 files reformatted, 2 files left unchanged, 2 files failed to"
" reformat."
),
) )
self.assertEqual(report.return_code, 123) self.assertEqual(report.return_code, 123)
report.path_ignored(Path("wat"), "no match") report.path_ignored(Path("wat"), "no match")
@ -712,8 +746,10 @@ def err(msg: str, **kwargs: Any) -> None:
self.assertEqual(len(err_lines), 2) self.assertEqual(len(err_lines), 2)
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"2 files reformatted, 2 files left unchanged, 2 files failed to" (
" reformat.", "2 files reformatted, 2 files left unchanged, 2 files failed to"
" reformat."
),
) )
self.assertEqual(report.return_code, 123) self.assertEqual(report.return_code, 123)
report.done(Path("f4"), black.Changed.NO) report.done(Path("f4"), black.Changed.NO)
@ -721,22 +757,28 @@ def err(msg: str, **kwargs: Any) -> None:
self.assertEqual(len(err_lines), 2) self.assertEqual(len(err_lines), 2)
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"2 files reformatted, 3 files left unchanged, 2 files failed to" (
" reformat.", "2 files reformatted, 3 files left unchanged, 2 files failed to"
" reformat."
),
) )
self.assertEqual(report.return_code, 123) self.assertEqual(report.return_code, 123)
report.check = True report.check = True
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"2 files would be reformatted, 3 files would be left unchanged, 2" (
" files would fail to reformat.", "2 files would be reformatted, 3 files would be left unchanged, 2"
" files would fail to reformat."
),
) )
report.check = False report.check = False
report.diff = True report.diff = True
self.assertEqual( self.assertEqual(
unstyle(str(report)), unstyle(str(report)),
"2 files would be reformatted, 3 files would be left unchanged, 2" (
" files would fail to reformat.", "2 files would be reformatted, 3 files would be left unchanged, 2"
" files would fail to reformat."
),
) )
def test_lib2to3_parse(self) -> None: def test_lib2to3_parse(self) -> None: