8000 bpo-42073: allow classmethod to wrap other classmethod-like descripto… · python/cpython@2ce8af3 · GitHub
[go: up one dir, main page]

Skip to content

Commit 2ce8af3

Browse files
bpo-42073: allow classmethod to wrap other classmethod-like descriptors (GH-27115) (GH-27162)
Patch by Erik Welch. bpo-19072 (GH-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 GH-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 <erik.n.welch@gmail.com> (cherry picked from commit b83861f)
1 parent 3026d13 commit 2ce8af3

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+
re 8000 turn 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
@@ -825,7 +825,7 @@ cm_descr_get(PyObject *self, PyObject *obj, PyObject *type)
825825
type = (PyObject *)(Py_TYPE(obj));
826826
if (Py_TYPE(cm->cm_callable)->tp_descr_get != NULL) {
827827
return Py_TYPE(cm->cm_callable)->tp_descr_get(cm->cm_callable, type,
828-
NULL);
828+
type);
829829
}
830830
return PyMethod_New(cm->cm_callable, type);
831831
}

0 commit comments

Comments
 (0)
0