Skip to content

Commit 8374d97

Browse files
committed
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 * Since default_object_init is inline, if the default instance base was no longer a PyHeapTypeObject it 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
1 parent c676036 commit 8374d97

File tree

3 files changed

+29
-3
lines changed

3 files changed

+29
-3
lines changed

include/pybind11/detail/class.h

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,13 @@ extern "C" inline PyObject *pybind11_meta_call(PyObject *type, PyObject *args, P
171171
// Ensure that the base __init__ function(s) were called
172172
for (const auto &vh : values_and_holders(instance)) {
173173
if (!vh.holder_constructed()) {
174-
PyErr_Format(PyExc_TypeError, "%.200s.__init__() must be called when overriding __init__",
175-
vh.type->type->tp_name);
174+
auto message = "%.200s.__init__() must be called when overriding __init__";
175+
#if !defined(PYPY_VERSION)
176+
if (vh.type->type->tp_init == get_internals().default_object_init()) {
177+
message = "%.200s has no __init__ and cannot be used as a base class from Python";
178+
}
179+
#endif
180+
PyErr_Format(PyExc_TypeError, message, vh.type->type->tp_name);
176181
Py_DECREF(self);
177182
return nullptr;
178183
}
@@ -302,6 +307,8 @@ extern "C" inline PyObject *pybind11_object_new(PyTypeObject *type, PyObject *,
302307
/// An `__init__` function constructs the C++ object. Users should provide at least one
303308
/// of these using `py::init` or directly with `.def(__init__, ...)`. Otherwise, the
304309
/// following default function will be used which simply throws an exception.
310+
///
311+
/// Use `get_internals().default_object_init()` instead of using this function directly
305312
extern "C" inline int pybind11_object_init(PyObject *self, PyObject *, PyObject *) {
306313
PyTypeObject *type = Py_TYPE(self);
307314
std::string msg;
@@ -620,7 +627,7 @@ inline PyObject* make_new_python_type(const type_record &rec) {
620627
type->tp_bases = bases.release().ptr();
621628

622629
/* Don't inherit base __init__ */
623-
type->tp_init = pybind11_object_init;
630+
type->tp_init = internals.default_object_init();
624631

625632
/* Supported protocols */
626633
type->tp_as_number = &heap_type->as_number;

include/pybind11/detail/internals.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ struct internals {
121121
PYBIND11_TLS_FREE(tstate);
122122
}
123123
#endif
124+
125+
inline initproc default_object_init() {
126+
auto heap_type = (PyHeapTypeObject*)instance_base;
127+
return heap_type->ht_type.tp_init;
128+
}
124129
};
125130

126131
/// Additional type information which does not fit into the PyTypeObject.

tests/test_class.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,20 @@ def __init__(self):
132132
expected = "m.class_.Hamster.__init__() must be called when overriding __init__"
133133
assert msg(exc_info.value) == expected
134134

135+
# Base doesn't have __init__
136+
class ChChimera(m.Chimera):
137+
def __init__(self):
138+
pass
139+
140+
with pytest.raises(TypeError) as exc_info:
141+
ChChimera()
142+
if env.PYPY:
143+
# can't detect no __init__ in PyPy
144+
expected = "Chimera.__init__() must be called when overriding __init__"
145+
else:
146+
expected = "m.class_.Chimera has no __init__ and cannot be used as a base class from Python"
147+
assert msg(exc_info.value) == expected
148+
135149

136150
def test_automatic_upcasting():
137151
assert type(m.return_class_1()).__name__ == "DerivedClass1"

0 commit comments

Comments
 (0)