From e1e8d7a59d0d2d09be81100eef87c2b15a9d2ccb Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 23 Aug 2020 21:05:19 -0400 Subject: [PATCH] Improved errors when inheriting and forgetting to call __init__ 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 --- include/pybind11/detail/class.h | 12 +++++++++--- tests/test_class.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index 8d36744f27..ebf5d30846 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -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; } @@ -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; diff --git a/tests/test_class.py b/tests/test_class.py index 200deee266..8911cdf218 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -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"