diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -1575,10 +1575,21 @@ executed in a new namespace and the class name is bound locally to the result of ``type(name, bases, namespace)``. -The class creation process can be customised by passing the ``metaclass`` -keyword argument in the class definition line, or by inheriting from an -existing class that included such an argument. In the following example, -both ``MyClass`` and ``MySubclass`` are instances of ``Meta``:: +The class creation process can be customised by defining a class +initialization hook, or by using a metaclass (or both). A class +initialization hook can be defined by creating a class method +called ``__init_class__`` (or inheriting one), as in the following +example:: + + class MyClass: + @classmethod + def __init_class__(cls): + ... # initialize cls + +A metaclass can be used by passing the ``metaclass`` keyword argument +in the class definition line, or by inheriting from an existing class +that included such an argument. In the following example, both +``MyClass`` and ``MySubclass`` are instances of ``Meta``:: class Meta(type): pass @@ -1598,6 +1609,7 @@ * the class namespace is prepared * the class body is executed * the class object is created +* the class initialization hook is called (if it exists) Determining the appropriate metaclass ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1665,16 +1677,33 @@ lexical scoping, while the class or instance that was used to make the current call is identified based on the first argument passed to the method. -After the class object is created, it is passed to the class decorators -included in the class definition (if any) and the resulting object is bound -in the local namespace as the defined class. - .. seealso:: :pep:`3135` - New super Describes the implicit ``__class__`` closure reference +Calling the class initialization hook +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +After the class object is created, and the ``__class__`` reference is +initialized, the ``__init_class__`` method of the class object is called +as ``cls.__init_class__()``. If there is no such method, this step is omitted. +The ``__init_class__`` method can use the usual :class:`super` mechanisms, +so multiple inheritance is supported. Note, that when the class initialization +hook is called, the class object is not bound to the class name yet, so +``__init_class__`` cannot refer to the class object with its name. + +After the initialization hook is called, the class object is passed to the +class decorators included in the class definition (if any) and the resulting +object is bound in the local namespace as the defined class. + +.. seealso:: + + :pep:`422` - Simple class initialisation hook + Introduced the ``__init_class__`` hook. + + Metaclass example ^^^^^^^^^^^^^^^^^ diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -796,6 +796,182 @@ class X(int(), C): pass + def test___init_class__(self): + # PEP 422: Simple class initialisation hook + class C: + @classmethod + def __init_class__(cls): + cls.x = 0 + self.assertEqual(C.x, 0) + # inherited: + class D(C): + pass + self.assertEqual(D.__dict__['x'], 0) + # overwrite: + class E(C): + x = 1 + self.assertEqual(E.__dict__['x'], 0) + # override: + class F(C): + @classmethod + def __init_class__(cls): + cls.y = 1 + self.assertEqual(F.y, 1) + self.assertEqual(F.x, 0) + self.assertNotIn('x', F.__dict__) + self.assertFalse(hasattr(C, 'y')) + # super: + class G(C): + @classmethod + def __init_class__(cls): + super().__init_class__() + cls.y = 1 + self.assertEqual(G.y, 1) + self.assertEqual(G.__dict__['x'], 0) + self.assertFalse(hasattr(C, 'y')) + # staticmethod: + class H(C): + @staticmethod + def __init_class__(): + __class__.z = 2 + self.assertEqual(H.z, 2) + self.assertFalse(hasattr(C, 'z')) + # __class__: + class I: + @classmethod + def __init_class__(cls): + cls.x = 0 + __class__.y += 1 + y = 0 + self.assertEqual(I.x, 0) + self.assertEqual(I.y, 1) + class J(I): + pass + self.assertEqual(J.__dict__['x'], 0) + self.assertEqual(I.y, 2) + class K(J): + @classmethod + def __init_class__(cls): + super().__init_class__() + __class__.z += 1 + z = 0 + self.assertEqual(K.__dict__['x'], 0) + self.assertEqual(I.y, 3) + self.assertEqual(K.z, 1) + self.assertFalse(hasattr(J, 'z')) + # multiple inheritance: + class L: + @classmethod + def __init_class__(cls): + pass + class M(L): + @classmethod + def __init_class__(cls): + super().__init_class__() + cls.x = 0 + self.assertEqual(M.x, 0) + class N(L): + @classmethod + def __init_class__(cls): + super().__init_class__() + cls.y = 1 + self.assertEqual(N.y, 1) + class O(M, N): + @classmethod + def __init_class__(cls): + super().__init_class__() + cls.z = 2 + self.assertEqual(O.__dict__['x'], 0) + self.assertEqual(O.__dict__['y'], 1) + self.assertEqual(O.__dict__['z'], 2) + # object.__init_class__: + self.assertIsNone(object.__init_class__()) + self.assertIsNone(type.__init_class__()) # inherited from object + self.assertIsNone(int.__init_class__()) # inherited from object + class X: + @classmethod + def __init_class__(cls): + super().__init_class__() # this shouldn't raise + # decorators: + def dec1(cls): + cls.x = 1 + return cls + def dec2(cls): + cls.x = 2 + return cls + @dec2 + @dec1 + class P: + @classmethod + def __init_class__(cls): + cls.x = 0 + self.assertEqual(P.x, 2) + # block class initialization: + class Meta(type): + def __getattribute__(cls, name): + if name == '__init_class__': + raise AttributeError('__init_class__') + return super().__getattribute__(name) + class Q(C, metaclass=Meta): + pass + self.assertNotIn('x', Q.__dict__) + # other exceptions should be propagated: + class Meta2(type): + def __getattribute__(cls, name): + if name == '__init_class__': + raise KeyError('xxx') + return super().__getattribute__(name) + R = sentinel = object() + with self.assertRaisesRegex(KeyError, 'xxx'): + class R(C, metaclass=Meta2): + pass + self.assertIs(R, sentinel) + # __init_class__ raises an exception: + S = sentinel + with self.assertRaisesRegex(KeyError, 'xxx'): + class S: + @classmethod + def __init_class__(cls): + raise KeyError('xxx') + self.assertIs(S, sentinel) + + # DynamicDecorators example from the PEP: + def set_x(cls): + cls.x = 0 + return cls + def set_x_1(cls): + cls.x = 1 + return cls + def set_y(cls): + cls.y = 1 + return cls + def set_z(cls): + cls.z = 2 + return cls + + class DynamicDecoratorsBase: + @classmethod + def __init_class__(cls): + cls.x = 42 + + class DynamicDecorators(DynamicDecoratorsBase): + @classmethod + def __init_class__(cls): + super().__init_class__() + for entry in reversed(cls.mro()): + decorators = entry.__dict__.get("__decorators__", ()) + for deco in reversed(decorators): + cls = deco(cls) + __decorators__ = [set_x, set_y, set_x_1] + self.assertEqual(DynamicDecorators.x, 0) + self.assertEqual(DynamicDecorators.y, 1) + + class DynamicDecoratorsSub(DynamicDecorators): + __decorators__ = [set_z, set_x_1] + self.assertEqual(DynamicDecoratorsSub.x, 1) + self.assertEqual(DynamicDecoratorsSub.y, 1) + self.assertEqual(DynamicDecoratorsSub.z, 2) + def test_module_subclasses(self): # Testing Python subclass of module... log = [] diff --git a/Lib/test/test_descrtut.py b/Lib/test/test_descrtut.py --- a/Lib/test/test_descrtut.py +++ b/Lib/test/test_descrtut.py @@ -182,6 +182,7 @@ '__iadd__', '__imul__', '__init__', + '__init_class__', '__iter__', '__le__', '__len__', diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -996,6 +996,73 @@ with self.assertRaises(TypeError): X = types.new_class("X", (int(), C)) + def test___init_class__(self): + # PEP 422: Simple class initialisation hook + def c(ns): + @classmethod + def __init_class__(cls): + cls.x = 0 + ns['__init_class__'] = __init_class__ + C = types.new_class("C", exec_body=c) + self.assertEqual(C.x, 0) + # inherited: + D = types.new_class("D", (C,)) + self.assertEqual(D.__dict__['x'], 0) + # overwrite: + def e(ns): + ns['x'] = 1 + E = types.new_class("E", (C,), exec_body=e) + self.assertEqual(E.__dict__['x'], 0) + # override: + def f(ns): + @classmethod + def __init_class__(cls): + cls.y = 1 + ns['__init_class__'] = __init_class__ + F = types.new_class("F", (C,), exec_body=f) + self.assertEqual(F.y, 1) + self.assertEqual(F.x, 0) + self.assertNotIn('x', F.__dict__) + self.assertFalse(hasattr(C, 'y')) + # staticmethod: + z = 0 + def h(ns): + @staticmethod + def __init_class__(): + nonlocal z + z = 2 + ns['__init_class__'] = __init_class__ + H = types.new_class("H", (C,), exec_body=h) + self.assertEqual(z, 2) + # block class initialization: + class Meta(type): + def __getattribute__(cls, name): + if name == '__init_class__': + raise AttributeError('__init_class__') + return super().__getattribute__(name) + Q = types.new_class("Q", (C,), dict(metaclass=Meta)) + self.assertNotIn('x', Q.__dict__) + # other exceptions should be propagated: + class Meta2(type): + def __getattribute__(cls, name): + if name == '__init_class__': + raise KeyError('xxx') + return super().__getattribute__(name) + R = sentinel = object() + with self.assertRaisesRegex(KeyError, 'xxx'): + R = types.new_class("R", (C,), dict(metaclass=Meta2)) + self.assertIs(R, sentinel) + # __init_class__ raises an exception: + def s(ns): + @classmethod + def __init_class__(cls): + raise KeyError('xxx') + ns['__init_class__'] = __init_class__ + S = sentinel + with self.assertRaisesRegex(KeyError, 'xxx'): + S = types.new_class("S", exec_body=s) + self.assertIs(S, sentinel) + class SimpleNamespaceTests(unittest.TestCase): diff --git a/Lib/types.py b/Lib/types.py --- a/Lib/types.py +++ b/Lib/types.py @@ -49,7 +49,14 @@ meta, ns, kwds = prepare_class(name, bases, kwds) if exec_body is not None: exec_body(ns) - return meta(name, bases, ns, **kwds) + cls = meta(name, bases, ns, **kwds) + try: + initcl = cls.__init_class__ + except AttributeError: + pass + else: + initcl() + return cls def prepare_class(name, bases=(), kwds=None): """Call the __prepare__ method of the appropriate metaclass. diff --git a/Objects/typeobject.c b/Objects/typeobject.c --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -3055,6 +3055,12 @@ } static PyObject * +object_init_class(PyTypeObject *cls) +{ + Py_RETURN_NONE; +} + +static PyObject * object_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { if (excess_args(args, kwds) && @@ -3742,6 +3748,8 @@ PyDoc_STR("__sizeof__() -> int\nsize of object in memory, in bytes")}, {"__dir__", object_dir, METH_NOARGS, PyDoc_STR("__dir__() -> list\ndefault dir() implementation")}, + {"__init_class__", (PyCFunction)object_init_class, METH_NOARGS | METH_CLASS, + PyDoc_STR("__init_class__() -> None\ninitializes a class object")}, {0} }; diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -38,11 +38,13 @@ static PyObject * builtin___build_class__(PyObject *self, PyObject *args, PyObject *kwds) { - PyObject *func, *name, *bases, *mkw, *meta, *winner, *prep, *ns, *cell; + PyObject *func, *name, *bases, *mkw, *meta, *winner, *prep, *ns; + PyObject *cell, *initcl, *res; PyObject *cls = NULL; Py_ssize_t nargs; int isclass; _Py_IDENTIFIER(__prepare__); + _Py_IDENTIFIER(__init_class__); assert(args != NULL); if (!PyTuple_Check(args)) { @@ -163,8 +165,35 @@ cls = PyEval_CallObjectWithKeywords(meta, margs, mkw); Py_DECREF(margs); } - if (cls != NULL && PyCell_Check(cell)) - PyCell_Set(cell, cls); + if (cls != NULL) { + /* initialize the __class__ reference: */ + if (PyCell_Check(cell)) { + PyCell_Set(cell, cls); + } + /* call __init_class__: */ + initcl = _PyObject_GetAttrId(cls, &PyId___init_class__); + if (initcl == NULL) { + if (PyErr_ExceptionMatches(PyExc_AttributeError)) { + PyErr_Clear(); + /* no __init_class__, nothing to do */ + } + else { + /* propagate other exceptions: */ + Py_DECREF(cls); + cls = NULL; + } + } + else { + res = PyObject_CallObject(initcl, NULL); + Py_DECREF(initcl); + if (res == NULL) { + /* __init_class__ raised an exception */ + Py_DECREF(cls); + cls = NULL; + } + Py_XDECREF(res); + } + } Py_DECREF(cell); } Py_DECREF(ns);