bpo-42073: allow classmethod to wrap other classmethod-like descriptors.

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__`.
This commit is contained in:
Erik Welch 2020-10-18 16:41:50 -05:00
parent 7c949020ef
commit f210e7f15e
2 changed files with 87 additions and 1 deletions

View File

@ -1,4 +1,5 @@
import unittest
from types import MethodType
def funcattrs(**kwds):
def decorate(func):
@ -307,6 +308,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):

View File

@ -739,7 +739,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);
}