Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit f210e7f

Browse filesBrowse files
committed
bpo-42073: allow classmethod to wrap other classmethod-like descriptors.
bpo-19072 (python#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 python#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__`.
1 parent 7c94902 commit f210e7f
Copy full SHA for f210e7f

File tree

Expand file treeCollapse file tree

2 files changed

+87
-1
lines changed
Filter options
Expand file treeCollapse file tree

2 files changed

+87
-1
lines changed

‎Lib/test/test_decorators.py

Copy file name to clipboardExpand all lines: Lib/test/test_decorators.py
+86Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import unittest
2+
from types import MethodType
23

34
def funcattrs(**kwds):
45
def decorate(func):
@@ -307,6 +308,91 @@ def outer(cls):
307308
self.assertEqual(Class().inner(), 'spam')
308309
self.assertEqual(Class().outer(), 'eggs')
309310

311+
def test_wrapped_classmethod_inside_classmethod(self):
312+
class MyClassMethod1:
313+
def __init__(self, func):
314+
self.func = func
315+
316+
def __call__(self, cls):
317+
if hasattr(self.func, '__get__'):
318+
return self.func.__get__(cls, cls)()
319+
return self.func(cls)
320+
321+
def __get__(self, instance, owner=None):
322+
if owner is None:
323+
owner = type(instance)
324+
return MethodType(self, owner)
325+
326+
class MyClassMethod2:
327+
def __init__(self, func):
328+
if isinstance(func, classmethod):
329+
func = func.__func__
330+
self.func = func
331+
332+
def __call__(self, cls):
333+
return self.func(cls)
334+
335+
def __get__(self, instance, owner=None):
336+
if owner is None:
337+
owner = type(instance)
338+
return MethodType(self, owner)
339+
340+
for myclassmethod in [MyClassMethod1, MyClassMethod2]:
341+
class A:
342+
@myclassmethod
343+
def f1(cls):
344+
return cls
345+
346+
@classmethod
347+
@myclassmethod
348+
def f2(cls):
349+
return cls
350+
351+
@myclassmethod
352+
@classmethod
353+
def f3(cls):
354+
return cls
355+
356+
@classmethod
357+
@classmethod
358+
def f4(cls):
359+
return cls
360+
361+
@myclassmethod
362+
@MyClassMethod1
363+
def f5(cls):
364+
return cls
365+
366+
@myclassmethod
367+
@MyClassMethod2
368+
def f6(cls):
369+
return cls
370+
371+
self.assertIs(A.f1(), A)
372+
self.assertIs(A.f2(), A)
373+
self.assertIs(A.f3(), A)
374+
self.assertIs(A.f4(), A)
375+
self.assertIs(A.f5(), A)
376+
self.assertIs(A.f6(), A)
377+
a = A()
378+
self.assertIs(a.f1(), A)
379+
self.assertIs(a.f2(), A)
380+
self.assertIs(a.f3(), A)
381+
self.assertIs(a.f4(), A)
382+
self.assertIs(a.f5(), A)
383+
self.assertIs(a.f6(), A)
384+
385+
def f(cls):
386+
return cls
387+
388+
self.assertIs(myclassmethod(f).__get__(a)(), A)
389+
self.assertIs(myclassmethod(f).__get__(a, A)(), A)
390+
self.assertIs(myclassmethod(f).__get__(A, A)(), A)
391+
self.assertIs(myclassmethod(f).__get__(A)(), type(A))
392+
self.assertIs(classmethod(f).__get__(a)(), A)
393+
self.assertIs(classmethod(f).__get__(a, A)(), A)
394+
self.assertIs(classmethod(f).__get__(A, A)(), A)
395+
self.assertIs(classmethod(f).__get__(A)(), type(A))
310396

311397
class TestClassDecorators(unittest.TestCase):
312398

‎Objects/funcobject.c

Copy file name to clipboardExpand all lines: Objects/funcobject.c
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -739,7 +739,7 @@ cm_descr_get(PyObject *self, PyObject *obj, PyObject *type)
739739
type = (PyObject *)(Py_TYPE(obj));
740740
if (Py_TYPE(cm->cm_callable)->tp_descr_get != NULL) {
741741
return Py_TYPE(cm->cm_callable)->tp_descr_get(cm->cm_callable, type,
742-
NULL);
742+
type);
743743
}
744744
return PyMethod_New(cm->cm_callable, type);
745745
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.