Skip to content
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

Added support for PyErr_WriteUnraisable #2889

Merged
merged 1 commit into from
Jan 29, 2023
Merged
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
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.
mitsuhiko marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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]
mitsuhiko marked this conversation as resolved.
Show resolved Hide resolved
#[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();
});
}