]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-89519: Remove classmethod descriptor chaining, deprecated since 3.11 (gh-110163)
authorRaymond Hettinger <rhettinger@users.noreply.github.com>
Fri, 27 Oct 2023 05:24:56 +0000 (00:24 -0500)
committerGitHub <noreply@github.com>
Fri, 27 Oct 2023 05:24:56 +0000 (00:24 -0500)
Doc/howto/descriptor.rst
Doc/library/functions.rst
Doc/whatsnew/3.13.rst
Lib/test/test_decorators.py
Lib/test/test_doctest.py
Lib/test/test_property.py
Misc/NEWS.d/next/Core and Builtins/2023-09-30-17-30-11.gh-issue-89519.hz2pZf.rst [new file with mode: 0644]
Objects/funcobject.c

index 1d9424cb735a462dc725d96d0010487976009e8a..024c1eb3e3b20000163dd830d121d6869a586ef1 100644 (file)
@@ -1141,6 +1141,16 @@ roughly equivalent to:
             obj = self.__self__
             return func(obj, *args, **kwargs)
 
+        def __getattribute__(self, name):
+            "Emulate method_getset() in Objects/classobject.c"
+            if name == '__doc__':
+                return self.__func__.__doc__
+            return object.__getattribute__(self, name)
+
+        def __getattr__(self, name):
+            "Emulate method_getattro() in Objects/classobject.c"
+            return getattr(self.__func__, name)
+
 To support automatic creation of methods, functions include the
 :meth:`__get__` method for binding methods during attribute access.  This
 means that functions are non-data descriptors that return bound methods
@@ -1420,10 +1430,6 @@ Using the non-data descriptor protocol, a pure Python version of
         def __get__(self, obj, cls=None):
             if cls is None:
                 cls = type(obj)
-            if hasattr(type(self.f), '__get__'):
-                # This code path was added in Python 3.9
-                # and was deprecated in Python 3.11.
-                return self.f.__get__(cls, cls)
             return MethodType(self.f, cls)
 
 .. testcode::
@@ -1436,11 +1442,6 @@ Using the non-data descriptor protocol, a pure Python version of
             "Class method that returns a tuple"
             return (cls.__name__, x, y)
 
-        @ClassMethod
-        @property
-        def __doc__(cls):
-            return f'A doc for {cls.__name__!r}'
-
 
 .. doctest::
     :hide:
@@ -1453,10 +1454,6 @@ Using the non-data descriptor protocol, a pure Python version of
     >>> t.cm(11, 22)
     ('T', 11, 22)
 
-    # Check the alternate path for chained descriptors
-    >>> T.__doc__
-    "A doc for 'T'"
-
     # Verify that T uses our emulation
     >>> type(vars(T)['cm']).__name__
     'ClassMethod'
@@ -1481,24 +1478,6 @@ Using the non-data descriptor protocol, a pure Python version of
     ('T', 11, 22)
 
 
-The code path for ``hasattr(type(self.f), '__get__')`` was added in
-Python 3.9 and makes it possible for :func:`classmethod` to support
-chained decorators.  For example, a classmethod and property could be
-chained together.  In Python 3.11, this functionality was deprecated.
-
-.. testcode::
-
-    class G:
-        @classmethod
-        @property
-        def __doc__(cls):
-            return f'A doc for {cls.__name__!r}'
-
-.. doctest::
-
-    >>> G.__doc__
-    "A doc for 'G'"
-
 The :func:`functools.update_wrapper` call in ``ClassMethod`` adds a
 ``__wrapped__`` attribute that refers to the underlying function.  Also
 it carries forward the attributes necessary to make the wrapper look
index a5f580c07bdbf12cb4ebba7517ab65cda8063397..a72f779f69714a7914bf123bca734c79cf48f03a 100644 (file)
@@ -285,7 +285,7 @@ are always available.  They are listed here in alphabetical order.
       ``__name__``, ``__qualname__``, ``__doc__`` and ``__annotations__``) and
       have a new ``__wrapped__`` attribute.
 
-   .. versionchanged:: 3.11
+   .. deprecated-removed:: 3.11 3.13
       Class methods can no longer wrap other :term:`descriptors <descriptor>` such as
       :func:`property`.
 
index 1053aa5729ede41db33354650ad191086537ef9d..34dd3ea8858ea22d108c3e3669ff5508376920d5 100644 (file)
@@ -1228,6 +1228,14 @@ Deprecated
 Removed
 -------
 
+* Removed chained :class:`classmethod` descriptors (introduced in
+  :issue:`19072`).  This can no longer be used to wrap other descriptors
+  such as :class:`property`.  The core design of this feature was flawed
+  and caused a number of downstream problems.  To "pass-through" a
+  :class:`classmethod`, consider using the :attr:`!__wrapped__`
+  attribute that was added in Python 3.10.  (Contributed by Raymond
+  Hettinger in :gh:`89519`.)
+
 * Remove many APIs (functions, macros, variables) with names prefixed by
   ``_Py`` or ``_PY`` (considered as private API). If your project is affected
   by one of these removals and you consider that the removed API should remain
index 4b492178c1581fe083f65ff90882e9e4ea2461cb..3a4fc959f6f8a72cdd905ebc62f3f2b62e60b6b6 100644 (file)
@@ -291,44 +291,6 @@ 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')
-
     def test_bound_function_inside_classmethod(self):
         class A:
             def foo(self, cls):
@@ -339,91 +301,6 @@ class TestDecorators(unittest.TestCase):
 
         self.assertEqual(B.bar(), 'spam')
 
-    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):
 
index e5b08a3c47a90157e74ab6a79997d36f894196fe..5c59b00e729aa012639edf7bcc6ce694454c8f2b 100644 (file)
@@ -102,15 +102,6 @@ class SampleClass:
 
     a_class_attribute = 42
 
-    @classmethod
-    @property
-    def a_classmethod_property(cls):
-        """
-        >>> print(SampleClass.a_classmethod_property)
-        42
-        """
-        return cls.a_class_attribute
-
     @functools.cached_property
     def a_cached_property(self):
         """
@@ -525,7 +516,6 @@ methods, classmethods, staticmethods, properties, and nested classes.
      1  SampleClass.__init__
      1  SampleClass.a_cached_property
      2  SampleClass.a_classmethod
-     1  SampleClass.a_classmethod_property
      1  SampleClass.a_property
      1  SampleClass.a_staticmethod
      1  SampleClass.double
@@ -582,7 +572,6 @@ functions, classes, and the `__test__` dictionary, if it exists:
      1  some_module.SampleClass.__init__
      1  some_module.SampleClass.a_cached_property
      2  some_module.SampleClass.a_classmethod
-     1  some_module.SampleClass.a_classmethod_property
      1  some_module.SampleClass.a_property
      1  some_module.SampleClass.a_staticmethod
      1  some_module.SampleClass.double
@@ -625,7 +614,6 @@ By default, an object with no doctests doesn't create any tests:
      1  SampleClass.__init__
      1  SampleClass.a_cached_property
      2  SampleClass.a_classmethod
-     1  SampleClass.a_classmethod_property
      1  SampleClass.a_property
      1  SampleClass.a_staticmethod
      1  SampleClass.double
@@ -647,7 +635,6 @@ displays.
      1  SampleClass.__init__
      1  SampleClass.a_cached_property
      2  SampleClass.a_classmethod
-     1  SampleClass.a_classmethod_property
      1  SampleClass.a_property
      1  SampleClass.a_staticmethod
      1  SampleClass.double
index 45aa9e51c06de0552b4f310a52bc2babcaa01d28..c12c908d2ee32d0974fc9da9b47ba3ae03e3d439 100644 (file)
@@ -183,27 +183,6 @@ 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')
-
     def test_property_set_name_incorrect_args(self):
         p = property()
 
diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-09-30-17-30-11.gh-issue-89519.hz2pZf.rst b/Misc/NEWS.d/next/Core and Builtins/2023-09-30-17-30-11.gh-issue-89519.hz2pZf.rst
new file mode 100644 (file)
index 0000000..fd9d0ed
--- /dev/null
@@ -0,0 +1,6 @@
+Removed chained :class:`classmethod` descriptors (introduced in
+:issue:`19072`).  This can no longer be used to wrap other descriptors such
+as :class:`property`.  The core design of this feature was flawed and caused
+a number of downstream problems.  To "pass-through" a :class:`classmethod`,
+consider using the :attr:`!__wrapped__` attribute that was added in Python
+3.10.
index 8665c7745ffb39c7506f791f93a9b37fe6b54017..56c5af6de8989d21e6f11209dba29eb054344838 100644 (file)
@@ -1110,10 +1110,6 @@ 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,
-                                                      type);
-    }
     return PyMethod_New(cm->cm_callable, type);
 }