Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5b1fdc1

Browse files
committed
Issue #21669: Special case print & exec syntax errors
1 parent b6d1f48 commit 5b1fdc1

3 files changed

Lines changed: 165 additions & 0 deletions

File tree

Lib/test/test_grammar.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,31 @@ def test_expr_stmt(self):
390390
check_syntax_error(self, "x + 1 = 1")
391391
check_syntax_error(self, "a + 1 = b + 2")
392392

393+
# Check the heuristic for print & exec covers significant cases
394+
# As well as placing some limits on false positives
395+
def test_former_statements_refer_to_builtins(self):
396+
keywords = "print", "exec"
397+
# Cases where we want the custom error
398+
cases = [
399+
"{} foo",
400+
"{} {{1:foo}}",
401+
"if 1: {} foo",
402+
"if 1: {} {{1:foo}}",
403+
"if 1:\n {} foo",
404+
"if 1:\n {} {{1:foo}}",
405+
]
406+
for keyword in keywords:
407+
custom_msg = "call to '{}'".format(keyword)
408+
for case in cases:
409+
source = case.format(keyword)
410+
with self.subTest(source=source):
411+
with self.assertRaisesRegex(SyntaxError, custom_msg):
412+
exec(source)
413+
source = source.replace("foo", "(foo.)")
414+
with self.subTest(source=source):
415+
with self.assertRaisesRegex(SyntaxError, "invalid syntax"):
416+
exec(source)
417+
393418
def test_del_stmt(self):
394419
# 'del' exprlist
395420
abc = [1,2,3]

Misc/NEWS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ Release date: XXXX-XX-XX
1010
Core and Builtins
1111
-----------------
1212

13+
- Issue #21669: With the aid of heuristics in SyntaxError.__init__, the
14+
parser now attempts to generate more meaningful (or at least more search
15+
engine friendly) error messages when "exec" and "print" are used as
16+
statements.
17+
1318
- Issue #21642: If the conditional if-else expression, allow an integer written
1419
with no space between itself and the ``else`` keyword (e.g. ``True if 42else
1520
False``) to be valid syntax.

Objects/exceptions.c

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1254,6 +1254,9 @@ SimpleExtendsException(PyExc_Exception, AttributeError,
12541254
* SyntaxError extends Exception
12551255
*/
12561256

1257+
/* Helper function to customise error message for some syntax errors */
1258+
static int _report_missing_parentheses(PySyntaxErrorObject *self);
1259+
12571260
static int
12581261
SyntaxError_init(PySyntaxErrorObject *self, PyObject *args, PyObject *kwds)
12591262
{
@@ -1298,6 +1301,13 @@ SyntaxError_init(PySyntaxErrorObject *self, PyObject *args, PyObject *kwds)
12981301
Py_INCREF(self->text);
12991302

13001303
Py_DECREF(info);
1304+
1305+
/* Issue #21669: Custom error for 'print' & 'exec' as statements */
1306+
if (self->text && PyUnicode_Check(self->text)) {
1307+
if (_report_missing_parentheses(self) < 0) {
1308+
return -1;
1309+
}
1310+
}
13011311
}
13021312
return 0;
13031313
}
@@ -2783,3 +2793,128 @@ _PyErr_TrySetFromCause(const char *format, ...)
27832793
PyErr_Restore(new_exc, new_val, new_tb);
27842794
return new_val;
27852795
}
2796+
2797+
2798+
/* To help with migration from Python 2, SyntaxError.__init__ applies some
2799+
* heuristics to try to report a more meaningful exception when print and
2800+
* exec are used like statements.
2801+
*
2802+
* The heuristics are currently expected to detect the following cases:
2803+
* - top level statement
2804+
* - statement in a nested suite
2805+
* - trailing section of a one line complex statement
2806+
*
2807+
* They're currently known not to trigger:
2808+
* - after a semi-colon
2809+
*
2810+
* The error message can be a bit odd in cases where the "arguments" are
2811+
* completely illegal syntactically, but that isn't worth the hassle of
2812+
* fixing.
2813+
*
2814+
* We also can't do anything about cases that are legal Python 3 syntax
2815+
* but mean something entirely different from what they did in Python 2
2816+
* (omitting the arguments entirely, printing items preceded by a unary plus
2817+
* or minus, using the stream redirection syntax).
2818+
*/
2819+
2820+
static int
2821+
_check_for_legacy_statements(PySyntaxErrorObject *self, Py_ssize_t start)
2822+
{
2823+
/* Return values:
2824+
* -1: an error occurred
2825+
* 0: nothing happened
2826+
* 1: the check triggered & the error message was changed
2827+
*/
2828+
static PyObject *print_prefix = NULL;
2829+
static PyObject *exec_prefix = NULL;
2830+
Py_ssize_t text_len = PyUnicode_GET_LENGTH(self->text);
2831+
int kind = PyUnicode_KIND(self->text);
2832+
void *data = PyUnicode_DATA(self->text);
2833+
2834+
/* Ignore leading whitespace */
2835+
while (start < text_len) {
2836+
Py_UCS4 ch = PyUnicode_READ(kind, data, start);
2837+
if (!Py_UNICODE_ISSPACE(ch))
2838+
break;
2839+
start++;
2840+
}
2841+
/* Checking against an empty or whitespace-only part of the string */
2842+
if (start == text_len) {
2843+
return 0;
2844+
}
2845+
2846+
/* Check for legacy print statements */
2847+
if (print_prefix == NULL) {
2848+
print_prefix = PyUnicode_InternFromString("print ");
2849+
if (print_prefix == NULL) {
2850+
return -1;
2851+
}
2852+
}
2853+
if (PyUnicode_Tailmatch(self->text, print_prefix,
2854+
start, text_len, -1)) {
2855+
Py_CLEAR(self->msg);
2856+
self->msg = PyUnicode_FromString(
2857+
"Missing parentheses in call to 'print'");
2858+
return 1;
2859+
}
2860+
2861+
/* Check for legacy exec statements */
2862+
if (exec_prefix == NULL) {
2863+
exec_prefix = PyUnicode_InternFromString("exec ");
2864+
if (exec_prefix == NULL) {
2865+
return -1;
2866+
}
2867+
}
2868+
if (PyUnicode_Tailmatch(self->text, exec_prefix,
2869+
start, text_len, -1)) {
2870+
Py_CLEAR(self->msg);
2871+
self->msg = PyUnicode_FromString(
2872+
"Missing parentheses in call to 'exec'");
2873+
return 1;
2874+
}
2875+
/* Fall back to the default error message */
2876+
return 0;
2877+
}
2878+
2879+
static int
2880+
_report_missing_parentheses(PySyntaxErrorObject *self)
2881+
{
2882+
Py_UCS4 left_paren = 40;
2883+
Py_ssize_t left_paren_index;
2884+
Py_ssize_t text_len = PyUnicode_GET_LENGTH(self->text);
2885+
int legacy_check_result = 0;
2886+
2887+
/* Skip entirely if there is an opening parenthesis */
2888+
left_paren_index = PyUnicode_FindChar(self->text, left_paren,
2889+
0, text_len, 1);
2890+
if (left_paren_index < -1) {
2891+
return -1;
2892+
}
2893+
if (left_paren_index != -1) {
2894+
/* Use default error message for any line with an opening paren */
2895+
return 0;
2896+
}
2897+
/* Handle the simple statement case */
2898+
legacy_check_result = _check_for_legacy_statements(self, 0);
2899+
if (legacy_check_result < 0) {
2900+
return -1;
2901+
2902+
}
2903+
if (legacy_check_result == 0) {
2904+
/* Handle the one-line complex statement case */
2905+
Py_UCS4 colon = 58;
2906+
Py_ssize_t colon_index;
2907+
colon_index = PyUnicode_FindChar(self->text, colon,
2908+
0, text_len, 1);
2909+
if (colon_index < -1) {
2910+
return -1;
2911+
}
2912+
if (colon_index >= 0 && colon_index < text_len) {
2913+
/* Check again, starting from just after the colon */
2914+
if (_check_for_legacy_statements(self, colon_index+1) < 0) {
2915+
return -1;
2916+
}
2917+
}
2918+
}
2919+
return 0;
2920+
}

0 commit comments

Comments
 (0)