Skip to content

Commit db96edd

Browse files
gh-121027: Add a future warning in functools.partial.__get__ (#121086)
1 parent 223c03a commit db96edd

File tree

7 files changed

+75
-17
lines changed

7 files changed

+75
-17
lines changed

Doc/whatsnew/3.13.rst

+6
Original file line numberDiff line numberDiff line change
@@ -2262,6 +2262,12 @@ Changes in the Python API
22622262
returned by :meth:`zipfile.ZipFile.open` was changed from ``'r'`` to ``'rb'``.
22632263
(Contributed by Serhiy Storchaka in :gh:`115961`.)
22642264

2265+
* :class:`functools.partial` now emits a :exc:`FutureWarning` when it is
2266+
used as a method.
2267+
Its behavior will be changed in future Python versions.
2268+
Wrap it in :func:`staticmethod` if you want to preserve the old behavior.
2269+
(Contributed by Serhiy Storchaka in :gh:`121027`.)
2270+
22652271
.. _pep667-porting-notes-py:
22662272

22672273
* Calling :func:`locals` in an :term:`optimized scope` now produces an

Lib/functools.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,16 @@ def __repr__(self):
311311
args.extend(f"{k}={v!r}" for (k, v) in self.keywords.items())
312312
return f"{module}.{qualname}({', '.join(args)})"
313313

314+
def __get__(self, obj, objtype=None):
315+
if obj is None:
316+
return self
317+
import warnings
318+
warnings.warn('functools.partial will be a method descriptor in '
319+
'future Python versions; wrap it in staticmethod() '
320+
'if you want to preserve the old behavior',
321+
FutureWarning, 2)
322+
return self
323+
314324
def __reduce__(self):
315325
return type(self), (self.func,), (self.func, self.args,
316326
self.keywords or None, self.__dict__ or None)
@@ -392,7 +402,7 @@ def _method(cls_or_self, /, *args, **keywords):
392402
def __get__(self, obj, cls=None):
393403
get = getattr(self.func, "__get__", None)
394404
result = None
395-
if get is not None:
405+
if get is not None and not isinstance(self.func, partial):
396406
new_func = get(obj, cls)
397407
if new_func is not self.func:
398408
# Assume __get__ returning something new indicates the

Lib/inspect.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -2550,6 +2550,10 @@ def _signature_from_callable(obj, *,
25502550
new_params = (first_wrapped_param,) + sig_params
25512551
return sig.replace(parameters=new_params)
25522552

2553+
if isinstance(obj, functools.partial):
2554+
wrapped_sig = _get_signature_of(obj.func)
2555+
return _signature_get_partial(wrapped_sig, obj)
2556+
25532557
if isfunction(obj) or _signature_is_functionlike(obj):
25542558
# If it's a pure Python function, or an object that is duck type
25552559
# of a Python function (Cython functions, for instance), then:
@@ -2561,10 +2565,6 @@ def _signature_from_callable(obj, *,
25612565
return _signature_from_builtin(sigcls, obj,
25622566
skip_bound_arg=skip_bound_arg)
25632567

2564-
if isinstance(obj, functools.partial):
2565-
wrapped_sig = _get_signature_of(obj.func)
2566-
return _signature_get_partial(wrapped_sig, obj)
2567-
25682568
if isinstance(obj, type):
25692569
# obj is a class or a metaclass
25702570

Lib/test/test_functools.py

+17
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,23 @@ def __getitem__(self, key):
395395
f = self.partial(object)
396396
self.assertRaises(TypeError, f.__setstate__, BadSequence())
397397

398+
def test_partial_as_method(self):
399+
class A:
400+
meth = self.partial(capture, 1, a=2)
401+
cmeth = classmethod(self.partial(capture, 1, a=2))
402+
smeth = staticmethod(self.partial(capture, 1, a=2))
403+
404+
a = A()
405+
self.assertEqual(A.meth(3, b=4), ((1, 3), {'a': 2, 'b': 4}))
406+
self.assertEqual(A.cmeth(3, b=4), ((1, A, 3), {'a': 2, 'b': 4}))
407+
self.assertEqual(A.smeth(3, b=4), ((1, 3), {'a': 2, 'b': 4}))
408+
with self.assertWarns(FutureWarning) as w:
409+
self.assertEqual(a.meth(3, b=4), ((1, 3), {'a': 2, 'b': 4}))
410+
self.assertEqual(w.filename, __file__)
411+
self.assertEqual(a.cmeth(3, b=4), ((1, A, 3), {'a': 2, 'b': 4}))
412+
self.assertEqual(a.smeth(3, b=4), ((1, 3), {'a': 2, 'b': 4}))
413+
414+
398415
@unittest.skipUnless(c_functools, 'requires the C _functools module')
399416
class TestPartialC(TestPartial, unittest.TestCase):
400417
if c_functools:

Lib/test/test_inspect/test_inspect.py

+19-12
Original file line numberDiff line numberDiff line change
@@ -3873,10 +3873,12 @@ class C(metaclass=CM):
38733873
def __init__(self, b):
38743874
pass
38753875

3876-
self.assertEqual(C(1), (2, 1))
3877-
self.assertEqual(self.signature(C),
3878-
((('a', ..., ..., "positional_or_keyword"),),
3879-
...))
3876+
with self.assertWarns(FutureWarning):
3877+
self.assertEqual(C(1), (2, 1))
3878+
with self.assertWarns(FutureWarning):
3879+
self.assertEqual(self.signature(C),
3880+
((('a', ..., ..., "positional_or_keyword"),),
3881+
...))
38803882

38813883
with self.subTest('partialmethod'):
38823884
class CM(type):
@@ -4024,10 +4026,12 @@ class C:
40244026
class C:
40254027
__init__ = functools.partial(lambda x, a: None, 2)
40264028

4027-
C(1) # does not raise
4028-
self.assertEqual(self.signature(C),
4029-
((('a', ..., ..., "positional_or_keyword"),),
4030-
...))
4029+
with self.assertWarns(FutureWarning):
4030+
C(1) # does not raise
4031+
with self.assertWarns(FutureWarning):
4032+
self.assertEqual(self.signature(C),
4033+
((('a', ..., ..., "positional_or_keyword"),),
4034+
...))
40314035

40324036
with self.subTest('partialmethod'):
40334037
class C:
@@ -4282,10 +4286,13 @@ class C:
42824286
class C:
42834287
__call__ = functools.partial(lambda x, a: (x, a), 2)
42844288

4285-
self.assertEqual(C()(1), (2, 1))
4286-
self.assertEqual(self.signature(C()),
4287-
((('a', ..., ..., "positional_or_keyword"),),
4288-
...))
4289+
c = C()
4290+
with self.assertWarns(FutureWarning):
4291+
self.assertEqual(c(1), (2, 1))
4292+
with self.assertWarns(FutureWarning):
4293+
self.assertEqual(self.signature(c),
4294+
((('a', ..., ..., "positional_or_keyword"),),
4295+
...))
42894296

42904297
with self.subTest('partialmethod'):
42914298
class C:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add a future warning in :meth:`!functools.partial.__get__`. In future Python
2+
versions :class:`functools.partial` will be a method descriptor.

Modules/_functoolsmodule.c

+16
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,21 @@ partial_dealloc(partialobject *pto)
197197
Py_DECREF(tp);
198198
}
199199

200+
static PyObject *
201+
partial_descr_get(PyObject *self, PyObject *obj, PyObject *type)
202+
{
203+
if (obj == Py_None || obj == NULL) {
204+
return Py_NewRef(self);
205+
}
206+
if (PyErr_WarnEx(PyExc_FutureWarning,
207+
"functools.partial will be a method descriptor in "
208+
"future Python versions; wrap it in staticmethod() "
209+
"if you want to preserve the old behavior", 1) < 0)
210+
{
211+
return NULL;
212+
}
213+
return Py_NewRef(self);
214+
}
200215

201216
/* Merging keyword arguments using the vectorcall convention is messy, so
202217
* if we would need to do that, we stop using vectorcall and fall back
@@ -514,6 +529,7 @@ static PyType_Slot partial_type_slots[] = {
514529
{Py_tp_methods, partial_methods},
515530
{Py_tp_members, partial_memberlist},
516531
{Py_tp_getset, partial_getsetlist},
532+
{Py_tp_descr_get, (descrgetfunc)partial_descr_get},
517533
{Py_tp_new, partial_new},
518534
{Py_tp_free, PyObject_GC_Del},
519535
{0, 0}

0 commit comments

Comments
 (0)