Throw TypeError when subclasses forget to call __init__ (#2152)

- Fixes #2103
This commit is contained in:
Dustin Spicuzza 2020-07-07 06:04:06 -04:00 committed by GitHub
parent d54d6d8c61
commit 1b0bf352fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 53 additions and 0 deletions

View File

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

View File

@ -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<detail::instance *>(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;

View File

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