Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AUTHORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,6 @@ Batuhan Taskaya (@isidentical) <isidentical@gmail.com>
Jocelyn Boullier (@Kazy) <jocelyn@boullier.bzh>


Sarbajit Sengupta (@Sarbajit-Sengupta)

Note: (@user) means a github user name.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Changelog
Unreleased
++++++++++

- Fix f-string tokenization when a backslash precedes a replacement-field
brace, including raw f-strings with format specifications (#207).

0.8.7 (2026-05-02)
++++++++++++++++++

Expand Down
4 changes: 2 additions & 2 deletions parso/python/tokenize.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ 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'[^{}]+')
Expand Down
31 changes: 31 additions & 0 deletions test/test_fstring_backslash_braces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""A backslash does not escape replacement-field braces in f-strings."""

import ast

import pytest

import parso
from parso.parser import ParserSyntaxError


@pytest.mark.parametrize('prefix', ['rf', 'fr', 'RF', 'FR'])
@pytest.mark.parametrize('quote', ['"', "'", '"""', "'''"])
@pytest.mark.parametrize('slashes', [1, 2, 3])
def test_raw_fstring_backslash_before_field(prefix, quote, slashes):
code = prefix + quote + '\\' * slashes + '{value:0>3o}' + quote
ast.parse(code)
grammar = parso.load_grammar(version='3.13')
module = grammar.parse(code, error_recovery=False)
assert module.get_code() == code
assert not list(grammar.iter_errors(module))
assert any(leaf.value == 'value' for leaf in module.get_used_names()['value'])


@pytest.mark.parametrize('prefix', ['rf', 'fr'])
@pytest.mark.parametrize('quote', ['"', '"""'])
def test_backslash_cannot_escape_unmatched_closing_brace(prefix, quote):
code = prefix + quote + '\\}' + quote
with pytest.raises(SyntaxError):
ast.parse(code)
with pytest.raises(ParserSyntaxError):
parso.parse(code, version='3.13', error_recovery=False)
Loading