cpython: ecc7bff738e0 (original) (raw)
Mercurial > cpython
changeset 102491:ecc7bff738e0
Issue #27366: Implement PEP 487 - __init_subclass__ called when new subclasses defined - __set_name__ called when descriptors are part of a class definition [#27366]
Nick Coghlan ncoghlan@gmail.com | |
---|---|
date | Sat, 30 Jul 2016 16:26:03 +1000 |
parents | 520c0d391f00 |
children | 92b3ce7ca95c |
files | 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 Misc/ACKS Misc/NEWS Objects/typeobject.c |
diffstat | 9 files changed, 411 insertions(+), 24 deletions(-)[+] [-] 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 |
line wrap: on
line diff
--- 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)
+
+
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:[](#l1.44)
def __init_subclass__(cls, default_name, **kwargs):[](#l1.45)
super().__init_subclass__(**kwargs)[](#l1.46)
cls.default_name = default_name[](#l1.47)
class AustralianPhilosopher(Philosopher, default_name="Bruce"):[](#l1.49)
pass[](#l1.50)
- 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)
.
--- a/Doc/whatsnew/3.6.rst
+++ b/Doc/whatsnew/3.6.rst
@@ -110,6 +110,26 @@ evaluated at run time, and then formatte
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
---------------------------------
--- 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:[](#l3.8)
def ham(self):[](#l3.9)
return 'ham%d' % self[](#l3.10)
C = type.__new__(type,[](#l3.11)
name='C',[](#l3.12)
bases=(B, int),[](#l3.13)
dict={'spam': lambda self: 'spam%s' % self})[](#l3.14)
self.assertEqual(C.__name__, 'C')[](#l3.15)
self.assertEqual(C.__qualname__, 'C')[](#l3.16)
self.assertEqual(C.__module__, __name__)[](#l3.17)
self.assertEqual(C.__bases__, (B, int))[](#l3.18)
self.assertIs(C.__base__, int)[](#l3.19)
self.assertIn('spam', C.__dict__)[](#l3.20)
self.assertNotIn('ham', C.__dict__)[](#l3.21)
- def test_type_nokwargs(self):
with self.assertRaises(TypeError):[](#l3.23)
type('a', (), {}, x=5)[](#l3.24)
with self.assertRaises(TypeError):[](#l3.25)
type('a', (), dict={})[](#l3.26)
def test_type_name(self): for name in 'A', '\xc4', '\U0001f40d', 'B.A', '42', '':
--- a/Lib/test/test_descrtut.py +++ b/Lib/test/test_descrtut.py @@ -182,6 +182,7 @@ You can get the information from the lis 'iadd', 'imul', 'init',
'__init_subclass__',[](#l4.7) '__iter__',[](#l4.8) '__le__',[](#l4.9) '__len__',[](#l4.10)
--- 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__.[](#l5.7)
# so we need to update __subclasshook__ and __init_subclass__.[](#l5.8) expected['__subclasshook__'] = TestClass.__subclasshook__[](#l5.9)
expected['__init_subclass__'] = TestClass.__init_subclass__[](#l5.10)
methods = pydoc.allmethods(TestClass) self.assertDictEqual(methods, expected)
new file mode 100644 --- /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 __init_subclass__(cls):[](#l6.15)
super().__init_subclass__()[](#l6.16)
cls.initialized = True[](#l6.17)
class B(A):[](#l6.19)
pass[](#l6.20)
self.assertFalse(A.initialized)[](#l6.22)
self.assertTrue(B.initialized)[](#l6.23)
def __init_subclass__(cls):[](#l6.29)
super().__init_subclass__()[](#l6.30)
cls.initialized = True[](#l6.31)
class B(A):[](#l6.33)
pass[](#l6.34)
self.assertFalse(A.initialized)[](#l6.36)
self.assertTrue(B.initialized)[](#l6.37)
- def test_init_subclass_kwargs(self):
class A(object):[](#l6.40)
def __init_subclass__(cls, **kwargs):[](#l6.41)
cls.kwargs = kwargs[](#l6.42)
class B(A, x=3):[](#l6.44)
pass[](#l6.45)
self.assertEqual(B.kwargs, dict(x=3))[](#l6.47)
- def test_init_subclass_error(self):
class A(object):[](#l6.50)
def __init_subclass__(cls):[](#l6.51)
raise RuntimeError[](#l6.52)
with self.assertRaises(RuntimeError):[](#l6.54)
class B(A):[](#l6.55)
pass[](#l6.56)
- def test_init_subclass_wrong(self):
class A(object):[](#l6.59)
def __init_subclass__(cls, whatever):[](#l6.60)
pass[](#l6.61)
with self.assertRaises(TypeError):[](#l6.63)
class B(A):[](#l6.64)
pass[](#l6.65)
- def test_init_subclass_skipped(self):
class BaseWithInit(object):[](#l6.68)
def __init_subclass__(cls, **kwargs):[](#l6.69)
super().__init_subclass__(**kwargs)[](#l6.70)
cls.initialized = cls[](#l6.71)
class BaseWithoutInit(BaseWithInit):[](#l6.73)
pass[](#l6.74)
class A(BaseWithoutInit):[](#l6.76)
pass[](#l6.77)
self.assertIs(A.initialized, A)[](#l6.79)
self.assertIs(BaseWithoutInit.initialized, BaseWithoutInit)[](#l6.80)
- def test_init_subclass_diamond(self):
class Base(object):[](#l6.83)
def __init_subclass__(cls, **kwargs):[](#l6.84)
super().__init_subclass__(**kwargs)[](#l6.85)
cls.calls = [][](#l6.86)
class Left(Base):[](#l6.88)
pass[](#l6.89)
class Middle(object):[](#l6.91)
def __init_subclass__(cls, middle, **kwargs):[](#l6.92)
super().__init_subclass__(**kwargs)[](#l6.93)
cls.calls += [middle][](#l6.94)
class Right(Base):[](#l6.96)
def __init_subclass__(cls, right="right", **kwargs):[](#l6.97)
super().__init_subclass__(**kwargs)[](#l6.98)
cls.calls += [right][](#l6.99)
class A(Left, Middle, Right, middle="middle"):[](#l6.101)
pass[](#l6.102)
self.assertEqual(A.calls, ["right", "middle"])[](#l6.104)
self.assertEqual(Left.calls, [])[](#l6.105)
self.assertEqual(Right.calls, [])[](#l6.106)
- def test_set_name(self):
class Descriptor:[](#l6.109)
def __set_name__(self, owner, name):[](#l6.110)
self.owner = owner[](#l6.111)
self.name = name[](#l6.112)
class A(object):[](#l6.114)
d = Descriptor()[](#l6.115)
self.assertEqual(A.d.name, "d")[](#l6.117)
self.assertIs(A.d.owner, A)[](#l6.118)
- def test_set_name_metaclass(self):
class Meta(type):[](#l6.121)
def __new__(cls, name, bases, ns):[](#l6.122)
ret = super().__new__(cls, name, bases, ns)[](#l6.123)
self.assertEqual(ret.d.name, "d")[](#l6.124)
self.assertIs(ret.d.owner, ret)[](#l6.125)
return 0[](#l6.126)
class Descriptor(object):[](#l6.128)
def __set_name__(self, owner, name):[](#l6.129)
self.owner = owner[](#l6.130)
self.name = name[](#l6.131)
class A(object, metaclass=Meta):[](#l6.133)
d = Descriptor()[](#l6.134)
self.assertEqual(A, 0)[](#l6.135)
- def test_set_name_error(self):
class Descriptor:[](#l6.138)
def __set_name__(self, owner, name):[](#l6.139)
raise RuntimeError[](#l6.140)
with self.assertRaises(RuntimeError):[](#l6.142)
class A(object):[](#l6.143)
d = Descriptor()[](#l6.144)
- def test_set_name_wrong(self):
class Descriptor:[](#l6.147)
def __set_name__(self):[](#l6.148)
pass[](#l6.149)
with self.assertRaises(TypeError):[](#l6.151)
class A(object):[](#l6.152)
d = Descriptor()[](#l6.153)
- def test_set_name_init_subclass(self):
class Descriptor:[](#l6.156)
def __set_name__(self, owner, name):[](#l6.157)
self.owner = owner[](#l6.158)
self.name = name[](#l6.159)
class Meta(type):[](#l6.161)
def __new__(cls, name, bases, ns):[](#l6.162)
self = super().__new__(cls, name, bases, ns)[](#l6.163)
self.meta_owner = self.owner[](#l6.164)
self.meta_name = self.name[](#l6.165)
return self[](#l6.166)
class A(object):[](#l6.168)
def __init_subclass__(cls):[](#l6.169)
cls.owner = cls.d.owner[](#l6.170)
cls.name = cls.d.name[](#l6.171)
class B(A, metaclass=Meta):[](#l6.173)
d = Descriptor()[](#l6.174)
self.assertIs(B.owner, B)[](#l6.176)
self.assertEqual(B.name, 'd')[](#l6.177)
self.assertIs(B.meta_owner, B)[](#l6.178)
self.assertEqual(B.name, 'd')[](#l6.179)
with self.assertRaises(TypeError):[](#l6.185)
class MyClass(object, metaclass=MyMeta, otherarg=1):[](#l6.186)
pass[](#l6.187)
with self.assertRaises(TypeError):[](#l6.189)
types.new_class("MyClass", (object,),[](#l6.190)
dict(metaclass=MyMeta, otherarg=1))[](#l6.191)
types.prepare_class("MyClass", (object,),[](#l6.192)
dict(metaclass=MyMeta, otherarg=1))[](#l6.193)
class MyMeta(type):[](#l6.195)
def __init__(self, name, bases, namespace, otherarg):[](#l6.196)
super().__init__(name, bases, namespace)[](#l6.197)
with self.assertRaises(TypeError):[](#l6.199)
class MyClass(object, metaclass=MyMeta, otherarg=1):[](#l6.200)
pass[](#l6.201)
class MyMeta(type):[](#l6.203)
def __new__(cls, name, bases, namespace, otherarg):[](#l6.204)
return super().__new__(cls, name, bases, namespace)[](#l6.205)
def __init__(self, name, bases, namespace, otherarg):[](#l6.207)
super().__init__(name, bases, namespace)[](#l6.208)
self.otherarg = otherarg[](#l6.209)
class MyClass(object, metaclass=MyMeta, otherarg=1):[](#l6.211)
pass[](#l6.212)
self.assertEqual(MyClass.otherarg, 1)[](#l6.214)
- def test_errors_changed_pep487(self):
# These tests failed before Python 3.6, PEP 487[](#l6.217)
class MyMeta(type):[](#l6.218)
def __new__(cls, name, bases, namespace):[](#l6.219)
return super().__new__(cls, name=name, bases=bases,[](#l6.220)
dict=namespace)[](#l6.221)
with self.assertRaises(TypeError):[](#l6.223)
class MyClass(object, metaclass=MyMeta):[](#l6.224)
pass[](#l6.225)
class MyMeta(type):[](#l6.227)
def __new__(cls, name, bases, namespace, otherarg):[](#l6.228)
self = super().__new__(cls, name, bases, namespace)[](#l6.229)
self.otherarg = otherarg[](#l6.230)
return self[](#l6.231)
class MyClass(object, metaclass=MyMeta, otherarg=1):[](#l6.233)
pass[](#l6.234)
self.assertEqual(MyClass.otherarg, 1)[](#l6.236)
- def test_type(self):
t = type('NewClass', (object,), {})[](#l6.239)
self.assertIsInstance(t, type)[](#l6.240)
self.assertEqual(t.__name__, 'NewClass')[](#l6.241)
with self.assertRaises(TypeError):[](#l6.243)
type(name='NewClass', bases=(object,), dict={})[](#l6.244)
--- 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
--- 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 -------
--- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -53,10 +53,12 @@ static size_t method_cache_collisions = _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, assert(args != NULL && PyTuple_Check(args)); assert(kwds == NULL || PyDict_Check(kwds));
- if (kwds != NULL && PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1 &&
PyDict_Check(kwds) && PyDict_Size(kwds) != 0) {[](#l9.31) PyErr_SetString(PyExc_TypeError,[](#l9.32) "type.__init__() takes no keyword arguments");[](#l9.33) return -1;[](#l9.34)
@@ -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, PyObjec / 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) {[](#l9.47)
if (nargs != 3) {[](#l9.48) PyErr_SetString(PyExc_TypeError,[](#l9.49) "type() takes 1 or 3 arguments");[](#l9.50) return NULL;[](#l9.51)
@@ -2304,10 +2308,8 @@ type_new(PyTypeObject metatype, PyObjec } / Check arguments: (name, bases, dict) */
- if (!PyArg_ParseTupleAndKeywords(args, kwds, "UO!O!:type", kwlist,
&name,[](#l9.57)
&PyTuple_Type, &bases,[](#l9.58)
&PyDict_Type, &orig_dict))[](#l9.59)
- if (!PyArg_ParseTuple(args, "UO!O!:type", &name, &PyTuple_Type, &bases,
&PyDict_Type, &orig_dict))[](#l9.61) return NULL;[](#l9.62)
/* Determine the proper metatype to deal with this: */ @@ -2587,6 +2589,20 @@ type_new(PyTypeObject *metatype, PyObjec Py_DECREF(tmp); }
- /* Special-case init_subclass: if it's a plain function,
make it a classmethod */[](#l9.70)
- tmp = PyDict_GetItemId(dict, &PyId___init_subclass_);
- if (tmp != NULL && PyFunction_Check(tmp)) {
tmp = PyClassMethod_New(tmp);[](#l9.73)
if (tmp == NULL)[](#l9.74)
goto error;[](#l9.75)
if (_PyDict_SetItemId(dict, &PyId___init_subclass__, tmp) < 0) {[](#l9.76)
Py_DECREF(tmp);[](#l9.77)
goto error;[](#l9.78)
}[](#l9.79)
Py_DECREF(tmp);[](#l9.80)
- }
+ /* 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, PyObjec et->ht_cached_keys = _PyDict_NewKeysForClass(); }
+ 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) +{
+} + +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,
{"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) +{object_init_subclass_doc},[](#l9.124)
- 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__))) {[](#l9.140)
tmp = PyObject_CallMethodObjArgs([](#l9.141)
value, _PyUnicode_FromId(&PyId___set_name__),[](#l9.142)
type, key, NULL);[](#l9.143)
if (tmp == NULL)[](#l9.144)
return -1;[](#l9.145)
else[](#l9.146)
Py_DECREF(tmp);[](#l9.147)
}[](#l9.148)
- }
+} + +/* Call init_subclass on the parent of a newly generated type */ +static int +init_subclass(PyTypeObject *type, PyObject *kwds) +{
- super = PyObject_CallFunctionObjArgs((PyObject *) &PySuper_Type,
type, type, NULL);[](#l9.161)
- func = PyObject_GetAttrId(super, &PyId___init_subclass_);
- Py_DECREF(super);
+} + /* 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'. */