Add support for nested C++11 exceptions (#3608)

* Add support for nested C++11 exceptions

* Remove wrong include

* Fix if directive

* Fix missing skipif

* Simplify code and try to work around MSVC bug

* Clarify comment

* Further simplify code

* Remove the last extra throw statement

* Qualify auto

* Fix typo

* Add missing return for consistency

* Fix clang-tidy complaint

* Fix python2 stub

* Make clang-tidy happy

* Fix compile error

* Fix python2 function signature

* Extract C++20 utility and backport

* Cleanup code a bit more

* Improve test case

* Consolidate code and fix signature

* Fix typo
This commit is contained in:
Aaron Gokaslan 2022-01-14 14:22:47 -05:00 committed by GitHub
parent f8d4aa47b6
commit d2ec836712
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 131 additions and 13 deletions

View File

@ -36,6 +36,9 @@
# define PYBIND11_CPP14 # define PYBIND11_CPP14
# if __cplusplus >= 201703L # if __cplusplus >= 201703L
# define PYBIND11_CPP17 # define PYBIND11_CPP17
# if __cplusplus >= 202002L
# define PYBIND11_CPP20
# endif
# endif # endif
# endif # endif
#elif defined(_MSC_VER) && __cplusplus == 199711L #elif defined(_MSC_VER) && __cplusplus == 199711L
@ -45,6 +48,9 @@
# define PYBIND11_CPP14 # define PYBIND11_CPP14
# if _MSVC_LANG > 201402L && _MSC_VER >= 1910 # if _MSVC_LANG > 201402L && _MSC_VER >= 1910
# define PYBIND11_CPP17 # define PYBIND11_CPP17
# if _MSVC_LANG >= 202002L
# define PYBIND11_CPP20
# endif
# endif # endif
# endif # endif
#endif #endif
@ -612,6 +618,18 @@ template <typename T> using remove_cv_t = typename std::remove_cv<T>::type;
template <typename T> using remove_reference_t = typename std::remove_reference<T>::type; template <typename T> using remove_reference_t = typename std::remove_reference<T>::type;
#endif #endif
#if defined(PYBIND11_CPP20)
using std::remove_cvref;
using std::remove_cvref_t;
#else
template <class T>
struct remove_cvref {
using type = remove_cv_t<remove_reference_t<T>>;
};
template <class T>
using remove_cvref_t = typename remove_cvref<T>::type;
#endif
/// Index sequences /// Index sequences
#if defined(PYBIND11_CPP14) #if defined(PYBIND11_CPP14)
using std::index_sequence; using std::index_sequence;

View File

@ -10,6 +10,7 @@
#pragma once #pragma once
#include "../pytypes.h" #include "../pytypes.h"
#include <exception>
/// Tracks the `internals` and `type_info` ABI version independent of the main library version. /// Tracks the `internals` and `type_info` ABI version independent of the main library version.
/// ///
@ -280,21 +281,104 @@ inline internals **&get_internals_pp() {
return internals_pp; return internals_pp;
} }
#if PY_VERSION_HEX >= 0x03030000
// forward decl
inline void translate_exception(std::exception_ptr);
template <class T,
enable_if_t<std::is_same<std::nested_exception, remove_cvref_t<T>>::value, int> = 0>
bool handle_nested_exception(const T &exc, const std::exception_ptr &p) {
std::exception_ptr nested = exc.nested_ptr();
if (nested != nullptr && nested != p) {
translate_exception(nested);
return true;
}
return false;
}
template <class T,
enable_if_t<!std::is_same<std::nested_exception, remove_cvref_t<T>>::value, int> = 0>
bool handle_nested_exception(const T &exc, const std::exception_ptr &p) {
if (auto *nep = dynamic_cast<const std::nested_exception *>(std::addressof(exc))) {
return handle_nested_exception(*nep, p);
}
return false;
}
#else
template <class T>
bool handle_nested_exception(const T &, std::exception_ptr &) {
return false;
}
#endif
inline bool raise_err(PyObject *exc_type, const char *msg) {
#if PY_VERSION_HEX >= 0x03030000
if (PyErr_Occurred()) {
raise_from(exc_type, msg);
return true;
}
#endif
PyErr_SetString(exc_type, msg);
return false;
};
inline void translate_exception(std::exception_ptr p) { inline void translate_exception(std::exception_ptr p) {
if (!p) {
return;
}
try { try {
if (p) std::rethrow_exception(p); std::rethrow_exception(p);
} catch (error_already_set &e) { e.restore(); return; } catch (error_already_set &e) {
} catch (const builtin_exception &e) { e.set_error(); return; handle_nested_exception(e, p);
} catch (const std::bad_alloc &e) { PyErr_SetString(PyExc_MemoryError, e.what()); return; e.restore();
} catch (const std::domain_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return; return;
} catch (const std::invalid_argument &e) { PyErr_SetString(PyExc_ValueError, e.what()); return; } catch (const builtin_exception &e) {
} catch (const std::length_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return; // Could not use template since it's an abstract class.
} catch (const std::out_of_range &e) { PyErr_SetString(PyExc_IndexError, e.what()); return; if (auto *nep = dynamic_cast<const std::nested_exception *>(std::addressof(e))) {
} catch (const std::range_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return; handle_nested_exception(*nep, p);
} catch (const std::overflow_error &e) { PyErr_SetString(PyExc_OverflowError, e.what()); return; }
} catch (const std::exception &e) { PyErr_SetString(PyExc_RuntimeError, e.what()); return; e.set_error();
return;
} catch (const std::bad_alloc &e) {
handle_nested_exception(e, p);
raise_err(PyExc_MemoryError, e.what());
return;
} catch (const std::domain_error &e) {
handle_nested_exception(e, p);
raise_err(PyExc_ValueError, e.what());
return;
} catch (const std::invalid_argument &e) {
handle_nested_exception(e, p);
raise_err(PyExc_ValueError, e.what());
return;
} catch (const std::length_error &e) {
handle_nested_exception(e, p);
raise_err(PyExc_ValueError, e.what());
return;
} catch (const std::out_of_range &e) {
handle_nested_exception(e, p);
raise_err(PyExc_IndexError, e.what());
return;
} catch (const std::range_error &e) {
handle_nested_exception(e, p);
raise_err(PyExc_ValueError, e.what());
return;
} catch (const std::overflow_error &e) {
handle_nested_exception(e, p);
raise_err(PyExc_OverflowError, e.what());
return;
} catch (const std::exception &e) {
handle_nested_exception(e, p);
raise_err(PyExc_RuntimeError, e.what());
return;
} catch (const std::nested_exception &e) {
handle_nested_exception(e, p);
raise_err(PyExc_RuntimeError, "Caught an unknown nested exception!");
return;
} catch (...) { } catch (...) {
PyErr_SetString(PyExc_RuntimeError, "Caught an unknown exception!"); raise_err(PyExc_RuntimeError, "Caught an unknown exception!");
return; return;
} }
} }

View File

@ -11,6 +11,8 @@
#include "local_bindings.h" #include "local_bindings.h"
#include "pybind11_tests.h" #include "pybind11_tests.h"
#include <exception>
#include <stdexcept>
#include <utility> #include <utility>
// A type that should be raised as an exception in Python // A type that should be raised as an exception in Python
@ -105,7 +107,6 @@ struct PythonAlreadySetInDestructor {
py::str s; py::str s;
}; };
TEST_SUBMODULE(exceptions, m) { TEST_SUBMODULE(exceptions, m) {
m.def("throw_std_exception", []() { m.def("throw_std_exception", []() {
throw std::runtime_error("This exception was intentionally thrown."); throw std::runtime_error("This exception was intentionally thrown.");
@ -281,5 +282,12 @@ TEST_SUBMODULE(exceptions, m) {
} }
}); });
m.def("throw_nested_exception", []() {
try {
throw std::runtime_error("Inner Exception");
} catch (const std::runtime_error &) {
std::throw_with_nested(std::runtime_error("Outer Exception"));
}
});
#endif #endif
} }

View File

@ -239,6 +239,14 @@ def test_nested_throws(capture):
assert str(excinfo.value) == "this is a helper-defined translated exception" assert str(excinfo.value) == "this is a helper-defined translated exception"
@pytest.mark.skipif("env.PY2")
def test_throw_nested_exception():
with pytest.raises(RuntimeError) as excinfo:
m.throw_nested_exception()
assert str(excinfo.value) == "Outer Exception"
assert str(excinfo.value.__cause__) == "Inner Exception"
# This can often happen if you wrap a pybind11 class in a Python wrapper # This can often happen if you wrap a pybind11 class in a Python wrapper
def test_invalid_repr(): def test_invalid_repr():
class MyRepr(object): class MyRepr(object):