Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: a long-standing bug in the handling of Python multiple inheritance #4762

Merged
merged 35 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c9a56ac
Equivalent of https://github.com/google/clif/commit/5718e4d0807fd3b6a…
Jul 25, 2023
bdd938a
Resolve clang-tidy errors.
Jul 26, 2023
61f7529
Moving test_PPCCInit() first changes the behavior!
Jul 26, 2023
1fb7dc3
Resolve new Clang dev C++11 errors:
Jul 26, 2023
b9f3380
Resolve gcc 4.8.5 error:
Jul 26, 2023
8ea1b8c
Specifically exclude `__clang__`
Jul 26, 2023
1d4f9ff
Snapshot of debugging code (does NOT pass pre-commit checks).
Jul 27, 2023
775aab5
Revert "Snapshot of debugging code (does NOT pass pre-commit checks)."
Jul 27, 2023
d37b540
[ci skip] Order Dependence Demo
Jul 27, 2023
7f8b208
Revert "[ci skip] Order Dependence Demo"
Jul 27, 2023
eb09c6c
One way to deal with the order dependency issue. This is not the best…
Jul 28, 2023
a77c1c6
Move test_PC() first again.
Jul 28, 2023
eec2d81
Add `all_type_info_add_base_most_derived_first()`, use in `all_type_i…
Jul 28, 2023
9c7d267
Revert "One way to deal with the order dependency issue. This is not …
Jul 28, 2023
7c7d78d
clang-tidy fixes (automatic)
Jul 28, 2023
d708836
Add `is_redundant_value_and_holder()` and use to avoid forcing `__ini…
Jul 28, 2023
cf5958d
Streamline implementation and avoid unsafe `reinterpret_cast<instance…
Jul 31, 2023
c89561f
Fix actual undefined behavior exposed by previous changes.
Jul 31, 2023
a2f95e1
Add test_mock_new()
Jul 31, 2023
4f90d85
Experiment: specify indirect bases
Jul 31, 2023
0a0debd
Merge branch 'master' into python_multiple_inheritance_test
Aug 4, 2023
52b7993
Revert "Experiment: specify indirect bases"
Aug 4, 2023
33805e2
Merge branch 'master' into python_multiple_inheritance_test
Aug 15, 2023
adb5bad
Merge branch 'master' into python_multiple_inheritance_test
Sep 12, 2023
c516151
Merge branch 'master' into python_multiple_inheritance_test
Oct 30, 2023
f3bb31e
Merge branch 'master' into python_multiple_inheritance_test
Nov 2, 2023
0a9599f
Add `all_type_info_check_for_divergence()` and some tests.
Nov 3, 2023
86ca4e1
Merge branch 'master' into python_multiple_inheritance_test
Nov 5, 2023
9ae6cba
Merge branch 'master' into python_multiple_inheritance_test
Nov 7, 2023
5f5fd6a
Call `all_type_info_check_for_divergence()` also from `type_caster_ge…
Nov 7, 2023
df27188
Resolve clang-tidy error:
Nov 7, 2023
52de174
Merge branch 'master' into python_multiple_inheritance_test
Nov 8, 2023
1b157fc
Revert "Resolve clang-tidy error:"
Nov 8, 2023
7a6c436
Revert "Call `all_type_info_check_for_divergence()` also from `type_c…
Nov 8, 2023
6ab605b
Revert "Add `all_type_info_check_for_divergence()` and some tests."
Nov 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions include/pybind11/detail/class.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,12 +189,10 @@ extern "C" inline PyObject *pybind11_meta_call(PyObject *type, PyObject *args, P
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 (const auto &vh : values_and_holders(instance)) {
if (!vh.holder_constructed()) {
values_and_holders vhs(self);
for (const auto &vh : vhs) {
if (!vh.holder_constructed() && !vhs.is_redundant_value_and_holder(vh)) {
PyErr_Format(PyExc_TypeError,
"%.200s.__init__() must be called when overriding __init__",
get_fully_qualified_tp_name(vh.type->type).c_str());
Expand Down
49 changes: 42 additions & 7 deletions include/pybind11/detail/type_caster_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,22 @@ class loader_life_support {
inline std::pair<decltype(internals::registered_types_py)::iterator, bool>
all_type_info_get_cache(PyTypeObject *type);

// Band-aid workaround to fix a subtle but serious bug in a minimalistic fashion. See PR #4762.
inline void all_type_info_add_base_most_derived_first(std::vector<type_info *> &bases,
type_info *addl_base) {
for (auto it = bases.begin(); it != bases.end(); it++) {
type_info *existing_base = *it;
if (PyType_IsSubtype(addl_base->type, existing_base->type) != 0) {
bases.insert(it, addl_base);
return;
}
}
bases.push_back(addl_base);
}

// Populates a just-created cache entry.
PYBIND11_NOINLINE void all_type_info_populate(PyTypeObject *t, std::vector<type_info *> &bases) {
assert(bases.empty());
std::vector<PyTypeObject *> check;
for (handle parent : reinterpret_borrow<tuple>(t->tp_bases)) {
check.push_back((PyTypeObject *) parent.ptr());
Expand Down Expand Up @@ -136,7 +150,7 @@ PYBIND11_NOINLINE void all_type_info_populate(PyTypeObject *t, std::vector<type_
}
}
if (!found) {
bases.push_back(tinfo);
all_type_info_add_base_most_derived_first(bases, tinfo);
}
}
} else if (type->tp_bases) {
Expand Down Expand Up @@ -322,18 +336,29 @@ struct values_and_holders {
explicit values_and_holders(instance *inst)
: inst{inst}, tinfo(all_type_info(Py_TYPE(inst))) {}

explicit values_and_holders(PyObject *obj)
: inst{nullptr}, tinfo(all_type_info(Py_TYPE(obj))) {
if (!tinfo.empty()) {
inst = reinterpret_cast<instance *>(obj);
}
}

struct iterator {
private:
instance *inst = nullptr;
const type_vec *types = nullptr;
value_and_holder curr;
friend struct values_and_holders;
iterator(instance *inst, const type_vec *tinfo)
: inst{inst}, types{tinfo},
curr(inst /* instance */,
types->empty() ? nullptr : (*types)[0] /* type info */,
0, /* vpos: (non-simple types only): the first vptr comes first */
0 /* index */) {}
iterator(instance *inst, const type_vec *tinfo) : inst{inst}, types{tinfo} {
if (inst != nullptr) {
assert(!types->empty());
curr = value_and_holder(
inst /* instance */,
(*types)[0] /* type info */,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks dangerous. No initializer guarantees types is not nullptr.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved because inst check above.

0, /* vpos: (non-simple types only): the first vptr comes first */
0 /* index */);
}
}
// Past-the-end iterator:
explicit iterator(size_t end) : curr(end) {}

Expand Down Expand Up @@ -364,6 +389,16 @@ struct values_and_holders {
}

size_t size() { return tinfo.size(); }

// Band-aid workaround to fix a subtle but serious bug in a minimalistic fashion. See PR #4762.
bool is_redundant_value_and_holder(const value_and_holder &vh) {
for (size_t i = 0; i < vh.index; i++) {
if (PyType_IsSubtype(tinfo[i]->type, tinfo[vh.index]->type) != 0) {
return true;
}
}
return false;
}
};

/**
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ set(PYBIND11_TEST_FILES
test_opaque_types
test_operator_overloading
test_pickling
test_python_multiple_inheritance
test_pytypes
test_sequences_and_iterators
test_smart_ptr
Expand Down
14 changes: 14 additions & 0 deletions tests/test_class.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from unittest import mock

import pytest

import env
Expand Down Expand Up @@ -203,6 +205,18 @@ def __init__(self):
assert msg(exc_info.value) == expected


@pytest.mark.parametrize(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a new feature enabled by this PR, or a bug fixed along with the PR?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a bug fixed along with this PR.

"mock_return_value", [None, (1, 2, 3), m.Pet("Polly", "parrot"), m.Dog("Molly")]
)
def test_mock_new(mock_return_value):
with mock.patch.object(
m.Pet, "__new__", return_value=mock_return_value
) as mock_new:
obj = m.Pet("Noname", "Nospecies")
assert obj is mock_return_value
mock_new.assert_called_once_with(m.Pet, "Noname", "Nospecies")


def test_automatic_upcasting():
assert type(m.return_class_1()).__name__ == "DerivedClass1"
assert type(m.return_class_2()).__name__ == "DerivedClass2"
Expand Down
45 changes: 45 additions & 0 deletions tests/test_python_multiple_inheritance.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#include "pybind11_tests.h"

namespace test_python_multiple_inheritance {

// Copied from:
// https://github.com/google/clif/blob/5718e4d0807fd3b6a8187dde140069120b81ecef/clif/testing/python_multiple_inheritance.h

struct CppBase {
explicit CppBase(int value) : base_value(value) {}
int get_base_value() const { return base_value; }
void reset_base_value(int new_value) { base_value = new_value; }

private:
int base_value;
};

struct CppDrvd : CppBase {
explicit CppDrvd(int value) : CppBase(value), drvd_value(value * 3) {}
int get_drvd_value() const { return drvd_value; }
void reset_drvd_value(int new_value) { drvd_value = new_value; }

int get_base_value_from_drvd() const { return get_base_value(); }
void reset_base_value_from_drvd(int new_value) { reset_base_value(new_value); }

private:
int drvd_value;
};

} // namespace test_python_multiple_inheritance

TEST_SUBMODULE(python_multiple_inheritance, m) {
using namespace test_python_multiple_inheritance;

py::class_<CppBase>(m, "CppBase")
.def(py::init<int>())
.def("get_base_value", &CppBase::get_base_value)
.def("reset_base_value", &CppBase::reset_base_value);

py::class_<CppDrvd, CppBase>(m, "CppDrvd")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By convention, do we require users to list all of the base classes in the template arg list? I thought the usual way is

py::class_, but maybe just doing that Python won't be aware the Py object should have a base class of Py.CppBase?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the convention is to list all of the base classes if we want the base class methods to surface to the Pybind class.

.def(py::init<int>())
.def("get_drvd_value", &CppDrvd::get_drvd_value)
.def("reset_drvd_value", &CppDrvd::reset_drvd_value)
.def("get_base_value_from_drvd", &CppDrvd::get_base_value_from_drvd)
.def("reset_base_value_from_drvd", &CppDrvd::reset_base_value_from_drvd);
}
35 changes: 35 additions & 0 deletions tests/test_python_multiple_inheritance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Adapted from:
# https://github.com/google/clif/blob/5718e4d0807fd3b6a8187dde140069120b81ecef/clif/testing/python/python_multiple_inheritance_test.py

from pybind11_tests import python_multiple_inheritance as m


class PC(m.CppBase):
pass


class PPCC(PC, m.CppDrvd):
pass


def test_PC():
d = PC(11)
assert d.get_base_value() == 11
d.reset_base_value(13)
assert d.get_base_value() == 13


def test_PPCC():
d = PPCC(11)
assert d.get_drvd_value() == 33
d.reset_drvd_value(55)
assert d.get_drvd_value() == 55

assert d.get_base_value() == 11
assert d.get_base_value_from_drvd() == 11
d.reset_base_value(20)
assert d.get_base_value() == 20
assert d.get_base_value_from_drvd() == 20
d.reset_base_value_from_drvd(30)
assert d.get_base_value() == 30
assert d.get_base_value_from_drvd() == 30