Skip to content

Commit ecc3c8e

Browse files
authored
bpo-34013: Move the Python 2 hints from the exception constructor to the parser (GH-27392)
1 parent 6948964 commit ecc3c8e

File tree

5 files changed

+28
-224
lines changed

5 files changed

+28
-224
lines changed

Grammar/python.gram

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -848,9 +848,10 @@ expression_without_invalid[expr_ty]:
848848
| disjunction
849849
| lambdef
850850
invalid_legacy_expression:
851-
| a=NAME b=expression_without_invalid {
852-
_PyPegen_check_legacy_stmt(p, a) ? RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "Missing parentheses in call to '%U'.", a->v.Name.id) : NULL}
853-
851+
| a=NAME b=star_expressions {
852+
_PyPegen_check_legacy_stmt(p, a) ? RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b,
853+
"Missing parentheses in call to '%U'. Did you mean %U(...)?", a->v.Name.id, a->v.Name.id) : NULL}
854+
854855
invalid_expression:
855856
| invalid_legacy_expression
856857
# !(NAME STRING) is not matched so we don't show this error with some invalid string prefixes like: kf"dsfsdf"

Lib/test/test_exceptions.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -168,21 +168,19 @@ def ckmsg(src, msg, exception=SyntaxError):
168168
self.fail("failed to get expected SyntaxError")
169169

170170
s = '''print "old style"'''
171-
ckmsg(s, "Missing parentheses in call to 'print'. "
172-
"Did you mean print(\"old style\")?")
171+
ckmsg(s, "Missing parentheses in call to 'print'. Did you mean print(...)?")
173172

174173
s = '''print "old style",'''
175-
ckmsg(s, "Missing parentheses in call to 'print'. "
176-
"Did you mean print(\"old style\", end=\" \")?")
174+
ckmsg(s, "Missing parentheses in call to 'print'. Did you mean print(...)?")
177175

178176
s = 'print f(a+b,c)'
179-
ckmsg(s, "Missing parentheses in call to 'print'.")
177+
ckmsg(s, "Missing parentheses in call to 'print'. Did you mean print(...)?")
180178

181179
s = '''exec "old style"'''
182-
ckmsg(s, "Missing parentheses in call to 'exec'")
180+
ckmsg(s, "Missing parentheses in call to 'exec'. Did you mean exec(...)?")
183181

184182
s = 'exec f(a+b,c)'
185-
ckmsg(s, "Missing parentheses in call to 'exec'.")
183+
ckmsg(s, "Missing parentheses in call to 'exec'. Did you mean exec(...)?")
186184

187185
# should not apply to subclasses, see issue #31161
188186
s = '''if True:\nprint "No indent"'''

Lib/test/test_print.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,21 +140,24 @@ def test_normal_string(self):
140140
with self.assertRaises(SyntaxError) as context:
141141
exec(python2_print_str)
142142

143-
self.assertIn('print("Hello World")', str(context.exception))
143+
self.assertIn("Missing parentheses in call to 'print'. Did you mean print(...)",
144+
str(context.exception))
144145

145146
def test_string_with_soft_space(self):
146147
python2_print_str = 'print "Hello World",'
147148
with self.assertRaises(SyntaxError) as context:
148149
exec(python2_print_str)
149150

150-
self.assertIn('print("Hello World", end=" ")', str(context.exception))
151+
self.assertIn("Missing parentheses in call to 'print'. Did you mean print(...)",
152+
str(context.exception))
151153

152154
def test_string_with_excessive_whitespace(self):
153155
python2_print_str = 'print "Hello World", '
154156
with self.assertRaises(SyntaxError) as context:
155157
exec(python2_print_str)
156158

157-
self.assertIn('print("Hello World", end=" ")', str(context.exception))
159+
self.assertIn("Missing parentheses in call to 'print'. Did you mean print(...)",
160+
str(context.exception))
158161

159162
def test_string_with_leading_whitespace(self):
160163
python2_print_str = '''if 1:
@@ -163,7 +166,8 @@ def test_string_with_leading_whitespace(self):
163166
with self.assertRaises(SyntaxError) as context:
164167
exec(python2_print_str)
165168

166-
self.assertIn('print("Hello World")', str(context.exception))
169+
self.assertIn("Missing parentheses in call to 'print'. Did you mean print(...)",
170+
str(context.exception))
167171

168172
# bpo-32685: Suggestions for print statement should be proper when
169173
# it is in the same line as the header of a compound statement
@@ -173,14 +177,16 @@ def test_string_with_semicolon(self):
173177
with self.assertRaises(SyntaxError) as context:
174178
exec(python2_print_str)
175179

176-
self.assertIn('print(p)', str(context.exception))
180+
self.assertIn("Missing parentheses in call to 'print'. Did you mean print(...)",
181+
str(context.exception))
177182

178183
def test_string_in_loop_on_same_line(self):
179184
python2_print_str = 'for i in s: print i'
180185
with self.assertRaises(SyntaxError) as context:
181186
exec(python2_print_str)
182187

183-
self.assertIn('print(i)', str(context.exception))
188+
self.assertIn("Missing parentheses in call to 'print'. Did you mean print(...)",
189+
str(context.exception))
184190

185191
def test_stream_redirection_hint_for_py2_migration(self):
186192
# Test correct hint produced for Py2 redirection syntax

Objects/exceptions.c

Lines changed: 0 additions & 201 deletions
Original file line numberDiff line numberDiff line change
@@ -1475,9 +1475,6 @@ ComplexExtendsException(PyExc_Exception, AttributeError,
14751475
* SyntaxError extends Exception
14761476
*/
14771477

1478-
/* Helper function to customize error message for some syntax errors */
1479-
static int _report_missing_parentheses(PySyntaxErrorObject *self);
1480-
14811478
static int
14821479
SyntaxError_init(PySyntaxErrorObject *self, PyObject *args, PyObject *kwds)
14831480
{
@@ -1520,18 +1517,6 @@ SyntaxError_init(PySyntaxErrorObject *self, PyObject *args, PyObject *kwds)
15201517
PyErr_SetString(PyExc_TypeError, "end_offset must be provided when end_lineno is provided");
15211518
return -1;
15221519
}
1523-
1524-
/*
1525-
* Issue #21669: Custom error for 'print' & 'exec' as statements
1526-
*
1527-
* Only applies to SyntaxError instances, not to subclasses such
1528-
* as TabError or IndentationError (see issue #31161)
1529-
*/
1530-
if (Py_IS_TYPE(self, (PyTypeObject *)PyExc_SyntaxError) &&
1531-
self->text && PyUnicode_Check(self->text) &&
1532-
_report_missing_parentheses(self) < 0) {
1533-
return -1;
1534-
}
15351520
}
15361521
return 0;
15371522
}
@@ -3033,189 +3018,3 @@ _PyErr_TrySetFromCause(const char *format, ...)
30333018
PyErr_Restore(new_exc, new_val, new_tb);
30343019
return new_val;
30353020
}
3036-
3037-
3038-
/* To help with migration from Python 2, SyntaxError.__init__ applies some
3039-
* heuristics to try to report a more meaningful exception when print and
3040-
* exec are used like statements.
3041-
*
3042-
* The heuristics are currently expected to detect the following cases:
3043-
* - top level statement
3044-
* - statement in a nested suite
3045-
* - trailing section of a one line complex statement
3046-
*
3047-
* They're currently known not to trigger:
3048-
* - after a semi-colon
3049-
*
3050-
* The error message can be a bit odd in cases where the "arguments" are
3051-
* completely illegal syntactically, but that isn't worth the hassle of
3052-
* fixing.
3053-
*
3054-
* We also can't do anything about cases that are legal Python 3 syntax
3055-
* but mean something entirely different from what they did in Python 2
3056-
* (omitting the arguments entirely, printing items preceded by a unary plus
3057-
* or minus, using the stream redirection syntax).
3058-
*/
3059-
3060-
3061-
// Static helper for setting legacy print error message
3062-
static int
3063-
_set_legacy_print_statement_msg(PySyntaxErrorObject *self, Py_ssize_t start)
3064-
{
3065-
// PRINT_OFFSET is to remove the `print ` prefix from the data.
3066-
const int PRINT_OFFSET = 6;
3067-
const int STRIP_BOTH = 2;
3068-
Py_ssize_t start_pos = start + PRINT_OFFSET;
3069-
Py_ssize_t text_len = PyUnicode_GET_LENGTH(self->text);
3070-
Py_UCS4 semicolon = ';';
3071-
Py_ssize_t end_pos = PyUnicode_FindChar(self->text, semicolon,
3072-
start_pos, text_len, 1);
3073-
if (end_pos < -1) {
3074-
return -1;
3075-
} else if (end_pos == -1) {
3076-
end_pos = text_len;
3077-
}
3078-
3079-
PyObject *data = PyUnicode_Substring(self->text, start_pos, end_pos);
3080-
if (data == NULL) {
3081-
return -1;
3082-
}
3083-
3084-
PyObject *strip_sep_obj = PyUnicode_FromString(" \t\r\n");
3085-
if (strip_sep_obj == NULL) {
3086-
Py_DECREF(data);
3087-
return -1;
3088-
}
3089-
3090-
PyObject *new_data = _PyUnicode_XStrip(data, STRIP_BOTH, strip_sep_obj);
3091-
Py_DECREF(data);
3092-
Py_DECREF(strip_sep_obj);
3093-
if (new_data == NULL) {
3094-
return -1;
3095-
}
3096-
// gets the modified text_len after stripping `print `
3097-
text_len = PyUnicode_GET_LENGTH(new_data);
3098-
const char *maybe_end_arg = "";
3099-
if (text_len > 0 && PyUnicode_READ_CHAR(new_data, text_len-1) == ',') {
3100-
maybe_end_arg = " end=\" \"";
3101-
}
3102-
PyObject *error_msg = PyUnicode_FromFormat(
3103-
"Missing parentheses in call to 'print'. Did you mean print(%U%s)?",
3104-
new_data, maybe_end_arg
3105-
);
3106-
Py_DECREF(new_data);
3107-
if (error_msg == NULL)
3108-
return -1;
3109-
3110-
Py_XSETREF(self->msg, error_msg);
3111-
return 1;
3112-
}
3113-
3114-
static int
3115-
_check_for_legacy_statements(PySyntaxErrorObject *self, Py_ssize_t start)
3116-
{
3117-
/* Return values:
3118-
* -1: an error occurred
3119-
* 0: nothing happened
3120-
* 1: the check triggered & the error message was changed
3121-
*/
3122-
static PyObject *print_prefix = NULL;
3123-
static PyObject *exec_prefix = NULL;
3124-
Py_ssize_t text_len = PyUnicode_GET_LENGTH(self->text), match;
3125-
int kind = PyUnicode_KIND(self->text);
3126-
const void *data = PyUnicode_DATA(self->text);
3127-
3128-
/* Ignore leading whitespace */
3129-
while (start < text_len) {
3130-
Py_UCS4 ch = PyUnicode_READ(kind, data, start);
3131-
if (!Py_UNICODE_ISSPACE(ch))
3132-
break;
3133-
start++;
3134-
}
3135-
/* Checking against an empty or whitespace-only part of the string */
3136-
if (start == text_len) {
3137-
return 0;
3138-
}
3139-
3140-
/* Check for legacy print statements */
3141-
if (print_prefix == NULL) {
3142-
print_prefix = PyUnicode_InternFromString("print ");
3143-
if (print_prefix == NULL) {
3144-
return -1;
3145-
}
3146-
}
3147-
match = PyUnicode_Tailmatch(self->text, print_prefix,
3148-
start, text_len, -1);
3149-
if (match == -1) {
3150-
return -1;
3151-
}
3152-
if (match) {
3153-
return _set_legacy_print_statement_msg(self, start);
3154-
}
3155-
3156-
/* Check for legacy exec statements */
3157-
if (exec_prefix == NULL) {
3158-
exec_prefix = PyUnicode_InternFromString("exec ");
3159-
if (exec_prefix == NULL) {
3160-
return -1;
3161-
}
3162-
}
3163-
match = PyUnicode_Tailmatch(self->text, exec_prefix, start, text_len, -1);
3164-
if (match == -1) {
3165-
return -1;
3166-
}
3167-
if (match) {
3168-
PyObject *msg = PyUnicode_FromString("Missing parentheses in call "
3169-
"to 'exec'");
3170-
if (msg == NULL) {
3171-
return -1;
3172-
}
3173-
Py_XSETREF(self->msg, msg);
3174-
return 1;
3175-
}
3176-
/* Fall back to the default error message */
3177-
return 0;
3178-
}
3179-
3180-
static int
3181-
_report_missing_parentheses(PySyntaxErrorObject *self)
3182-
{
3183-
Py_UCS4 left_paren = 40;
3184-
Py_ssize_t left_paren_index;
3185-
Py_ssize_t text_len = PyUnicode_GET_LENGTH(self->text);
3186-
int legacy_check_result = 0;
3187-
3188-
/* Skip entirely if there is an opening parenthesis */
3189-
left_paren_index = PyUnicode_FindChar(self->text, left_paren,
3190-
0, text_len, 1);
3191-
if (left_paren_index < -1) {
3192-
return -1;
3193-
}
3194-
if (left_paren_index != -1) {
3195-
/* Use default error message for any line with an opening paren */
3196-
return 0;
3197-
}
3198-
/* Handle the simple statement case */
3199-
legacy_check_result = _check_for_legacy_statements(self, 0);
3200-
if (legacy_check_result < 0) {
3201-
return -1;
3202-
3203-
}
3204-
if (legacy_check_result == 0) {
3205-
/* Handle the one-line complex statement case */
3206-
Py_UCS4 colon = 58;
3207-
Py_ssize_t colon_index;
3208-
colon_index = PyUnicode_FindChar(self->text, colon,
3209-
0, text_len, 1);
3210-
if (colon_index < -1) {
3211-
return -1;
3212-
}
3213-
if (colon_index >= 0 && colon_index < text_len) {
3214-
/* Check again, starting from just after the colon */
3215-
if (_check_for_legacy_statements(self, colon_index+1) < 0) {
3216-
return -1;
3217-
}
3218-
}
3219-
}
3220-
return 0;
3221-
}

Parser/parser.c

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18158,7 +18158,7 @@ expression_without_invalid_rule(Parser *p)
1815818158
return _res;
1815918159
}
1816018160

18161-
// invalid_legacy_expression: NAME expression_without_invalid
18161+
// invalid_legacy_expression: NAME star_expressions
1816218162
static void *
1816318163
invalid_legacy_expression_rule(Parser *p)
1816418164
{
@@ -18169,22 +18169,22 @@ invalid_legacy_expression_rule(Parser *p)
1816918169
}
1817018170
void * _res = NULL;
1817118171
int _mark = p->mark;
18172-
{ // NAME expression_without_invalid
18172+
{ // NAME star_expressions
1817318173
if (p->error_indicator) {
1817418174
D(p->level--);
1817518175
return NULL;
1817618176
}
18177-
D(fprintf(stderr, "%*c> invalid_legacy_expression[%d-%d]: %s\n", p->level, ' ', _mark, p->mark, "NAME expression_without_invalid"));
18177+
D(fprintf(stderr, "%*c> invalid_legacy_expression[%d-%d]: %s\n", p->level, ' ', _mark, p->mark, "NAME star_expressions"));
1817818178
expr_ty a;
1817918179
expr_ty b;
1818018180
if (
1818118181
(a = _PyPegen_name_token(p)) // NAME
1818218182
&&
18183-
(b = expression_without_invalid_rule(p)) // expression_without_invalid
18183+
(b = star_expressions_rule(p)) // star_expressions
1818418184
)
1818518185
{
18186-
D(fprintf(stderr, "%*c+ invalid_legacy_expression[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "NAME expression_without_invalid"));
18187-
_res = _PyPegen_check_legacy_stmt ( p , a ) ? RAISE_SYNTAX_ERROR_KNOWN_RANGE ( a , b , "Missing parentheses in call to '%U'." , a -> v . Name . id ) : NULL;
18186+
D(fprintf(stderr, "%*c+ invalid_legacy_expression[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "NAME star_expressions"));
18187+
_res = _PyPegen_check_legacy_stmt ( p , a ) ? RAISE_SYNTAX_ERROR_KNOWN_RANGE ( a , b , "Missing parentheses in call to '%U'. Did you mean %U(...)?" , a -> v . Name . id , a -> v . Name . id ) : NULL;
1818818188
if (_res == NULL && PyErr_Occurred()) {
1818918189
p->error_indicator = 1;
1819018190
D(p->level--);
@@ -18194,7 +18194,7 @@ invalid_legacy_expression_rule(Parser *p)
1819418194
}
1819518195
p->mark = _mark;
1819618196
D(fprintf(stderr, "%*c%s invalid_legacy_expression[%d-%d]: %s failed!\n", p->level, ' ',
18197-
p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "NAME expression_without_invalid"));
18197+
p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "NAME star_expressions"));
1819818198
}
1819918199
_res = NULL;
1820018200
done:

0 commit comments

Comments
 (0)