]> granicus.if.org Git - python/commitdiff
bpo-19072: Make @classmethod support chained decorators (GH-8405)
authorBerker Peksag <berker.peksag@gmail.com>
Sat, 24 Aug 2019 22:37:25 +0000 (01:37 +0300)
committerRaymond Hettinger <rhettinger@users.noreply.github.com>
Sat, 24 Aug 2019 22:37:25 +0000 (15:37 -0700)
Doc/library/functions.rst
Lib/test/test_decorators.py
Lib/test/test_property.py
Misc/NEWS.d/next/Core and Builtins/2018-07-23-13-09-54.bpo-19072.Gc59GS.rst [new file with mode: 0644]
Objects/funcobject.c

index c225f3dee9215faead323c794d91e3f8d08192f8..a7b6610ebdf1edd54387cbea677d960b4f2c7983 100644 (file)
@@ -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)
 
index d0a2ec9fddb866d28e5ad1e17537d7ab9c776964..8953f64806122514183e4cde87af6cff30c1faf4 100644 (file)
@@ -265,6 +265,45 @@ class TestDecorators(unittest.TestCase):
         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):
index f6f8f5ed0e45ee604ec517dc211e5cc4172f6cc2..172737ade143fa642a5a71f37865d66f243bfd8e 100644 (file)
@@ -183,6 +183,27 @@ class PropertyTests(unittest.TestCase):
             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):
diff --git a/Misc/NEWS.d/next/Core and Builtins/2018-07-23-13-09-54.bpo-19072.Gc59GS.rst b/Misc/NEWS.d/next/Core and Builtins/2018-07-23-13-09-54.bpo-19072.Gc59GS.rst
new file mode 100644 (file)
index 0000000..1d27789
--- /dev/null
@@ -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.
index a65c1f4a55bb4de4a0979e18ac783df490b162c1..b6ffc2a184c99bf62203f3c0a15ea66b1c8b85a9 100644 (file)
@@ -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);
 }