Clone of @virtuald's PR #2112 with minor enhancements. (#3215)

* Add py::raise_from to enable chaining exceptions on Python 3.3+

* Use 'raise from' in initialization

* Documenting the exact base version of _PyErr_FormatVFromCause, adding back `assert`s.

Co-authored-by: Dustin Spicuzza <dustin@virtualroadside.com>
This commit is contained in:
Ralf W. Grosse-Kunstleve 2021-08-23 17:30:01 -07:00 committed by GitHub
parent 6cbabc4b8c
commit c8ce4b8df8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 136 additions and 0 deletions

View File

@ -323,6 +323,34 @@ Alternately, to ignore the error, call `PyErr_Clear
Any Python error must be thrown or cleared, or Python/pybind11 will be left in Any Python error must be thrown or cleared, or Python/pybind11 will be left in
an invalid state. an invalid state.
Chaining exceptions ('raise from')
==================================
In Python 3.3 a mechanism for indicating that exceptions were caused by other
exceptions was introduced:
.. code-block:: py
try:
print(1 / 0)
except Exception as exc:
raise RuntimeError("could not divide by zero") from exc
To do a similar thing in pybind11, you can use the ``py::raise_from`` function. It
sets the current python error indicator, so to continue propagating the exception
you should ``throw py::error_already_set()`` (Python 3 only).
.. code-block:: cpp
try {
py::eval("print(1 / 0"));
} catch (py::error_already_set &e) {
py::raise_from(e, PyExc_RuntimeError, "could not divide by zero");
throw py::error_already_set();
}
.. versionadded:: 2.8
.. _unraisable_exceptions: .. _unraisable_exceptions:
Handling unraisable exceptions Handling unraisable exceptions

View File

@ -315,6 +315,19 @@ extern "C" {
} \ } \
} }
#if PY_VERSION_HEX >= 0x03030000
#define PYBIND11_CATCH_INIT_EXCEPTIONS \
catch (pybind11::error_already_set &e) { \
pybind11::raise_from(e, PyExc_ImportError, "initialization failed"); \
return nullptr; \
} catch (const std::exception &e) { \
PyErr_SetString(PyExc_ImportError, e.what()); \
return nullptr; \
} \
#else
#define PYBIND11_CATCH_INIT_EXCEPTIONS \ #define PYBIND11_CATCH_INIT_EXCEPTIONS \
catch (pybind11::error_already_set &e) { \ catch (pybind11::error_already_set &e) { \
PyErr_SetString(PyExc_ImportError, e.what()); \ PyErr_SetString(PyExc_ImportError, e.what()); \
@ -324,6 +337,8 @@ extern "C" {
return nullptr; \ return nullptr; \
} \ } \
#endif
/** \rst /** \rst
***Deprecated in favor of PYBIND11_MODULE*** ***Deprecated in favor of PYBIND11_MODULE***

View File

@ -382,6 +382,47 @@ private:
# pragma warning(pop) # pragma warning(pop)
#endif #endif
#if PY_VERSION_HEX >= 0x03030000
/// Replaces the current Python error indicator with the chosen error, performing a
/// 'raise from' to indicate that the chosen error was caused by the original error.
inline void raise_from(PyObject *type, const char *message) {
// Based on _PyErr_FormatVFromCause:
// https://github.com/python/cpython/blob/467ab194fc6189d9f7310c89937c51abeac56839/Python/errors.c#L405
// See https://github.com/pybind/pybind11/pull/2112 for details.
PyObject *exc = nullptr, *val = nullptr, *val2 = nullptr, *tb = nullptr;
assert(PyErr_Occurred());
PyErr_Fetch(&exc, &val, &tb);
PyErr_NormalizeException(&exc, &val, &tb);
if (tb != nullptr) {
PyException_SetTraceback(val, tb);
Py_DECREF(tb);
}
Py_DECREF(exc);
assert(!PyErr_Occurred());
PyErr_SetString(type, message);
PyErr_Fetch(&exc, &val2, &tb);
PyErr_NormalizeException(&exc, &val2, &tb);
Py_INCREF(val);
PyException_SetCause(val2, val);
PyException_SetContext(val2, val);
PyErr_Restore(exc, val2, tb);
}
/// Sets the current Python error indicator with the chosen error, performing a 'raise from'
/// from the error contained in error_already_set to indicate that the chosen error was
/// caused by the original error. After this function is called error_already_set will
/// no longer contain an error.
inline void raise_from(error_already_set& err, PyObject *type, const char *message) {
err.restore();
raise_from(type, message);
}
#endif
/** \defgroup python_builtins _ /** \defgroup python_builtins _
Unless stated otherwise, the following C++ functions behave the same Unless stated otherwise, the following C++ functions behave the same
as their Python counterparts. as their Python counterparts.

View File

@ -74,8 +74,24 @@ TEST_CASE("Import error handling") {
REQUIRE_NOTHROW(py::module_::import("widget_module")); REQUIRE_NOTHROW(py::module_::import("widget_module"));
REQUIRE_THROWS_WITH(py::module_::import("throw_exception"), REQUIRE_THROWS_WITH(py::module_::import("throw_exception"),
"ImportError: C++ Error"); "ImportError: C++ Error");
#if PY_VERSION_HEX >= 0x03030000
REQUIRE_THROWS_WITH(py::module_::import("throw_error_already_set"),
Catch::Contains("ImportError: initialization failed"));
auto locals = py::dict("is_keyerror"_a=false, "message"_a="not set");
py::exec(R"(
try:
import throw_error_already_set
except ImportError as e:
is_keyerror = type(e.__cause__) == KeyError
message = str(e.__cause__)
)", py::globals(), locals);
REQUIRE(locals["is_keyerror"].cast<bool>() == true);
REQUIRE(locals["message"].cast<std::string>() == "'missing'");
#else
REQUIRE_THROWS_WITH(py::module_::import("throw_error_already_set"), REQUIRE_THROWS_WITH(py::module_::import("throw_error_already_set"),
Catch::Contains("ImportError: KeyError")); Catch::Contains("ImportError: KeyError"));
#endif
} }
TEST_CASE("There can be only one interpreter") { TEST_CASE("There can be only one interpreter") {

View File

@ -262,4 +262,24 @@ TEST_SUBMODULE(exceptions, m) {
m.def("simple_bool_passthrough", [](bool x) {return x;}); m.def("simple_bool_passthrough", [](bool x) {return x;});
m.def("throw_should_be_translated_to_key_error", []() { throw shared_exception(); }); m.def("throw_should_be_translated_to_key_error", []() { throw shared_exception(); });
#if PY_VERSION_HEX >= 0x03030000
m.def("raise_from", []() {
PyErr_SetString(PyExc_ValueError, "inner");
py::raise_from(PyExc_ValueError, "outer");
throw py::error_already_set();
});
m.def("raise_from_already_set", []() {
try {
PyErr_SetString(PyExc_ValueError, "inner");
throw py::error_already_set();
} catch (py::error_already_set& e) {
py::raise_from(e, PyExc_ValueError, "outer");
throw py::error_already_set();
}
});
#endif
} }

View File

@ -24,6 +24,22 @@ def test_error_already_set(msg):
assert msg(excinfo.value) == "foo" assert msg(excinfo.value) == "foo"
@pytest.mark.skipif("env.PY2")
def test_raise_from(msg):
with pytest.raises(ValueError) as excinfo:
m.raise_from()
assert msg(excinfo.value) == "outer"
assert msg(excinfo.value.__cause__) == "inner"
@pytest.mark.skipif("env.PY2")
def test_raise_from_already_set(msg):
with pytest.raises(ValueError) as excinfo:
m.raise_from_already_set()
assert msg(excinfo.value) == "outer"
assert msg(excinfo.value.__cause__) == "inner"
def test_cross_module_exceptions(msg): def test_cross_module_exceptions(msg):
with pytest.raises(RuntimeError) as excinfo: with pytest.raises(RuntimeError) as excinfo:
cm.raise_runtime_error() cm.raise_runtime_error()