Skip to content

Commit

Permalink
gh-121027: Make the functools.partial object a method descriptor
Browse files Browse the repository at this point in the history
Co-authored-by: d.grigonis <[email protected]>
  • Loading branch information
serhiy-storchaka and d.grigonis committed Jun 27, 2024
1 parent b057ae6 commit 5ecccee
Show file tree
Hide file tree
Showing 6 changed files with 28 additions and 40 deletions.
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,12 @@ Porting to Python 3.14
This section lists previously described changes and other bugfixes
that may require changes to your code.

Changes in the Python API
-------------------------

* :class:`functools.partial` is now a method descriptor.
Wrap it in :func:`staticmethod` if you want to preserve the old behavior.
(Contributed by Serhiy Storchaka and D. Grigonis in :gh:`121027`.)

Build Changes
=============
Expand Down
10 changes: 3 additions & 7 deletions Lib/functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from collections import namedtuple
# import types, weakref # Deferred to single_dispatch()
from reprlib import recursive_repr
from types import MethodType
from _thread import RLock

# Avoid importing types, so we can speedup import time
Expand Down Expand Up @@ -314,12 +315,7 @@ def __repr__(self):
def __get__(self, obj, objtype=None):
if obj is None:
return self
import warnings
warnings.warn('functools.partial will be a method descriptor in '
'future Python versions; wrap it in staticmethod() '
'if you want to preserve the old behavior',
FutureWarning, 2)
return self
return MethodType(self, obj)

def __reduce__(self):
return type(self), (self.func,), (self.func, self.args,
Expand Down Expand Up @@ -402,7 +398,7 @@ def _method(cls_or_self, /, *args, **keywords):
def __get__(self, obj, cls=None):
get = getattr(self.func, "__get__", None)
result = None
if get is not None and not isinstance(self.func, partial):
if get is not None:
new_func = get(obj, cls)
if new_func is not self.func:
# Assume __get__ returning something new indicates the
Expand Down
4 changes: 1 addition & 3 deletions Lib/test/test_functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,9 +405,7 @@ class A:
self.assertEqual(A.meth(3, b=4), ((1, 3), {'a': 2, 'b': 4}))
self.assertEqual(A.cmeth(3, b=4), ((1, A, 3), {'a': 2, 'b': 4}))
self.assertEqual(A.smeth(3, b=4), ((1, 3), {'a': 2, 'b': 4}))
with self.assertWarns(FutureWarning) as w:
self.assertEqual(a.meth(3, b=4), ((1, 3), {'a': 2, 'b': 4}))
self.assertEqual(w.filename, __file__)
self.assertEqual(a.meth(3, b=4), ((1, a, 3), {'a': 2, 'b': 4}))
self.assertEqual(a.cmeth(3, b=4), ((1, A, 3), {'a': 2, 'b': 4}))
self.assertEqual(a.smeth(3, b=4), ((1, 3), {'a': 2, 'b': 4}))

Expand Down
38 changes: 16 additions & 22 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -3868,17 +3868,15 @@ def __init__(self, b):

with self.subTest('partial'):
class CM(type):
__call__ = functools.partial(lambda x, a: (x, a), 2)
__call__ = functools.partial(lambda x, a, b: (x, a, b), 2)
class C(metaclass=CM):
def __init__(self, b):
def __init__(self, c):
pass

with self.assertWarns(FutureWarning):
self.assertEqual(C(1), (2, 1))
with self.assertWarns(FutureWarning):
self.assertEqual(self.signature(C),
((('a', ..., ..., "positional_or_keyword"),),
...))
self.assertEqual(C(1), (2, C, 1))
self.assertEqual(self.signature(C),
((('b', ..., ..., "positional_or_keyword"),),
...))

with self.subTest('partialmethod'):
class CM(type):
Expand Down Expand Up @@ -4024,14 +4022,12 @@ class C:

with self.subTest('partial'):
class C:
__init__ = functools.partial(lambda x, a: None, 2)
__init__ = functools.partial(lambda x, a, b: None, 2)

with self.assertWarns(FutureWarning):
C(1) # does not raise
with self.assertWarns(FutureWarning):
self.assertEqual(self.signature(C),
((('a', ..., ..., "positional_or_keyword"),),
...))
C(1) # does not raise
self.assertEqual(self.signature(C),
((('b', ..., ..., "positional_or_keyword"),),
...))

with self.subTest('partialmethod'):
class C:
Expand Down Expand Up @@ -4284,15 +4280,13 @@ class C:

with self.subTest('partial'):
class C:
__call__ = functools.partial(lambda x, a: (x, a), 2)
__call__ = functools.partial(lambda x, a, b: (x, a, b), 2)

c = C()
with self.assertWarns(FutureWarning):
self.assertEqual(c(1), (2, 1))
with self.assertWarns(FutureWarning):
self.assertEqual(self.signature(c),
((('a', ..., ..., "positional_or_keyword"),),
...))
self.assertEqual(c(1), (2, c, 1))
self.assertEqual(self.signature(C()),
((('b', ..., ..., "positional_or_keyword"),),
...))

with self.subTest('partialmethod'):
class C:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make the :class:`functools.partial` object a method descriptor.
9 changes: 1 addition & 8 deletions Modules/_functoolsmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -203,14 +203,7 @@ partial_descr_get(PyObject *self, PyObject *obj, PyObject *type)
if (obj == Py_None || obj == NULL) {
return Py_NewRef(self);
}
if (PyErr_WarnEx(PyExc_FutureWarning,
"functools.partial will be a method descriptor in "
"future Python versions; wrap it in staticmethod() "
"if you want to preserve the old behavior", 1) < 0)
{
return NULL;
}
return Py_NewRef(self);
return PyMethod_New(self, obj);
}

/* Merging keyword arguments using the vectorcall convention is messy, so
Expand Down

0 comments on commit 5ecccee

Please sign in to comment.