From 1b0bf352fa51f07402f998d358565bf141f94c69 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Tue, 7 Jul 2020 06:04:06 -0400 Subject: [PATCH] Throw TypeError when subclasses forget to call __init__ (#2152) - Fixes #2103 --- docs/advanced/classes.rst | 5 +++++ include/pybind11/detail/class.h | 27 +++++++++++++++++++++++++++ tests/test_class.py | 21 +++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/docs/advanced/classes.rst b/docs/advanced/classes.rst index 0d50cba51..b3da0378c 100644 --- a/docs/advanced/classes.rst +++ b/docs/advanced/classes.rst @@ -149,6 +149,11 @@ memory for the C++ portion of the instance will be left uninitialized, which will generally leave the C++ instance in an invalid state and cause undefined behavior if the C++ instance is subsequently used. +.. versionadded:: 2.5.1 + + The default pybind11 metaclass will throw a ``TypeError`` when it detects + that ``__init__`` was not called by a derived class. + Here is an example: .. code-block:: python diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index a05edeb4c..e5783d9f6 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -156,6 +156,31 @@ extern "C" inline PyObject *pybind11_meta_getattro(PyObject *obj, PyObject *name } #endif +/// metaclass `__call__` function that is used to create all pybind11 objects. +extern "C" inline PyObject *pybind11_meta_call(PyObject *type, PyObject *args, PyObject *kwargs) { + + // use the default metaclass call to create/initialize the object + PyObject *self = PyType_Type.tp_call(type, args, kwargs); + if (self == nullptr) { + return nullptr; + } + + // This must be a pybind11 instance + auto instance = reinterpret_cast(self); + + // Ensure that the base __init__ function(s) were called + for (auto vh : values_and_holders(instance)) { + if (!vh.holder_constructed()) { + PyErr_Format(PyExc_TypeError, "%.200s.__init__() must be called when overriding __init__", + vh.type->type->tp_name); + Py_DECREF(self); + return nullptr; + } + } + + return self; +} + /** This metaclass is assigned by default to all pybind11 types and is required in order for static properties to function correctly. Users may override this using `py::metaclass`. Return value: New reference. */ @@ -181,6 +206,8 @@ inline PyTypeObject* make_default_metaclass() { type->tp_base = type_incref(&PyType_Type); type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HEAPTYPE; + type->tp_call = pybind11_meta_call; + type->tp_setattro = pybind11_meta_setattro; #if PY_MAJOR_VERSION >= 3 type->tp_getattro = pybind11_meta_getattro; diff --git a/tests/test_class.py b/tests/test_class.py index e58fef698..9807535e8 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -101,6 +101,27 @@ def test_inheritance(msg): assert "No constructor defined!" in str(excinfo.value) +def test_inheritance_init(msg): + + # Single base + class Python(m.Pet): + def __init__(self): + pass + with pytest.raises(TypeError) as exc_info: + Python() + assert msg(exc_info.value) == "m.class_.Pet.__init__() must be called when overriding __init__" + + # Multiple bases + class RabbitHamster(m.Rabbit, m.Hamster): + def __init__(self): + m.Rabbit.__init__(self, "RabbitHamster") + + with pytest.raises(TypeError) as exc_info: + RabbitHamster() + expected = "m.class_.Hamster.__init__() must be called when overriding __init__" + assert msg(exc_info.value) == expected + + def test_automatic_upcasting(): assert type(m.return_class_1()).__name__ == "DerivedClass1" assert type(m.return_class_2()).__name__ == "DerivedClass2"