diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 45890f8fb35dcb..60675099f90d5b 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 74653c77c55de1..9e8d5fdefd1426 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 00000000000000..e5f90663ce7583 --- /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 e546954ba5c0d8..006764201f6230 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