Skip to content

Commit

Permalink
Merge #2889
Browse files Browse the repository at this point in the history
2889: Added support for PyErr_WriteUnraisable r=davidhewitt a=mitsuhiko

Fixes #2884

Co-authored-by: Armin Ronacher <armin.ronacher@active-4.com>
  • Loading branch information
bors[bot] and mitsuhiko authored Jan 29, 2023
2 parents 3149a80 + 066880e commit cb38ff0
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 2 deletions.
1 change: 1 addition & 0 deletions newsfragments/2889.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added `PyErr::write_unraisable()` to report an unraisable exception to Python.
34 changes: 34 additions & 0 deletions src/err/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 1 addition & 2 deletions src/impl_/trampoline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
47 changes: 47 additions & 0 deletions tests/test_exceptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
}

0 comments on commit cb38ff0

Please sign in to comment.