Skip to content

Commit d34c94c

Browse files
Ankitsinghsisodyapre-commit-ci[bot]cobaltt7
committed
Fix string_processing error with unassigned long string literal with trailing comma (psf#4929)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: cobalt <61329810+cobaltt7@users.noreply.github.com>
1 parent 0f6a50f commit d34c94c

3 files changed

Lines changed: 58 additions & 1 deletion

File tree

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 `string_processing` crashing on unassigned long string literals with trailing
21+
commas (one-item tuples) (#4929)
2022
- Simplify implementation of the power operator "hugging" logic (#4918)
2123

2224
- Always wrap `in` clauses of comprehensions if they have delimiters (#4881)

src/black/trans.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1431,6 +1431,20 @@ def do_splitter_match(self, line: Line) -> TMatchResult:
14311431
if self._prefer_paren_wrap_match(LL) is not None:
14321432
return TErr("Line needs to be wrapped in parens first.")
14331433

1434+
# If the line is just STRING + COMMA (a one-item tuple) and not inside
1435+
# brackets, we need to defer to StringParenWrapper to wrap it first.
1436+
# Otherwise, splitting the string would create multiple expressions where
1437+
# only the last has the comma, breaking AST equivalence. See issue #4912.
1438+
if (
1439+
not line.inside_brackets
1440+
and len(LL) == 2
1441+
and LL[0].type == token.STRING
1442+
and LL[1].type == token.COMMA
1443+
):
1444+
return TErr(
1445+
"Line with trailing comma tuple needs to be wrapped in parens first."
1446+
)
1447+
14341448
is_valid_index = is_valid_index_factory(LL)
14351449

14361450
idx = 0
@@ -1964,9 +1978,14 @@ def do_splitter_match(self, line: Line) -> TMatchResult:
19641978
or self._assert_match(LL)
19651979
or self._assign_match(LL)
19661980
or self._dict_or_lambda_match(LL)
1967-
or self._prefer_paren_wrap_match(LL)
19681981
)
19691982

1983+
if string_idx is None:
1984+
string_idx = self._trailing_comma_tuple_match(line)
1985+
1986+
if string_idx is None:
1987+
string_idx = self._prefer_paren_wrap_match(LL)
1988+
19701989
if string_idx is not None:
19711990
string_value = line.leaves[string_idx].value
19721991
# If the string has neither spaces nor East Asian stops...
@@ -2161,6 +2180,32 @@ def _dict_or_lambda_match(LL: list[Leaf]) -> int | None:
21612180

21622181
return None
21632182

2183+
@staticmethod
2184+
def _trailing_comma_tuple_match(line: Line) -> int | None:
2185+
"""
2186+
Returns:
2187+
string_idx such that @line.leaves[string_idx] is equal to our target
2188+
(i.e. matched) string, if the line is a bare trailing comma tuple
2189+
(STRING + COMMA) not inside brackets.
2190+
OR
2191+
None, otherwise.
2192+
2193+
This handles the case from issue #4912 where a long string with a
2194+
trailing comma (making it a one-item tuple) needs to be wrapped in
2195+
parentheses before splitting to preserve AST equivalence.
2196+
"""
2197+
LL = line.leaves
2198+
# Match: STRING followed by COMMA, not inside brackets
2199+
if (
2200+
not line.inside_brackets
2201+
and len(LL) == 2
2202+
and LL[0].type == token.STRING
2203+
and LL[1].type == token.COMMA
2204+
):
2205+
return 0
2206+
2207+
return None
2208+
21642209
def do_transform(
21652210
self, line: Line, string_indices: list[int]
21662211
) -> Iterator[TResult[Line]]:

tests/data/cases/preview_long_strings.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939

4040
T2 = ("This is a really long string that can't be expected to fit in one line and is the only child of a tuple literal.",)
4141

42+
# Test case for https://github.com/psf/black/issues/4912 - unassigned long string with trailing comma
43+
"A long string literal that is not assigned to a variable, exceeds line length when string-processing is enabled, and has a trailing comma (to make it a one-item tuple)",
44+
4245
func_with_keywords(my_arg, my_kwarg="Long keyword strings also need to be wrapped, but they will probably need to be handled a little bit differently.")
4346

4447
bad_split1 = (
@@ -506,6 +509,13 @@ def foo():
506509
),
507510
)
508511

512+
# Test case for https://github.com/psf/black/issues/4912 - unassigned long string with trailing comma
513+
(
514+
"A long string literal that is not assigned to a variable, exceeds line length when"
515+
" string-processing is enabled, and has a trailing comma (to make it a one-item"
516+
" tuple)"
517+
),
518+
509519
func_with_keywords(
510520
my_arg,
511521
my_kwarg=(

0 commit comments

Comments
 (0)