]> granicus.if.org Git - python/commitdiff
Issue #27366: Implement PEP 487
authorNick Coghlan <ncoghlan@gmail.com>
Sat, 30 Jul 2016 06:26:03 +0000 (16:26 +1000)
committerNick Coghlan <ncoghlan@gmail.com>
Sat, 30 Jul 2016 06:26:03 +0000 (16:26 +1000)
- __init_subclass__ called when new subclasses defined
- __set_name__ called when descriptors are part of a
  class definition

Doc/reference/datamodel.rst
Doc/whatsnew/3.6.rst
Lib/test/test_builtin.py
Lib/test/test_descrtut.py
Lib/test/test_pydoc.py
Lib/test/test_subclassinit.py [new file with mode: 0644]
Misc/ACKS
Misc/NEWS
Objects/typeobject.c

index 4c88e38cd2e29d6d53550c1592e74d6a5b6b8e24..2a8579812320503a85517acd8509a4655c3e78a2 100644 (file)
@@ -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)``.
index e13800cb6e8a5be7a4dcf50ea9b7a2fe65cf0961..0d535133cf066c704ac1a2c0a8a523f8c678ee98 100644 (file)
@@ -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
 ---------------------------------
 
index 8cc1b0074bb152e2ce4811722f8201810fc3ad5f..acc4f9ce8194245e82fb2616f896cf99b4ac9b13 100644 (file)
@@ -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', '':
index 506d1abeb6963d67e874adc3b7924553c3737a74..b84d6447850a069435e508f2177826449375a342 100644 (file)
@@ -182,6 +182,7 @@ You can get the information from the list type:
      '__iadd__',
      '__imul__',
      '__init__',
+     '__init_subclass__',
      '__iter__',
      '__le__',
      '__len__',
index 889ce592db8759cdf3a0c6e706802b76d14d3558..4998597e21655b654f365846335d24618f26b8fd 100644 (file)
@@ -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 (file)
index 0000000..eb5ed70
--- /dev/null
@@ -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()
index 1d96fb20f5ed342e648d5925200a9143a1d96ebe..b9af7265f918223fe88d2bdf99599dca45006e13 100644 (file)
--- 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
index 6ad1c3a72376dc0788c99e7bb064c7a8def5b2cb..98814c371ef16463bacafab69bcaa50b22737e5c 100644 (file)
--- 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
 -------
 
index 5227f6a6094e7264b1db6a1dd9509613513fddac..2498b1f6fde45ee7337c97c6d1906af6d27be40a 100644 (file)
@@ -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'. */