Skip to content

Commit

Permalink
Throw TypeError when subclasses forget to call __init__
Browse files Browse the repository at this point in the history
- Fixes #2103
  • Loading branch information
virtuald committed Apr 5, 2020
1 parent 0234871 commit 0146834
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 0 deletions.
5 changes: 5 additions & 0 deletions docs/advanced/classes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

pybind11 will throw a ``TypeError`` when it detects that ``__init__`` was
not called by a derived class.

Here is an example:

.. code-block:: python
Expand Down
27 changes: 27 additions & 0 deletions include/pybind11/detail/class.h
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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;
Expand Down
20 changes: 20 additions & 0 deletions tests/test_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,26 @@ 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()
assert msg(exc_info.value) == "m.class_.Hamster.__init__() must be called when overriding __init__"


def test_automatic_upcasting():
assert type(m.return_class_1()).__name__ == "DerivedClass1"
assert type(m.return_class_2()).__name__ == "DerivedClass2"
Expand Down

0 comments on commit 0146834

Please sign in to comment.