From 872bb2970c804d7b020e4f21c8fbb4a3ea71fab5 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 27 Jun 2024 14:47:20 +0300 Subject: [PATCH] gh-121027: Add a future warning in functools.partial.__get__ (GH-121086) (cherry picked from commit db96edd6d1a58045196a71aff565743f493b5fbb) Co-authored-by: Serhiy Storchaka --- Doc/whatsnew/3.13.rst | 6 ++++ Lib/functools.py | 12 ++++++- Lib/inspect.py | 8 ++--- Lib/test/test_functools.py | 17 ++++++++++ Lib/test/test_inspect/test_inspect.py | 31 ++++++++++++------- ...-06-27-13-47-14.gh-issue-121027.jh55EC.rst | 2 ++ Modules/_functoolsmodule.c | 16 ++++++++++ 7 files changed, 75 insertions(+), 17 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-06-27-13-47-14.gh-issue-121027.jh55EC.rst diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 971f92c39298ce..992d39e3152a64 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -2250,6 +2250,12 @@ Changes in the Python API returned by :meth:`zipfile.ZipFile.open` was changed from ``'r'`` to ``'rb'``. (Contributed by Serhiy Storchaka in :gh:`115961`.) +* :class:`functools.partial` now emits a :exc:`FutureWarning` when it is + used as a method. + Its behavior will be changed in future Python versions. + Wrap it in :func:`staticmethod` if you want to preserve the old behavior. + (Contributed by Serhiy Storchaka in :gh:`121027`.) + .. _pep667-porting-notes-py: * Calling :func:`locals` in an :term:`optimized scope` now produces an diff --git a/Lib/functools.py b/Lib/functools.py index 3d0fd6671fb63e..d04957c555295e 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -311,6 +311,16 @@ def __repr__(self): args.extend(f"{k}={v!r}" for (k, v) in self.keywords.items()) return f"{module}.{qualname}({', '.join(args)})" + 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 + def __reduce__(self): return type(self), (self.func,), (self.func, self.args, self.keywords or None, self.__dict__ or None) @@ -392,7 +402,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: + if get is not None and not isinstance(self.func, partial): new_func = get(obj, cls) if new_func is not self.func: # Assume __get__ returning something new indicates the diff --git a/Lib/inspect.py b/Lib/inspect.py index 2c82ad53cb7746..bf979e8e63ff65 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2556,6 +2556,10 @@ def _signature_from_callable(obj, *, new_params = (first_wrapped_param,) + sig_params return sig.replace(parameters=new_params) + if isinstance(obj, functools.partial): + wrapped_sig = _get_signature_of(obj.func) + return _signature_get_partial(wrapped_sig, obj) + if isfunction(obj) or _signature_is_functionlike(obj): # If it's a pure Python function, or an object that is duck type # of a Python function (Cython functions, for instance), then: @@ -2567,10 +2571,6 @@ def _signature_from_callable(obj, *, return _signature_from_builtin(sigcls, obj, skip_bound_arg=skip_bound_arg) - if isinstance(obj, functools.partial): - wrapped_sig = _get_signature_of(obj.func) - return _signature_get_partial(wrapped_sig, obj) - if isinstance(obj, type): # obj is a class or a metaclass diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 559213fef1313d..1ce0f4d0aea6ee 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -395,6 +395,23 @@ def __getitem__(self, key): f = self.partial(object) self.assertRaises(TypeError, f.__setstate__, BadSequence()) + def test_partial_as_method(self): + class A: + meth = self.partial(capture, 1, a=2) + cmeth = classmethod(self.partial(capture, 1, a=2)) + smeth = staticmethod(self.partial(capture, 1, a=2)) + + a = 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.cmeth(3, b=4), ((1, A, 3), {'a': 2, 'b': 4})) + self.assertEqual(a.smeth(3, b=4), ((1, 3), {'a': 2, 'b': 4})) + + @unittest.skipUnless(c_functools, 'requires the C _functools module') class TestPartialC(TestPartial, unittest.TestCase): if c_functools: diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 34739b6f5db2f5..5d0f32884d1406 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -3873,10 +3873,12 @@ class C(metaclass=CM): def __init__(self, b): pass - self.assertEqual(C(1), (2, 1)) - self.assertEqual(self.signature(C), - ((('a', ..., ..., "positional_or_keyword"),), - ...)) + with self.assertWarns(FutureWarning): + self.assertEqual(C(1), (2, 1)) + with self.assertWarns(FutureWarning): + self.assertEqual(self.signature(C), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) with self.subTest('partialmethod'): class CM(type): @@ -4024,10 +4026,12 @@ class C: class C: __init__ = functools.partial(lambda x, a: None, 2) - C(1) # does not raise - self.assertEqual(self.signature(C), - ((('a', ..., ..., "positional_or_keyword"),), - ...)) + with self.assertWarns(FutureWarning): + C(1) # does not raise + with self.assertWarns(FutureWarning): + self.assertEqual(self.signature(C), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) with self.subTest('partialmethod'): class C: @@ -4282,10 +4286,13 @@ class C: class C: __call__ = functools.partial(lambda x, a: (x, a), 2) - self.assertEqual(C()(1), (2, 1)) - self.assertEqual(self.signature(C()), - ((('a', ..., ..., "positional_or_keyword"),), - ...)) + 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"),), + ...)) with self.subTest('partialmethod'): class C: diff --git a/Misc/NEWS.d/next/Library/2024-06-27-13-47-14.gh-issue-121027.jh55EC.rst b/Misc/NEWS.d/next/Library/2024-06-27-13-47-14.gh-issue-121027.jh55EC.rst new file mode 100644 index 00000000000000..8470c8b37ac83d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-06-27-13-47-14.gh-issue-121027.jh55EC.rst @@ -0,0 +1,2 @@ +Add a future warning in :meth:`!functools.partial.__get__`. In future Python +versions :class:`functools.partial` will be a method descriptor. diff --git a/Modules/_functoolsmodule.c b/Modules/_functoolsmodule.c index 9dee7bf3062710..564c271915959a 100644 --- a/Modules/_functoolsmodule.c +++ b/Modules/_functoolsmodule.c @@ -197,6 +197,21 @@ partial_dealloc(partialobject *pto) Py_DECREF(tp); } +static PyObject * +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); +} /* Merging keyword arguments using the vectorcall convention is messy, so * if we would need to do that, we stop using vectorcall and fall back @@ -514,6 +529,7 @@ static PyType_Slot partial_type_slots[] = { {Py_tp_methods, partial_methods}, {Py_tp_members, partial_memberlist}, {Py_tp_getset, partial_getsetlist}, + {Py_tp_descr_get, (descrgetfunc)partial_descr_get}, {Py_tp_new, partial_new}, {Py_tp_free, PyObject_GC_Del}, {0, 0} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy