mirror of https://github.com/python/cpython
gh-89519: Remove classmethod descriptor chaining, deprecated since 3.11 (gh-110163)
This commit is contained in:
parent
ee2d22f06d
commit
7f9a99e854
|
@ -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
|
||||
|
|
|
@ -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`.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue