Make TypeErrors more informative when an optional header is missing

E.g. trying to convert a `list` to a `std::vector<int>` without
including <pybind11/stl.h> will now raise an error with a note that
suggests checking the headers.

The note is only appended if `std::` is found in the function
signature. This should only be the case when a header is missing.
E.g. when stl.h is included, the signature would contain `List[int]`
instead of `std::vector<int>` while using stl_bind.h would produce
something like `MyVector`. Similarly for `std::map`/`Dict`, `complex`,
`std::function`/`Callable`, etc.

There's a possibility for false positives, but it's pretty low.
This commit is contained in:
Dean Moldovan 2017-09-09 20:21:34 +02:00
parent c64e6b1670
commit 2b4477eb65
5 changed files with 42 additions and 0 deletions

View File

@ -35,6 +35,10 @@ v2.2.1 (Not yet released)
that's only registered in an external module. that's only registered in an external module.
`#1058 <https://github.com/pybind/pybind11/pull/1058>`_. `#1058 <https://github.com/pybind/pybind11/pull/1058>`_.
* Conversion errors now try to be more informative when it's likely that
a missing header is the cause (e.g. forgetting ``<pybind11/stl.h>``).
`#1077 <https://github.com/pybind/pybind11/pull/1077>`_.
v2.2.0 (August 31, 2017) v2.2.0 (August 31, 2017)
----------------------------------------------------- -----------------------------------------------------

View File

@ -694,6 +694,16 @@ protected:
return nullptr; return nullptr;
} }
auto append_note_if_missing_header_is_suspected = [](std::string &msg) {
if (msg.find("std::") != std::string::npos) {
msg += "\n\n"
"Did you forget to `#include <pybind11/stl.h>`? Or <pybind11/complex.h>,\n"
"<pybind11/functional.h>, <pybind11/chrono.h>, etc. Some automatic\n"
"conversions are optional and require extra headers to be included\n"
"when compiling your pybind11 module.";
}
};
if (result.ptr() == PYBIND11_TRY_NEXT_OVERLOAD) { if (result.ptr() == PYBIND11_TRY_NEXT_OVERLOAD) {
if (overloads->is_operator) if (overloads->is_operator)
return handle(Py_NotImplemented).inc_ref().ptr(); return handle(Py_NotImplemented).inc_ref().ptr();
@ -751,12 +761,14 @@ protected:
} }
} }
append_note_if_missing_header_is_suspected(msg);
PyErr_SetString(PyExc_TypeError, msg.c_str()); PyErr_SetString(PyExc_TypeError, msg.c_str());
return nullptr; return nullptr;
} else if (!result) { } else if (!result) {
std::string msg = "Unable to convert function return value to a " std::string msg = "Unable to convert function return value to a "
"Python type! The signature was\n\t"; "Python type! The signature was\n\t";
msg += it->signature; msg += it->signature;
append_note_if_missing_header_is_suspected(msg);
PyErr_SetString(PyExc_TypeError, msg.c_str()); PyErr_SetString(PyExc_TypeError, msg.c_str());
return nullptr; return nullptr;
} else { } else {

View File

@ -76,6 +76,7 @@ string(REPLACE ".cpp" ".py" PYBIND11_PYTEST_FILES "${PYBIND11_TEST_FILES}")
set(PYBIND11_CROSS_MODULE_TESTS set(PYBIND11_CROSS_MODULE_TESTS
test_exceptions.py test_exceptions.py
test_local_bindings.py test_local_bindings.py
test_stl.py
test_stl_binders.py test_stl_binders.py
) )

View File

@ -114,4 +114,10 @@ PYBIND11_MODULE(pybind11_cross_module_tests, m) {
// the same module (it would be an ODR violation). Therefore `bind_vector` of `bool` // the same module (it would be an ODR violation). Therefore `bind_vector` of `bool`
// is defined here and tested in `test_stl_binders.py`. // is defined here and tested in `test_stl_binders.py`.
py::bind_vector<std::vector<bool>>(m, "VectorBool"); py::bind_vector<std::vector<bool>>(m, "VectorBool");
// test_missing_header_message
// The main module already includes stl.h, but we need to test the error message
// which appears when this header is missing.
m.def("missing_header_arg", [](std::vector<float>) { });
m.def("missing_header_return", []() { return std::vector<float>(); });
} }

View File

@ -179,3 +179,22 @@ def test_stl_pass_by_pointer(msg):
""" # noqa: E501 line too long """ # noqa: E501 line too long
assert m.stl_pass_by_pointer([1, 2, 3]) == [1, 2, 3] assert m.stl_pass_by_pointer([1, 2, 3]) == [1, 2, 3]
def test_missing_header_message():
"""Trying convert `list` to a `std::vector`, or vice versa, without including
<pybind11/stl.h> should result in a helpful suggestion in the error message"""
import pybind11_cross_module_tests as cm
expected_message = ("Did you forget to `#include <pybind11/stl.h>`? Or <pybind11/complex.h>,\n"
"<pybind11/functional.h>, <pybind11/chrono.h>, etc. Some automatic\n"
"conversions are optional and require extra headers to be included\n"
"when compiling your pybind11 module.")
with pytest.raises(TypeError) as excinfo:
cm.missing_header_arg([1.0, 2.0, 3.0])
assert expected_message in str(excinfo.value)
with pytest.raises(TypeError) as excinfo:
cm.missing_header_return()
assert expected_message in str(excinfo.value)