From 3de01fa77c80d9b98c66881b4311764e9b4c3269 Mon Sep 17 00:00:00 2001 From: Arnesh Banerjee Date: Thu, 30 Jul 2026 14:26:23 +0530 Subject: [PATCH] gh-154711: Strip comments after escaped quotes in f-string and t-string debug text When a replacement field ended with a string that had an escaped quote and was followed by a comment, the comment leaked into the output. It showed up in debug f-string results and in t-string metadata. The comment detection loop in _PyLexer_set_ftstring_expr skips escaped characters, but the comment stripping loop did not. So an escaped quote left the scanner thinking it was still inside a string, and the comment was never removed. This makes the stripping loop skip escaped characters the same way the detection loop does, so the comment is removed as expected. Tests are added for f-strings and t-strings. --- Lib/test/test_fstring.py | 5 +++++ Lib/test/test_tstring.py | 12 ++++++++++++ .../2026-07-30-14-40-00.gh-issue-154711.nUxYXc.rst | 3 +++ Parser/lexer/string.c | 13 +++++++++++++ 4 files changed, 33 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-07-30-14-40-00.gh-issue-154711.nUxYXc.rst diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 45890f8fb35dcb7..60675099f90d5b5 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1678,6 +1678,11 @@ def __repr__(self): self.assertEqual(f'{" # nooo "=}', '" # nooo "=\' # nooo \'') self.assertEqual(f'{" \" # nooo \" "=}', '" \\" # nooo \\" "=\' " # nooo " \'') + # A real comment after a string that ends with an escaped quote must + # still be stripped (gh-154711). + self.assertEqual(f"{'\'' = # comment +}", "'\\'' = \n" + repr("'")) + self.assertEqual(f'{ # some comment goes here """hello"""=}', ' \n """hello"""=\'hello\'') self.assertEqual(f'{"""# this is not a comment diff --git a/Lib/test/test_tstring.py b/Lib/test/test_tstring.py index 74653c77c55de17..9e8d5fdefd14265 100644 --- a/Lib/test/test_tstring.py +++ b/Lib/test/test_tstring.py @@ -140,6 +140,18 @@ def test_debug_specifier(self): ) self.assertEqual(fstring(t), "Value: value = 42") + def test_comment_after_escaped_quote(self): + # A comment after a string that ends with an escaped quote must be + # stripped from the interpolation expression (gh-154711). + t = t"{'a\'b' # comment +}" + self.assertEqual(t.interpolations[0].expression, "'a\\'b'") + + t = t"{'a\'b' = # comment +}" + self.assertEqual(t.interpolations[0].expression, "'a\\'b'") + self.assertEqual(t.strings, ("'a\\'b' = \n", "")) + def test_raw_tstrings(self): path = r"C:\Users" t = rt"{path}\Documents" diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-30-14-40-00.gh-issue-154711.nUxYXc.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-30-14-40-00.gh-issue-154711.nUxYXc.rst new file mode 100644 index 000000000000000..e5f90663ce7583a --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-30-14-40-00.gh-issue-154711.nUxYXc.rst @@ -0,0 +1,3 @@ +Fix a bug where a comment placed after a string that ends with an escaped +quote could leak into the output of a debug f-string and into t-string +metadata. Now the comment is stripped as expected. diff --git a/Parser/lexer/string.c b/Parser/lexer/string.c index e546954ba5c0d82..006764201f62305 100644 --- a/Parser/lexer/string.c +++ b/Parser/lexer/string.c @@ -74,6 +74,19 @@ _PyLexer_set_ftstring_expr(struct tok_state* tok, struct token *token, char c) { while (i < tok_mode->last_expr_size - tok_mode->last_expr_end) { char ch = tok_mode->last_expr_buffer[i]; + // Copy escaped characters as-is. This keeps an escaped quote from + // flipping the in_string state, which would otherwise stop a real + // comment from being detected (see the detection loop above). + if (ch == '\\') { + result[j++] = ch; + i++; + if (i < tok_mode->last_expr_size - tok_mode->last_expr_end) { + result[j++] = tok_mode->last_expr_buffer[i]; + i++; + } + continue; + } + // Handle string quotes if (ch == '"' || ch == '\'') { // See comment above to understand this part