Skip to content

Commit 6f02c7f

Browse files
itamarofacebook-github-bot
authored andcommitted
GH-89519: Remove classmethod descriptor chaining, deprecated since 3.11 (#110163)
Summary: backporting [Raymond Hettinger's `remove_classmethod_descriptor_chaining` branch](https://github.com/rhettinger/cpython/tree/remove_classmethod_descriptor_chaining) (pending upstream PR [GH-110163](python/cpython#110163)), part of upstream issue [GH-89519](python/cpython#89519) Reviewed By: swtaarrs Differential Revision: D50534886 fbshipit-source-id: 05b29193ce220f0d91fd0df8d5cef34d7e923456
1 parent be6ebb2 commit 6f02c7f

7 files changed

Lines changed: 17 additions & 190 deletions

File tree

Doc/howto/descriptor.rst

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,16 @@ roughly equivalent to:
11211121
obj = self.__self__
11221122
return func(obj, *args, **kwargs)
11231123
1124+
def __getattribute__(self, name):
1125+
"Emulate method_getset() in Objects/classobject.c"
1126+
if name == '__doc__':
1127+
return self.__func__.__doc__
1128+
return object.__getattribute__(self, name)
1129+
1130+
def __getattr__(self, name):
1131+
"Emulate method_getattro() in Objects/classobject.c"
1132+
return getattr(self.__func__, name)
1133+
11241134
To support automatic creation of methods, functions include the
11251135
:meth:`__get__` method for binding methods during attribute access. This
11261136
means that functions are non-data descriptors that return bound methods
@@ -1348,10 +1358,6 @@ Using the non-data descriptor protocol, a pure Python version of
13481358
def __get__(self, obj, cls=None):
13491359
if cls is None:
13501360
cls = type(obj)
1351-
if hasattr(type(self.f), '__get__'):
1352-
# This code path was added in Python 3.9
1353-
# and was deprecated in Python 3.11.
1354-
return self.f.__get__(cls, cls)
13551361
return MethodType(self.f, cls)
13561362

13571363
.. testcode::
@@ -1363,11 +1369,6 @@ Using the non-data descriptor protocol, a pure Python version of
13631369
def cm(cls, x, y):
13641370
return (cls, x, y)
13651371

1366-
@ClassMethod
1367-
@property
1368-
def __doc__(cls):
1369-
return f'A doc for {cls.__name__!r}'
1370-
13711372

13721373
.. doctest::
13731374
:hide:
@@ -1385,25 +1386,6 @@ Using the non-data descriptor protocol, a pure Python version of
13851386
"A doc for 'T'"
13861387

13871388

1388-
The code path for ``hasattr(type(self.f), '__get__')`` was added in
1389-
Python 3.9 and makes it possible for :func:`classmethod` to support
1390-
chained decorators. For example, a classmethod and property could be
1391-
chained together. In Python 3.11, this functionality was deprecated.
1392-
1393-
.. testcode::
1394-
1395-
class G:
1396-
@classmethod
1397-
@property
1398-
def __doc__(cls):
1399-
return f'A doc for {cls.__name__!r}'
1400-
1401-
.. doctest::
1402-
1403-
>>> G.__doc__
1404-
"A doc for 'G'"
1405-
1406-
14071389
Member objects and __slots__
14081390
----------------------------
14091391

Doc/library/functions.rst

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

274-
.. versionchanged:: 3.11
274+
.. deprecated-removed:: 3.11 3.13
275275
Class methods can no longer wrap other :term:`descriptors <descriptor>` such as
276276
:func:`property`.
277277

Lib/test/test_decorators.py

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

295-
def test_wrapped_descriptor_inside_classmethod(self):
296-
class BoundWrapper:
297-
def __init__(self, wrapped):
298-
self.__wrapped__ = wrapped
299-
300-
def __call__(self, *args, **kwargs):
301-
return self.__wrapped__(*args, **kwargs)
302-
303-
class Wrapper:
304-
def __init__(self, wrapped):
305-
self.__wrapped__ = wrapped
306-
307-
def __get__(self, instance, owner):
308-
bound_function = self.__wrapped__.__get__(instance, owner)
309-
return BoundWrapper(bound_function)
310-
311-
def decorator(wrapped):
312-
return Wrapper(wrapped)
313-
314-
class Class:
315-
@decorator
316-
@classmethod
317-
def inner(cls):
318-
# This should already work.
319-
return 'spam'
320-
321-
@classmethod
322-
@decorator
323-
def outer(cls):
324-
# Raised TypeError with a message saying that the 'Wrapper'
325-
# object is not callable.
326-
return 'eggs'
327-
328-
self.assertEqual(Class.inner(), 'spam')
329-
self.assertEqual(Class.outer(), 'eggs')
330-
self.assertEqual(Class().inner(), 'spam')
331-
self.assertEqual(Class().outer(), 'eggs')
332-
333-
def test_wrapped_classmethod_inside_classmethod(self):
334-
class MyClassMethod1:
335-
def __init__(self, func):
336-
self.func = func
337-
338-
def __call__(self, cls):
339-
if hasattr(self.func, '__get__'):
340-
return self.func.__get__(cls, cls)()
341-
return self.func(cls)
342-
343-
def __get__(self, instance, owner=None):
344-
if owner is None:
345-
owner = type(instance)
346-
return MethodType(self, owner)
347-
348-
class MyClassMethod2:
349-
def __init__(self, func):
350-
if isinstance(func, classmethod):
351-
func = func.__func__
352-
self.func = func
353-
354-
def __call__(self, cls):
355-
return self.func(cls)
356-
357-
def __get__(self, instance, owner=None):
358-
if owner is None:
359-
owner = type(instance)
360-
return MethodType(self, owner)
361-
362-
for myclassmethod in [MyClassMethod1, MyClassMethod2]:
363-
class A:
364-
@myclassmethod
365-
def f1(cls):
366-
return cls
367-
368-
@classmethod
369-
@myclassmethod
370-
def f2(cls):
371-
return cls
372-
373-
@myclassmethod
374-
@classmethod
375-
def f3(cls):
376-
return cls
377-
378-
@classmethod
379-
@classmethod
380-
def f4(cls):
381-
return cls
382-
383-
@myclassmethod
384-
@MyClassMethod1
385-
def f5(cls):
386-
return cls
387-
388-
@myclassmethod
389-
@MyClassMethod2
390-
def f6(cls):
391-
return cls
392-
393-
self.assertIs(A.f1(), A)
394-
self.assertIs(A.f2(), A)
395-
self.assertIs(A.f3(), A)
396-
self.assertIs(A.f4(), A)
397-
self.assertIs(A.f5(), A)
398-
self.assertIs(A.f6(), A)
399-
a = A()
400-
self.assertIs(a.f1(), A)
401-
self.assertIs(a.f2(), A)
402-
self.assertIs(a.f3(), A)
403-
self.assertIs(a.f4(), A)
404-
self.assertIs(a.f5(), A)
405-
self.assertIs(a.f6(), A)
406-
407-
def f(cls):
408-
return cls
409-
410-
self.assertIs(myclassmethod(f).__get__(a)(), A)
411-
self.assertIs(myclassmethod(f).__get__(a, A)(), A)
412-
self.assertIs(myclassmethod(f).__get__(A, A)(), A)
413-
self.assertIs(myclassmethod(f).__get__(A)(), type(A))
414-
self.assertIs(classmethod(f).__get__(a)(), A)
415-
self.assertIs(classmethod(f).__get__(a, A)(), A)
416-
self.assertIs(classmethod(f).__get__(A, A)(), A)
417-
self.assertIs(classmethod(f).__get__(A)(), type(A))
418295

419296
class TestClassDecorators(unittest.TestCase):
420297

Lib/test/test_doctest.py

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

101101
a_class_attribute = 42
102102

103-
@classmethod
104-
@property
105-
def a_classmethod_property(cls):
106-
"""
107-
>>> print(SampleClass.a_classmethod_property)
108-
42
109-
"""
110-
return cls.a_class_attribute
111-
112103
class NestedClass:
113104
"""
114105
>>> x = SampleClass.NestedClass(5)
@@ -514,7 +505,6 @@ def basics(): r"""
514505
1 SampleClass.NestedClass.__init__
515506
1 SampleClass.__init__
516507
2 SampleClass.a_classmethod
517-
1 SampleClass.a_classmethod_property
518508
1 SampleClass.a_property
519509
1 SampleClass.a_staticmethod
520510
1 SampleClass.double
@@ -570,7 +560,6 @@ def basics(): r"""
570560
1 some_module.SampleClass.NestedClass.__init__
571561
1 some_module.SampleClass.__init__
572562
2 some_module.SampleClass.a_classmethod
573-
1 some_module.SampleClass.a_classmethod_property
574563
1 some_module.SampleClass.a_property
575564
1 some_module.SampleClass.a_staticmethod
576565
1 some_module.SampleClass.double
@@ -612,7 +601,6 @@ def basics(): r"""
612601
1 SampleClass.NestedClass.__init__
613602
1 SampleClass.__init__
614603
2 SampleClass.a_classmethod
615-
1 SampleClass.a_classmethod_property
616604
1 SampleClass.a_property
617605
1 SampleClass.a_staticmethod
618606
1 SampleClass.double
@@ -633,7 +621,6 @@ def basics(): r"""
633621
0 SampleClass.NestedClass.square
634622
1 SampleClass.__init__
635623
2 SampleClass.a_classmethod
636-
1 SampleClass.a_classmethod_property
637624
1 SampleClass.a_property
638625
1 SampleClass.a_staticmethod
639626
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
@@ -955,10 +955,6 @@ cm_descr_get(PyObject *self, PyObject *obj, PyObject *type)
955955
}
956956
if (type == NULL)
957957
type = (PyObject *)(Py_TYPE(obj));
958-
if (Py_TYPE(cm->cm_callable)->tp_descr_get != NULL) {
959-
return Py_TYPE(cm->cm_callable)->tp_descr_get(cm->cm_callable, type,
960-
type);
961-
}
962958
return PyMethod_New(cm->cm_callable, type);
963959
}
964960

0 commit comments

Comments
 (0)