From d2ec836712ed126b2726fdeee6a6695e777f7bf7 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Fri, 14 Jan 2022 14:22:47 -0500 Subject: [PATCH] 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 --- include/pybind11/detail/common.h | 18 +++++ include/pybind11/detail/internals.h | 108 ++++++++++++++++++++++++---- tests/test_exceptions.cpp | 10 ++- tests/test_exceptions.py | 8 +++ 4 files changed, 131 insertions(+), 13 deletions(-) diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index b08bbc559..b3513da83 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -36,6 +36,9 @@ # define PYBIND11_CPP14 # if __cplusplus >= 201703L # define PYBIND11_CPP17 +# if __cplusplus >= 202002L +# define PYBIND11_CPP20 +# endif # endif # endif #elif defined(_MSC_VER) && __cplusplus == 199711L @@ -45,6 +48,9 @@ # define PYBIND11_CPP14 # if _MSVC_LANG > 201402L && _MSC_VER >= 1910 # define PYBIND11_CPP17 +# if _MSVC_LANG >= 202002L +# define PYBIND11_CPP20 +# endif # endif # endif #endif @@ -612,6 +618,18 @@ template using remove_cv_t = typename std::remove_cv::type; template using remove_reference_t = typename std::remove_reference::type; #endif +#if defined(PYBIND11_CPP20) +using std::remove_cvref; +using std::remove_cvref_t; +#else +template +struct remove_cvref { + using type = remove_cv_t>; +}; +template +using remove_cvref_t = typename remove_cvref::type; +#endif + /// Index sequences #if defined(PYBIND11_CPP14) using std::index_sequence; diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 98d21eb98..462d32474 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -10,6 +10,7 @@ #pragma once #include "../pytypes.h" +#include /// 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; } +#if PY_VERSION_HEX >= 0x03030000 +// forward decl +inline void translate_exception(std::exception_ptr); + +template >::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 >::value, int> = 0> +bool handle_nested_exception(const T &exc, const std::exception_ptr &p) { + if (auto *nep = dynamic_cast(std::addressof(exc))) { + return handle_nested_exception(*nep, p); + } + return false; +} + +#else + +template +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) { + if (!p) { + return; + } try { - if (p) std::rethrow_exception(p); - } catch (error_already_set &e) { e.restore(); return; - } catch (const builtin_exception &e) { e.set_error(); return; - } catch (const std::bad_alloc &e) { PyErr_SetString(PyExc_MemoryError, e.what()); return; - } catch (const std::domain_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return; - } catch (const std::invalid_argument &e) { PyErr_SetString(PyExc_ValueError, e.what()); return; - } catch (const std::length_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return; - } catch (const std::out_of_range &e) { PyErr_SetString(PyExc_IndexError, e.what()); return; - } catch (const std::range_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return; - } 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; + std::rethrow_exception(p); + } catch (error_already_set &e) { + handle_nested_exception(e, p); + e.restore(); + return; + } catch (const builtin_exception &e) { + // Could not use template since it's an abstract class. + if (auto *nep = dynamic_cast(std::addressof(e))) { + handle_nested_exception(*nep, p); + } + 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 (...) { - PyErr_SetString(PyExc_RuntimeError, "Caught an unknown exception!"); + raise_err(PyExc_RuntimeError, "Caught an unknown exception!"); return; } } diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp index 25adb32ed..3aa967382 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -11,6 +11,8 @@ #include "local_bindings.h" #include "pybind11_tests.h" +#include +#include #include // A type that should be raised as an exception in Python @@ -105,7 +107,6 @@ struct PythonAlreadySetInDestructor { py::str s; }; - TEST_SUBMODULE(exceptions, m) { m.def("throw_std_exception", []() { 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 } diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 56201a81c..d698b1312 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -239,6 +239,14 @@ def test_nested_throws(capture): 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 def test_invalid_repr(): class MyRepr(object):