summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRaymond Hettinger <rhettinger@users.noreply.github.com>2023-10-27 05:24:56 (GMT)
committerGitHub <noreply@github.com>2023-10-27 05:24:56 (GMT)
commit7f9a99e8549b792662f2cd28bf38a4d4625bd402 (patch)
tree3e31df6a620f6a41c9a3547187ee794bbe343823
parentee2d22f06d8a4ca13b2dba5e8a7a639a3997cc69 (diff)
downloadcpython-7f9a99e8549b792662f2cd28bf38a4d4625bd402.zip
cpython-7f9a99e8549b792662f2cd28bf38a4d4625bd402.tar.gz
cpython-7f9a99e8549b792662f2cd28bf38a4d4625bd402.tar.bz2
gh-89519: Remove classmethod descriptor chaining, deprecated since 3.11 (gh-110163)
-rw-r--r--Doc/howto/descriptor.rst41
-rw-r--r--Doc/library/functions.rst2
-rw-r--r--Doc/whatsnew/3.13.rst8
-rw-r--r--Lib/test/test_decorators.py123
-rw-r--r--Lib/test/test_doctest.py13
-rw-r--r--Lib/test/test_property.py21
-rw-r--r--Misc/NEWS.d/next/Core and Builtins/2023-09-30-17-30-11.gh-issue-89519.hz2pZf.rst6
-rw-r--r--Objects/funcobject.c4
8 files changed, 25 insertions, 193 deletions
diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst
index 1d9424c..024c1eb 100644
--- a/Doc/howto/descriptor.rst
+++ b/Doc/howto/descriptor.rst
@@ -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
diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst
index a5f580c..a72f779 100644
--- a/Doc/library/functions.rst
+++ b/Doc/library/functions.rst
@@ -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`.
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index 1053aa5..34dd3ea 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -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
diff --git a/Lib/test/test_decorators.py b/Lib/test/test_decorators.py
index 4b49217..3a4fc95 100644
--- a/Lib/test/test_decorators.py
+++ b/Lib/test/test_decorators.py
@@ -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):
diff --git a/Lib/test/test_doctest.py b/Lib/test/test_doctest.py
index e5b08a3..5c59b00 100644
--- a/Lib/test/test_doctest.py
+++ b/Lib/test/test_doctest.py
@@ -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
diff --git a/Lib/test/test_property.py b/Lib/test/test_property.py
index 45aa9e5..c12c908 100644
--- a/Lib/test/test_property.py
+++ b/Lib/test/test_property.py
@@ -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
index 0000000..fd9d0ed
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2023-09-30-17-30-11.gh-issue-89519.hz2pZf.rst
@@ -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.
diff --git a/Objects/funcobject.c b/Objects/funcobject.c
index 8665c77..56c5af6 100644
--- a/Objects/funcobject.c
+++ b/Objects/funcobject.c
@@ -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);
}