Skip to content

Commit 2c549eb

Browse files
authored
Move PyErr_NormalizeException() up a few lines (#3971)
* Add error_already_set_what what tests, asserting the status quo. * Move PyErr_NormalizeException() up a few lines. * @pytest.mark.skipif("env.PYPY") from PR #1895 is required even for this much simpler PR * Move PyException_SetTraceback() with PyErr_NormalizeException() as suggested by @Skylion007 * Insert a std::move() as suggested by @Skylion007
1 parent 2d4a20c commit 2c549eb

File tree

3 files changed

+63
-6
lines changed

3 files changed

+63
-6
lines changed

include/pybind11/detail/type_caster_base.h

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,11 @@ PYBIND11_NOINLINE std::string error_string() {
478478

479479
error_scope scope; // Preserve error state
480480

481+
PyErr_NormalizeException(&scope.type, &scope.value, &scope.trace);
482+
if (scope.trace != nullptr) {
483+
PyException_SetTraceback(scope.value, scope.trace);
484+
}
485+
481486
std::string errorString;
482487
if (scope.type) {
483488
errorString += handle(scope.type).attr("__name__").cast<std::string>();
@@ -487,12 +492,6 @@ PYBIND11_NOINLINE std::string error_string() {
487492
errorString += (std::string) str(scope.value);
488493
}
489494

490-
PyErr_NormalizeException(&scope.type, &scope.value, &scope.trace);
491-
492-
if (scope.trace != nullptr) {
493-
PyException_SetTraceback(scope.value, scope.trace);
494-
}
495-
496495
#if !defined(PYPY_VERSION)
497496
if (scope.trace) {
498497
auto *trace = (PyTracebackObject *) scope.trace;

tests/test_exceptions.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,4 +299,12 @@ TEST_SUBMODULE(exceptions, m) {
299299
std::throw_with_nested(std::runtime_error("Outer Exception"));
300300
}
301301
});
302+
303+
m.def("error_already_set_what", [](const py::object &exc_type, const py::object &exc_value) {
304+
PyErr_SetObject(exc_type.ptr(), exc_value.ptr());
305+
std::string what = py::error_already_set().what();
306+
bool py_err_set_after_what = (PyErr_Occurred() != nullptr);
307+
PyErr_Clear();
308+
return py::make_tuple(std::move(what), py_err_set_after_what);
309+
});
302310
}

tests/test_exceptions.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,53 @@ def test_local_translator(msg):
270270
m.throws_local_simple_error()
271271
assert not isinstance(excinfo.value, cm.LocalSimpleException)
272272
assert msg(excinfo.value) == "this mod"
273+
274+
275+
class FlakyException(Exception):
276+
def __init__(self, failure_point):
277+
if failure_point == "failure_point_init":
278+
raise ValueError("triggered_failure_point_init")
279+
self.failure_point = failure_point
280+
281+
def __str__(self):
282+
if self.failure_point == "failure_point_str":
283+
raise ValueError("triggered_failure_point_str")
284+
return "FlakyException.__str__"
285+
286+
287+
@pytest.mark.parametrize(
288+
"exc_type, exc_value, expected_what",
289+
(
290+
(ValueError, "plain_str", "ValueError: plain_str"),
291+
(ValueError, ("tuple_elem",), "ValueError: tuple_elem"),
292+
(FlakyException, ("happy",), "FlakyException: FlakyException.__str__"),
293+
),
294+
)
295+
def test_error_already_set_what_with_happy_exceptions(
296+
exc_type, exc_value, expected_what
297+
):
298+
what, py_err_set_after_what = m.error_already_set_what(exc_type, exc_value)
299+
assert not py_err_set_after_what
300+
assert what == expected_what
301+
302+
303+
@pytest.mark.skipif("env.PYPY", reason="PyErr_NormalizeException Segmentation fault")
304+
def test_flaky_exception_failure_point_init():
305+
what, py_err_set_after_what = m.error_already_set_what(
306+
FlakyException, ("failure_point_init",)
307+
)
308+
assert not py_err_set_after_what
309+
lines = what.splitlines()
310+
# PyErr_NormalizeException replaces the original FlakyException with ValueError:
311+
assert lines[:3] == ["ValueError: triggered_failure_point_init", "", "At:"]
312+
# Checking the first two lines of the traceback as formatted in error_string():
313+
assert "test_exceptions.py(" in lines[3]
314+
assert lines[3].endswith("): __init__")
315+
assert lines[4].endswith("): test_flaky_exception_failure_point_init")
316+
317+
318+
def test_flaky_exception_failure_point_str():
319+
# The error_already_set ctor fails due to a ValueError in error_string():
320+
with pytest.raises(ValueError) as excinfo:
321+
m.error_already_set_what(FlakyException, ("failure_point_str",))
322+
assert str(excinfo.value) == "triggered_failure_point_str"

0 commit comments

Comments
 (0)