Skip to content
21 changes: 21 additions & 0 deletions Lib/test/test_print.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,5 +128,26 @@ def flush(self):
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.assertTrue('print("Hello World")' in str(context.exception))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use assertIn().

Or use assertRaisesRegex() with the argument r'print\("Hello World"\)' or re.escape('print("Hello World")').


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

self.assertTrue('print("Hello World", end=" ")' in str(context.exception))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same as above.


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add also tests for excessive whitespaces: 'print "Hello World", '.


if __name__ == "__main__":
unittest.main()
50 changes: 47 additions & 3 deletions Objects/exceptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -2862,6 +2862,51 @@ _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)
{

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove excessive empty lines.

PyObject *strip_sep_obj = PyUnicode_FromString("\r\n");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Virtually any API call can fail and raise an exception. Always check the result.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add also a space and a tab.

Py_UCS4 soft_space_check = PyUnicode_READ_CHAR(PyUnicode_FromString(","), 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The result of PyUnicode_FromString(",") is not checked for NULL and leaked.

You don't need this. You can use just soft_space_check = ','. Actually I think the named variable is not needed.


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)
return -1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strip_sep_obj is leaked here.


data = _PyUnicode_XStrip(data, 1, strip_sep_obj);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The result of _PyUnicode_XStrip() is not checked for error. Old value of data is leaked.


Py_DECREF(strip_sep_obj);

// gets the modified text_len after stripping `print `
text_len = PyUnicode_GET_LENGTH(data);
const char *maybe_end_arg = "";

if (text_len > 0) {
if (PyUnicode_READ_CHAR(data, text_len-1) == soft_space_check) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two if's can be merged.

maybe_end_arg = " end=\" \"";
}
}

PyObject *error_msg = PyUnicode_FromFormat(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The result is not checked for error.

"Missing parentheses in call to 'print'. Did you mean print(%U%s)?",
data, maybe_end_arg);

Py_DECREF(data);

Py_XSETREF(self->msg, error_msg);

return 1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At that point multiple results of PyUnicode_XXX calls are leaked.

}

static int
_check_for_legacy_statements(PySyntaxErrorObject *self, Py_ssize_t start)
{
Expand Down Expand Up @@ -2897,9 +2942,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 */
Expand Down