Skip to content

Commit 5e5d3cf

Browse files
committed
Throw TypeError when subclasses forget to call __init__
- Fixes #2103
1 parent 0234871 commit 5e5d3cf

File tree

3 files changed

+52
-0
lines changed

3 files changed

+52
-0
lines changed

docs/advanced/classes.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ memory for the C++ portion of the instance will be left uninitialized, which
149149
will generally leave the C++ instance in an invalid state and cause undefined
150150
behavior if the C++ instance is subsequently used.
151151

152+
.. versionadded:: 2.5.1
153+
154+
The default pybind11 metaclass will throw a ``TypeError`` when it detects
155+
that ``__init__`` was not called by a derived class.
156+
152157
Here is an example:
153158

154159
.. code-block:: python

include/pybind11/detail/class.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,31 @@ extern "C" inline PyObject *pybind11_meta_getattro(PyObject *obj, PyObject *name
156156
}
157157
#endif
158158

159+
/// metaclass `__call__` function that is used to create all pybind11 objects.
160+
extern "C" inline PyObject *pybind11_meta_call(PyObject *type, PyObject *args, PyObject *kwargs) {
161+
162+
// use the default metaclass call to create/initialize the object
163+
PyObject *self = PyType_Type.tp_call(type, args, kwargs);
164+
if (self == nullptr) {
165+
return nullptr;
166+
}
167+
168+
// This must be a pybind11 instance
169+
auto instance = reinterpret_cast<detail::instance *>(self);
170+
171+
// Ensure that the base __init__ function(s) were called
172+
for (auto vh : values_and_holders(instance)) {
173+
if (!vh.holder_constructed()) {
174+
PyErr_Format(PyExc_TypeError, "%.200s.__init__() must be called when overriding __init__",
175+
vh.type->type->tp_name);
176+
Py_DECREF(self);
177+
return nullptr;
178+
}
179+
}
180+
181+
return self;
182+
}
183+
159184
/** This metaclass is assigned by default to all pybind11 types and is required in order
160185
for static properties to function correctly. Users may override this using `py::metaclass`.
161186
Return value: New reference. */
@@ -181,6 +206,8 @@ inline PyTypeObject* make_default_metaclass() {
181206
type->tp_base = type_incref(&PyType_Type);
182207
type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HEAPTYPE;
183208

209+
type->tp_call = pybind11_meta_call;
210+
184211
type->tp_setattro = pybind11_meta_setattro;
185212
#if PY_MAJOR_VERSION >= 3
186213
type->tp_getattro = pybind11_meta_getattro;

tests/test_class.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,26 @@ def test_inheritance(msg):
101101
assert "No constructor defined!" in str(excinfo.value)
102102

103103

104+
def test_inheritance_init(msg):
105+
106+
# Single base
107+
class Python(m.Pet):
108+
def __init__(self):
109+
pass
110+
with pytest.raises(TypeError) as exc_info:
111+
Python()
112+
assert msg(exc_info.value) == "m.class_.Pet.__init__() must be called when overriding __init__"
113+
114+
# Multiple bases
115+
class RabbitHamster(m.Rabbit, m.Hamster):
116+
def __init__(self):
117+
m.Rabbit.__init__(self, "RabbitHamster")
118+
119+
with pytest.raises(TypeError) as exc_info:
120+
RabbitHamster()
121+
assert msg(exc_info.value) == "m.class_.Hamster.__init__() must be called when overriding __init__"
122+
123+
104124
def test_automatic_upcasting():
105125
assert type(m.return_class_1()).__name__ == "DerivedClass1"
106126
assert type(m.return_class_2()).__name__ == "DerivedClass2"

0 commit comments

Comments
 (0)