From d78448e912126410117723c7d240bbdfff06df16 Mon Sep 17 00:00:00 2001 From: Nick Coghlan Date: Sat, 30 Jul 2016 16:26:03 +1000 Subject: Issue #27366: Implement PEP 487 - __init_subclass__ called when new subclasses defined - __set_name__ called when descriptors are part of a class definition --- Doc/reference/datamodel.rst | 43 +++++++- Doc/whatsnew/3.6.rst | 20 ++++ Lib/test/test_builtin.py | 20 +--- Lib/test/test_descrtut.py | 1 + Lib/test/test_pydoc.py | 3 +- Lib/test/test_subclassinit.py | 244 ++++++++++++++++++++++++++++++++++++++++++ Misc/ACKS | 1 + Misc/NEWS | 4 + Objects/typeobject.c | 99 +++++++++++++++-- 9 files changed, 411 insertions(+), 24 deletions(-) create mode 100644 Lib/test/test_subclassinit.py diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index 4c88e38..2a85798 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -1492,6 +1492,12 @@ class' :attr:`~object.__dict__`. Called to delete the attribute on an instance *instance* of the owner class. +.. method:: object.__set_name__(self, owner, name) + + Called at the time the owning class *owner* is created. The + descriptor has been assigned to *name*. + + The attribute :attr:`__objclass__` is interpreted by the :mod:`inspect` module as specifying the class where this object was defined (setting this appropriately can assist in runtime introspection of dynamic class attributes). @@ -1629,11 +1635,46 @@ Notes on using *__slots__* * *__class__* assignment works only if both classes have the same *__slots__*. -.. _metaclasses: +.. _class-customization: Customizing class creation -------------------------- +Whenever a class inherits from another class, *__init_subclass__* is +called on that class. This way, it is possible to write classes which +change the behavior of subclasses. This is closely related to class +decorators, but where class decorators only affect the specific class they're +applied to, ``__init_subclass__`` solely applies to future subclasses of the +class defining the method. + +.. classmethod:: object.__init_subclass__(cls) + This method is called whenever the containing class is subclassed. + *cls* is then the new subclass. If defined as a normal instance method, + this method is implicitly converted to a class method. + + Keyword arguments which are given to a new class are passed to + the parent's class ``__init_subclass__``. For compatibility with + other classes using ``__init_subclass__``, one should take out the + needed keyword arguments and pass the others over to the base + class, as in:: + + class Philosopher: + def __init_subclass__(cls, default_name, **kwargs): + super().__init_subclass__(**kwargs) + cls.default_name = default_name + + class AustralianPhilosopher(Philosopher, default_name="Bruce"): + pass + + The default implementation ``object.__init_subclass__`` does + nothing, but raises an error if it is called with any arguments. + + +.. _metaclasses: + +Metaclasses +^^^^^^^^^^^ + By default, classes are constructed using :func:`type`. The class body is executed in a new namespace and the class name is bound locally to the result of ``type(name, bases, namespace)``. diff --git a/Doc/whatsnew/3.6.rst b/Doc/whatsnew/3.6.rst index e13800c..0d53513 100644 --- a/Doc/whatsnew/3.6.rst +++ b/Doc/whatsnew/3.6.rst @@ -110,6 +110,26 @@ evaluated at run time, and then formatted using the :func:`format` protocol. See :pep:`498` and the main documentation at :ref:`f-strings`. +PEP 487: Simpler customization of class creation +------------------------------------------------ + +Upon subclassing a class, the ``__init_subclass__`` classmethod (if defined) is +called on the base class. This makes it straightforward to write classes that +customize initialization of future subclasses without introducing the +complexity of a full custom metaclass. + +The descriptor protocol has also been expanded to include a new optional method, +``__set_name__``. Whenever a new class is defined, the new method will be called +on all descriptors included in the definition, providing them with a reference +to the class being defined and the name given to the descriptor within the +class namespace. + +Also see :pep:`487` and the updated class customization documentation at +:ref:`class-customization` and :ref:`descriptors`. + +(Contributed by Martin Teichmann in :issue:`27366`) + + PYTHONMALLOC environment variable --------------------------------- diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 8cc1b00..acc4f9c 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -1699,21 +1699,11 @@ class TestType(unittest.TestCase): self.assertEqual(x.spam(), 'spam42') self.assertEqual(x.to_bytes(2, 'little'), b'\x2a\x00') - def test_type_new_keywords(self): - class B: - def ham(self): - return 'ham%d' % self - C = type.__new__(type, - name='C', - bases=(B, int), - dict={'spam': lambda self: 'spam%s' % self}) - self.assertEqual(C.__name__, 'C') - self.assertEqual(C.__qualname__, 'C') - self.assertEqual(C.__module__, __name__) - self.assertEqual(C.__bases__, (B, int)) - self.assertIs(C.__base__, int) - self.assertIn('spam', C.__dict__) - self.assertNotIn('ham', C.__dict__) + def test_type_nokwargs(self): + with self.assertRaises(TypeError): + type('a', (), {}, x=5) + with self.assertRaises(TypeError): + type('a', (), dict={}) def test_type_name(self): for name in 'A', '\xc4', '\U0001f40d', 'B.A', '42', '': diff --git a/Lib/test/test_descrtut.py b/Lib/test/test_descrtut.py index 506d1ab..b84d644 100644 --- a/Lib/test/test_descrtut.py +++ b/Lib/test/test_descrtut.py @@ -182,6 +182,7 @@ You can get the information from the list type: '__iadd__', '__imul__', '__init__', + '__init_subclass__', '__iter__', '__le__', '__len__', diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py index 889ce59..4998597 100644 --- a/Lib/test/test_pydoc.py +++ b/Lib/test/test_pydoc.py @@ -638,8 +638,9 @@ class PydocDocTest(unittest.TestCase): del expected['__doc__'] del expected['__class__'] # inspect resolves descriptors on type into methods, but vars doesn't, - # so we need to update __subclasshook__. + # so we need to update __subclasshook__ and __init_subclass__. expected['__subclasshook__'] = TestClass.__subclasshook__ + expected['__init_subclass__'] = TestClass.__init_subclass__ methods = pydoc.allmethods(TestClass) self.assertDictEqual(methods, expected) diff --git a/Lib/test/test_subclassinit.py b/Lib/test/test_subclassinit.py new file mode 100644 index 0000000..eb5ed70 --- /dev/null +++ b/Lib/test/test_subclassinit.py @@ -0,0 +1,244 @@ +from unittest import TestCase, main +import sys +import types + + +class Test(TestCase): + def test_init_subclass(self): + class A(object): + initialized = False + + def __init_subclass__(cls): + super().__init_subclass__() + cls.initialized = True + + class B(A): + pass + + self.assertFalse(A.initialized) + self.assertTrue(B.initialized) + + def test_init_subclass_dict(self): + class A(dict, object): + initialized = False + + def __init_subclass__(cls): + super().__init_subclass__() + cls.initialized = True + + class B(A): + pass + + self.assertFalse(A.initialized) + self.assertTrue(B.initialized) + + def test_init_subclass_kwargs(self): + class A(object): + def __init_subclass__(cls, **kwargs): + cls.kwargs = kwargs + + class B(A, x=3): + pass + + self.assertEqual(B.kwargs, dict(x=3)) + + def test_init_subclass_error(self): + class A(object): + def __init_subclass__(cls): + raise RuntimeError + + with self.assertRaises(RuntimeError): + class B(A): + pass + + def test_init_subclass_wrong(self): + class A(object): + def __init_subclass__(cls, whatever): + pass + + with self.assertRaises(TypeError): + class B(A): + pass + + def test_init_subclass_skipped(self): + class BaseWithInit(object): + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + cls.initialized = cls + + class BaseWithoutInit(BaseWithInit): + pass + + class A(BaseWithoutInit): + pass + + self.assertIs(A.initialized, A) + self.assertIs(BaseWithoutInit.initialized, BaseWithoutInit) + + def test_init_subclass_diamond(self): + class Base(object): + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + cls.calls = [] + + class Left(Base): + pass + + class Middle(object): + def __init_subclass__(cls, middle, **kwargs): + super().__init_subclass__(**kwargs) + cls.calls += [middle] + + class Right(Base): + def __init_subclass__(cls, right="right", **kwargs): + super().__init_subclass__(**kwargs) + cls.calls += [right] + + class A(Left, Middle, Right, middle="middle"): + pass + + self.assertEqual(A.calls, ["right", "middle"]) + self.assertEqual(Left.calls, []) + self.assertEqual(Right.calls, []) + + def test_set_name(self): + class Descriptor: + def __set_name__(self, owner, name): + self.owner = owner + self.name = name + + class A(object): + d = Descriptor() + + self.assertEqual(A.d.name, "d") + self.assertIs(A.d.owner, A) + + def test_set_name_metaclass(self): + class Meta(type): + def __new__(cls, name, bases, ns): + ret = super().__new__(cls, name, bases, ns) + self.assertEqual(ret.d.name, "d") + self.assertIs(ret.d.owner, ret) + return 0 + + class Descriptor(object): + def __set_name__(self, owner, name): + self.owner = owner + self.name = name + + class A(object, metaclass=Meta): + d = Descriptor() + self.assertEqual(A, 0) + + def test_set_name_error(self): + class Descriptor: + def __set_name__(self, owner, name): + raise RuntimeError + + with self.assertRaises(RuntimeError): + class A(object): + d = Descriptor() + + def test_set_name_wrong(self): + class Descriptor: + def __set_name__(self): + pass + + with self.assertRaises(TypeError): + class A(object): + d = Descriptor() + + def test_set_name_init_subclass(self): + class Descriptor: + def __set_name__(self, owner, name): + self.owner = owner + self.name = name + + class Meta(type): + def __new__(cls, name, bases, ns): + self = super().__new__(cls, name, bases, ns) + self.meta_owner = self.owner + self.meta_name = self.name + return self + + class A(object): + def __init_subclass__(cls): + cls.owner = cls.d.owner + cls.name = cls.d.name + + class B(A, metaclass=Meta): + d = Descriptor() + + self.assertIs(B.owner, B) + self.assertEqual(B.name, 'd') + self.assertIs(B.meta_owner, B) + self.assertEqual(B.name, 'd') + + def test_errors(self): + class MyMeta(type): + pass + + with self.assertRaises(TypeError): + class MyClass(object, metaclass=MyMeta, otherarg=1): + pass + + with self.assertRaises(TypeError): + types.new_class("MyClass", (object,), + dict(metaclass=MyMeta, otherarg=1)) + types.prepare_class("MyClass", (object,), + dict(metaclass=MyMeta, otherarg=1)) + + class MyMeta(type): + def __init__(self, name, bases, namespace, otherarg): + super().__init__(name, bases, namespace) + + with self.assertRaises(TypeError): + class MyClass(object, metaclass=MyMeta, otherarg=1): + pass + + class MyMeta(type): + def __new__(cls, name, bases, namespace, otherarg): + return super().__new__(cls, name, bases, namespace) + + def __init__(self, name, bases, namespace, otherarg): + super().__init__(name, bases, namespace) + self.otherarg = otherarg + + class MyClass(object, metaclass=MyMeta, otherarg=1): + pass + + self.assertEqual(MyClass.otherarg, 1) + + def test_errors_changed_pep487(self): + # These tests failed before Python 3.6, PEP 487 + class MyMeta(type): + def __new__(cls, name, bases, namespace): + return super().__new__(cls, name=name, bases=bases, + dict=namespace) + + with self.assertRaises(TypeError): + class MyClass(object, metaclass=MyMeta): + pass + + class MyMeta(type): + def __new__(cls, name, bases, namespace, otherarg): + self = super().__new__(cls, name, bases, namespace) + self.otherarg = otherarg + return self + + class MyClass(object, metaclass=MyMeta, otherarg=1): + pass + + self.assertEqual(MyClass.otherarg, 1) + + def test_type(self): + t = type('NewClass', (object,), {}) + self.assertIsInstance(t, type) + self.assertEqual(t.__name__, 'NewClass') + + with self.assertRaises(TypeError): + type(name='NewClass', bases=(object,), dict={}) + + +if __name__ == "__main__": + main() diff --git a/Misc/ACKS b/Misc/ACKS index 1d96fb2..b9af726 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1475,6 +1475,7 @@ Amy Taylor Julian Taylor Monty Taylor Anatoly Techtonik +Martin Teichmann Gustavo Temple Mikhail Terekhov Victor TerrĂ³n diff --git a/Misc/NEWS b/Misc/NEWS index 6ad1c3a..98814c3 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -31,6 +31,10 @@ Core and Builtins - Issue #27514: Make having too many statically nested blocks a SyntaxError instead of SystemError. +- Issue #27366: Implemented PEP 487 (Simpler customization of class creation). + Upon subclassing, the __init_subclass__ classmethod is called on the base + class. Descriptors are initialized with __set_name__ after class creation. + Library ------- diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 5227f6a..2498b1f 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -53,10 +53,12 @@ _Py_IDENTIFIER(__doc__); _Py_IDENTIFIER(__getattribute__); _Py_IDENTIFIER(__getitem__); _Py_IDENTIFIER(__hash__); +_Py_IDENTIFIER(__init_subclass__); _Py_IDENTIFIER(__len__); _Py_IDENTIFIER(__module__); _Py_IDENTIFIER(__name__); _Py_IDENTIFIER(__new__); +_Py_IDENTIFIER(__set_name__); _Py_IDENTIFIER(__setitem__); _Py_IDENTIFIER(builtins); @@ -2027,6 +2029,8 @@ static void object_dealloc(PyObject *); static int object_init(PyObject *, PyObject *, PyObject *); static int update_slot(PyTypeObject *, PyObject *); static void fixup_slot_dispatchers(PyTypeObject *); +static int set_names(PyTypeObject *); +static int init_subclass(PyTypeObject *, PyObject *); /* * Helpers for __dict__ descriptor. We don't want to expose the dicts @@ -2202,7 +2206,8 @@ type_init(PyObject *cls, PyObject *args, PyObject *kwds) assert(args != NULL && PyTuple_Check(args)); assert(kwds == NULL || PyDict_Check(kwds)); - if (kwds != NULL && PyDict_Check(kwds) && PyDict_Size(kwds) != 0) { + if (kwds != NULL && PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1 && + PyDict_Check(kwds) && PyDict_Size(kwds) != 0) { PyErr_SetString(PyExc_TypeError, "type.__init__() takes no keyword arguments"); return -1; @@ -2269,7 +2274,6 @@ static PyObject * type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds) { PyObject *name, *bases = NULL, *orig_dict, *dict = NULL; - static char *kwlist[] = {"name", "bases", "dict", 0}; PyObject *qualname, *slots = NULL, *tmp, *newslots; PyTypeObject *type = NULL, *base, *tmptype, *winner; PyHeapTypeObject *et; @@ -2296,7 +2300,7 @@ type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds) /* SF bug 475327 -- if that didn't trigger, we need 3 arguments. but PyArg_ParseTupleAndKeywords below may give a msg saying type() needs exactly 3. */ - if (nargs + nkwds != 3) { + if (nargs != 3) { PyErr_SetString(PyExc_TypeError, "type() takes 1 or 3 arguments"); return NULL; @@ -2304,10 +2308,8 @@ type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds) } /* Check arguments: (name, bases, dict) */ - if (!PyArg_ParseTupleAndKeywords(args, kwds, "UO!O!:type", kwlist, - &name, - &PyTuple_Type, &bases, - &PyDict_Type, &orig_dict)) + if (!PyArg_ParseTuple(args, "UO!O!:type", &name, &PyTuple_Type, &bases, + &PyDict_Type, &orig_dict)) return NULL; /* Determine the proper metatype to deal with this: */ @@ -2587,6 +2589,20 @@ type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds) Py_DECREF(tmp); } + /* Special-case __init_subclass__: if it's a plain function, + make it a classmethod */ + tmp = _PyDict_GetItemId(dict, &PyId___init_subclass__); + if (tmp != NULL && PyFunction_Check(tmp)) { + tmp = PyClassMethod_New(tmp); + if (tmp == NULL) + goto error; + if (_PyDict_SetItemId(dict, &PyId___init_subclass__, tmp) < 0) { + Py_DECREF(tmp); + goto error; + } + Py_DECREF(tmp); + } + /* Add descriptors for custom slots from __slots__, or for __dict__ */ mp = PyHeapType_GET_MEMBERS(et); slotoffset = base->tp_basicsize; @@ -2667,6 +2683,12 @@ type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds) et->ht_cached_keys = _PyDict_NewKeysForClass(); } + if (set_names(type) < 0) + goto error; + + if (init_subclass(type, kwds) < 0) + goto error; + Py_DECREF(dict); return (PyObject *)type; @@ -4312,6 +4334,19 @@ PyDoc_STRVAR(object_subclasshook_doc, "NotImplemented, the normal algorithm is used. Otherwise, it\n" "overrides the normal algorithm (and the outcome is cached).\n"); +static PyObject * +object_init_subclass(PyObject *cls, PyObject *arg) +{ + Py_RETURN_NONE; +} + +PyDoc_STRVAR(object_init_subclass_doc, +"This method is called when a class is subclassed\n" +"\n" +"Whenever a class is subclassed, this method is called. The default\n" +"implementation does nothing. It may be overridden to extend\n" +"subclasses.\n"); + /* from PEP 3101, this code implements: @@ -4416,6 +4451,8 @@ static PyMethodDef object_methods[] = { PyDoc_STR("helper for pickle")}, {"__subclasshook__", object_subclasshook, METH_CLASS | METH_VARARGS, object_subclasshook_doc}, + {"__init_subclass__", object_init_subclass, METH_CLASS | METH_NOARGS, + object_init_subclass_doc}, {"__format__", object_format, METH_VARARGS, PyDoc_STR("default object formatter")}, {"__sizeof__", object_sizeof, METH_NOARGS, @@ -6925,6 +6962,54 @@ update_all_slots(PyTypeObject* type) } } +/* Call __set_name__ on all descriptors in a newly generated type */ +static int +set_names(PyTypeObject *type) +{ + PyObject *key, *value, *tmp; + Py_ssize_t i = 0; + + while (PyDict_Next(type->tp_dict, &i, &key, &value)) { + if (PyObject_HasAttr(value, _PyUnicode_FromId(&PyId___set_name__))) { + tmp = PyObject_CallMethodObjArgs( + value, _PyUnicode_FromId(&PyId___set_name__), + type, key, NULL); + if (tmp == NULL) + return -1; + else + Py_DECREF(tmp); + } + } + + return 0; +} + +/* Call __init_subclass__ on the parent of a newly generated type */ +static int +init_subclass(PyTypeObject *type, PyObject *kwds) +{ + PyObject *super, *func, *tmp, *tuple; + + super = PyObject_CallFunctionObjArgs((PyObject *) &PySuper_Type, + type, type, NULL); + func = _PyObject_GetAttrId(super, &PyId___init_subclass__); + Py_DECREF(super); + + if (func == NULL) + return -1; + + tuple = PyTuple_New(0); + tmp = PyObject_Call(func, tuple, kwds); + Py_DECREF(tuple); + Py_DECREF(func); + + if (tmp == NULL) + return -1; + + Py_DECREF(tmp); + return 0; +} + /* recurse_down_subclasses() and update_subclasses() are mutually recursive functions to call a callback for all subclasses, but refraining from recursing into subclasses that define 'name'. */ -- cgit v0.12