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