mirror of https://github.com/python/cpython
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 <erik.n.welch@gmail.com>
This commit is contained in:
parent
641345d636
commit
b83861f026
|
@ -1,5 +1,6 @@
|
|||
from test import support
|
||||
import unittest
|
||||
from types import MethodType
|
||||
|
||||
def funcattrs(**kwds):
|
||||
def decorate(func):
|
||||
|
@ -329,6 +330,91 @@ class TestDecorators(unittest.TestCase):
|
|||
self.assertEqual(Class().inner(), 'spam')
|
||||
self.assertEqual(Class().outer(), 'eggs')
|
||||
|
||||
def test_wrapped_classmethod_inside_classmethod(self):
|
||||
class MyClassMethod1:
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
||||
def __call__(self, cls):
|
||||
if hasattr(self.func, '__get__'):
|
||||
return self.func.__get__(cls, cls)()
|
||||
return self.func(cls)
|
||||
|
||||
def __get__(self, instance, owner=None):
|
||||
if owner is None:
|
||||
owner = type(instance)
|
||||
return MethodType(self, owner)
|
||||
|
||||
class MyClassMethod2:
|
||||
def __init__(self, func):
|
||||
if isinstance(func, classmethod):
|
||||
func = func.__func__
|
||||
self.func = func
|
||||
|
||||
def __call__(self, cls):
|
||||
return self.func(cls)
|
||||
|
||||
def __get__(self, instance, owner=None):
|
||||
if owner is None:
|
||||
owner = type(instance)
|
||||
return MethodType(self, owner)
|
||||
|
||||
for myclassmethod in [MyClassMethod1, MyClassMethod2]:
|
||||
class A:
|
||||
@myclassmethod
|
||||
def f1(cls):
|
||||
return cls
|
||||
|
||||
@classmethod
|
||||
@myclassmethod
|
||||
def f2(cls):
|
||||
return cls
|
||||
|
||||
@myclassmethod
|
||||
@classmethod
|
||||
def f3(cls):
|
||||
return cls
|
||||
|
||||
@classmethod
|
||||
@classmethod
|
||||
def f4(cls):
|
||||
return cls
|
||||
|
||||
@myclassmethod
|
||||
@MyClassMethod1
|
||||
def f5(cls):
|
||||
return cls
|
||||
|
||||
@myclassmethod
|
||||
@MyClassMethod2
|
||||
def f6(cls):
|
||||
return cls
|
||||
|
||||
self.assertIs(A.f1(), A)
|
||||
self.assertIs(A.f2(), A)
|
||||
self.assertIs(A.f3(), A)
|
||||
self.assertIs(A.f4(), A)
|
||||
self.assertIs(A.f5(), A)
|
||||
self.assertIs(A.f6(), A)
|
||||
a = A()
|
||||
self.assertIs(a.f1(), A)
|
||||
self.assertIs(a.f2(), A)
|
||||
self.assertIs(a.f3(), A)
|
||||
self.assertIs(a.f4(), A)
|
||||
self.assertIs(a.f5(), A)
|
||||
self.assertIs(a.f6(), A)
|
||||
|
||||
def f(cls):
|
||||
return cls
|
||||
|
||||
self.assertIs(myclassmethod(f).__get__(a)(), A)
|
||||
self.assertIs(myclassmethod(f).__get__(a, A)(), A)
|
||||
self.assertIs(myclassmethod(f).__get__(A, A)(), A)
|
||||
self.assertIs(myclassmethod(f).__get__(A)(), type(A))
|
||||
self.assertIs(classmethod(f).__get__(a)(), A)
|
||||
self.assertIs(classmethod(f).__get__(a, A)(), A)
|
||||
self.assertIs(classmethod(f).__get__(A, A)(), A)
|
||||
self.assertIs(classmethod(f).__get__(A)(), type(A))
|
||||
|
||||
class TestClassDecorators(unittest.TestCase):
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
The ``@classmethod`` decorator can now wrap other classmethod-like
|
||||
descriptors.
|
|
@ -851,7 +851,7 @@ cm_descr_get(PyObject *self, PyObject *obj, PyObject *type)
|
|||
type = (PyObject *)(Py_TYPE(obj));
|
||||
if (Py_TYPE(cm->cm_callable)->tp_descr_get != NULL) {
|
||||
return Py_TYPE(cm->cm_callable)->tp_descr_get(cm->cm_callable, type,
|
||||
NULL);
|
||||
type);
|
||||
}
|
||||
return PyMethod_New(cm->cm_callable, type);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue