Skip to content

Use 'raise from' in initialization #2112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/advanced/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions include/pybind11/detail/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,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()); \
Expand All @@ -285,6 +298,8 @@ extern "C" {
return nullptr; \
} \

#endif

/** \rst
***Deprecated in favor of PYBIND11_MODULE***

Expand Down
36 changes: 36 additions & 0 deletions include/pybind11/pytypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,42 @@ class PYBIND11_EXPORT error_already_set : public std::runtime_error {
# 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) {
// from cpython/errors.c _PyErr_FormatVFromCause
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future reference it would be ideal to have a full link, even if it's long, e.g. (the code looks the same at first glance):

https://github.com/python/cpython/blob/8f010dc920e1f6dc6a357e7cc1460a7a567c05c6/Python/errors.c#L589

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked a bit a the history of errors.c, I think @virtuald's copy is most similar to the oldest version (467ab19 on Oct 21, 2016) that still has any blamed lines today:

https://github.com/python/cpython/blob/467ab194fc6189d9f7310c89937c51abeac56839/Python/errors.c#L405

The diff is:

--- commit_a_467ab194fc6        2021-08-23 16:31:10.128182694 -0700
+++ copy_pr2112 2021-08-23 16:32:52.148562421 -0700
@@ -1,26 +1,20 @@
-static PyObject *
-_PyErr_FormatVFromCause(PyObject *exception, const char *format, va_list vargs)
-{
-    PyObject *exc, *val, *val2, *tb;
-
-    assert(PyErr_Occurred());
+inline void raise_from(PyObject *type, const char *message) {
+    // from cpython/errors.c _PyErr_FormatVFromCause
+    PyObject *exc = nullptr, *val = nullptr, *val2 = nullptr, *tb = nullptr;
     PyErr_Fetch(&exc, &val, &tb);
+
     PyErr_NormalizeException(&exc, &val, &tb);
-    if (tb != NULL) {
+    if (tb != nullptr) {
         PyException_SetTraceback(val, tb);
         Py_DECREF(tb);
     }
     Py_DECREF(exc);
-    assert(!PyErr_Occurred());
-
-    PyErr_FormatV(exception, format, vargs);
 
+    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);
-
-    return NULL;
 }

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For completeness, the 3 commits I looked at are:

467ab194fc6 (Serhiy Storchaka   2016-10-21 17:09:17 +0300  602) static PyObject *
b4bdecd0fc9 (Victor Stinner     2019-05-24 13:44:24 +0200  603) _PyErr_FormatVFromCause(PyThreadState *tstate, PyObject *exception,
b4bdecd0fc9 (Victor Stinner     2019-05-24 13:44:24 +0200  604)                         const char *format, va_list vargs)
467ab194fc6 (Serhiy Storchaka   2016-10-21 17:09:17 +0300  605) {
467ab194fc6 (Serhiy Storchaka   2016-10-21 17:09:17 +0300  606)     PyObject *exc, *val, *val2, *tb;
467ab194fc6 (Serhiy Storchaka   2016-10-21 17:09:17 +0300  607) 
b4bdecd0fc9 (Victor Stinner     2019-05-24 13:44:24 +0200  608)     assert(_PyErr_Occurred(tstate));
b4bdecd0fc9 (Victor Stinner     2019-05-24 13:44:24 +0200  609)     _PyErr_Fetch(tstate, &exc, &val, &tb);
b4bdecd0fc9 (Victor Stinner     2019-05-24 13:44:24 +0200  610)     _PyErr_NormalizeException(tstate, &exc, &val, &tb);
467ab194fc6 (Serhiy Storchaka   2016-10-21 17:09:17 +0300  611)     if (tb != NULL) {
467ab194fc6 (Serhiy Storchaka   2016-10-21 17:09:17 +0300  612)         PyException_SetTraceback(val, tb);
467ab194fc6 (Serhiy Storchaka   2016-10-21 17:09:17 +0300  613)         Py_DECREF(tb);
467ab194fc6 (Serhiy Storchaka   2016-10-21 17:09:17 +0300  614)     }
467ab194fc6 (Serhiy Storchaka   2016-10-21 17:09:17 +0300  615)     Py_DECREF(exc);
b4bdecd0fc9 (Victor Stinner     2019-05-24 13:44:24 +0200  616)     assert(!_PyErr_Occurred(tstate));
467ab194fc6 (Serhiy Storchaka   2016-10-21 17:09:17 +0300  617) 
438a12dd9d8 (Victor Stinner     2019-05-24 17:01:38 +0200  618)     _PyErr_FormatV(tstate, exception, format, vargs);
467ab194fc6 (Serhiy Storchaka   2016-10-21 17:09:17 +0300  619) 
b4bdecd0fc9 (Victor Stinner     2019-05-24 13:44:24 +0200  620)     _PyErr_Fetch(tstate, &exc, &val2, &tb);
b4bdecd0fc9 (Victor Stinner     2019-05-24 13:44:24 +0200  621)     _PyErr_NormalizeException(tstate, &exc, &val2, &tb);
467ab194fc6 (Serhiy Storchaka   2016-10-21 17:09:17 +0300  622)     Py_INCREF(val);
467ab194fc6 (Serhiy Storchaka   2016-10-21 17:09:17 +0300  623)     PyException_SetCause(val2, val);
467ab194fc6 (Serhiy Storchaka   2016-10-21 17:09:17 +0300  624)     PyException_SetContext(val2, val);
b4bdecd0fc9 (Victor Stinner     2019-05-24 13:44:24 +0200  625)     _PyErr_Restore(tstate, exc, val2, tb);
467ab194fc6 (Serhiy Storchaka   2016-10-21 17:09:17 +0300  626) 
467ab194fc6 (Serhiy Storchaka   2016-10-21 17:09:17 +0300  627)     return NULL;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For easy future reference, this is the merged PR #3215 diff relative to the base version:

--- commit_a_467ab194fc6        2021-08-23 16:31:10.128182694 -0700
+++ copy_final  2021-08-23 17:33:04.173103658 -0700
@@ -1,19 +1,20 @@
-static PyObject *
-_PyErr_FormatVFromCause(PyObject *exception, const char *format, va_list vargs)
-{
-    PyObject *exc, *val, *val2, *tb;
+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 != NULL) {
+    if (tb != nullptr) {
         PyException_SetTraceback(val, tb);
         Py_DECREF(tb);
     }
     Py_DECREF(exc);
     assert(!PyErr_Occurred());
 
-    PyErr_FormatV(exception, format, vargs);
+    PyErr_SetString(type, message);
 
     PyErr_Fetch(&exc, &val2, &tb);
     PyErr_NormalizeException(&exc, &val2, &tb);
@@ -21,6 +22,4 @@
     PyException_SetCause(val2, val);
     PyException_SetContext(val2, val);
     PyErr_Restore(exc, val2, tb);
-
-    return NULL;
 }

PyObject *exc = nullptr, *val = nullptr, *val2 = nullptr, *tb = nullptr;
PyErr_Fetch(&exc, &val, &tb);

PyErr_NormalizeException(&exc, &val, &tb);
if (tb != nullptr) {
PyException_SetTraceback(val, tb);
Py_DECREF(tb);
}
Py_DECREF(exc);

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.
Expand Down
16 changes: 16 additions & 0 deletions tests/test_embed/test_interpreter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
20 changes: 20 additions & 0 deletions tests/test_exceptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
16 changes: 16 additions & 0 deletions tests/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,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()
Expand Down