diff --git a/newsfragments/2889.added.md b/newsfragments/2889.added.md new file mode 100644 index 00000000..0d4c9265 --- /dev/null +++ b/newsfragments/2889.added.md @@ -0,0 +1 @@ +Added `PyErr::write_unraisable()` to report an unraisable exception to Python. diff --git a/src/err/mod.rs b/src/err/mod.rs index 0f397153..88b03986 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -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. diff --git a/src/impl_/trampoline.rs b/src/impl_/trampoline.rs index c75a68be..c7bea9ab 100644 --- a/src/impl_/trampoline.rs +++ b/src/impl_/trampoline.rs @@ -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(); } diff --git a/tests/test_exceptions.rs b/tests/test_exceptions.rs index 98dab27b..cbbbd636 100644 --- a/tests/test_exceptions.rs +++ b/tests/test_exceptions.rs @@ -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(); + }); +}