@@ -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+
12571260static int
12581261SyntaxError_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