Skip to content

Commit d3f2f91

Browse files
berkerpeksaglisroach
authored andcommitted
bpo-19072: Make @classmethod support chained decorators (pythonGH-8405)
1 parent f3a1286 commit d3f2f91

File tree

5 files changed

+71
-2
lines changed

5 files changed

+71
-2
lines changed

Doc/library/functions.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,12 @@ are always available. They are listed here in alphabetical order.
222222
implied first argument.
223223

224224
Class methods are different than C++ or Java static methods. If you want those,
225-
see :func:`staticmethod`.
226-
225+
see :func:`staticmethod` in this section.
227226
For more information on class methods, see :ref:`types`.
228227

228+
.. versionchanged:: 3.9
229+
Class methods can now wrap other :term:`descriptors <descriptor>` such as
230+
:func:`property`.
229231

230232
.. function:: compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1)
231233

Lib/test/test_decorators.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,45 @@ def bar(): return 42
265265
self.assertEqual(bar(), 42)
266266
self.assertEqual(actions, expected_actions)
267267

268+
def test_wrapped_descriptor_inside_classmethod(self):
269+
class BoundWrapper:
270+
def __init__(self, wrapped):
271+
self.__wrapped__ = wrapped
272+
273+
def __call__(self, *args, **kwargs):
274+
return self.__wrapped__(*args, **kwargs)
275+
276+
class Wrapper:
277+
def __init__(self, wrapped):
278+
self.__wrapped__ = wrapped
279+
280+
def __get__(self, instance, owner):
281+
bound_function = self.__wrapped__.__get__(instance, owner)
282+
return BoundWrapper(bound_function)
283+
284+
def decorator(wrapped):
285+
return Wrapper(wrapped)
286+
287+
class Class:
288+
@decorator
289+
@classmethod
290+
def inner(cls):
291+
# This should already work.
292+
return 'spam'
293+
294+
@classmethod
295+
@decorator
296+
def outer(cls):
297+
# Raised TypeError with a message saying that the 'Wrapper'
298+
# object is not callable.
299+
return 'eggs'
300+
301+
self.assertEqual(Class.inner(), 'spam')
302+
self.assertEqual(Class.outer(), 'eggs')
303+
self.assertEqual(Class().inner(), 'spam')
304+
self.assertEqual(Class().outer(), 'eggs')
305+
306+
268307
class TestClassDecorators(unittest.TestCase):
269308

270309
def test_simple(self):

Lib/test/test_property.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,27 @@ def test_refleaks_in___init__(self):
183183
fake_prop.__init__('fget', 'fset', 'fdel', 'doc')
184184
self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10)
185185

186+
@unittest.skipIf(sys.flags.optimize >= 2,
187+
"Docstrings are omitted with -O2 and above")
188+
def test_class_property(self):
189+
class A:
190+
@classmethod
191+
@property
192+
def __doc__(cls):
193+
return 'A doc for %r' % cls.__name__
194+
self.assertEqual(A.__doc__, "A doc for 'A'")
195+
196+
@unittest.skipIf(sys.flags.optimize >= 2,
197+
"Docstrings are omitted with -O2 and above")
198+
def test_class_property_override(self):
199+
class A:
200+
"""First"""
201+
@classmethod
202+
@property
203+
def __doc__(cls):
204+
return 'Second'
205+
self.assertEqual(A.__doc__, 'Second')
206+
186207

187208
# Issue 5890: subclasses of property do not preserve method __doc__ strings
188209
class PropertySub(property):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The :class:`classmethod` decorator can now wrap other descriptors
2+
such as property objects. Adapted from a patch written by Graham
3+
Dumpleton.

Objects/funcobject.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,10 @@ cm_descr_get(PyObject *self, PyObject *obj, PyObject *type)
741741
}
742742
if (type == NULL)
743743
type = (PyObject *)(Py_TYPE(obj));
744+
if (Py_TYPE(cm->cm_callable)->tp_descr_get != NULL) {
745+
return Py_TYPE(cm->cm_callable)->tp_descr_get(cm->cm_callable, type,
746+
NULL);
747+
}
744748
return PyMethod_New(cm->cm_callable, type);
745749
}
746750

0 commit comments

Comments
 (0)