From 49e5740135670e04ae6da7e6f52dbe380655e0f1 Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Thu, 27 Jun 2024 14:13:01 +0200 Subject: [3.13] gh-121027: Add a future warning in functools.partial.__get__ (GH-121086) (#121092) 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 +++++++++++++--------- .../2024-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 971f92c..992d39e 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 3d0fd66..d04957c 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -311,6 +311,16 @@ class partial: 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 @@ class partialmethod(object): 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 2c82ad5..bf979e8 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 559213f..1ce0f4d 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -395,6 +395,23 @@ class TestPartial: 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 34739b6..5d0f328 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -3873,10 +3873,12 @@ class TestSignatureObject(unittest.TestCase): 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 TestSignatureObject(unittest.TestCase): 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 TestSignatureObject(unittest.TestCase): 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 0000000..8470c8b --- /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 9dee7bf..564c271 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} -- cgit v0.12