bpo-23722: Raise a RuntimeError for absent __classcell__. (GH-6931)

A DeprecationWarning was emitted in Python 3.6-3.7.
This commit is contained in:
Serhiy Storchaka 2018-05-20 08:48:12 +03:00 committed by GitHub
parent 8ae8e6af37
commit f5e7b1999f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 24 additions and 51 deletions

View File

@ -1967,8 +1967,7 @@ current call is identified based on the first argument passed to the method.
as a ``__classcell__`` entry in the class namespace. If present, this must as a ``__classcell__`` entry in the class namespace. If present, this must
be propagated up to the ``type.__new__`` call in order for the class to be be propagated up to the ``type.__new__`` call in order for the class to be
initialised correctly. initialised correctly.
Failing to do so will result in a :exc:`DeprecationWarning` in Python 3.6, Failing to do so will result in a :exc:`RuntimeError` in Python 3.8.
and a :exc:`RuntimeError` in Python 3.8.
When using the default metaclass :class:`type`, or any metaclass that ultimately When using the default metaclass :class:`type`, or any metaclass that ultimately
calls ``type.__new__``, the following additional customisation steps are calls ``type.__new__``, the following additional customisation steps are

View File

@ -146,6 +146,11 @@ Changes in the Python API
a database if it does not exist. a database if it does not exist.
(Contributed by Serhiy Storchaka in :issue:`32749`.) (Contributed by Serhiy Storchaka in :issue:`32749`.)
* A :exc:`RuntimeError` is now raised when the custom metaclass doesn't
provide the ``__classcell__`` entry in the namespace passed to
``type.__new__``. A :exc:`DeprecationWarning` was emitted in Python
3.6--3.7. (Contributed by Serhiy Storchaka in :issue:`23722`.)
CPython bytecode changes CPython bytecode changes
------------------------ ------------------------

View File

@ -1,8 +1,6 @@
"""Unit tests for zero-argument super() & related machinery.""" """Unit tests for zero-argument super() & related machinery."""
import unittest import unittest
import warnings
from test.support import check_warnings
class A: class A:
@ -173,14 +171,10 @@ class TestSuper(unittest.TestCase):
test_namespace = namespace test_namespace = namespace
return None return None
# This case shouldn't trigger the __classcell__ deprecation warning
with check_warnings() as w:
warnings.simplefilter("always", DeprecationWarning)
class A(metaclass=Meta): class A(metaclass=Meta):
@staticmethod @staticmethod
def f(): def f():
return __class__ return __class__
self.assertEqual(w.warnings, [])
self.assertIs(A, None) self.assertIs(A, None)
@ -244,34 +238,16 @@ class TestSuper(unittest.TestCase):
namespace.pop('__classcell__', None) namespace.pop('__classcell__', None)
return super().__new__(cls, name, bases, namespace) return super().__new__(cls, name, bases, namespace)
# The default case should continue to work without any warnings # The default case should continue to work without any errors
with check_warnings() as w:
warnings.simplefilter("always", DeprecationWarning)
class WithoutClassRef(metaclass=Meta): class WithoutClassRef(metaclass=Meta):
pass pass
self.assertEqual(w.warnings, [])
# With zero-arg super() or an explicit __class__ reference, we expect # With zero-arg super() or an explicit __class__ reference, we expect
# __build_class__ to emit a DeprecationWarning complaining that # __build_class__ to raise a RuntimeError complaining that
# __class__ was not set, and asking if __classcell__ was propagated # __class__ was not set, and asking if __classcell__ was propagated
# to type.__new__. # to type.__new__.
# In Python 3.7, that warning will become a RuntimeError. expected_error = '__class__ not set.*__classcell__ propagated'
expected_warning = ( with self.assertRaisesRegex(RuntimeError, expected_error):
'__class__ not set.*__classcell__ propagated',
DeprecationWarning
)
with check_warnings(expected_warning):
warnings.simplefilter("always", DeprecationWarning)
class WithClassRef(metaclass=Meta):
def f(self):
return __class__
# Check __class__ still gets set despite the warning
self.assertIs(WithClassRef().f(), WithClassRef)
# Check the warning is turned into an error as expected
with warnings.catch_warnings():
warnings.simplefilter("error", DeprecationWarning)
with self.assertRaises(DeprecationWarning):
class WithClassRef(metaclass=Meta): class WithClassRef(metaclass=Meta):
def f(self): def f(self):
return __class__ return __class__

View File

@ -0,0 +1,4 @@
A :exc:`RuntimeError` is now raised when the custom metaclass doesn't
provide the ``__classcell__`` entry in the namespace passed to
``type.__new__``. A :exc:`DeprecationWarning` was emitted in Python
3.6--3.7.

View File

@ -254,30 +254,19 @@ builtin___build_class__(PyObject *self, PyObject *const *args, Py_ssize_t nargs,
if (cls != NULL && PyType_Check(cls) && PyCell_Check(cell)) { if (cls != NULL && PyType_Check(cls) && PyCell_Check(cell)) {
PyObject *cell_cls = PyCell_GET(cell); PyObject *cell_cls = PyCell_GET(cell);
if (cell_cls != cls) { if (cell_cls != cls) {
/* TODO: In 3.7, DeprecationWarning will become RuntimeError.
* At that point, cell_error won't be needed.
*/
int cell_error;
if (cell_cls == NULL) { if (cell_cls == NULL) {
const char *msg = const char *msg =
"__class__ not set defining %.200R as %.200R. " "__class__ not set defining %.200R as %.200R. "
"Was __classcell__ propagated to type.__new__?"; "Was __classcell__ propagated to type.__new__?";
cell_error = PyErr_WarnFormat( PyErr_Format(PyExc_RuntimeError, msg, name, cls);
PyExc_DeprecationWarning, 1, msg, name, cls);
} else { } else {
const char *msg = const char *msg =
"__class__ set to %.200R defining %.200R as %.200R"; "__class__ set to %.200R defining %.200R as %.200R";
PyErr_Format(PyExc_TypeError, msg, cell_cls, name, cls); PyErr_Format(PyExc_TypeError, msg, cell_cls, name, cls);
cell_error = 1;
} }
if (cell_error) {
Py_DECREF(cls); Py_DECREF(cls);
cls = NULL; cls = NULL;
goto error; goto error;
} else {
/* Fill in the cell, since type.__new__ didn't do it */
PyCell_Set(cell, cls);
}
} }
} }
} }