pyo3/conversions/eyre.rs
1#![cfg(feature = "eyre")]
2
3//! A conversion from
4//! [eyre](https://docs.rs/eyre/ "A library for easy idiomatic error handling and reporting in Rust applications.")’s
5//! [`Report`] type to [`PyErr`].
6//!
7//! Use of an error handling library like [eyre] is common in application code and when you just
8//! want error handling to be easy. If you are writing a library or you need more control over your
9//! errors you might want to design your own error type instead.
10//!
11//! When the inner error is a [`PyErr`] without source, it will be extracted out.
12//! Otherwise a Python [`RuntimeError`] will be created.
13//! You might find that you need to map the error from your Rust code into another Python exception.
14//! See [`PyErr::new`] for more information about that.
15//!
16//! For information about error handling in general, see the [Error handling] chapter of the Rust
17//! book.
18//!
19//! # Setup
20//!
21//! To use this feature, add this to your **`Cargo.toml`**:
22//!
23//! ```toml
24//! [dependencies]
25//! ## change * to the version you want to use, ideally the latest.
26//! eyre = "*"
27#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"eyre\"] }")]
28//! ```
29//!
30//! Note that you must use compatible versions of eyre and PyO3.
31//! The required eyre version may vary based on the version of PyO3.
32//!
33//! # Example: Propagating a `PyErr` into [`eyre::Report`]
34//!
35//! ```rust
36//! use pyo3::prelude::*;
37//! use std::path::PathBuf;
38//!
39//! // A wrapper around a Rust function.
40//! // The pyfunction macro performs the conversion to a PyErr
41//! #[pyfunction]
42//! fn py_open(filename: PathBuf) -> eyre::Result<Vec<u8>> {
43//! let data = std::fs::read(filename)?;
44//! Ok(data)
45//! }
46//!
47//! fn main() {
48//! let error = Python::with_gil(|py| -> PyResult<Vec<u8>> {
49//! let fun = wrap_pyfunction!(py_open, py)?;
50//! let text = fun.call1(("foo.txt",))?.extract::<Vec<u8>>()?;
51//! Ok(text)
52//! }).unwrap_err();
53//!
54//! println!("{}", error);
55//! }
56//! ```
57//!
58//! # Example: Using `eyre` in general
59//!
60//! Note that you don't need this feature to convert a [`PyErr`] into an [`eyre::Report`], because
61//! it can already convert anything that implements [`Error`](std::error::Error):
62//!
63//! ```rust
64//! use pyo3::prelude::*;
65//! use pyo3::types::PyBytes;
66//!
67//! // An example function that must handle multiple error types.
68//! //
69//! // To do this you usually need to design your own error type or use
70//! // `Box<dyn Error>`. `eyre` is a convenient alternative for this.
71//! pub fn decompress(bytes: &[u8]) -> eyre::Result<String> {
72//! // An arbitrary example of a Python api you
73//! // could call inside an application...
74//! // This might return a `PyErr`.
75//! let res = Python::with_gil(|py| {
76//! let zlib = PyModule::import(py, "zlib")?;
77//! let decompress = zlib.getattr("decompress")?;
78//! let bytes = PyBytes::new(py, bytes);
79//! let value = decompress.call1((bytes,))?;
80//! value.extract::<Vec<u8>>()
81//! })?;
82//!
83//! // This might be a `FromUtf8Error`.
84//! let text = String::from_utf8(res)?;
85//!
86//! Ok(text)
87//! }
88//!
89//! fn main() -> eyre::Result<()> {
90//! let bytes: &[u8] = b"x\x9c\x8b\xcc/U(\xce\xc8/\xcdIQ((\xcaOJL\xca\xa9T\
91//! (-NU(\xc9HU\xc8\xc9LJ\xcbI,IUH.\x02\x91\x99y\xc5%\
92//! \xa9\x89)z\x00\xf2\x15\x12\xfe";
93//! let text = decompress(bytes)?;
94//!
95//! println!("The text is \"{}\"", text);
96//! # assert_eq!(text, "You should probably use the libflate crate instead.");
97//! Ok(())
98//! }
99//! ```
100//!
101//! [eyre]: https://docs.rs/eyre/ "A library for easy idiomatic error handling and reporting in Rust applications."
102//! [`RuntimeError`]: https://docs.python.org/3/library/exceptions.html#RuntimeError "Built-in Exceptions — Python documentation"
103//! [Error handling]: https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html "Recoverable Errors with Result - The Rust Programming Language"
104
105use crate::exceptions::PyRuntimeError;
106use crate::PyErr;
107use eyre::Report;
108
109/// Converts [`eyre::Report`] to a [`PyErr`] containing a [`PyRuntimeError`].
110///
111/// If you want to raise a different Python exception you will have to do so manually. See
112/// [`PyErr::new`] for more information about that.
113impl From<eyre::Report> for PyErr {
114 fn from(mut error: Report) -> Self {
115 // Errors containing a PyErr without chain or context are returned as the underlying error
116 if error.source().is_none() {
117 error = match error.downcast::<Self>() {
118 Ok(py_err) => return py_err,
119 Err(error) => error,
120 };
121 }
122 PyRuntimeError::new_err(format!("{:?}", error))
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use crate::exceptions::{PyRuntimeError, PyValueError};
129 use crate::types::IntoPyDict;
130 use crate::{ffi, prelude::*};
131
132 use eyre::{bail, eyre, Report, Result, WrapErr};
133
134 fn f() -> Result<()> {
135 use std::io;
136 bail!(io::Error::new(io::ErrorKind::PermissionDenied, "oh no!"));
137 }
138
139 fn g() -> Result<()> {
140 f().wrap_err("f failed")
141 }
142
143 fn h() -> Result<()> {
144 g().wrap_err("g failed")
145 }
146
147 #[test]
148 fn test_pyo3_exception_contents() {
149 let err = h().unwrap_err();
150 let expected_contents = format!("{:?}", err);
151 let pyerr = PyErr::from(err);
152
153 Python::with_gil(|py| {
154 let locals = [("err", pyerr)].into_py_dict(py).unwrap();
155 let pyerr = py
156 .run(ffi::c_str!("raise err"), None, Some(&locals))
157 .unwrap_err();
158 assert_eq!(pyerr.value(py).to_string(), expected_contents);
159 })
160 }
161
162 fn k() -> Result<()> {
163 Err(eyre!("Some sort of error"))
164 }
165
166 #[test]
167 fn test_pyo3_exception_contents2() {
168 let err = k().unwrap_err();
169 let expected_contents = format!("{:?}", err);
170 let pyerr = PyErr::from(err);
171
172 Python::with_gil(|py| {
173 let locals = [("err", pyerr)].into_py_dict(py).unwrap();
174 let pyerr = py
175 .run(ffi::c_str!("raise err"), None, Some(&locals))
176 .unwrap_err();
177 assert_eq!(pyerr.value(py).to_string(), expected_contents);
178 })
179 }
180
181 #[test]
182 fn test_pyo3_unwrap_simple_err() {
183 let origin_exc = PyValueError::new_err("Value Error");
184 let report: Report = origin_exc.into();
185 let converted: PyErr = report.into();
186 assert!(Python::with_gil(
187 |py| converted.is_instance_of::<PyValueError>(py)
188 ))
189 }
190 #[test]
191 fn test_pyo3_unwrap_complex_err() {
192 let origin_exc = PyValueError::new_err("Value Error");
193 let mut report: Report = origin_exc.into();
194 report = report.wrap_err("Wrapped");
195 let converted: PyErr = report.into();
196 assert!(Python::with_gil(
197 |py| converted.is_instance_of::<PyRuntimeError>(py)
198 ))
199 }
200}