diff --git a/Lib/test/test_print.py b/Lib/test/test_print.py index 7eea349115a..03f13b4edfc 100644 --- a/Lib/test/test_print.py +++ b/Lib/test/test_print.py @@ -128,5 +128,33 @@ class TestPrint(unittest.TestCase): raise RuntimeError self.assertRaises(RuntimeError, print, 1, file=noflush(), flush=True) + +class TestPy2MigrationHint(unittest.TestCase): + """Test that correct hint is produced analogous to Python3 syntax, + if print statement is executed as in Python 2. + """ + + def test_normal_string(self): + python2_print_str = 'print "Hello World"' + with self.assertRaises(SyntaxError) as context: + exec(python2_print_str) + + self.assertIn('print("Hello World")', str(context.exception)) + + def test_string_with_soft_space(self): + python2_print_str = 'print "Hello World",' + with self.assertRaises(SyntaxError) as context: + exec(python2_print_str) + + self.assertIn('print("Hello World", end=" ")', str(context.exception)) + + def test_string_with_excessive_whitespace(self): + python2_print_str = 'print "Hello World", ' + with self.assertRaises(SyntaxError) as context: + exec(python2_print_str) + + self.assertIn('print("Hello World", end=" ")', str(context.exception)) + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS b/Misc/NEWS index 47f3c379049..6469986b48c 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -10,6 +10,9 @@ What's New in Python 3.7.0 alpha 1? Core and Builtins ----------------- +- bpo-30597: ``print`` now shows expected input in custom error message when + used as a Python 2 statement. Patch by Sanyam Khurana. + - bpo-30682: Removed a too-strict assertion that failed for certain f-strings, such as eval("f'\\\n'") and eval("f'\\\r'"). diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 858eff5fc26..190ad065402 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -2862,6 +2862,49 @@ _PyErr_TrySetFromCause(const char *format, ...) * or minus, using the stream redirection syntax). */ + +// Static helper for setting legacy print error message +static int +_set_legacy_print_statement_msg(PySyntaxErrorObject *self, Py_ssize_t start) +{ + PyObject *strip_sep_obj = PyUnicode_FromString(" \t\r\n"); + if (strip_sep_obj == NULL) + return -1; + + // PRINT_OFFSET is to remove `print ` word from the data. + const int PRINT_OFFSET = 6; + Py_ssize_t text_len = PyUnicode_GET_LENGTH(self->text); + PyObject *data = PyUnicode_Substring(self->text, PRINT_OFFSET, text_len); + + if (data == NULL) { + Py_DECREF(strip_sep_obj); + return -1; + } + PyObject *new_data = _PyUnicode_XStrip(data, 2, strip_sep_obj); + Py_DECREF(data); + Py_DECREF(strip_sep_obj); + + if (new_data == NULL) { + return -1; + } + // gets the modified text_len after stripping `print ` + text_len = PyUnicode_GET_LENGTH(new_data); + const char *maybe_end_arg = ""; + if (text_len > 0 && PyUnicode_READ_CHAR(new_data, text_len-1) == ',') { + maybe_end_arg = " end=\" \""; + } + PyObject *error_msg = PyUnicode_FromFormat( + "Missing parentheses in call to 'print'. Did you mean print(%U%s)?", + new_data, maybe_end_arg + ); + Py_DECREF(new_data); + if (error_msg == NULL) + return -1; + + Py_XSETREF(self->msg, error_msg); + return 1; +} + static int _check_for_legacy_statements(PySyntaxErrorObject *self, Py_ssize_t start) { @@ -2897,9 +2940,8 @@ _check_for_legacy_statements(PySyntaxErrorObject *self, Py_ssize_t start) } if (PyUnicode_Tailmatch(self->text, print_prefix, start, text_len, -1)) { - Py_XSETREF(self->msg, - PyUnicode_FromString("Missing parentheses in call to 'print'")); - return 1; + + return _set_legacy_print_statement_msg(self, start); } /* Check for legacy exec statements */