-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
bpo-29800: Fix crashes in partial.__repr__ if the keys of partial.keywords are not strings #649
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-29800: Fix crashes in partial.__repr__ if the keys of partial.keywords are not strings #649
Conversation
Hello, and thanks for your contribution! I'm a bot set up to make sure that the project can legally accept your contribution by verifying you have signed the PSF contributor agreement (CLA). Unfortunately our records indicate you have not signed the CLA. For legal reasons we need you to sign this before we can look at your contribution. Please follow these steps to rectify the issue:
Thanks again to your contribution and we look forward to looking at it! |
@MSeifert04, thanks for your PR! By analyzing the history of the files in this pull request, we identified @serhiy-storchaka, @rhettinger, @ncoghlan, @ambv and @abalkin to be potential reviewers. |
Lib/test/test_functools.py
Outdated
# Adding a non-string/unicode keyword to partial kwargs | ||
p.keywords[1] = 10 | ||
self.assertIn('1=10', repr(p)) | ||
try: |
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.
Just use assertRaises()
.
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 failed because assertRaises
needs the __name__
of the class/function. But _functools.partial
doesn't have a __name__
.
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.
with self.assertRaises(TypeError):
p()
Lib/test/test_functools.py
Outdated
p = self.partial(capture) | ||
# Adding a non-string/unicode keyword to partial kwargs | ||
p.keywords[1] = 10 | ||
self.assertIn('1=10', repr(p)) |
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 test is fragile. Since 1=10
is not valid Python syntax, more comprehensive implementation can return partial(<foo>, **{1: 10})
.
I would test that the repr() of the partial object contains either str() or repr() of the key and the repr() of the value.
p.keywords[123] = 'value'
r = repr(p)
self.assertIn('123', r)
self.assertIn("'value'", r)
Lib/test/test_functools.py
Outdated
else: | ||
self.fail('partial object allowed passing non-string keywords') | ||
|
||
# Deleting the key-value pair during key formatting shouldn't segfault. |
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 a separate test method.
Lib/test/test_functools.py
Outdated
|
||
# Deleting the key-value pair during key formatting shouldn't segfault. | ||
class MutatesYourDict(object): | ||
def __init__(self, dct): |
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.
The __init__
method and the dct
attribute are not needed. Just use p.keywords
instead of self.dct
.
Lib/test/test_functools.py
Outdated
def __str__(self): | ||
del self.dct[self] | ||
return 'astr' | ||
def __repr__(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 is not needed as well as wrapping the value with MutatesYourDict
.
Lib/test/test_functools.py
Outdated
|
||
p = partial(capture) | ||
p.keywords[MutatesYourDict(p.keywords)] = MutatesYourDict(p.keywords) | ||
self.assertIn('astr=arepr', repr(p)) |
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 test is fragile. See the comment above.
Lib/test/test_functools.py
Outdated
return 'arepr' | ||
|
||
p = partial(capture) | ||
p.keywords[MutatesYourDict(p.keywords)] = MutatesYourDict(p.keywords) |
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 that wrapping the value with MutatesYourDict is not needed. Just use some unique value with predicable repr. For example ['value']
.
Check that this test is failed when don't add increfs.
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.
Should I reorder the commits to add the tests first to show they are segfaulting before doing the commit that fixes them? Or do you mean I should check it locally?
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.
Check it locally. Remove increfs, check that the test is failed/crashed, add increfs, check that the same test is passed now.
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.
Just to report back: It segfaulted (on Windows 10).
Modules/_functoolsmodule.c
Outdated
@@ -253,8 +253,13 @@ partial_repr(partialobject *pto) | |||
/* Pack keyword arguments */ | |||
assert (PyDict_Check(pto->kw)); | |||
for (i = 0; PyDict_Next(pto->kw, &i, &key, &value);) { | |||
Py_SETREF(arglist, PyUnicode_FromFormat("%U, %U=%R", arglist, | |||
/* Prevent key.__str__ from deleting key or value during formatting. */ | |||
Py_INCREF(key); |
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 that increfing the key is not needed.
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, indeed. I have no problems without that INCREF
and I will remove that. That part was copied from dict_repr because I thought what works there needs to work here as well.
Lib/test/test_functools.py
Outdated
def __init__(self, dct): | ||
self.dct = dct | ||
def __str__(self): | ||
del self.dct[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.
It would be better to replace the value rather than delete the key. This wouldn't break conditions for dict iteration.
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.
Replacing was kind of weird - I think because it's undefined behaviour I tried to test in this case and replacing could create an object that "fills" the memory location where the dangling value had been. I had some trouble to make that case fail in a predictable way without the INCREF
s. It sometimes threw an AttributeError instead of segfaulting.
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.
The predictability of failure is not required. It enough if the test failed or crashed sometimes.
If your replace the value with other value it can be catched since other value have different repr.
Lib/test/test_functools.py
Outdated
with self.assertRaises(TypeError): | ||
p() | ||
|
||
def test_manually_adding_non_string_keyword(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 is the same name as in the above test! This test hides the above test.
Lib/test/test_functools.py
Outdated
def test_manually_adding_non_string_keyword(self): | ||
p = self.partial(capture) | ||
# Adding a non-string/unicode keyword to partial kwargs | ||
p.keywords[1234] = 4321 |
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.
It would be better to test the value that have different repr and str. For example a string.
Modules/_functoolsmodule.c
Outdated
@@ -253,8 +253,11 @@ partial_repr(partialobject *pto) | |||
/* Pack keyword arguments */ | |||
assert (PyDict_Check(pto->kw)); | |||
for (i = 0; PyDict_Next(pto->kw, &i, &key, &value);) { | |||
Py_SETREF(arglist, PyUnicode_FromFormat("%U, %U=%R", arglist, | |||
/* Prevent key.__str__ from deleting key or value during formatting. */ |
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.
...from deleting value...
Lib/test/test_functools.py
Outdated
# value alive (at least long enough). | ||
class MutatesYourDict(object): | ||
def __str__(self): | ||
p[self] = ['sth2'] |
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.
p.keywords[self]
@the-knights-who-say-ni Steps completed and CLA received. |
And the last thing is left @MSeifert04. Please add an entry in |
@serhiy-storchaka Thank you for your helpful comments on the pull request and for merging it! 👍 |
This is my first contribution for CPython and I must admit I haven't read through all the developer guide.
I signed the CLA (probably still in review) but if you notice anything else that needs to be done please let me know.