]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-121027: Add a future warning in functools.partial.__get__ (GH-121086) ...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Thu, 27 Jun 2024 12:13:01 +0000 (14:13 +0200)
committerGitHub <noreply@github.com>
Thu, 27 Jun 2024 12:13:01 +0000 (12:13 +0000)
gh-121027: Add a future warning in functools.partial.__get__ (GH-121086)
(cherry picked from commit db96edd6d1a58045196a71aff565743f493b5fbb)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Doc/whatsnew/3.13.rst
Lib/functools.py
Lib/inspect.py
Lib/test/test_functools.py
Lib/test/test_inspect/test_inspect.py
Misc/NEWS.d/next/Library/2024-06-27-13-47-14.gh-issue-121027.jh55EC.rst [new file with mode: 0644]
Modules/_functoolsmodule.c

index 971f92c39298ce3ac7510196398b0d421f6c91a5..992d39e3152a648afe558647928bba1eeefaf720 100644 (file)
@@ -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
index 3d0fd6671fb63e1965f24399e63bdff95ceb1969..d04957c555295e41ed221f856bbf5a5a45e0dd6f 100644 (file)
@@ -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
index 2c82ad53cb77461d9a6133d3edcad8261bcc8a47..bf979e8e63ff6530bcc52f616d65a5e51de4860c 100644 (file)
@@ -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
 
index 559213fef1313d9ee169c49d1bbff0aa4fa5977b..1ce0f4d0aea6ee477cbb09a54595c02317081981 100644 (file)
@@ -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:
index 34739b6f5db2f57f9c9a3d372a6e9f51c381c8b7..5d0f32884d14064671d09553204b23c7f8974e66 100644 (file)
@@ -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 (file)
index 0000000..8470c8b
--- /dev/null
@@ -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.
index 9dee7bf30627100712465eddfa6301458e8e07e6..564c271915959adbe69d1d9c578ed08eebef4227 100644 (file)
@@ -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}