You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
functools.partial is implemented in functools.py and in _functoolsmodule.c. The former is almost never used, so libraries come to depend on the quirks and corner cases of the C implementation. This is a problem for PyPy, where the Python implementation is the only one as of the most recent PyPy version. Here's one such difference, which was uncovered by the lxml library. The following code leads to a RecursionError:
importsyssys.modules['_functools'] =None# force use of pure python version, if this is commented out it worksfromfunctoolsimportpartialclassBuilder:
def__call__(self, tag, *children, **attrib):
return (tag, children, attrib)
def__getattr__(self, tag):
returnpartial(self, tag)
B=Builder()
m=B.m
this is the traceback:
Traceback (most recent call last):
File "/home/cfbolz/projects/cpython/bug.py", line 14, in <module>
m = B.m
^^^
File "/home/cfbolz/projects/cpython/bug.py", line 11, in __getattr__
return partial(self, tag)
^^^^^^^^^^^^^^^^^^
File "/home/cfbolz/projects/cpython/Lib/functools.py", line 287, in __new__
if hasattr(func, "func"):
^^^^^^^^^^^^^^^^^^^^^
File "/home/cfbolz/projects/cpython/bug.py", line 11, in __getattr__
return partial(self, tag)
^^^^^^^^^^^^^^^^^^
File "/home/cfbolz/projects/cpython/Lib/functools.py", line 287, in __new__
if hasattr(func, "func"):
^^^^^^^^^^^^^^^^^^^^^
... and repeated
The problem is the following performance shortcut in partial.__new__:
classpartial:
...
def__new__(cls, func, /, *args, **keywords):
ifnotcallable(func):
raiseTypeError("the first argument must be callable")
ifhasattr(func, "func"): # <------------------- problemargs=func.args+argskeywords= {**func.keywords, **keywords}
func=func.func
Basically in this case func is an object where calling hasattr(func, "func") is not safe. The equivalent C code does this check:
if (Py_TYPE(func)->tp_call== (ternaryfunc)partial_call) {
// The type of "func" might not be exactly the same type object// as "type", but if it is called using partial_call, it must have the// same memory layout (fn, args and kw members).// We can use its underlying function directly and merge the arguments.partialobject*part= (partialobject*)func;
In particular, it does not simply call hasattr on func.
Real World Version
This is not an artificial problem, we discovered this via the classlxml.builder.ElementMaker. It has a __call__ method implemented. It also has __getattr__ that looks like this:
One approach would be to file a bug with lxml, but it is likely that more libraries depend on this behaviour. So I would suggest to change the __new__ Python code to add an isinstance check, to bring its behaviour closer to that of the C code:
def__new__(cls, func, /, *args, **keywords):
ifnotcallable(func):
raiseTypeError("the first argument must be callable")
ifisinstance(func, partial) andhasattr(func, "func"):
args=func.args+args
...
I'll open a PR with this approach soon. /cc @mgorny
…h C code (GH-100244)
in partial.__new__, before checking for the existence of the attribute
'func', first check whether the argument is an instance of partial.
Bug
functools.partial
is implemented infunctools.py
and in_functoolsmodule.c
. The former is almost never used, so libraries come to depend on the quirks and corner cases of the C implementation. This is a problem for PyPy, where the Python implementation is the only one as of the most recent PyPy version. Here's one such difference, which was uncovered by thelxml
library. The following code leads to aRecursionError
:this is the traceback:
The problem is the following performance shortcut in
partial.__new__
:Basically in this case
func
is an object where callinghasattr(func, "func")
is not safe. The equivalent C code does this check:In particular, it does not simply call
hasattr
onfunc
.Real World Version
This is not an artificial problem, we discovered this via the class
lxml.builder.ElementMaker
. It has a__call__
method implemented. It also has__getattr__
that looks like this:Which yields the above
RecursionError
on PyPy.Solution ideas
One approach would be to file a bug with
lxml
, but it is likely that more libraries depend on this behaviour. So I would suggest to change the__new__
Python code to add anisinstance
check, to bring its behaviour closer to that of the C code:I'll open a PR with this approach soon. /cc @mgorny
Linked PRs
The text was updated successfully, but these errors were encountered: