diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 9662044915b8ca..02a091127e5fda 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -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 Dominykas Grigonis in :gh:`121027`.) Build Changes ============= diff --git a/Lib/functools.py b/Lib/functools.py index d04957c555295e..a10493f0e25360 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -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 @@ -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, @@ -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 diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 1ce0f4d0aea6ee..492a16a8c7ff45 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -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})) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 308c09874fe2ac..d39c3ccdc847bd 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -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): @@ -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: @@ -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: diff --git a/Misc/NEWS.d/next/Library/2024-06-27-12-27-52.gh-issue-121027.D4K1OX.rst b/Misc/NEWS.d/next/Library/2024-06-27-12-27-52.gh-issue-121027.D4K1OX.rst new file mode 100644 index 00000000000000..a450726d9afed9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-06-27-12-27-52.gh-issue-121027.D4K1OX.rst @@ -0,0 +1 @@ +Make the :class:`functools.partial` object a method descriptor. diff --git a/Modules/_functoolsmodule.c b/Modules/_functoolsmodule.c index 564c271915959a..64766b474514bf 100644 --- a/Modules/_functoolsmodule.c +++ b/Modules/_functoolsmodule.c @@ -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