gh-121027: Make the functools.partial object a method descriptor (GH-121089)

Co-authored-by: d.grigonis <dgrigonis@users.noreply.github.com>
This commit is contained in:
Serhiy Storchaka 2024-07-03 09:02:15 +03:00 committed by GitHub
parent f09d184821
commit ff5806c78e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 28 additions and 40 deletions

View File

@ -305,6 +305,12 @@ Porting to Python 3.14
This section lists previously described changes and other bugfixes This section lists previously described changes and other bugfixes
that may require changes to your code. 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 Dominykas Grigonis in :gh:`121027`.)
Build Changes Build Changes
============= =============

View File

@ -18,6 +18,7 @@ from abc import get_cache_token
from collections import namedtuple from collections import namedtuple
# import types, weakref # Deferred to single_dispatch() # import types, weakref # Deferred to single_dispatch()
from reprlib import recursive_repr from reprlib import recursive_repr
from types import MethodType
from _thread import RLock from _thread import RLock
# Avoid importing types, so we can speedup import time # Avoid importing types, so we can speedup import time
@ -314,12 +315,7 @@ class partial:
def __get__(self, obj, objtype=None): def __get__(self, obj, objtype=None):
if obj is None: if obj is None:
return self return self
import warnings return MethodType(self, obj)
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
def __reduce__(self): def __reduce__(self):
return type(self), (self.func,), (self.func, self.args, return type(self), (self.func,), (self.func, self.args,
@ -402,7 +398,7 @@ class partialmethod(object):
def __get__(self, obj, cls=None): def __get__(self, obj, cls=None):
get = getattr(self.func, "__get__", None) get = getattr(self.func, "__get__", None)
result = None result = None
if get is not None and not isinstance(self.func, partial): if get is not None:
new_func = get(obj, cls) new_func = get(obj, cls)
if new_func is not self.func: if new_func is not self.func:
# Assume __get__ returning something new indicates the # Assume __get__ returning something new indicates the

View File

@ -405,9 +405,7 @@ class TestPartial:
self.assertEqual(A.meth(3, b=4), ((1, 3), {'a': 2, 'b': 4})) 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.cmeth(3, b=4), ((1, A, 3), {'a': 2, 'b': 4}))
self.assertEqual(A.smeth(3, b=4), ((1, 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, a, 3), {'a': 2, 'b': 4}))
self.assertEqual(a.meth(3, b=4), ((1, 3), {'a': 2, 'b': 4}))
self.assertEqual(w.filename, __file__)
self.assertEqual(a.cmeth(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})) self.assertEqual(a.smeth(3, b=4), ((1, 3), {'a': 2, 'b': 4}))

View File

@ -3868,17 +3868,15 @@ class TestSignatureObject(unittest.TestCase):
with self.subTest('partial'): with self.subTest('partial'):
class CM(type): 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): class C(metaclass=CM):
def __init__(self, b): def __init__(self, c):
pass pass
with self.assertWarns(FutureWarning): self.assertEqual(C(1), (2, C, 1))
self.assertEqual(C(1), (2, 1)) self.assertEqual(self.signature(C),
with self.assertWarns(FutureWarning): ((('b', ..., ..., "positional_or_keyword"),),
self.assertEqual(self.signature(C), ...))
((('a', ..., ..., "positional_or_keyword"),),
...))
with self.subTest('partialmethod'): with self.subTest('partialmethod'):
class CM(type): class CM(type):
@ -4024,14 +4022,12 @@ class TestSignatureObject(unittest.TestCase):
with self.subTest('partial'): with self.subTest('partial'):
class C: 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
C(1) # does not raise self.assertEqual(self.signature(C),
with self.assertWarns(FutureWarning): ((('b', ..., ..., "positional_or_keyword"),),
self.assertEqual(self.signature(C), ...))
((('a', ..., ..., "positional_or_keyword"),),
...))
with self.subTest('partialmethod'): with self.subTest('partialmethod'):
class C: class C:
@ -4284,15 +4280,13 @@ class TestSignatureObject(unittest.TestCase):
with self.subTest('partial'): with self.subTest('partial'):
class C: class C:
__call__ = functools.partial(lambda x, a: (x, a), 2) __call__ = functools.partial(lambda x, a, b: (x, a, b), 2)
c = C() c = C()
with self.assertWarns(FutureWarning): self.assertEqual(c(1), (2, c, 1))
self.assertEqual(c(1), (2, 1)) self.assertEqual(self.signature(C()),
with self.assertWarns(FutureWarning): ((('b', ..., ..., "positional_or_keyword"),),
self.assertEqual(self.signature(c), ...))
((('a', ..., ..., "positional_or_keyword"),),
...))
with self.subTest('partialmethod'): with self.subTest('partialmethod'):
class C: class C:

View File

@ -0,0 +1 @@
Make the :class:`functools.partial` object a method descriptor.

View File

@ -203,14 +203,7 @@ partial_descr_get(PyObject *self, PyObject *obj, PyObject *type)
if (obj == Py_None || obj == NULL) { if (obj == Py_None || obj == NULL) {
return Py_NewRef(self); return Py_NewRef(self);
} }
if (PyErr_WarnEx(PyExc_FutureWarning, return PyMethod_New(self, obj);
"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);
} }
/* Merging keyword arguments using the vectorcall convention is messy, so /* Merging keyword arguments using the vectorcall convention is messy, so