mirror of
https://github.com/pybind/pybind11.git
synced 2024-11-25 06:35:12 +00:00
* 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:
parent
6cbabc4b8c
commit
c8ce4b8df8
@ -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
|
||||
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:
|
||||
|
||||
Handling unraisable exceptions
|
||||
|
@ -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 \
|
||||
catch (pybind11::error_already_set &e) { \
|
||||
PyErr_SetString(PyExc_ImportError, e.what()); \
|
||||
@ -324,6 +337,8 @@ extern "C" {
|
||||
return nullptr; \
|
||||
} \
|
||||
|
||||
#endif
|
||||
|
||||
/** \rst
|
||||
***Deprecated in favor of PYBIND11_MODULE***
|
||||
|
||||
|
@ -382,6 +382,47 @@ private:
|
||||
# pragma warning(pop)
|
||||
#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 _
|
||||
Unless stated otherwise, the following C++ functions behave the same
|
||||
as their Python counterparts.
|
||||
|
@ -74,8 +74,24 @@ TEST_CASE("Import error handling") {
|
||||
REQUIRE_NOTHROW(py::module_::import("widget_module"));
|
||||
REQUIRE_THROWS_WITH(py::module_::import("throw_exception"),
|
||||
"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"),
|
||||
Catch::Contains("ImportError: KeyError"));
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_CASE("There can be only one interpreter") {
|
||||
|
@ -262,4 +262,24 @@ TEST_SUBMODULE(exceptions, m) {
|
||||
m.def("simple_bool_passthrough", [](bool x) {return x;});
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -24,6 +24,22 @@ def test_error_already_set(msg):
|
||||
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):
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
cm.raise_runtime_error()
|
||||
|
Loading…
Reference in New Issue
Block a user