[Python-checkins] bpo-42073: allow classmethod to wrap other classmethod-like descriptors (GH-27115) (GH-27162)

ambv webhook-mailer at python.org
Thu Jul 15 09:42:16 EDT 2021


https://github.com/python/cpython/commit/2ce8af3cbcb368a35a05a5a9f97a09405124f239
commit: 2ce8af3cbcb368a35a05a5a9f97a09405124f239
branch: 3.10
author: Miss Islington (bot) <31488909+miss-islington at users.noreply.github.com>
committer: ambv <lukasz at langa.pl>
date: 2021-07-15T15:42:11+02:00
summary:

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 at gmail.com>
(cherry picked from commit b83861f0265e07207a6ae2c49c40fa8f447893f2)

files:
A Misc/NEWS.d/next/Core and Builtins/2021-07-13-17-47-32.bpo-42073.9wopiC.rst
M Lib/test/test_decorators.py
M Objects/funcobject.c

diff --git a/Lib/test/test_decorators.py b/Lib/test/test_decorators.py
index d4353457933f3..57a741ffd2974 100644
--- a/Lib/test/test_decorators.py
+++ b/Lib/test/test_decorators.py
@@ -1,5 +1,6 @@
 from test import support
 import unittest
+from types import MethodType
 
 def funcattrs(**kwds):
     def decorate(func):
@@ -329,6 +330,91 @@ def outer(cls):
         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
index 0000000000000..988fe67b99dc9
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2021-07-13-17-47-32.bpo-42073.9wopiC.rst	
@@ -0,0 +1,2 @@
+The ``@classmethod`` decorator can now wrap other classmethod-like
+descriptors.
diff --git a/Objects/funcobject.c b/Objects/funcobject.c
index f0b0b673d4fa4..da648b7a0ebad 100644
--- a/Objects/funcobject.c
+++ b/Objects/funcobject.c
@@ -825,7 +825,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);
 }



More information about the Python-checkins mailing list