Skip to content

Commit 3fab5ad

Browse files
authored
Prevent f-string merge quote changes with nested quotes (#4498)
1 parent e54f86b commit 3fab5ad

File tree

6 files changed

+39
-10
lines changed

6 files changed

+39
-10
lines changed

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
<!-- Changes that affect Black's preview style -->
1919

20+
- Fix/remove string merging changing f-string quotes on f-strings with internal quotes
21+
(#4498)
2022
- Remove parentheses around sole list items (#4312)
2123
- Collapse multiple empty lines after an import into one (#4489)
2224

docs/the_black_code_style/future_style.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,11 @@ foo(
137137

138138
_Black_ will split long string literals and merge short ones. Parentheses are used where
139139
appropriate. When split, parts of f-strings that don't need formatting are converted to
140-
plain strings. User-made splits are respected when they do not exceed the line length
141-
limit. Line continuation backslashes are converted into parenthesized strings.
142-
Unnecessary parentheses are stripped. The stability and status of this feature is
143-
tracked in [this issue](https://github.com/psf/black/issues/2188).
140+
plain strings. f-strings will not be merged if they contain internal quotes and it would
141+
change their quotation mark style. User-made splits are respected when they do not
142+
exceed the line length limit. Line continuation backslashes are converted into
143+
parenthesized strings. Unnecessary parentheses are stripped. The stability and status of
144+
this feature istracked in [this issue](https://github.com/psf/black/issues/2188).
144145

145146
(labels/wrap-long-dict-values)=
146147

src/black/trans.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,8 @@ def _validate_msg(line: Line, string_idx: int) -> TResult[None]:
794794
- The set of all string prefixes in the string group is of
795795
length greater than one and is not equal to {"", "f"}.
796796
- The string group consists of raw strings.
797+
- The string group would merge f-strings with different quote types
798+
and internal quotes.
797799
- The string group is stringified type annotations. We don't want to
798800
process stringified type annotations since pyright doesn't support
799801
them spanning multiple string values. (NOTE: mypy, pytype, pyre do
@@ -820,6 +822,8 @@ def _validate_msg(line: Line, string_idx: int) -> TResult[None]:
820822

821823
i += inc
822824

825+
QUOTE = line.leaves[string_idx].value[-1]
826+
823827
num_of_inline_string_comments = 0
824828
set_of_prefixes = set()
825829
num_of_strings = 0
@@ -842,6 +846,19 @@ def _validate_msg(line: Line, string_idx: int) -> TResult[None]:
842846

843847
set_of_prefixes.add(prefix)
844848

849+
if (
850+
"f" in prefix
851+
and leaf.value[-1] != QUOTE
852+
and (
853+
"'" in leaf.value[len(prefix) + 1 : -1]
854+
or '"' in leaf.value[len(prefix) + 1 : -1]
855+
)
856+
):
857+
return TErr(
858+
"StringMerger does NOT merge f-strings with different quote types"
859+
"and internal quotes."
860+
)
861+
845862
if id(leaf) in line.comments:
846863
num_of_inline_string_comments += 1
847864
if contains_pragma_comment(line.comments[id(leaf)]):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# flags: --unstable
2+
f"{''=}" f'{""=}'

tests/data/cases/preview_long_strings.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,7 @@ def foo():
882882

883883
log.info(
884884
"Skipping:"
885-
f" {desc['db_id']} {foo('bar',x=123)} {'foo' != 'bar'} {(x := 'abc=')} {pos_share=} {desc['status']} {desc['exposure_max']}"
885+
f' {desc["db_id"]} {foo("bar",x=123)} {"foo" != "bar"} {(x := "abc=")} {pos_share=} {desc["status"]} {desc["exposure_max"]}'
886886
)
887887

888888
log.info(
@@ -902,7 +902,7 @@ def foo():
902902

903903
log.info(
904904
"Skipping:"
905-
f" {'a' == 'b' == 'c' == 'd'} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}"
905+
f' {"a" == "b" == "c" == "d"} {desc["ms_name"]} {money=} {dte=} {pos_share=} {desc["status"]} {desc["exposure_max"]}'
906906
)
907907

908908
log.info(
@@ -925,4 +925,4 @@ def foo():
925925

926926
log.info(
927927
f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share=} {desc['status']} {desc['exposure_max']}"""
928-
)
928+
)

tests/data/cases/preview_long_strings__regression.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,7 @@ async def foo(self):
552552
}
553553

554554
# Regression test for https://github.com/psf/black/issues/3506.
555+
# Regressed again by https://github.com/psf/black/pull/4498
555556
s = (
556557
"With single quote: ' "
557558
f" {my_dict['foo']}"
@@ -1239,9 +1240,15 @@ async def foo(self):
12391240
}
12401241

12411242
# Regression test for https://github.com/psf/black/issues/3506.
1242-
s = f"With single quote: ' {my_dict['foo']} With double quote: \" {my_dict['bar']}"
1243+
# Regressed again by https://github.com/psf/black/pull/4498
1244+
s = (
1245+
"With single quote: ' "
1246+
f" {my_dict['foo']}"
1247+
' With double quote: " '
1248+
f' {my_dict["bar"]}'
1249+
)
12431250

12441251
s = (
12451252
"Lorem Ipsum is simply dummy text of the printing and typesetting"
1246-
f" industry:'{my_dict['foo']}'"
1247-
)
1253+
f' industry:\'{my_dict["foo"]}\''
1254+
)

0 commit comments

Comments
 (0)