]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
bpo-42073: allow classmethod to wrap other classmethod-like descriptors (#27115)
authorŁukasz Langa <lukasz@langa.pl>
Thu, 15 Jul 2021 13:16:19 +0000 (15:16 +0200)
committerGitHub <noreply@github.com>
Thu, 15 Jul 2021 13:16:19 +0000 (15:16 +0200)
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>
Lib/test/test_decorators.py
Misc/NEWS.d/next/Core and Builtins/2021-07-13-17-47-32.bpo-42073.9wopiC.rst [new file with mode: 0644]
Objects/funcobject.c

index d4353457933f3db890dedd84a1032631d6f4c2ff..57a741ffd29742d1e80ba18ae57192ee5b3ac2d8 100644 (file)
@@ -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):
 
diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-07-13-17-47-32.bpo-42073.9wopiC.rst b/Misc/NEWS.d/next/Core and Builtins/2021-07-13-17-47-32.bpo-42073.9wopiC.rst
new file mode 100644 (file)
index 0000000..988fe67
--- /dev/null
@@ -0,0 +1,2 @@
+The ``@classmethod`` decorator can now wrap other classmethod-like
+descriptors.
index f74d96dfc81c25bce486e049e325d3dda80385db..5a170380cb3e9bff16f7e3793bd788c99b7535c3 100644 (file)
@@ -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);
 }