From 3192c7de3680f991f9723e339da56c78e9f89b48 Mon Sep 17 00:00:00 2001 From: dataflow-solutions-sk Date: Sun, 16 Aug 2026 20:40:21 +0200 Subject: [PATCH] Fix SyntaxError for backslash before { in raw f-strings Raw f-strings (rf..., fr...) do not process backslash escape sequences, so a backslash immediately before a '{' must not suppress the opening of a replacement field, e.g. rfscript\temp\{c}.png is valid syntax to CPython. The f-string tokenizer regexes (fstring_string_single_line / fstring_string_multi_line) unconditionally treated any backslash followed by a non-newline/non-N character as a single escaped unit, swallowing the following { as literal text. FStringNode was never told whether the enclosing f-string was raw. Track raw-ness on FStringNode (derived from the string prefix at the point FSTRING_START is tokenized) and use dedicated regexes for raw f-strings where a backslash is ordinary literal text (braces/{{ }} still behave normally, and backslash-newline line continuation for single-line raw f-strings is preserved). Non-raw f-string tokenization is unchanged. Fixes davidhalter/parso#197 --- parso/python/tokenize.py | 29 ++++++++++++++++++++++++----- test/test_fstring.py | 14 ++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/parso/python/tokenize.py b/parso/python/tokenize.py index 64610ddb..814a17e1 100644 --- a/parso/python/tokenize.py +++ b/parso/python/tokenize.py @@ -119,6 +119,15 @@ def _get_token_collection(version_info): fstring_string_multi_line = _compile( r'(?:\{\{|\}\}|\\N\{' + unicode_character_name + r'\}|\\[^N]|[^{}\\])+' ) +# Raw f-strings don't process backslash escapes at all, so a backslash is +# just ordinary literal text and must not swallow a following '{'/'}' (the +# `\N{...}` unicode-name escape doesn't exist either). Only the +# backslash-newline line continuation still needs special handling, because +# an unescaped newline would otherwise end a single-line string. +fstring_raw_string_single_line = _compile( + r'(?:\{\{|\}\}|\\(?:\r\n?|\n)|[^{}\r\n])+' +) +fstring_raw_string_multi_line = _compile(r'(?:\{\{|\}\}|[^{}])+') fstring_format_spec_single_line = _compile(r'(?:\\(?:\r\n?|\n)|[^{}\r\n])+') fstring_format_spec_multi_line = _compile(r'[^{}]+') @@ -253,8 +262,9 @@ def __repr__(self): class FStringNode: - def __init__(self, quote): + def __init__(self, quote, raw): self.quote = quote + self.raw = raw self.parentheses_count = 0 self.previous_lines = '' self.last_string_start_pos: Any = None @@ -308,10 +318,16 @@ def _find_fstring_string(endpats, fstring_stack, line, lnum, pos): else: regex = fstring_format_spec_single_line else: - if allow_multiline: - regex = fstring_string_multi_line + if tos.raw: + if allow_multiline: + regex = fstring_raw_string_multi_line + else: + regex = fstring_raw_string_single_line else: - regex = fstring_string_single_line + if allow_multiline: + regex = fstring_string_multi_line + else: + regex = fstring_string_single_line match = regex.match(line, pos) if match is None: @@ -593,7 +609,10 @@ def dedent_if_necessary(start): else: # ordinary string yield PythonToken(STRING, token, spos, prefix) elif token in fstring_pattern_map: # The start of an fstring. - fstring_stack.append(FStringNode(fstring_pattern_map[token])) + fstring_stack.append(FStringNode( + fstring_pattern_map[token], + raw='r' in token.rstrip('"\'').lower(), + )) yield PythonToken(FSTRING_START, token, spos, prefix) elif initial == '\\' and line[start:] in ('\\\n', '\\\r\n', '\\\r'): # continued stmt additional_prefix += prefix + line[start:] diff --git a/test/test_fstring.py b/test/test_fstring.py index c81d027a..7afc2965 100644 --- a/test/test_fstring.py +++ b/test/test_fstring.py @@ -78,6 +78,16 @@ def grammar(): 'f"\\N{SOYOMBO LETTER -A}"', 'f"\\N{DOMINO TILE HORIZONTAL-00-00}"', 'f"""\\N{NO ENTRY}"""', + + # raw f-strings: backslashes are literal and don't escape braces + 'rf"script\\{c}.png"', + 'rf"script\\\\{c}.png"', + 'fr"script\\{c}.png"', + 'Rf"script\\{c}.png"', + 'fR"script\\{c}.png"', + 'rf"{{}}\\{c}"', + 'rf"""script\\{c}.png"""', + 'rf"""abc\\\ndef{1}"""', ] ) def test_valid(code, grammar): @@ -120,6 +130,10 @@ def test_valid(code, grammar): 'f"\\N{ BULLET }"', 'f"\\N{NO ENTRY}"', 'f"""\\N{NO\nENTRY}"""', + + # in a raw f-string a backslash doesn't escape braces, so a lone + # backslash followed by an unmatched '}' is still an unmatched brace + 'rf"\\}"', ] ) def test_invalid(code, grammar):