gh-89519: Remove classmethod descriptor chaining, deprecated since 3.11 (gh-110163)

This commit is contained in:
Raymond Hettinger 2023-10-27 00:24:56 -05:00 committed by GitHub
parent ee2d22f06d
commit 7f9a99e854
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 25 additions and 193 deletions

View file

@ -1141,6 +1141,16 @@ roughly equivalent to:
obj = self.__self__ obj = self.__self__
return func(obj, *args, **kwargs) 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 To support automatic creation of methods, functions include the
:meth:`__get__` method for binding methods during attribute access. This :meth:`__get__` method for binding methods during attribute access. This
means that functions are non-data descriptors that return bound methods 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): def __get__(self, obj, cls=None):
if cls is None: if cls is None:
cls = type(obj) 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) return MethodType(self.f, cls)
.. testcode:: .. testcode::
@ -1436,11 +1442,6 @@ Using the non-data descriptor protocol, a pure Python version of
"Class method that returns a tuple" "Class method that returns a tuple"
return (cls.__name__, x, y) return (cls.__name__, x, y)
@ClassMethod
@property
def __doc__(cls):
return f'A doc for {cls.__name__!r}'
.. doctest:: .. doctest::
:hide: :hide:
@ -1453,10 +1454,6 @@ Using the non-data descriptor protocol, a pure Python version of
>>> t.cm(11, 22) >>> t.cm(11, 22)
('T', 11, 22) ('T', 11, 22)
# Check the alternate path for chained descriptors
>>> T.__doc__
"A doc for 'T'"
# Verify that T uses our emulation # Verify that T uses our emulation
>>> type(vars(T)['cm']).__name__ >>> type(vars(T)['cm']).__name__
'ClassMethod' 'ClassMethod'
@ -1481,24 +1478,6 @@ Using the non-data descriptor protocol, a pure Python version of
('T', 11, 22) ('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 The :func:`functools.update_wrapper` call in ``ClassMethod`` adds a
``__wrapped__`` attribute that refers to the underlying function. Also ``__wrapped__`` attribute that refers to the underlying function. Also
it carries forward the attributes necessary to make the wrapper look it carries forward the attributes necessary to make the wrapper look

View file

@ -285,7 +285,7 @@ are always available. They are listed here in alphabetical order.
``__name__``, ``__qualname__``, ``__doc__`` and ``__annotations__``) and ``__name__``, ``__qualname__``, ``__doc__`` and ``__annotations__``) and
have a new ``__wrapped__`` attribute. 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 Class methods can no longer wrap other :term:`descriptors <descriptor>` such as
:func:`property`. :func:`property`.

View file

@ -1228,6 +1228,14 @@ Deprecated
Removed 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 * Remove many APIs (functions, macros, variables) with names prefixed by
``_Py`` or ``_PY`` (considered as private API). If your project is affected ``_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 by one of these removals and you consider that the removed API should remain

View file

@ -291,44 +291,6 @@ class TestDecorators(unittest.TestCase):
self.assertEqual(bar(), 42) self.assertEqual(bar(), 42)
self.assertEqual(actions, expected_actions) 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): def test_bound_function_inside_classmethod(self):
class A: class A:
def foo(self, cls): def foo(self, cls):
@ -339,91 +301,6 @@ class TestDecorators(unittest.TestCase):
self.assertEqual(B.bar(), 'spam') 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): class TestClassDecorators(unittest.TestCase):

View file

@ -102,15 +102,6 @@ class SampleClass:
a_class_attribute = 42 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 @functools.cached_property
def a_cached_property(self): def a_cached_property(self):
""" """
@ -525,7 +516,6 @@ methods, classmethods, staticmethods, properties, and nested classes.
1 SampleClass.__init__ 1 SampleClass.__init__
1 SampleClass.a_cached_property 1 SampleClass.a_cached_property
2 SampleClass.a_classmethod 2 SampleClass.a_classmethod
1 SampleClass.a_classmethod_property
1 SampleClass.a_property 1 SampleClass.a_property
1 SampleClass.a_staticmethod 1 SampleClass.a_staticmethod
1 SampleClass.double 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.__init__
1 some_module.SampleClass.a_cached_property 1 some_module.SampleClass.a_cached_property
2 some_module.SampleClass.a_classmethod 2 some_module.SampleClass.a_classmethod
1 some_module.SampleClass.a_classmethod_property
1 some_module.SampleClass.a_property 1 some_module.SampleClass.a_property
1 some_module.SampleClass.a_staticmethod 1 some_module.SampleClass.a_staticmethod
1 some_module.SampleClass.double 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.__init__
1 SampleClass.a_cached_property 1 SampleClass.a_cached_property
2 SampleClass.a_classmethod 2 SampleClass.a_classmethod
1 SampleClass.a_classmethod_property
1 SampleClass.a_property 1 SampleClass.a_property
1 SampleClass.a_staticmethod 1 SampleClass.a_staticmethod
1 SampleClass.double 1 SampleClass.double
@ -647,7 +635,6 @@ displays.
1 SampleClass.__init__ 1 SampleClass.__init__
1 SampleClass.a_cached_property 1 SampleClass.a_cached_property
2 SampleClass.a_classmethod 2 SampleClass.a_classmethod
1 SampleClass.a_classmethod_property
1 SampleClass.a_property 1 SampleClass.a_property
1 SampleClass.a_staticmethod 1 SampleClass.a_staticmethod
1 SampleClass.double 1 SampleClass.double

View file

@ -183,27 +183,6 @@ class PropertyTests(unittest.TestCase):
fake_prop.__init__('fget', 'fset', 'fdel', 'doc') fake_prop.__init__('fget', 'fset', 'fdel', 'doc')
self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10) 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): def test_property_set_name_incorrect_args(self):
p = property() p = property()

View file

@ -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.

View file

@ -1110,10 +1110,6 @@ cm_descr_get(PyObject *self, PyObject *obj, PyObject *type)
} }
if (type == NULL) if (type == NULL)
type = (PyObject *)(Py_TYPE(obj)); 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); return PyMethod_New(cm->cm_callable, type);
} }