Added support for PyErr_WriteUnraisable

This commit is contained in:
Armin Ronacher 2023-01-18 19:49:39 +01:00 committed by David Hewitt
parent d118ee3a73
commit 066880e7d5
4 changed files with 83 additions and 2 deletions

View File

@ -0,0 +1 @@
Added `PyErr::write_unraisable()` to report an unraisable exception to Python.

View File

@ -476,6 +476,40 @@ impl PyErr {
unsafe { ffi::PyErr_Restore(ptype, pvalue, ptraceback) }
}
/// Reports the error as unraisable.
///
/// This calls `sys.unraisablehook()` using the current exception and obj argument.
///
/// This method is useful to report errors in situations where there is no good mechanism
/// to report back to the Python land. In Python this is used to indicate errors in
/// background threads or destructors which are protected. In Rust code this is commonly
/// useful when you are calling into a Python callback which might fail, but there is no
/// obvious way to handle this error other than logging it.
///
/// Calling this method has the benefit that the error goes back into a standardized callback
/// in Python which for instance allows unittests to ensure that no unraisable error
/// actually happend by hooking `sys.unraisablehook`.
///
/// Example:
/// ```rust
/// # use pyo3::prelude::*;
/// # use pyo3::exceptions::PyRuntimeError;
/// # fn failing_function() -> PyResult<()> { Err(PyRuntimeError::new_err("foo")) }
/// # fn main() -> PyResult<()> {
/// Python::with_gil(|py| {
/// match failing_function() {
/// Err(pyerr) => pyerr.write_unraisable(py, None),
/// Ok(..) => { /* do something here */ }
/// }
/// Ok(())
/// })
/// # }
#[inline]
pub fn write_unraisable(self, py: Python<'_>, obj: Option<&PyAny>) {
self.restore(py);
unsafe { ffi::PyErr_WriteUnraisable(obj.map_or(std::ptr::null_mut(), |x| x.as_ptr())) }
}
/// Issues a warning message.
///
/// May return an `Err(PyErr)` if warnings-as-errors is enabled.

View File

@ -247,8 +247,7 @@ where
if let Err(py_err) = panic::catch_unwind(move || body(py))
.unwrap_or_else(|payload| Err(PanicException::from_panic_payload(payload)))
{
py_err.restore(py);
ffi::PyErr_WriteUnraisable(ctx);
py_err.write_unraisable(py, py.from_borrowed_ptr_or_opt(ctx));
}
trap.disarm();
}

View File

@ -96,3 +96,50 @@ fn test_exception_nosegfault() {
assert!(io_err().is_err());
assert!(parse_int().is_err());
}
#[test]
#[cfg(Py_3_8)]
fn test_write_unraisable() {
use pyo3::{exceptions::PyRuntimeError, ffi, AsPyPointer};
#[pyclass]
struct UnraisableCapture {
capture: Option<(PyErr, PyObject)>,
}
#[pymethods]
impl UnraisableCapture {
fn hook(&mut self, unraisable: &PyAny) {
let err = PyErr::from_value(unraisable.getattr("exc_value").unwrap());
let instance = unraisable.getattr("object").unwrap();
self.capture = Some((err, instance.into()));
}
}
Python::with_gil(|py| {
let sys = py.import("sys").unwrap();
let old_hook = sys.getattr("unraisablehook").unwrap();
let capture = Py::new(py, UnraisableCapture { capture: None }).unwrap();
sys.setattr("unraisablehook", capture.getattr(py, "hook").unwrap())
.unwrap();
assert!(capture.borrow(py).capture.is_none());
let err = PyRuntimeError::new_err("foo");
err.write_unraisable(py, None);
let (err, object) = capture.borrow_mut(py).capture.take().unwrap();
assert_eq!(err.to_string(), "RuntimeError: foo");
assert!(object.is_none(py));
let err = PyRuntimeError::new_err("bar");
err.write_unraisable(py, Some(py.NotImplemented().as_ref(py)));
let (err, object) = capture.borrow_mut(py).capture.take().unwrap();
assert_eq!(err.to_string(), "RuntimeError: bar");
assert!(object.as_ptr() == unsafe { ffi::Py_NotImplemented() });
sys.setattr("unraisablehook", old_hook).unwrap();
});
}