pyo3/conversions/
eyre.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
#![cfg(feature = "eyre")]

//! A conversion from
//! [eyre](https://docs.rs/eyre/ "A library for easy idiomatic error handling and reporting in Rust applications.")’s
//! [`Report`] type to [`PyErr`].
//!
//! Use of an error handling library like [eyre] is common in application code and when you just
//! want error handling to be easy. If you are writing a library or you need more control over your
//! errors you might want to design your own error type instead.
//!
//! When the inner error is a [`PyErr`] without source, it will be extracted out.
//! Otherwise a Python [`RuntimeError`] will be created.
//! You might find that you need to map the error from your Rust code into another Python exception.
//! See [`PyErr::new`] for more information about that.
//!
//! For information about error handling in general, see the [Error handling] chapter of the Rust
//! book.
//!
//! # Setup
//!
//! To use this feature, add this to your **`Cargo.toml`**:
//!
//! ```toml
//! [dependencies]
//! ## change * to the version you want to use, ideally the latest.
//! eyre = "*"
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"),  "\", features = [\"eyre\"] }")]
//! ```
//!
//! Note that you must use compatible versions of eyre and PyO3.
//! The required eyre version may vary based on the version of PyO3.
//!
//! # Example: Propagating a `PyErr` into [`eyre::Report`]
//!
//! ```rust
//! use pyo3::prelude::*;
//! use std::path::PathBuf;
//!
//! // A wrapper around a Rust function.
//! // The pyfunction macro performs the conversion to a PyErr
//! #[pyfunction]
//! fn py_open(filename: PathBuf) -> eyre::Result<Vec<u8>> {
//!     let data = std::fs::read(filename)?;
//!     Ok(data)
//! }
//!
//! fn main() {
//!     let error = Python::with_gil(|py| -> PyResult<Vec<u8>> {
//!         let fun = wrap_pyfunction!(py_open, py)?;
//!         let text = fun.call1(("foo.txt",))?.extract::<Vec<u8>>()?;
//!         Ok(text)
//!     }).unwrap_err();
//!
//!     println!("{}", error);
//! }
//! ```
//!
//! # Example: Using `eyre` in general
//!
//! Note that you don't need this feature to convert a [`PyErr`] into an [`eyre::Report`], because
//! it can already convert anything that implements [`Error`](std::error::Error):
//!
//! ```rust
//! use pyo3::prelude::*;
//! use pyo3::types::PyBytes;
//!
//! // An example function that must handle multiple error types.
//! //
//! // To do this you usually need to design your own error type or use
//! // `Box<dyn Error>`. `eyre` is a convenient alternative for this.
//! pub fn decompress(bytes: &[u8]) -> eyre::Result<String> {
//!     // An arbitrary example of a Python api you
//!     // could call inside an application...
//!     // This might return a `PyErr`.
//!     let res = Python::with_gil(|py| {
//!         let zlib = PyModule::import(py, "zlib")?;
//!         let decompress = zlib.getattr("decompress")?;
//!         let bytes = PyBytes::new(py, bytes);
//!         let value = decompress.call1((bytes,))?;
//!         value.extract::<Vec<u8>>()
//!     })?;
//!
//!     // This might be a `FromUtf8Error`.
//!     let text = String::from_utf8(res)?;
//!
//!     Ok(text)
//! }
//!
//! fn main() -> eyre::Result<()> {
//!     let bytes: &[u8] = b"x\x9c\x8b\xcc/U(\xce\xc8/\xcdIQ((\xcaOJL\xca\xa9T\
//!                         (-NU(\xc9HU\xc8\xc9LJ\xcbI,IUH.\x02\x91\x99y\xc5%\
//!                         \xa9\x89)z\x00\xf2\x15\x12\xfe";
//!     let text = decompress(bytes)?;
//!
//!     println!("The text is \"{}\"", text);
//! # assert_eq!(text, "You should probably use the libflate crate instead.");
//!     Ok(())
//! }
//! ```
//!
//! [eyre]: https://docs.rs/eyre/ "A library for easy idiomatic error handling and reporting in Rust applications."
//! [`RuntimeError`]: https://docs.python.org/3/library/exceptions.html#RuntimeError "Built-in Exceptions — Python documentation"
//! [Error handling]: https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html "Recoverable Errors with Result - The Rust Programming Language"

use crate::exceptions::PyRuntimeError;
use crate::PyErr;
use eyre::Report;

/// Converts [`eyre::Report`] to a [`PyErr`] containing a [`PyRuntimeError`].
///
/// If you want to raise a different Python exception you will have to do so manually. See
/// [`PyErr::new`] for more information about that.
impl From<eyre::Report> for PyErr {
    fn from(mut error: Report) -> Self {
        // Errors containing a PyErr without chain or context are returned as the underlying error
        if error.source().is_none() {
            error = match error.downcast::<Self>() {
                Ok(py_err) => return py_err,
                Err(error) => error,
            };
        }
        PyRuntimeError::new_err(format!("{:?}", error))
    }
}

#[cfg(test)]
mod tests {
    use crate::exceptions::{PyRuntimeError, PyValueError};
    use crate::types::IntoPyDict;
    use crate::{ffi, prelude::*};

    use eyre::{bail, eyre, Report, Result, WrapErr};

    fn f() -> Result<()> {
        use std::io;
        bail!(io::Error::new(io::ErrorKind::PermissionDenied, "oh no!"));
    }

    fn g() -> Result<()> {
        f().wrap_err("f failed")
    }

    fn h() -> Result<()> {
        g().wrap_err("g failed")
    }

    #[test]
    fn test_pyo3_exception_contents() {
        let err = h().unwrap_err();
        let expected_contents = format!("{:?}", err);
        let pyerr = PyErr::from(err);

        Python::with_gil(|py| {
            let locals = [("err", pyerr)].into_py_dict(py).unwrap();
            let pyerr = py
                .run(ffi::c_str!("raise err"), None, Some(&locals))
                .unwrap_err();
            assert_eq!(pyerr.value(py).to_string(), expected_contents);
        })
    }

    fn k() -> Result<()> {
        Err(eyre!("Some sort of error"))
    }

    #[test]
    fn test_pyo3_exception_contents2() {
        let err = k().unwrap_err();
        let expected_contents = format!("{:?}", err);
        let pyerr = PyErr::from(err);

        Python::with_gil(|py| {
            let locals = [("err", pyerr)].into_py_dict(py).unwrap();
            let pyerr = py
                .run(ffi::c_str!("raise err"), None, Some(&locals))
                .unwrap_err();
            assert_eq!(pyerr.value(py).to_string(), expected_contents);
        })
    }

    #[test]
    fn test_pyo3_unwrap_simple_err() {
        let origin_exc = PyValueError::new_err("Value Error");
        let report: Report = origin_exc.into();
        let converted: PyErr = report.into();
        assert!(Python::with_gil(
            |py| converted.is_instance_of::<PyValueError>(py)
        ))
    }
    #[test]
    fn test_pyo3_unwrap_complex_err() {
        let origin_exc = PyValueError::new_err("Value Error");
        let mut report: Report = origin_exc.into();
        report = report.wrap_err("Wrapped");
        let converted: PyErr = report.into();
        assert!(Python::with_gil(
            |py| converted.is_instance_of::<PyRuntimeError>(py)
        ))
    }
}