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__
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

View 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`.

View 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

View file

@ -291,44 +291,6 @@ def bar(): return 42
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 B:
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):

View file

@ -102,15 +102,6 @@ def a_classmethod(cls, v):
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 @@ def basics(): r"""
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 @@ def basics(): r"""
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 @@ def basics(): r"""
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 @@ def basics(): r"""
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

View file

@ -183,27 +183,6 @@ def test_refleaks_in___init__(self):
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()

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)
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);
}