From ef070f7750e72c43963d0aaa36ed305be83caa00 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Mon, 10 Jan 2022 21:18:00 -0500 Subject: [PATCH] Add additional info to TypeError when C++->Python casting fails (#3605) * Add additional info to TypeInfo when C++->Python casting fails * Fix typo * Address reviewer comments --- include/pybind11/pybind11.h | 7 +++++++ tests/test_operator_overloading.cpp | 25 ++++++++++++++++++++++--- tests/test_operator_overloading.py | 13 +++++++++++-- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 8c7545ba3..ec60429b1 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -996,6 +996,13 @@ protected: "Python type! The signature was\n\t"; msg += it->signature; append_note_if_missing_header_is_suspected(msg); +#if PY_VERSION_HEX >= 0x03030000 + // Attach additional error info to the exception if supported + if (PyErr_Occurred()) { + raise_from(PyExc_TypeError, msg.c_str()); + return nullptr; + } +#endif PyErr_SetString(PyExc_TypeError, msg.c_str()); return nullptr; } diff --git a/tests/test_operator_overloading.cpp b/tests/test_operator_overloading.cpp index 0b6c496cf..ffa059d5b 100644 --- a/tests/test_operator_overloading.cpp +++ b/tests/test_operator_overloading.cpp @@ -7,10 +7,11 @@ BSD-style license that can be found in the LICENSE file. */ -#include "pybind11_tests.h" #include "constructor_stats.h" -#include +#include "pybind11_tests.h" #include +#include +#include class Vector2 { public: @@ -71,6 +72,12 @@ int operator+(const C2 &, const C2 &) { return 22; } int operator+(const C2 &, const C1 &) { return 21; } int operator+(const C1 &, const C2 &) { return 12; } +struct HashMe { + std::string member; +}; + +bool operator==(const HashMe &lhs, const HashMe &rhs) { return lhs.member == rhs.member; } + // Note: Specializing explicit within `namespace std { ... }` is done due to a // bug in GCC<7. If you are supporting compilers later than this, consider // specializing `using template<> struct std::hash<...>` in the global @@ -82,6 +89,14 @@ namespace std { // Not a good hash function, but easy to test size_t operator()(const Vector2 &) { return 4; } }; + + // HashMe has a hash function in C++ but no `__hash__` for Python. + template <> + struct hash { + std::size_t operator()(const HashMe &selector) const { + return std::hash()(selector.member); + } + }; } // namespace std // Not a good abs function, but easy to test. @@ -228,8 +243,12 @@ TEST_SUBMODULE(operators, m) { .def("__hash__", &Hashable::hash) .def(py::init()) .def(py::self == py::self); -} + // define __eq__ but not __hash__ + py::class_(m, "HashMe").def(py::self == py::self); + + m.def("get_unhashable_HashMe_set", []() { return std::unordered_set{{"one"}}; }); +} #if !defined(_MSC_VER) && !defined(__INTEL_COMPILER) #pragma GCC diagnostic pop #endif diff --git a/tests/test_operator_overloading.py b/tests/test_operator_overloading.py index b7137d159..8cf375b6d 100644 --- a/tests/test_operator_overloading.py +++ b/tests/test_operator_overloading.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import pytest +import env from pybind11_tests import ConstructorStats from pybind11_tests import operators as m @@ -135,8 +136,9 @@ def test_overriding_eq_reset_hash(): assert m.Comparable(15) is not m.Comparable(15) assert m.Comparable(15) == m.Comparable(15) - with pytest.raises(TypeError): - hash(m.Comparable(15)) # TypeError: unhashable type: 'm.Comparable' + with pytest.raises(TypeError) as excinfo: + hash(m.Comparable(15)) + assert str(excinfo.value).startswith("unhashable type:") for hashable in (m.Hashable, m.Hashable2): assert hashable(15) is not hashable(15) @@ -144,3 +146,10 @@ def test_overriding_eq_reset_hash(): assert hash(hashable(15)) == 15 assert hash(hashable(15)) == hash(hashable(15)) + + +def test_return_set_of_unhashable(): + with pytest.raises(TypeError) as excinfo: + m.get_unhashable_HashMe_set() + if not env.PY2: + assert str(excinfo.value.__cause__).startswith("unhashable type:")