Skip to content

Always display python type information in cast errors #4463

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

Merged
merged 4 commits into from
Feb 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 19 additions & 14 deletions include/pybind11/cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -1017,11 +1017,14 @@ type_caster<T, SFINAE> &load_type(type_caster<T, SFINAE> &conv, const handle &ha
"Internal error: type_caster should only be used for C++ types");
if (!conv.load(handle, true)) {
#if !defined(PYBIND11_DETAILED_ERROR_MESSAGES)
throw cast_error("Unable to cast Python instance to C++ type (#define "
"PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for details)");
throw cast_error(
"Unable to cast Python instance of type "
+ str(type::handle_of(handle)).cast<std::string>()
+ " to C++ type '?' (#define "
"PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for details)");
#else
throw cast_error("Unable to cast Python instance of type "
+ (std::string) str(type::handle_of(handle)) + " to C++ type '"
+ str(type::handle_of(handle)).cast<std::string>() + " to C++ type '"
+ type_id<T>() + "'");
#endif
}
Expand Down Expand Up @@ -1085,12 +1088,13 @@ detail::enable_if_t<!detail::move_never<T>::value, T> move(object &&obj) {
if (obj.ref_count() > 1) {
#if !defined(PYBIND11_DETAILED_ERROR_MESSAGES)
throw cast_error(
"Unable to cast Python instance to C++ rvalue: instance has multiple references"
" (#define PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for details)");
"Unable to cast Python " + str(type::handle_of(obj)).cast<std::string>()
+ " instance to C++ rvalue: instance has multiple references"
" (#define PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for details)");
#else
throw cast_error("Unable to move from Python " + (std::string) str(type::handle_of(obj))
+ " instance to C++ " + type_id<T>()
+ " instance: instance has multiple references");
throw cast_error("Unable to move from Python "
+ str(type::handle_of(obj)).cast<std::string>() + " instance to C++ "
+ type_id<T>() + " instance: instance has multiple references");
#endif
}

Expand Down Expand Up @@ -1195,9 +1199,10 @@ PYBIND11_NAMESPACE_END(detail)
// The overloads could coexist, i.e. the #if is not strictly speaking needed,
// but it is an easy minor optimization.
#if !defined(PYBIND11_DETAILED_ERROR_MESSAGES)
inline cast_error cast_error_unable_to_convert_call_arg() {
return cast_error("Unable to convert call argument to Python object (#define "
"PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for details)");
inline cast_error cast_error_unable_to_convert_call_arg(const std::string &name) {
return cast_error("Unable to convert call argument '" + name
+ "' to Python object (#define "
"PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for details)");
}
#else
inline cast_error cast_error_unable_to_convert_call_arg(const std::string &name,
Expand All @@ -1220,7 +1225,7 @@ tuple make_tuple(Args &&...args_) {
for (size_t i = 0; i < args.size(); i++) {
if (!args[i]) {
#if !defined(PYBIND11_DETAILED_ERROR_MESSAGES)
throw cast_error_unable_to_convert_call_arg();
throw cast_error_unable_to_convert_call_arg(std::to_string(i));
#else
std::array<std::string, size> argtypes{{type_id<Args>()...}};
throw cast_error_unable_to_convert_call_arg(std::to_string(i), argtypes[i]);
Expand Down Expand Up @@ -1510,7 +1515,7 @@ class unpacking_collector {
detail::make_caster<T>::cast(std::forward<T>(x), policy, {}));
if (!o) {
#if !defined(PYBIND11_DETAILED_ERROR_MESSAGES)
throw cast_error_unable_to_convert_call_arg();
throw cast_error_unable_to_convert_call_arg(std::to_string(args_list.size()));
#else
throw cast_error_unable_to_convert_call_arg(std::to_string(args_list.size()),
type_id<T>());
Expand Down Expand Up @@ -1542,7 +1547,7 @@ class unpacking_collector {
}
if (!a.value) {
#if !defined(PYBIND11_DETAILED_ERROR_MESSAGES)
throw cast_error_unable_to_convert_call_arg();
throw cast_error_unable_to_convert_call_arg(a.name);
#else
throw cast_error_unable_to_convert_call_arg(a.name, a.type);
#endif
Expand Down
5 changes: 3 additions & 2 deletions include/pybind11/detail/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -1225,8 +1225,9 @@ constexpr
#endif

// Pybind offers detailed error messages by default for all builts that are debug (through the
// negation of ndebug). This can also be manually enabled by users, for any builds, through
// defining PYBIND11_DETAILED_ERROR_MESSAGES.
// negation of NDEBUG). This can also be manually enabled by users, for any builds, through
// defining PYBIND11_DETAILED_ERROR_MESSAGES. This information is primarily useful for those
// who are writing (as opposed to merely using) libraries that use pybind11.
#if !defined(PYBIND11_DETAILED_ERROR_MESSAGES) && !defined(NDEBUG)
# define PYBIND11_DETAILED_ERROR_MESSAGES
#endif
Expand Down
14 changes: 12 additions & 2 deletions tests/test_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import env # noqa: F401
from pybind11_tests import callbacks as m
from pybind11_tests import detailed_error_messages_enabled


def test_callbacks():
Expand Down Expand Up @@ -70,11 +71,20 @@ def f(*args, **kwargs):

with pytest.raises(RuntimeError) as excinfo:
m.test_arg_conversion_error1(f)
assert "Unable to convert call argument" in str(excinfo.value)
assert str(excinfo.value) == "Unable to convert call argument " + (
"'1' of type 'UnregisteredType' to Python object"
if detailed_error_messages_enabled
else "'1' to Python object (#define PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for details)"
)

with pytest.raises(RuntimeError) as excinfo:
m.test_arg_conversion_error2(f)
assert "Unable to convert call argument" in str(excinfo.value)
assert str(excinfo.value) == "Unable to convert call argument " + (
"'expected_name' of type 'UnregisteredType' to Python object"
if detailed_error_messages_enabled
else "'expected_name' to Python object "
"(#define PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for details)"
)


def test_lambda_closure_cleanup():
Expand Down
5 changes: 5 additions & 0 deletions tests/test_exceptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -339,4 +339,9 @@ TEST_SUBMODULE(exceptions, m) {
}
return py::str("UNEXPECTED");
});

m.def("test_fn_cast_int", [](const py::function &fn) {
// function returns None instead of int, should give a useful error message
fn().cast<int>();
});
}
9 changes: 9 additions & 0 deletions tests/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,3 +381,12 @@ def test_pypy_oserror_normalization():
# https://github.com/pybind/pybind11/issues/4075
what = m.test_pypy_oserror_normalization()
assert "this_filename_must_not_exist" in what


def test_fn_cast_int_exception():
with pytest.raises(RuntimeError) as excinfo:
m.test_fn_cast_int(lambda: None)

assert str(excinfo.value).startswith(
"Unable to cast Python instance of type <class 'NoneType'> to C++ type"
)
2 changes: 1 addition & 1 deletion tests/test_pytypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ def test_print(capture):
assert str(excinfo.value) == "Unable to convert call argument " + (
"'1' of type 'UnregisteredType' to Python object"
if detailed_error_messages_enabled
else "to Python object (#define PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for details)"
else "'1' to Python object (#define PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for details)"
)


Expand Down