From 1ab49208ad258f0e5320f9007930d8e92ee0866e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 13 Jul 2021 17:11:13 +0200 Subject: [PATCH 1/4] bpo-42073: allow classmethod to wrap other classmethod-like descriptors. 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 --- Lib/test/test_decorators.py | 86 +++++++++++++++++++++++++++++++++++++ Objects/funcobject.c | 2 +- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_decorators.py b/Lib/test/test_decorators.py index d4353457933f3d..57a741ffd29742 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/Objects/funcobject.c b/Objects/funcobject.c index f74d96dfc81c25..5a170380cb3e9b 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -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); } From b3dd8d3a5c6066e772bca1d26e25b8bc0e5b2902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 13 Jul 2021 17:47:39 +0200 Subject: [PATCH 2/4] Add blurb --- .../Core and Builtins/2021-07-13-17-47-32.bpo-42073.9wopiC.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2021-07-13-17-47-32.bpo-42073.9wopiC.rst 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 00000000000000..988fe67b99dc96 --- /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. From a3a65a054efadb75608488980a2a4104ebe53f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Thu, 15 Jul 2021 13:32:24 +0200 Subject: [PATCH 3/4] Throw-away commit to check sanity of Azure Pipelines --- Objects/funcobject.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 5a170380cb3e9b..0d6c2ddcdf8555 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -851,7 +851,8 @@ 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, - type); + //type); + NULL); } return PyMethod_New(cm->cm_callable, type); } From 05e8247e0cdc4fae42b39c3db2688d3337f65181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Thu, 15 Jul 2021 14:45:05 +0200 Subject: [PATCH 4/4] Revert "Throw-away commit to check sanity of Azure Pipelines" This reverts commit a3a65a054efadb75608488980a2a4104ebe53f9b. --- Objects/funcobject.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 0d6c2ddcdf8555..5a170380cb3e9b 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -851,8 +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, - //type); - NULL); + type); } return PyMethod_New(cm->cm_callable, type); }