-
-
Notifications
You must be signed in to change notification settings - Fork 31.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
bpo-30721: Show correct syntax hint in Py3 when using Py2 redirection syntax #2345
bpo-30721: Show correct syntax hint in Py3 when using Py2 redirection syntax #2345
Conversation
@CuriousLearner, thanks for your PR! By analyzing the history of the files in this pull request, we identified @serhiy-storchaka, @benjaminp and @nascheme to be potential reviewers. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comments inline regarding some lower level details. I haven't looked at whether or not this is the right place to make the change yet, though.
Objects/abstract.c
Outdated
@@ -819,6 +819,21 @@ binary_op(PyObject *v, PyObject *w, const int op_slot, const char *op_name) | |||
PyObject *result = binary_op1(v, w, op_slot); | |||
if (result == Py_NotImplemented) { | |||
Py_DECREF(result); | |||
|
|||
if (strcmp(op_name, ">>") == 0 && \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to re-read the whole file myself to see if there may be a better place to inject this check, but in the mean time, at least compare against the numeric op_slot
here, rather than the string value.
Objects/abstract.c
Outdated
PyErr_Format(PyExc_TypeError, | ||
"unsupported operand type(s) for %.100s: " | ||
"'%.100s' and '%.100s'. Did you mean \"print(%s, " | ||
"file={:100!r}).format(%s))\"", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't include an actual format call in the error message - that was just what I used in the issue tracker to represent PyErr_Format
pllaceholders. Instead, you'll want to use the %R
format code to invoke PyObject_Repr
on the operands directly:
PyErr_Format(PyExc_TypeError,
"unsupported operand type(s) for %.100s: "
"'%.100s' and '%.100s'. Did you mean \"print(%.100R, "
"file={.100R})\"",
op_name,
v->ob_type->tp_name,
w->ob_type->tp_name,
w, v);
In the general case, you won't be able to recreate the exact text of the original input, as that's no longer available by the time we get here - we only have a reference to the final runtime objects.
We might decide it's worthwhile adding a special-case check for the RHS being exactly PySys_GetObject("stderr")
and displaying file=sys.stderr
in that case.
Alternatively, we could dispense with the idea of interpolating values entirely, and just use the hardcoded output "Did you mean \"print(<message>, file=<output_stream>)\""
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm leaning towards the "hardcoded placeholders" option, as it occurs to me that we don't actually have access to the message to be displayed when raising the exception:
print >> sys.stderr, 1, 2, 3
The LHS passed to >>
is print
, and the RHS is sys.stderr
. I think that's why I just had <expr>
as a placeholder in my example on the issue tracker.
However, the repr of a standard stream isn't particular helpful in this context either:
>>> sys.stderr
<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>
The generic <output_stream>
placeholder would be clearer than displaying that.
So yeah, forget about interpolating the inputs at all, and just append a string with generic placeholders in it.
@ncoghlan I just noticed, I cannot move this snippet to So, I formatted the error message in the I've made the changes to include generic statement, but I think if there is a way to access op_slot with op_name, then this patch could be simplified. What are your thoughts on this? |
66d0529
to
aa5cc11
Compare
test_string_with_stream_redirection (test.test_print.TestPy2MigrationHint) ... FAILFAIL: test_string_with_stream_redirection (test.test_print.TestPy2MigrationHint)Traceback (most recent call last): |
@terryjreedy The test passes on my local setup. I'm not sure why this is happening on CI. I will have a look at it. |
@terryjreedy Here is the result of test run on Mac OS
|
@CuriousLearner Travis, where it also passed, run linux. Appveyor, where it failed runs Windows. I asked on the issue for a Windows expert to take a look. |
Objects/abstract.c
Outdated
@@ -819,6 +819,20 @@ binary_op(PyObject *v, PyObject *w, const int op_slot, const char *op_name) | |||
PyObject *result = binary_op1(v, w, op_slot); | |||
if (result == Py_NotImplemented) { | |||
Py_DECREF(result); | |||
|
|||
if (op_slot == 96 && \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be op_slot == NB_SLOT(nb_rshift)
@ncoghlan @terryjreedy Please have a look now. The issue is resolved now. |
Objects/abstract.c
Outdated
@@ -819,6 +819,20 @@ binary_op(PyObject *v, PyObject *w, const int op_slot, const char *op_name) | |||
PyObject *result = binary_op1(v, w, op_slot); | |||
if (result == Py_NotImplemented) { | |||
Py_DECREF(result); | |||
|
|||
if (op_slot == 96 && \ | |||
strcmp(v->ob_type->tp_name, "builtin_function_or_method") == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't test type's name. Test the print object itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aye, you'll want to do something like:
_Py_IDENTIFIER(print);
builtins = PyEval_GetBuiltins();
print_ref = _PyDict_GetItemId(builtins, &PyId_print);
and then check for v == print_ref
.
(Only don't lay it out exactly like that, adjust it to fit the code - the above is just to show the key APIs you need)
Objects/abstract.c
Outdated
@@ -819,6 +819,20 @@ binary_op(PyObject *v, PyObject *w, const int op_slot, const char *op_name) | |||
PyObject *result = binary_op1(v, w, op_slot); | |||
if (result == Py_NotImplemented) { | |||
Py_DECREF(result); | |||
|
|||
if (op_slot == 96 && \ | |||
strcmp(v->ob_type->tp_name, "builtin_function_or_method") == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aye, you'll want to do something like:
_Py_IDENTIFIER(print);
builtins = PyEval_GetBuiltins();
print_ref = _PyDict_GetItemId(builtins, &PyId_print);
and then check for v == print_ref
.
(Only don't lay it out exactly like that, adjust it to fit the code - the above is just to show the key APIs you need)
Objects/abstract.c
Outdated
@@ -819,6 +819,20 @@ binary_op(PyObject *v, PyObject *w, const int op_slot, const char *op_name) | |||
PyObject *result = binary_op1(v, w, op_slot); | |||
if (result == Py_NotImplemented) { | |||
Py_DECREF(result); | |||
|
|||
if (op_slot == NB_SLOT(nb_rshift) && \ | |||
strcmp(v->ob_type->tp_name, "builtin_function_or_method") == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moving the review comment about checking for exactly the print builtin to the latest commit:
_Py_IDENTIFIER(print);
builtins = PyEval_GetBuiltins();
print_ref = _PyDict_GetItemId(builtins, &PyId_print);
and then check for v == print_ref
.
(Only don't lay it out exactly like that, adjust it to fit the code - the above is just to show the key APIs you need)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but since this code would be slow and complex, I think it can be replaced by something like
PyCFunction_Check(v) && PyCFunction_GET_FUNCTION(v) == builtin_print
or
PyCFunction_Check(v) && strcmp(((PyCFunctionObject *)v) -> m_ml -> ml_name, "print") == 0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair point. Since builtin_print
is a static symbol in bltinmodule
, your second suggestion looks good to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are many ways to get around this constraint. Export builtin_print
or implement the check in bltinmodule.c
and export the checking code for using in abstract.c
.
But testing the function name may be enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, that was my reasoning too - while we could make the actual symbol available here, "It's a function called 'print' implemented in C" is a robust enough check, and it means the C level changes can stay contained to this module.
Lib/test/test_print.py
Outdated
|
||
self.assertIn('Did you mean "print(<message>, ' | ||
'file=<output_stream>)', str(context.exception)) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add following tests:
max >> sys.stderr
.print >>
with right argument implemented__rrshift__
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please explain a little about the above cases?
For 1, As far as I understand the max
function when redirected to sys.stderr
should not cause the Did you mean ...
statement to be executed.
So, a test case can be
def test_stream_redirection_using_max(self):
import sys
python2_print_str = 'max >> sys.stderr'
with self.assertRaises(TypeError) as context:
exec(python2_print_str)
self.assertNotIn('Did you mean "print(<message>, '
'file=<output_stream>)', str(context.exception))
For the 2, I don't quite understand if we just want to test print >>
with the current patch. In that case again the Did you mean...
statement would not be executed.
Is this what we want to test here?
Please correct me.
Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@CuriousLearner Correct for the first case. For the second case, you'll want to implement a dummy class that does something like this:
class OverrideRRShift:
def __rrshift__(self, lhs):
return 42 # Force result independent of LHS
self.assertEqual(print >> OverrideRRShift(), 42)
That will specifically ensure that the special casing doesn't trigger when the rshift operation produces a sensible answer.
It would also be good to test that print >> 5
does produce the new exception message (that covers the case where the RHS implements __rrshift__
but returns NotImplemented
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ncoghlan Can you please take a look at the changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is starting to look pretty good to me, just one suggestion for another test case, as well as some suggestions for rearranging and renaming the existing test cases a bit.
Lib/test/test_print.py
Outdated
@@ -155,7 +155,7 @@ def test_string_with_excessive_whitespace(self): | |||
|
|||
self.assertIn('print("Hello World", end=" ")', str(context.exception)) | |||
|
|||
def test_string_with_stream_redirection(self): | |||
def test_print_string_with_stream_redirection(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggested test name: test_stream_redirection_hint_for_py2_migration
Lib/test/test_print.py
Outdated
@@ -164,6 +164,32 @@ def test_string_with_stream_redirection(self): | |||
self.assertIn('Did you mean "print(<message>, ' | |||
'file=<output_stream>)', str(context.exception)) | |||
|
|||
def test_print_with_stream_redirection_using_max(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggested test name: test_stream_redirection_hint_is_specific_to_print
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prompted by that, I'd also suggest another test case: test_stream_redirection_hint_is_specific_to_rshift
that checks:
print << sys.stderr
doesn't produce the new hint in its error message.
Lib/test/test_print.py
Outdated
|
||
self.assertEqual(print >> OverrideRRShift(), 0) | ||
|
||
python2_print_str = 'print >> 42' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a situation where we expect to see the hint, so it would fit better up in the first test case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a comment about what case is covered by this test. It looks as a duplicate of the first case.
Lib/test/test_print.py
Outdated
with self.assertRaises(TypeError) as context: | ||
exec(python2_print_str) | ||
|
||
self.assertNotIn('Did you mean "print(<message>, ' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd shorten the test string here to just 'Did you mean'
Lib/test/test_print.py
Outdated
@@ -155,7 +155,7 @@ def test_string_with_excessive_whitespace(self): | |||
|
|||
self.assertIn('print("Hello World", end=" ")', str(context.exception)) | |||
|
|||
def test_string_with_stream_redirection(self): | |||
def test_print_string_with_stream_redirection(self): | |||
import sys | |||
python2_print_str = 'print >> sys.stderr, "message"' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is legal Python 3 syntax, so there's no need to use exec
for these test cases - you can just put the expression in the body of the with statement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ncoghlan But that is just a string, we would need to execute it in the context manager, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's what I'm saying - the code that checked for the custom SyntaxError
needed to be tested with exec
, because putting it outside a string would cause the entire module to fail to compile. That isn't the case here, so you can just do print >> sys.stderr, "message"
directly in the body of the context manager (and similarly for the other cases).
Lib/test/test_print.py
Outdated
self.assertNotIn('Did you mean "print(<message>, ' | ||
'file=<output_stream>)', str(context.exception)) | ||
|
||
def test_print_with_stream_redirection_using_rrshift(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggested test name: test_no_stream_redirection_hint_when_rshift_succeeds
Objects/abstract.c
Outdated
@@ -821,7 +821,7 @@ binary_op(PyObject *v, PyObject *w, const int op_slot, const char *op_name) | |||
Py_DECREF(result); | |||
|
|||
if (op_slot == NB_SLOT(nb_rshift) && \ | |||
strcmp(v->ob_type->tp_name, "builtin_function_or_method") == 0) { | |||
PyCFunction_Check(v) && strcmp(((PyCFunctionObject *)v) -> m_ml -> ml_name, "print") == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When working with user supplied strings, we generally prefer strncmp
to strcmp
, which would give:
strncmp(((PyCFunctionObject *)v) -> m_ml -> ml_name, "print", 6)
for the comparison subexpression.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are reasons for using strncmp()
instead of strcmp()
here? "print" is NUL terminated, the comparison is limited to 6 chars in any case.
A Python core developer, ncoghlan, has requested some changes be Once you have made the requested changes, please leave a comment |
Lib/test/test_print.py
Outdated
self.assertIn('Did you mean "print(<message>, ' | ||
'file=<output_stream>)', str(context.exception)) | ||
|
||
def test_print_with_stream_redirection_using_max(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be better to use the single test method for all these tests. Use empty lines and/or comments for separating them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That also works - the suggested test_stream_redirection_hint_for_py2_migration
name is sufficiently broad to cover all of them, and then the comment on each case can explain which particular aspect it is checking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I'll do that.
Lib/test/test_print.py
Outdated
|
||
def test_print_with_stream_redirection_using_rrshift(self): | ||
import sys | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Too much empty lines. Use empty lines only for separating different tests in one method.
Lib/test/test_print.py
Outdated
class OverrideRRShift: | ||
|
||
def __rrshift__(self, lhs): | ||
return 0 # Force result independent of LHS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use something more unique. Like 42
or 'spam'
.
Objects/abstract.c
Outdated
@@ -819,6 +819,20 @@ binary_op(PyObject *v, PyObject *w, const int op_slot, const char *op_name) | |||
PyObject *result = binary_op1(v, w, op_slot); | |||
if (result == Py_NotImplemented) { | |||
Py_DECREF(result); | |||
|
|||
if (op_slot == NB_SLOT(nb_rshift) && \ | |||
PyCFunction_Check(v) && strcmp(((PyCFunctionObject *)v) -> m_ml -> ml_name, "print") == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move strcmp
to the next line. Align PyCFunction_Check
and strcmp
to op_slot
.
According to new rules of PEP 7 the opening {
after multiline condition should be at new line.
Objects/abstract.c
Outdated
@@ -819,6 +819,20 @@ binary_op(PyObject *v, PyObject *w, const int op_slot, const char *op_name) | |||
PyObject *result = binary_op1(v, w, op_slot); | |||
if (result == Py_NotImplemented) { | |||
Py_DECREF(result); | |||
|
|||
if (op_slot == NB_SLOT(nb_rshift) && \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
\
is not needed.
Lib/test/test_print.py
Outdated
|
||
self.assertEqual(print >> OverrideRRShift(), 0) | ||
|
||
python2_print_str = 'print >> 42' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a comment about what case is covered by this test. It looks as a duplicate of the first case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost there :)
The NEWS
entry needs to be adjusted to follow the new process for that, and I spotted a not-quite-right comment in the tests that I missed last time.
Lib/test/test_print.py
Outdated
self.assertNotIn('Did you mean "print(<message>, ' | ||
'file=<output_stream>)', str(context.exception)) | ||
print << sys.stderr | ||
self.assertNotIn('Did you mean', str(context.exception)) | ||
|
||
# Test stream redirection with right argument implemented __rrshift__ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I missed this comment on my last review: it should say something like "Ensure right operand implementing __rrshift__
still works"
Misc/NEWS
Outdated
@@ -10,6 +10,9 @@ What's New in Python 3.7.0 alpha 1? | |||
Core and Builtins | |||
----------------- | |||
|
|||
- bpo-30721: ``print`` now shows correct usage hint for using Python 2 | |||
redirection syntax. Patch by Sanyam Khurana. | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't edit Misc/NEWS
directly any more. Instead, you'll want to add a blurb entry under https://github.com/python/cpython/tree/master/Misc/NEWS.d/next/Core%20and%20Builtins
See the existing entries and https://docs.python.org/devguide/committing.html#what-s-new-and-news-entries for more details
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Blurb tool looks great! I faced a lot of merge conflicts earlier while rebasing my patches from the latest upstream changes.
This would greatly solve the problem.
A Python core developer, ncoghlan, has requested some changes be Once you have made the requested changes, please leave a comment |
Lib/test/test_print.py
Outdated
@@ -155,6 +156,39 @@ def test_string_with_excessive_whitespace(self): | |||
|
|||
self.assertIn('print("Hello World", end=" ")', str(context.exception)) | |||
|
|||
def test_stream_redirection_hint_for_py2_migration(self): | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one empty line is redundant.
Objects/abstract.c
Outdated
@@ -821,7 +821,7 @@ binary_op(PyObject *v, PyObject *w, const int op_slot, const char *op_name) | |||
Py_DECREF(result); | |||
|
|||
if (op_slot == NB_SLOT(nb_rshift) && \ | |||
strcmp(v->ob_type->tp_name, "builtin_function_or_method") == 0) { | |||
PyCFunction_Check(v) && strcmp(((PyCFunctionObject *)v) -> m_ml -> ml_name, "print") == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are reasons for using strncmp()
instead of strcmp()
here? "print" is NUL terminated, the comparison is limited to 6 chars in any case.
@serhiy-storchaka using |
Right, that was my suggestion, but I was getting my string comparison algorithms mixed up - So yeah, you should change it back to what you had originally - sorry about that :) |
Aside from needing to revert my less-than-helpful |
Alright, fixed everything! :) I didn't expect the Spanish Inquisition! |
Nobody expects the Spanish Inquisition! @serhiy-storchaka, @ncoghlan: please review the changes made to this pull request. |
Objects/abstract.c
Outdated
@@ -822,7 +822,7 @@ binary_op(PyObject *v, PyObject *w, const int op_slot, const char *op_name) | |||
|
|||
if (op_slot == NB_SLOT(nb_rshift) && | |||
PyCFunction_Check(v) && | |||
strncmp(((PyCFunctionObject *)v)->m_ml->ml_name, "print", 6) == 0) | |||
strcmp(((PyCFunctionObject *)v) -> m_ml -> ml_name, "print") == 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No spaces needed around ->
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
I didn't expect the Spanish Inquisition! |
Nobody expects the Spanish Inquisition! @serhiy-storchaka, @ncoghlan: please review the changes made to this pull request. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me now, so over to @serhiy-storchaka for the final review :)
Thanks @serhiy-storchaka @ncoghlan :) 🥇 |
Thanks @CuriousLearner for persevering with the change! I foresee a lot of folks making the Py2->Py3 migration benefiting from your work over the next few years :) |
This is a work in progress for issue: http://bugs.python.org/issue30721
@ncoghlan I'm not sure how should I get the value that is being entered on the command line. What I know is it would be in
PyObject *w
, but I'm unable to get the right method from the doc to read the buffer.So, for now, I've hard-coded the value just to make the test case pass. Could you please help me with the former problem and give me a pointer on it.
Also, after writing this patch, I realize, that
binary_op
is a generic function, and this code should be placed in functionbinop_type_error
. Would you please guide me about these things?Thanks!
https://bugs.python.org/issue30721