Skip to content

Commit 1682b67

Browse files
committed
Simplify error_already_set
`error_already_set` is more complicated than it needs to be, partly because it manages reference counts itself rather than using `py::object`, and partly because it tries to do more exception clearing than is needed. This commit greatly simplifies it, and fixes #927. Using `py::object` instead of `PyObject *` means we can rely on implicit copy/move constructors. The current logic did both a `PyErr_Clear` on deletion *and* a `PyErr_Fetch` on creation. I can't see how the `PyErr_Clear` on deletion is ever useful: the `Fetch` on creation itself clears the error, so the only way doing a `PyErr_Clear` on deletion could do anything if is some *other* exception was raised while the `error_already_set` object was alive--but in that case, clearing some other exception seems wrong. (Code that is worried about an exception handler raising another exception would already catch a second `error_already_set` from exception code). The destructor itself called `clear()`, but `clear()` was a little bit more paranoid that needed: it called `restore()` to restore the currently captured error, but then immediately cleared it, using the `PyErr_Restore` to release the references. That's unnecessary: it's valid for us to release the references manually. This updates the code to simply release the references on the three objects (preserving the gil acquire). `clear()`, however, also had the side effect of clearing the current error, even if the current `error_already_set` didn't have a current error (e.g. because of a previous `restore()` or `clear()` call). I don't really see how clearing the error here can ever actually be useful: the only way the current error could be set is if you called `restore()` (in which case the current stored error-related members have already been released), or if some *other* code raised the error, in which case `clear()` on *this* object is clearing an error for which it shouldn't be responsible. Neither of those seem like intentional or desirable features, and manually requesting deletion of the stored references similarly seems pointless, so I've just made `clear()` an empty method and marked it deprecated. This also fixes a minor potential issue with the destruction: it is technically possible for `value` to be null (though this seems likely to be rare in practice); this updates the check to look at `type` which will always be non-null for a `Fetch`ed exception. This also adds error_already_set round-trip throw tests to the test suite.
1 parent abcf43d commit 1682b67

File tree

6 files changed

+97
-41
lines changed

6 files changed

+97
-41
lines changed

include/pybind11/common.h

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,6 @@ extern "C" {
231231
try { \
232232
return pybind11_init(); \
233233
} catch (pybind11::error_already_set &e) { \
234-
e.clear(); \
235234
PyErr_SetString(PyExc_ImportError, e.what()); \
236235
return nullptr; \
237236
} catch (const std::exception &e) { \
@@ -278,7 +277,6 @@ extern "C" {
278277
pybind11_init_##name(m); \
279278
return m.ptr(); \
280279
} catch (pybind11::error_already_set &e) { \
281-
e.clear(); \
282280
PyErr_SetString(PyExc_ImportError, e.what()); \
283281
return nullptr; \
284282
} catch (const std::exception &e) { \
@@ -353,8 +351,6 @@ inline static constexpr int log2(size_t n, int k = 0) { return (n <= 1) ? k : lo
353351
// Returns the size as a multiple of sizeof(void *), rounded up.
354352
inline static constexpr size_t size_in_ptrs(size_t s) { return 1 + ((s - 1) >> log2(sizeof(void *))); }
355353

356-
inline std::string error_string();
357-
358354
/**
359355
* The space to allocate for simple layout instance holders (see below) in multiple of the size of
360356
* a pointer (e.g. 2 means 16 bytes on 64-bit architectures). The default is the minimum required
@@ -703,36 +699,6 @@ template<typename T> T& get_or_create_shared_data(const std::string& name) {
703699
return *ptr;
704700
}
705701

706-
/// Fetch and hold an error which was already set in Python
707-
class error_already_set : public std::runtime_error {
708-
public:
709-
error_already_set() : std::runtime_error(detail::error_string()) {
710-
PyErr_Fetch(&type, &value, &trace);
711-
}
712-
713-
error_already_set(const error_already_set &) = delete;
714-
715-
error_already_set(error_already_set &&e)
716-
: std::runtime_error(e.what()), type(e.type), value(e.value),
717-
trace(e.trace) { e.type = e.value = e.trace = nullptr; }
718-
719-
inline ~error_already_set(); // implementation in pybind11.h
720-
721-
error_already_set& operator=(const error_already_set &) = delete;
722-
723-
/// Give the error back to Python
724-
void restore() { PyErr_Restore(type, value, trace); type = value = trace = nullptr; }
725-
726-
/// Clear the held Python error state (the C++ `what()` message remains intact)
727-
void clear() { restore(); PyErr_Clear(); }
728-
729-
/// Check if the trapped exception matches a given Python exception class
730-
bool matches(PyObject *ex) const { return PyErr_GivenExceptionMatches(ex, type); }
731-
732-
private:
733-
PyObject *type, *value, *trace;
734-
};
735-
736702
/// C++ bindings of builtin Python exceptions
737703
class builtin_exception : public std::runtime_error {
738704
public:

include/pybind11/embed.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
pybind11_init_##name(m); \
5252
return m.ptr(); \
5353
} catch (pybind11::error_already_set &e) { \
54-
e.clear(); \
5554
PyErr_SetString(PyExc_ImportError, e.what()); \
5655
return nullptr; \
5756
} catch (const std::exception &e) { \

include/pybind11/pybind11.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1758,9 +1758,11 @@ class gil_scoped_release { };
17581758
#endif
17591759

17601760
error_already_set::~error_already_set() {
1761-
if (value) {
1761+
if (type) {
17621762
gil_scoped_acquire gil;
1763-
clear();
1763+
type.release().dec_ref();
1764+
value.release().dec_ref();
1765+
trace.release().dec_ref();
17641766
}
17651767
}
17661768

include/pybind11/pytypes.h

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,42 @@ template <typename T> T reinterpret_borrow(handle h) { return {h, object::borrow
279279
\endrst */
280280
template <typename T> T reinterpret_steal(handle h) { return {h, object::stolen_t{}}; }
281281

282+
NAMESPACE_BEGIN(detail)
283+
inline std::string error_string();
284+
NAMESPACE_END(detail)
285+
286+
/// Fetch and hold an error which was already set in Python. An instance of this is typically
287+
/// thrown to propagate python-side errors back through C++ which can either be caught manually or
288+
/// else falls back to the function dispatcher (which then raises the captured error back to
289+
/// python).
290+
class error_already_set : public std::runtime_error {
291+
public:
292+
/// Constructs a new exception from the current Python error indicator, if any. The current
293+
/// Python error indicator will be cleared.
294+
error_already_set() : std::runtime_error(detail::error_string()) {
295+
PyErr_Fetch(&type.ptr(), &value.ptr(), &trace.ptr());
296+
}
297+
298+
inline ~error_already_set();
299+
300+
/// Give the currently-held error back to Python, if any. If there is currently a Python error
301+
/// already set it is cleared first. After this call, the current object no longer stores the
302+
/// error variables (but the `.what()` string is still available).
303+
void restore() { PyErr_Restore(type.release().ptr(), value.release().ptr(), trace.release().ptr()); }
304+
305+
// Does nothing; provided for backwards compatibility.
306+
PYBIND11_DEPRECATED("Use of error_already_set.clear() is deprecated")
307+
void clear() {}
308+
309+
/// Check if the currently trapped error type matches the given Python exception class (or a
310+
/// subclass thereof). May also be passed a tuple to search for any exception class matches in
311+
/// the given tuple.
312+
bool matches(handle ex) const { return PyErr_GivenExceptionMatches(ex.ptr(), type.ptr()); }
313+
314+
private:
315+
object type, value, trace;
316+
};
317+
282318
/** \defgroup python_builtins _
283319
Unless stated otherwise, the following C++ functions behave the same
284320
as their Python counterparts.

tests/test_exceptions.cpp

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,7 @@ TEST_SUBMODULE(exceptions, m) {
120120
py::dict foo;
121121
try { foo["bar"]; }
122122
catch (py::error_already_set& ex) {
123-
if (ex.matches(PyExc_KeyError))
124-
ex.clear();
125-
else
126-
throw;
123+
if (!ex.matches(PyExc_KeyError)) throw;
127124
}
128125
});
129126

@@ -156,4 +153,16 @@ TEST_SUBMODULE(exceptions, m) {
156153
}
157154
return false;
158155
});
156+
157+
// test_nested_throws
158+
m.def("try_catch", [m](py::object exc_type, py::function f, py::args args) {
159+
try { f(*args); }
160+
catch (py::error_already_set &ex) {
161+
if (ex.matches(exc_type))
162+
py::print(ex.what());
163+
else
164+
throw;
165+
}
166+
});
167+
159168
}

tests/test_exceptions.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,47 @@ def test_custom(msg):
7676
except m.MyException5_1:
7777
raise RuntimeError("Exception error: caught child from parent")
7878
assert msg(excinfo.value) == "this is a helper-defined translated exception"
79+
80+
81+
def test_nested_throws(capture):
82+
"""Tests nested (e.g. C++ -> Python -> C++) exception handling"""
83+
84+
def throw_myex():
85+
raise m.MyException("nested error")
86+
87+
def throw_myex5():
88+
raise m.MyException5("nested error 5")
89+
90+
# In the comments below, the exception is caught in the first step, thrown in the last step
91+
92+
# C++ -> Python
93+
with capture:
94+
m.try_catch(m.MyException5, throw_myex5)
95+
assert str(capture).startswith("MyException5: nested error 5")
96+
97+
# Python -> C++ -> Python
98+
with pytest.raises(m.MyException) as excinfo:
99+
m.try_catch(m.MyException5, throw_myex)
100+
assert str(excinfo.value) == "nested error"
101+
102+
def pycatch(exctype, f, *args):
103+
try:
104+
f(*args)
105+
except m.MyException as e:
106+
print(e)
107+
108+
# C++ -> Python -> C++ -> Python
109+
with capture:
110+
m.try_catch(
111+
m.MyException5, pycatch, m.MyException, m.try_catch, m.MyException, throw_myex5)
112+
assert str(capture).startswith("MyException5: nested error 5")
113+
114+
# C++ -> Python -> C++
115+
with capture:
116+
m.try_catch(m.MyException, pycatch, m.MyException5, m.throws4)
117+
assert capture == "this error is rethrown"
118+
119+
# Python -> C++ -> Python -> C++
120+
with pytest.raises(m.MyException5) as excinfo:
121+
m.try_catch(m.MyException, pycatch, m.MyException, m.throws5)
122+
assert str(excinfo.value) == "this is a helper-defined translated exception"

0 commit comments

Comments
 (0)