diff --git a/AUTHORS.txt b/AUTHORS.txt index 451918f..b898061 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -54,6 +54,7 @@ Simon Ruggier (@sruggier) Tim Gates (@timgates42) Batuhan Taskaya (@isidentical) Jocelyn Boullier (@Kazy) +Rupayon Haldar (@rupayon123) Note: (@user) means a github user name. diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a4811fb..56ebeb8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,8 @@ Changelog Unreleased ++++++++++ +- Accept backslashes before braces in f-string literal text. + 0.8.7 (2026-05-02) ++++++++++++++++++ diff --git a/parso/python/tokenize.py b/parso/python/tokenize.py index 64610dd..3295423 100644 --- a/parso/python/tokenize.py +++ b/parso/python/tokenize.py @@ -114,10 +114,11 @@ def _get_token_collection(version_info): unicode_character_name = r'[A-Za-z0-9\-]+(?: [A-Za-z0-9\-]+)*' fstring_string_single_line = _compile( r'(?:\{\{|\}\}|\\N\{' + unicode_character_name - + r'\}|\\(?:\r\n?|\n)|\\[^\r\nN]|[^{}\r\n\\])+' + + r'\}|\\(?:\r\n?|\n)|\\[^\r\nN{}]|\\(?=[{}])|[^{}\r\n\\])+' ) fstring_string_multi_line = _compile( - r'(?:\{\{|\}\}|\\N\{' + unicode_character_name + r'\}|\\[^N]|[^{}\\])+' + r'(?:\{\{|\}\}|\\N\{' + unicode_character_name + + r'\}|\\[^N{}]|\\(?=[{}])|[^{}\\])+' ) fstring_format_spec_single_line = _compile(r'(?:\\(?:\r\n?|\n)|[^{}\r\n])+') fstring_format_spec_multi_line = _compile(r'[^{}]+') diff --git a/test/test_fstring.py b/test/test_fstring.py index c81d027..67807be 100644 --- a/test/test_fstring.py +++ b/test/test_fstring.py @@ -87,6 +87,24 @@ def test_valid(code, grammar): assert fstring.get_code() == code +@pytest.mark.parametrize('prefix', ['f', 'rf']) +@pytest.mark.parametrize('quote', ['"', '"""']) +@pytest.mark.parametrize('text', [r'\{value}', r'\{value:03o}', r'\{{value}}']) +def test_backslash_before_brace(prefix, quote, text, each_version): + code = prefix + quote + text + quote + grammar = load_grammar(version=each_version) + module = grammar.parse(code, error_recovery=False) + assert module.get_code() == code + assert not list(grammar.iter_errors(module)) + + +def test_backslash_before_brace_in_call(grammar): + code = r'stream.write(b_(rf"\{ord_(c):0>3o}"))' + module = grammar.parse(code, error_recovery=False) + assert module.get_code() == code + assert not list(grammar.iter_errors(module)) + + @pytest.mark.parametrize( 'code', [ # an f-string can't contain unmatched curly braces @@ -94,6 +112,10 @@ def test_valid(code, grammar): 'f"{"', 'f"""}"""', 'f"""{"""', + r'f"\}"', + r'rf"\}"', + r'f"""\}"""', + r'rf"""\}"""', # invalid conversion characters 'f"{1!{a}}"',