Skip to content

Commit c63736d

Browse files
rhettingeraisk
authored andcommitted
pythongh-89519: Remove classmethod descriptor chaining, deprecated since 3.11 (pythongh-110163)
1 parent 04a1894 commit c63736d

File tree

8 files changed

+25
-193
lines changed

8 files changed

+25
-193
lines changed

Doc/howto/descriptor.rst

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,6 +1141,16 @@ roughly equivalent to:
11411141
obj = self.__self__
11421142
return func(obj, *args, **kwargs)
11431143
1144+
def __getattribute__(self, name):
1145+
"Emulate method_getset() in Objects/classobject.c"
1146+
if name == '__doc__':
1147+
return self.__func__.__doc__
1148+
return object.__getattribute__(self, name)
1149+
1150+
def __getattr__(self, name):
1151+
"Emulate method_getattro() in Objects/classobject.c"
1152+
return getattr(self.__func__, name)
1153+
11441154
To support automatic creation of methods, functions include the
11451155
:meth:`__get__` method for binding methods during attribute access. This
11461156
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
14201430
def __get__(self, obj, cls=None):
14211431
if cls is None:
14221432
cls = type(obj)
1423-
if hasattr(type(self.f), '__get__'):
1424-
# This code path was added in Python 3.9
1425-
# and was deprecated in Python 3.11.
1426-
return self.f.__get__(cls, cls)
14271433
return MethodType(self.f, cls)
14281434

14291435
.. testcode::
@@ -1436,11 +1442,6 @@ Using the non-data descriptor protocol, a pure Python version of
14361442
"Class method that returns a tuple"
14371443
return (cls.__name__, x, y)
14381444

1439-
@ClassMethod
1440-
@property
1441-
def __doc__(cls):
1442-
return f'A doc for {cls.__name__!r}'
1443-
14441445

14451446
.. doctest::
14461447
:hide:
@@ -1453,10 +1454,6 @@ Using the non-data descriptor protocol, a pure Python version of
14531454
>>> t.cm(11, 22)
14541455
('T', 11, 22)
14551456

1456-
# Check the alternate path for chained descriptors
1457-
>>> T.__doc__
1458-
"A doc for 'T'"
1459-
14601457
# Verify that T uses our emulation
14611458
>>> type(vars(T)['cm']).__name__
14621459
'ClassMethod'
@@ -1481,24 +1478,6 @@ Using the non-data descriptor protocol, a pure Python version of
14811478
('T', 11, 22)
14821479

14831480

1484-
The code path for ``hasattr(type(self.f), '__get__')`` was added in
1485-
Python 3.9 and makes it possible for :func:`classmethod` to support
1486-
chained decorators. For example, a classmethod and property could be
1487-
chained together. In Python 3.11, this functionality was deprecated.
1488-
1489-
.. testcode::
1490-
1491-
class G:
1492-
@classmethod
1493-
@property
1494-
def __doc__(cls):
1495-
return f'A doc for {cls.__name__!r}'
1496-
1497-
.. doctest::
1498-
1499-
>>> G.__doc__
1500-
"A doc for 'G'"
1501-
15021481
The :func:`functools.update_wrapper` call in ``ClassMethod`` adds a
15031482
``__wrapped__`` attribute that refers to the underlying function. Also
15041483
it carries forward the attributes necessary to make the wrapper look

Doc/library/functions.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ are always available. They are listed here in alphabetical order.
285285
``__name__``, ``__qualname__``, ``__doc__`` and ``__annotations__``) and
286286
have a new ``__wrapped__`` attribute.
287287

288-
.. versionchanged:: 3.11
288+
.. deprecated-removed:: 3.11 3.13
289289
Class methods can no longer wrap other :term:`descriptors <descriptor>` such as
290290
:func:`property`.
291291

Doc/whatsnew/3.13.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,6 +1228,14 @@ Deprecated
12281228
Removed
12291229
-------
12301230

1231+
* Removed chained :class:`classmethod` descriptors (introduced in
1232+
:issue:`19072`). This can no longer be used to wrap other descriptors
1233+
such as :class:`property`. The core design of this feature was flawed
1234+
and caused a number of downstream problems. To "pass-through" a
1235+
:class:`classmethod`, consider using the :attr:`!__wrapped__`
1236+
attribute that was added in Python 3.10. (Contributed by Raymond
1237+
Hettinger in :gh:`89519`.)
1238+
12311239
* Remove many APIs (functions, macros, variables) with names prefixed by
12321240
``_Py`` or ``_PY`` (considered as private API). If your project is affected
12331241
by one of these removals and you consider that the removed API should remain

Lib/test/test_decorators.py

Lines changed: 0 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -291,44 +291,6 @@ def bar(): return 42
291291
self.assertEqual(bar(), 42)
292292
self.assertEqual(actions, expected_actions)
293293

294-
def test_wrapped_descriptor_inside_classmethod(self):
295-
class BoundWrapper:
296-
def __init__(self, wrapped):
297-
self.__wrapped__ = wrapped
298-
299-
def __call__(self, *args, **kwargs):
300-
return self.__wrapped__(*args, **kwargs)
301-
302-
class Wrapper:
303-
def __init__(self, wrapped):
304-
self.__wrapped__ = wrapped
305-
306-
def __get__(self, instance, owner):
307-
bound_function = self.__wrapped__.__get__(instance, owner)
308-
return BoundWrapper(bound_function)
309-
310-
def decorator(wrapped):
311-
return Wrapper(wrapped)
312-
313-
class Class:
314-
@decorator
315-
@classmethod
316-
def inner(cls):
317-
# This should already work.
318-
return 'spam'
319-
320-
@classmethod
321-
@decorator
322-
def outer(cls):
323-
# Raised TypeError with a message saying that the 'Wrapper'
324-
# object is not callable.
325-
return 'eggs'
326-
327-
self.assertEqual(Class.inner(), 'spam')
328-
self.assertEqual(Class.outer(), 'eggs')
329-
self.assertEqual(Class().inner(), 'spam')
330-
self.assertEqual(Class().outer(), 'eggs')
331-
332294
def test_bound_function_inside_classmethod(self):
333295
class A:
334296
def foo(self, cls):
@@ -339,91 +301,6 @@ class B:
339301

340302
self.assertEqual(B.bar(), 'spam')
341303

342-
def test_wrapped_classmethod_inside_classmethod(self):
343-
class MyClassMethod1:
344-
def __init__(self, func):
345-
self.func = func
346-
347-
def __call__(self, cls):
348-
if hasattr(self.func, '__get__'):
349-
return self.func.__get__(cls, cls)()
350-
return self.func(cls)
351-
352-
def __get__(self, instance, owner=None):
353-
if owner is None:
354-
owner = type(instance)
355-
return MethodType(self, owner)
356-
357-
class MyClassMethod2:
358-
def __init__(self, func):
359-
if isinstance(func, classmethod):
360-
func = func.__func__
361-
self.func = func
362-
363-
def __call__(self, cls):
364-
return self.func(cls)
365-
366-
def __get__(self, instance, owner=None):
367-
if owner is None:
368-
owner = type(instance)
369-
return MethodType(self, owner)
370-
371-
for myclassmethod in [MyClassMethod1, MyClassMethod2]:
372-
class A:
373-
@myclassmethod
374-
def f1(cls):
375-
return cls
376-
377-
@classmethod
378-
@myclassmethod
379-
def f2(cls):
380-
return cls
381-
382-
@myclassmethod
383-
@classmethod
384-
def f3(cls):
385-
return cls
386-
387-
@classmethod
388-
@classmethod
389-
def f4(cls):
390-
return cls
391-
392-
@myclassmethod
393-
@MyClassMethod1
394-
def f5(cls):
395-
return cls
396-
397-
@myclassmethod
398-
@MyClassMethod2
399-
def f6(cls):
400-
return cls
401-
402-
self.assertIs(A.f1(), A)
403-
self.assertIs(A.f2(), A)
404-
self.assertIs(A.f3(), A)
405-
self.assertIs(A.f4(), A)
406-
self.assertIs(A.f5(), A)
407-
self.assertIs(A.f6(), A)
408-
a = A()
409-
self.assertIs(a.f1(), A)
410-
self.assertIs(a.f2(), A)
411-
self.assertIs(a.f3(), A)
412-
self.assertIs(a.f4(), A)
413-
self.assertIs(a.f5(), A)
414-
self.assertIs(a.f6(), A)
415-
416-
def f(cls):
417-
return cls
418-
419-
self.assertIs(myclassmethod(f).__get__(a)(), A)
420-
self.assertIs(myclassmethod(f).__get__(a, A)(), A)
421-
self.assertIs(myclassmethod(f).__get__(A, A)(), A)
422-
self.assertIs(myclassmethod(f).__get__(A)(), type(A))
423-
self.assertIs(classmethod(f).__get__(a)(), A)
424-
self.assertIs(classmethod(f).__get__(a, A)(), A)
425-
self.assertIs(classmethod(f).__get__(A, A)(), A)
426-
self.assertIs(classmethod(f).__get__(A)(), type(A))
427304

428305
class TestClassDecorators(unittest.TestCase):
429306

Lib/test/test_doctest.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,6 @@ def a_classmethod(cls, v):
102102

103103
a_class_attribute = 42
104104

105-
@classmethod
106-
@property
107-
def a_classmethod_property(cls):
108-
"""
109-
>>> print(SampleClass.a_classmethod_property)
110-
42
111-
"""
112-
return cls.a_class_attribute
113-
114105
@functools.cached_property
115106
def a_cached_property(self):
116107
"""
@@ -525,7 +516,6 @@ def basics(): r"""
525516
1 SampleClass.__init__
526517
1 SampleClass.a_cached_property
527518
2 SampleClass.a_classmethod
528-
1 SampleClass.a_classmethod_property
529519
1 SampleClass.a_property
530520
1 SampleClass.a_staticmethod
531521
1 SampleClass.double
@@ -582,7 +572,6 @@ def basics(): r"""
582572
1 some_module.SampleClass.__init__
583573
1 some_module.SampleClass.a_cached_property
584574
2 some_module.SampleClass.a_classmethod
585-
1 some_module.SampleClass.a_classmethod_property
586575
1 some_module.SampleClass.a_property
587576
1 some_module.SampleClass.a_staticmethod
588577
1 some_module.SampleClass.double
@@ -625,7 +614,6 @@ def basics(): r"""
625614
1 SampleClass.__init__
626615
1 SampleClass.a_cached_property
627616
2 SampleClass.a_classmethod
628-
1 SampleClass.a_classmethod_property
629617
1 SampleClass.a_property
630618
1 SampleClass.a_staticmethod
631619
1 SampleClass.double
@@ -647,7 +635,6 @@ def basics(): r"""
647635
1 SampleClass.__init__
648636
1 SampleClass.a_cached_property
649637
2 SampleClass.a_classmethod
650-
1 SampleClass.a_classmethod_property
651638
1 SampleClass.a_property
652639
1 SampleClass.a_staticmethod
653640
1 SampleClass.double

Lib/test/test_property.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -183,27 +183,6 @@ 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-
207186
def test_property_set_name_incorrect_args(self):
208187
p = property()
209188

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Removed chained :class:`classmethod` descriptors (introduced in
2+
:issue:`19072`). This can no longer be used to wrap other descriptors such
3+
as :class:`property`. The core design of this feature was flawed and caused
4+
a number of downstream problems. To "pass-through" a :class:`classmethod`,
5+
consider using the :attr:`!__wrapped__` attribute that was added in Python
6+
3.10.

Objects/funcobject.c

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,10 +1110,6 @@ cm_descr_get(PyObject *self, PyObject *obj, PyObject *type)
11101110
}
11111111
if (type == NULL)
11121112
type = (PyObject *)(Py_TYPE(obj));
1113-
if (Py_TYPE(cm->cm_callable)->tp_descr_get != NULL) {
1114-
return Py_TYPE(cm->cm_callable)->tp_descr_get(cm->cm_callable, type,
1115-
type);
1116-
}
11171113
return PyMethod_New(cm->cm_callable, type);
11181114
}
11191115

0 commit comments

Comments
 (0)