Skip to content

Commit 9c67bd6

Browse files
Handle f-strings silent concatenation (#6722)
1 parent f495718 commit 9c67bd6

2 files changed

Lines changed: 44 additions & 21 deletions

File tree

rewrite-python/rewrite/src/rewrite/python/_parser_visitor.py

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3181,20 +3181,6 @@ def __map_fstring(self, node: ast.JoinedStr, prefix: Space, tok: TokenInfo, valu
31813181
tok = self._advance_token()
31823182
continue
31833183

3184-
# Handle nested FSTRING_START - this indicates nested f-string concatenation
3185-
# which is not fully supported. Skip to prevent infinite loop.
3186-
if tok.type == FSTRING_START:
3187-
# Skip until we find the matching FSTRING_END
3188-
depth = 1
3189-
while depth > 0 and self._token_idx < len(self._tokens):
3190-
tok = self._advance_token()
3191-
if tok.type == FSTRING_START:
3192-
depth += 1
3193-
elif tok.type == FSTRING_END:
3194-
depth -= 1
3195-
tok = self._advance_token() if self._token_idx < len(self._tokens) - 1 else tok
3196-
continue
3197-
31983184
value = node.values[value_idx]
31993185
if tok.type == FSTRING_MIDDLE:
32003186
# Accumulate text from consecutive FSTRING_MIDDLE tokens
@@ -3228,12 +3214,33 @@ def __map_fstring(self, node: ast.JoinedStr, prefix: Space, tok: TokenInfo, valu
32283214
value = node.values[value_idx]
32293215

32303216
if isinstance(cast(ast.FormattedValue, value).value, ast.JoinedStr):
3231-
nested, tok, _ = self.__map_fstring(cast(ast.JoinedStr, cast(ast.FormattedValue, value).value),
3232-
Space.EMPTY, tok)
3233-
expr = self.__pad_right(
3234-
nested,
3235-
Space.EMPTY
3236-
)
3217+
joined = cast(ast.JoinedStr, cast(ast.FormattedValue, value).value)
3218+
nested, tok, inner_vi = self.__map_fstring(joined, Space.EMPTY, tok)
3219+
3220+
# Handle concatenated f-strings/strings within this expression
3221+
while True:
3222+
peek_tok, _ = self._peek_significant_token()
3223+
if peek_tok.type not in (FSTRING_START, token.STRING):
3224+
break
3225+
concat_prefix = self.__whitespace()
3226+
tok = self._tokens[self._token_idx]
3227+
if tok.type == FSTRING_START:
3228+
right, tok, inner_vi = self.__map_fstring(joined, concat_prefix, tok, inner_vi)
3229+
else:
3230+
ast_val = (joined.values[inner_vi]
3231+
if inner_vi < len(joined.values)
3232+
else ast.Constant(value=ast.literal_eval(tok.string)))
3233+
right, tok = self.__map_literal(ast_val, tok)
3234+
right = right.replace(prefix=concat_prefix)
3235+
nested = py.Binary(
3236+
random_id(), Space.EMPTY, Markers.EMPTY,
3237+
nested,
3238+
self.__pad_left(Space.EMPTY, py.Binary.Type.StringConcatenation),
3239+
None, right,
3240+
self._type_mapping.type(node)
3241+
)
3242+
3243+
expr = self.__pad_right(nested, Space.EMPTY)
32373244
else:
32383245
expr = self.__pad_right(
32393246
self.__convert(cast(ast.FormattedValue, value).value),

rewrite-python/rewrite/tests/python/all/tree/fstring_test.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ def test_concat_fstring_6():
8585
))
8686

8787

88-
@pytest.mark.xfail(reason="F-strings with nested f-string literal concatenations are not yet supported", strict=True)
8988
def test_concat_fstring_7():
9089
# language=python
9190
RecipeSpec().rewrite_run(python("""
@@ -247,3 +246,20 @@ def test_nested_fstring_with_format_value():
247246
def test_adjoining_expressions():
248247
# language=python
249248
RecipeSpec().rewrite_run(python("""a = f'{1}{0}'"""))
249+
250+
251+
def test_nested_fstring_in_expression():
252+
# language=python
253+
RecipeSpec().rewrite_run(python(
254+
"""
255+
name = "Alice"
256+
score = 87
257+
max_score = 100
258+
259+
percentage = (score / max_score) * 100
260+
261+
message = f"{name} scored {score}/{max_score}, which is {f'{percentage:.1f}'}% of the total."
262+
263+
print(message)
264+
"""
265+
))

0 commit comments

Comments
 (0)