Skip to content

Commit

Permalink
bpo-19072: Make @classmethod support chained decorators (GH-8405)
Browse files Browse the repository at this point in the history
  • Loading branch information
berkerpeksag authored and rhettinger committed Aug 24, 2019
1 parent 0dfc025 commit 805f8f9
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 2 deletions.
6 changes: 4 additions & 2 deletions Doc/library/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,12 @@ are always available. They are listed here in alphabetical order.
implied first argument.

Class methods are different than C++ or Java static methods. If you want those,
see :func:`staticmethod`.

see :func:`staticmethod` in this section.
For more information on class methods, see :ref:`types`.

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

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

Expand Down
39 changes: 39 additions & 0 deletions Lib/test/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,45 @@ def bar(): return 42
self.assertEqual(bar(), 42)
self.assertEqual(actions, expected_actions)

def test_wrapped_descriptor_inside_classmethod(self):
class BoundWrapper:
def __init__(self, wrapped):
self.__wrapped__ = wrapped

def __call__(self, *args, **kwargs):
return self.__wrapped__(*args, **kwargs)

class Wrapper:
def __init__(self, wrapped):
self.__wrapped__ = wrapped

def __get__(self, instance, owner):
bound_function = self.__wrapped__.__get__(instance, owner)
return BoundWrapper(bound_function)

def decorator(wrapped):
return Wrapper(wrapped)

class Class:
@decorator
@classmethod
def inner(cls):
# This should already work.
return 'spam'

@classmethod
@decorator
def outer(cls):
# Raised TypeError with a message saying that the 'Wrapper'
# object is not callable.
return 'eggs'

self.assertEqual(Class.inner(), 'spam')
self.assertEqual(Class.outer(), 'eggs')
self.assertEqual(Class().inner(), 'spam')
self.assertEqual(Class().outer(), 'eggs')


class TestClassDecorators(unittest.TestCase):

def test_simple(self):
Expand Down
21 changes: 21 additions & 0 deletions Lib/test/test_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,27 @@ def test_refleaks_in___init__(self):
fake_prop.__init__('fget', 'fset', 'fdel', 'doc')
self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10)

@unittest.skipIf(sys.flags.optimize >= 2,
"Docstrings are omitted with -O2 and above")
def test_class_property(self):
class A:
@classmethod
@property
def __doc__(cls):
return 'A doc for %r' % cls.__name__
self.assertEqual(A.__doc__, "A doc for 'A'")

@unittest.skipIf(sys.flags.optimize >= 2,
"Docstrings are omitted with -O2 and above")
def test_class_property_override(self):
class A:
"""First"""
@classmethod
@property
def __doc__(cls):
return 'Second'
self.assertEqual(A.__doc__, 'Second')


# Issue 5890: subclasses of property do not preserve method __doc__ strings
class PropertySub(property):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The :class:`classmethod` decorator can now wrap other descriptors
such as property objects. Adapted from a patch written by Graham
Dumpleton.
4 changes: 4 additions & 0 deletions Objects/funcobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,10 @@ cm_descr_get(PyObject *self, PyObject *obj, PyObject *type)
}
if (type == NULL)
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);
}
return PyMethod_New(cm->cm_callable, type);
}

Expand Down

0 comments on commit 805f8f9

Please sign in to comment.