diff --git a/CHANGES.md b/CHANGES.md index 505fee4d743..c1e6320740d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,8 @@ +- Fix/remove string merging changing f-string quotes on f-strings with internal quotes + (#4498) - Remove parentheses around sole list items (#4312) - Collapse multiple empty lines after an import into one (#4489) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 3fc12e2dab6..bc3e233ed3d 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -137,10 +137,11 @@ foo( _Black_ will split long string literals and merge short ones. Parentheses are used where appropriate. When split, parts of f-strings that don't need formatting are converted to -plain strings. User-made splits are respected when they do not exceed the line length -limit. Line continuation backslashes are converted into parenthesized strings. -Unnecessary parentheses are stripped. The stability and status of this feature is -tracked in [this issue](https://github.com/psf/black/issues/2188). +plain strings. f-strings will not be merged if they contain internal quotes and it would +change their quotation mark style. User-made splits are respected when they do not +exceed the line length limit. Line continuation backslashes are converted into +parenthesized strings. Unnecessary parentheses are stripped. The stability and status of +this feature istracked in [this issue](https://github.com/psf/black/issues/2188). (labels/wrap-long-dict-values)= diff --git a/src/black/trans.py b/src/black/trans.py index 14699bd00d5..2e6ab488a0e 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -794,6 +794,8 @@ def _validate_msg(line: Line, string_idx: int) -> TResult[None]: - The set of all string prefixes in the string group is of length greater than one and is not equal to {"", "f"}. - The string group consists of raw strings. + - The string group would merge f-strings with different quote types + and internal quotes. - The string group is stringified type annotations. We don't want to process stringified type annotations since pyright doesn't support them spanning multiple string values. (NOTE: mypy, pytype, pyre do @@ -820,6 +822,8 @@ def _validate_msg(line: Line, string_idx: int) -> TResult[None]: i += inc + QUOTE = line.leaves[string_idx].value[-1] + num_of_inline_string_comments = 0 set_of_prefixes = set() num_of_strings = 0 @@ -842,6 +846,19 @@ def _validate_msg(line: Line, string_idx: int) -> TResult[None]: set_of_prefixes.add(prefix) + if ( + "f" in prefix + and leaf.value[-1] != QUOTE + and ( + "'" in leaf.value[len(prefix) + 1 : -1] + or '"' in leaf.value[len(prefix) + 1 : -1] + ) + ): + return TErr( + "StringMerger does NOT merge f-strings with different quote types" + "and internal quotes." + ) + if id(leaf) in line.comments: num_of_inline_string_comments += 1 if contains_pragma_comment(line.comments[id(leaf)]): diff --git a/tests/data/cases/preview_fstring.py b/tests/data/cases/preview_fstring.py new file mode 100644 index 00000000000..e0a3eacfa20 --- /dev/null +++ b/tests/data/cases/preview_fstring.py @@ -0,0 +1,2 @@ +# flags: --unstable +f"{''=}" f'{""=}' \ No newline at end of file diff --git a/tests/data/cases/preview_long_strings.py b/tests/data/cases/preview_long_strings.py index 86fa1b0c7e1..ba48c19d542 100644 --- a/tests/data/cases/preview_long_strings.py +++ b/tests/data/cases/preview_long_strings.py @@ -882,7 +882,7 @@ def foo(): log.info( "Skipping:" - f" {desc['db_id']} {foo('bar',x=123)} {'foo' != 'bar'} {(x := 'abc=')} {pos_share=} {desc['status']} {desc['exposure_max']}" + f' {desc["db_id"]} {foo("bar",x=123)} {"foo" != "bar"} {(x := "abc=")} {pos_share=} {desc["status"]} {desc["exposure_max"]}' ) log.info( @@ -902,7 +902,7 @@ def foo(): log.info( "Skipping:" - f" {'a' == 'b' == 'c' == 'd'} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}" + f' {"a" == "b" == "c" == "d"} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}' ) log.info( @@ -925,4 +925,4 @@ def foo(): log.info( f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}""" -) +) \ No newline at end of file diff --git a/tests/data/cases/preview_long_strings__regression.py b/tests/data/cases/preview_long_strings__regression.py index afe2b311cf4..123342f575c 100644 --- a/tests/data/cases/preview_long_strings__regression.py +++ b/tests/data/cases/preview_long_strings__regression.py @@ -552,6 +552,7 @@ async def foo(self): } # Regression test for https://github.com/psf/black/issues/3506. +# Regressed again by https://github.com/psf/black/pull/4498 s = ( "With single quote: ' " f" {my_dict['foo']}" @@ -1239,9 +1240,15 @@ async def foo(self): } # Regression test for https://github.com/psf/black/issues/3506. -s = f"With single quote: ' {my_dict['foo']} With double quote: \" {my_dict['bar']}" +# Regressed again by https://github.com/psf/black/pull/4498 +s = ( + "With single quote: ' " + f" {my_dict['foo']}" + ' With double quote: " ' + f' {my_dict["bar"]}' +) s = ( "Lorem Ipsum is simply dummy text of the printing and typesetting" - f" industry:'{my_dict['foo']}'" -) + f' industry:\'{my_dict["foo"]}\'' +) \ No newline at end of file