mirror of
https://github.com/pybind/pybind11.git
synced 2024-11-25 14:45: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
|
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
|
||||||
|
@ -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***
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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") {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user