From 6586cee27f32f0354fe4e77c7b8c6e399329b5e2 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Tue, 20 Jun 2023 14:38:46 +0200 Subject: [PATCH] gh-105938: Emit a SyntaxWarning for escaped braces in an f-string (#105939) --- Lib/test/test_fstring.py | 11 +++++++++-- Parser/string_parser.c | 7 ++++++- Parser/tokenizer.c | 6 +++--- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 8f6b576b5f7..ad5ac6a2f04 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -13,6 +13,7 @@ import re import types import decimal import unittest +import warnings from test import support from test.support.os_helper import temp_cwd from test.support.script_helper import assert_python_failure, assert_python_ok @@ -904,7 +905,7 @@ x = ( self.assertEqual(f'2\x203', '2 3') self.assertEqual(f'\x203', ' 3') - with self.assertWarns(DeprecationWarning): # invalid escape sequence + with self.assertWarns(SyntaxWarning): # invalid escape sequence value = eval(r"f'\{6*7}'") self.assertEqual(value, '\\42') with self.assertWarns(SyntaxWarning): # invalid escape sequence @@ -1037,7 +1038,7 @@ x = ( ] for case, expected_result in deprecated_cases: with self.subTest(case=case, expected_result=expected_result): - with self.assertWarns(DeprecationWarning): + with self.assertWarns(SyntaxWarning): result = eval(case) self.assertEqual(result, expected_result) self.assertEqual(fr'\{{\}}', '\\{\\}') @@ -1046,6 +1047,12 @@ x = ( self.assertEqual(fr'\}}{1+1}', '\\}2') self.assertEqual(fr'{1+1}\}}', '2\\}') + def test_fstring_backslash_before_double_bracket_warns_once(self): + with warnings.catch_warnings(record=True) as w: + eval(r"f'\{{'") + self.assertEqual(len(w), 1) + self.assertEqual(w[0].category, SyntaxWarning) + def test_fstring_backslash_prefix_raw(self): self.assertEqual(f'\\', '\\') self.assertEqual(f'\\\\', '\\\\') diff --git a/Parser/string_parser.c b/Parser/string_parser.c index d4ce33850f7..20459e89463 100644 --- a/Parser/string_parser.c +++ b/Parser/string_parser.c @@ -12,6 +12,11 @@ static int warn_invalid_escape_sequence(Parser *p, const char *first_invalid_escape, Token *t) { unsigned char c = *first_invalid_escape; + if ((t->type == FSTRING_MIDDLE || t->type == FSTRING_END) && (c == '{' || c == '}')) { // in this case the tokenizer has already emitted a warning, + // see tokenizer.c:warn_invalid_escape_sequence + return 0; + } + int octal = ('4' <= c && c <= '7'); PyObject *msg = octal @@ -31,7 +36,7 @@ warn_invalid_escape_sequence(Parser *p, const char *first_invalid_escape, Token if (PyErr_WarnExplicitObject(category, msg, p->tok->filename, t->lineno, NULL, NULL) < 0) { if (PyErr_ExceptionMatches(category)) { - /* Replace the DeprecationWarning exception with a SyntaxError + /* Replace the Syntax/DeprecationWarning exception with a SyntaxError to get a more accurate error report */ PyErr_Clear(); diff --git a/Parser/tokenizer.c b/Parser/tokenizer.c index 4f7b1f8114e..6bdf371a7ad 100644 --- a/Parser/tokenizer.c +++ b/Parser/tokenizer.c @@ -1559,12 +1559,12 @@ warn_invalid_escape_sequence(struct tok_state *tok, int first_invalid_escape_cha return -1; } - if (PyErr_WarnExplicitObject(PyExc_DeprecationWarning, msg, tok->filename, + if (PyErr_WarnExplicitObject(PyExc_SyntaxWarning, msg, tok->filename, tok->lineno, NULL, NULL) < 0) { Py_DECREF(msg); - if (PyErr_ExceptionMatches(PyExc_DeprecationWarning)) { - /* Replace the DeprecationWarning exception with a SyntaxError + if (PyErr_ExceptionMatches(PyExc_SyntaxWarning)) { + /* Replace the SyntaxWarning exception with a SyntaxError to get a more accurate error report */ PyErr_Clear(); return syntaxerror(tok, "invalid escape sequence '\\%c'", (char) first_invalid_escape_char);