Skip to content

Commit

Permalink
Improved errors when inheriting and forgetting to call __init__
Browse files Browse the repository at this point in the history
If the base class doesn't have a constructor, tell the user that it cannot be
inherited from.

Caveats:

* Doesn't work on PyPy
* If the default instance base was no longer a PyHeapTypeObject this could crash.
* If pybind11_object_init changed in the future, and multiple pybind11 modules are
  present, get_internals might return an old version of the function
  • Loading branch information
virtuald committed Aug 25, 2020
1 parent c676036 commit 49aa611
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 3 deletions.
12 changes: 9 additions & 3 deletions include/pybind11/detail/class.h
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,14 @@ extern "C" inline PyObject *pybind11_meta_call(PyObject *type, PyObject *args, P
// Ensure that the base __init__ function(s) were called
for (const 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);
auto message = "%.200s.__init__() must be called when overriding __init__";
#if !defined(PYPY_VERSION)
auto default_init = ((PyHeapTypeObject*)get_internals().instance_base)->ht_type.tp_init;
if (vh.type->type->tp_init == default_init) {
message = "%.200s has no __init__ and cannot be used as a base class from Python";
}
#endif
PyErr_Format(PyExc_TypeError, message, vh.type->type->tp_name);
Py_DECREF(self);
return nullptr;
}
Expand Down Expand Up @@ -620,7 +626,7 @@ inline PyObject* make_new_python_type(const type_record &rec) {
type->tp_bases = bases.release().ptr();

/* Don't inherit base __init__ */
type->tp_init = pybind11_object_init;
type->tp_init = ((PyHeapTypeObject*)internals.instance_base)->ht_type.tp_init;

/* Supported protocols */
type->tp_as_number = &heap_type->as_number;
Expand Down
15 changes: 15 additions & 0 deletions tests/test_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,21 @@ def __init__(self):
expected = "m.class_.Hamster.__init__() must be called when overriding __init__"
assert msg(exc_info.value) == expected

# Base doesn't have __init__
class ChChimera(m.Chimera):
def __init__(self):
pass

with pytest.raises(TypeError) as exc_info:
ChChimera()
if env.PYPY:
# can't detect no __init__ in PyPy
expected = "Chimera.__init__() must be called when overriding __init__"
else:
expected = \
"m.class_.Chimera has no __init__ and cannot be used as a base class from Python"
assert msg(exc_info.value) == expected


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

0 comments on commit 49aa611

Please sign in to comment.