Issue #27366: Implement PEP 487

- __init_subclass__ called when new subclasses defined
- __set_name__ called when descriptors are part of a
  class definition
This commit is contained in:
Nick Coghlan 2016-07-30 16:26:03 +10:00
parent f6daa690e4
commit d78448e912
9 changed files with 411 additions and 24 deletions

View 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)``.

View 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
---------------------------------

View 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', '':

View File

@ -182,6 +182,7 @@ You can get the information from the list type:
'__iadd__',
'__imul__',
'__init__',
'__init_subclass__',
'__iter__',
'__le__',
'__len__',

View 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)

View File

@ -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()

View File

@ -1475,6 +1475,7 @@ Amy Taylor
Julian Taylor
Monty Taylor
Anatoly Techtonik
Martin Teichmann
Gustavo Temple
Mikhail Terekhov
Victor Terrón

View File

@ -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
-------

View 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,9 +2308,7 @@ 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,
if (!PyArg_ParseTuple(args, "UO!O!:type", &name, &PyTuple_Type, &bases,
&PyDict_Type, &orig_dict))
return NULL;
@ -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'. */