Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b83861f

Browse files
ambveriknw
andauthored
bpo-42073: allow classmethod to wrap other classmethod-like descriptors (#27115)
Patch by Erik Welch. bpo-19072 (#8405) allows `classmethod` to wrap other descriptors, but this does not work when the wrapped descriptor mimics classmethod. The current PR fixes this. In Python 3.8 and before, one could create a callable descriptor such that this works as expected (see Lib/test/test_decorators.py for examples): ```python class A: @myclassmethod def f1(cls): return cls @classmethod @myclassmethod def f2(cls): return cls ``` In Python 3.8 and before, `A.f2()` return `A`. Currently in Python 3.9, it returns `type(A)`. This PR make `A.f2()` return `A` again. As of #8405, classmethod calls `obj.__get__(type)` if `obj` has `__get__`. This allows one to chain `@classmethod` and `@property` together. When using classmethod-like descriptors, it's the second argument to `__get__`--the owner or the type--that is important, but this argument is currently missing. Since it is None, the "owner" argument is assumed to be the type of the first argument, which, in this case, is wrong (we want `A`, not `type(A)`). This PR updates classmethod to call `obj.__get__(type, type)` if `obj` has `__get__`. Co-authored-by: Erik Welch <[email protected]>
1 parent 641345d commit b83861f

File tree

3 files changed

+89
-1
lines changed

3 files changed

+89
-1
lines changed

Lib/test/test_decorators.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from test import support
22
import unittest
3+
from types import MethodType
34

45
def funcattrs(**kwds):
56
def decorate(func):
@@ -329,6 +330,91 @@ def outer(cls):
329330
self.assertEqual(Class().inner(), 'spam')
330331
self.assertEqual(Class().outer(), 'eggs')
331332

333+
def test_wrapped_classmethod_inside_classmethod(self):
334+
class MyClassMethod1:
335+
def __init__(self, func):
336+
self.func = func
337+
338+
def __call__(self, cls):
339+
if hasattr(self.func, '__get__'):
340+
return self.func.__get__(cls, cls)()
341+
return self.func(cls)
342+
343+
def __get__(self, instance, owner=None):
344+
if owner is None:
345+
owner = type(instance)
346+
return MethodType(self, owner)
347+
348+
class MyClassMethod2:
349+
def __init__(self, func):
350+
if isinstance(func, classmethod):
351+
func = func.__func__
352+
self.func = func
353+
354+
def __call__(self, cls):
355+
return self.func(cls)
356+
357+
def __get__(self, instance, owner=None):
358+
if owner is None:
359+
owner = type(instance)
360+
return MethodType(self, owner)
361+
362+
for myclassmethod in [MyClassMethod1, MyClassMethod2]:
363+
class A:
364+
@myclassmethod
365+
def f1(cls):
366+
return cls
367+
368+
@classmethod
369+
@myclassmethod
370+
def f2(cls):
371+
return cls
372+
373+
@myclassmethod
374+
@classmethod
375+
def f3(cls):
376+
return cls
377+
378+
@classmethod
379+
@classmethod
380+
def f4(cls):
381+
return cls
382+
383+
@myclassmethod
384+
@MyClassMethod1
385+
def f5(cls):
386+
return cls
387+
388+
@myclassmethod
389+
@MyClassMethod2
390+
def f6(cls):
391+
return cls
392+
393+
self.assertIs(A.f1(), A)
394+
self.assertIs(A.f2(), A)
395+
self.assertIs(A.f3(), A)
396+
self.assertIs(A.f4(), A)
397+
self.assertIs(A.f5(), A)
398+
self.assertIs(A.f6(), A)
399+
a = A()
400+
self.assertIs(a.f1(), A)
401+
self.assertIs(a.f2(), A)
402+
self.assertIs(a.f3(), A)
403+
self.assertIs(a.f4(), A)
404+
self.assertIs(a.f5(), A)
405+
self.assertIs(a.f6(), A)
406+
407+
def f(cls):
408+
return cls
409+
410+
self.assertIs(myclassmethod(f).__get__(a)(), A)
411+
self.assertIs(myclassmethod(f).__get__(a, A)(), A)
412+
self.assertIs(myclassmethod(f).__get__(A, A)(), A)
413+
self.assertIs(myclassmethod(f).__get__(A)(), type(A))
414+
self.assertIs(classmethod(f).__get__(a)(), A)
415+
self.assertIs(classmethod(f).__get__(a, A)(), A)
416+
self.assertIs(classmethod(f).__get__(A, A)(), A)
417+
self.assertIs(classmethod(f).__get__(A)(), type(A))
332418

333419
class TestClassDecorators(unittest.TestCase):
334420

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The ``@classmethod`` decorator can now wrap other classmethod-like
2+
descriptors.

Objects/funcobject.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -851,7 +851,7 @@ cm_descr_get(PyObject *self, PyObject *obj, PyObject *type)
851851
type = (PyObject *)(Py_TYPE(obj));
852852
if (Py_TYPE(cm->cm_callable)->tp_descr_get != NULL) {
853853
return Py_TYPE(cm->cm_callable)->tp_descr_get(cm->cm_callable, type,
854-
NULL);
854+
type);
855855
}
856856
return PyMethod_New(cm->cm_callable, type);
857857
}

0 commit comments

Comments
 (0)