Add `anyhow-integration` feature which implements From<anyhow::Error> for PyErr (#1822)
* Add 'anyhow' feature which provides simple From<anyhow::Error> for PyErr impl This makes it possible to use anyhow::Result<T> as the return type for methods and functions exposed to Python. The current implementation just stringifies the anyhow::Error before shoving it into a PyRuntimeError. Conversion back to the anyhow::Error is not possible, but it is better than nothing. Signed-off-by: Chris Laplante <chris.laplante@agilent.com> * Document `anyhow` feature in the guide Signed-off-by: Chris Laplante <chris.laplante@agilent.com> * update changelog to document anyhow feature * WIP adding tests * Finish up anyhow feature * Fix formatting * Fix tests * Fix tests * Apply review suggestions Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> Co-authored-by: mejrs <brunokolenbrander@hotmail.com>
This commit is contained in:
parent
6cc53d2afe
commit
3b94f4b70c
|
@ -64,8 +64,8 @@ jobs:
|
|||
# TODO suppress linking using config file rather than extension-module feature
|
||||
PYO3_BUILD_CONFIG=$(pwd)/config.txt cargo check --all-targets --features "extension-module"
|
||||
PYO3_BUILD_CONFIG=$(pwd)/config.txt cargo check --all-targets --features "extension-module abi3"
|
||||
PYO3_BUILD_CONFIG=$(pwd)/config.txt cargo check --all-targets --features "extension-module macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods eyre"
|
||||
PYO3_BUILD_CONFIG=$(pwd)/config.txt cargo check --all-targets --features "extension-module abi3 macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods eyre"
|
||||
PYO3_BUILD_CONFIG=$(pwd)/config.txt cargo check --all-targets --features "extension-module macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods eyre anyhow"
|
||||
PYO3_BUILD_CONFIG=$(pwd)/config.txt cargo check --all-targets --features "extension-module abi3 macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods eyre anyhow"
|
||||
done
|
||||
|
||||
build:
|
||||
|
@ -175,7 +175,7 @@ jobs:
|
|||
id: settings
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::set-output name=all_additive_features::macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods eyre"
|
||||
echo "::set-output name=all_additive_features::macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods eyre anyhow"
|
||||
|
||||
- if: matrix.msrv == 'MSRV'
|
||||
name: Prepare minimal package versions (MSRV only)
|
||||
|
@ -291,7 +291,7 @@ jobs:
|
|||
cargo llvm-cov clean --workspace
|
||||
cargo llvm-cov --package $ALL_PACKAGES --no-report
|
||||
cargo llvm-cov --package $ALL_PACKAGES --no-report --features abi3
|
||||
cargo llvm-cov --package $ALL_PACKAGES --no-report --features macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods
|
||||
cargo llvm-cov --package $ALL_PACKAGES --no-report --features macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods eyre anyhow
|
||||
cargo llvm-cov --package $ALL_PACKAGES --no-run --lcov --output-path coverage.lcov
|
||||
env:
|
||||
ALL_PACKAGES: pyo3 pyo3-build-config pyo3-macros-backend pyo3-macros
|
||||
|
|
|
@ -44,7 +44,7 @@ jobs:
|
|||
# This adds the docs to gh-pages-build/doc
|
||||
- name: Build the doc
|
||||
run: |
|
||||
cargo +nightly rustdoc --lib --no-default-features --features="macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods eyre" -- --cfg docsrs
|
||||
cargo +nightly rustdoc --lib --no-default-features --features="macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods eyre anyhow" -- --cfg docsrs
|
||||
cp -r target/doc gh-pages-build/doc
|
||||
echo "<meta http-equiv=refresh content=0;url=pyo3/index.html>" > gh-pages-build/doc/index.html
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
- Support Python 3.10. [#1889](https://github.com/PyO3/pyo3/pull/1889)
|
||||
- Added optional `eyre` feature to convert `eyre::Report` into `PyErr`. [#1893](https://github.com/PyO3/pyo3/pull/1893)
|
||||
- Added optional `anyhow` feature to convert `anyhow::Error` into `PyErr`. [#1822](https://github.com/PyO3/pyo3/pull/1822)
|
||||
|
||||
### Added
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ unindent = { version = "0.1.4", optional = true }
|
|||
hashbrown = { version = ">= 0.9, < 0.12", optional = true }
|
||||
indexmap = { version = ">= 1.6, < 1.8", optional = true }
|
||||
serde = {version = "1.0", optional = true}
|
||||
anyhow = { version = "1.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_approx_eq = "1.1.0"
|
||||
|
|
|
@ -83,6 +83,10 @@ metadata about a Python interpreter.
|
|||
|
||||
These features enable conversions between Python types and types from other Rust crates, enabling easy access to the rest of the Rust ecosystem.
|
||||
|
||||
### `anyhow`
|
||||
|
||||
Adds a dependency on [anyhow](https://docs.rs/anyhow). Enables a conversion from [anyhow](https://docs.rs/anyhow)’s [`Error`]https://docs.rs/anyhow/latest/anyhow/struct.Error.html) type to [`PyErr`](https://docs.rs/pyo3/latest/pyo3/struct.PyErr.html), for easy error handling.
|
||||
|
||||
### `eyre`
|
||||
|
||||
Adds a dependency on [eyre](https://docs.rs/eyre). Enables a conversion from [eyre](https://docs.rs/eyre)’s [`Report`](https://docs.rs/eyre/latest/eyre/struct.Report.html) type to [`PyErr`](https://docs.rs/pyo3/latest/pyo3/struct.PyErr.html), for easy error handling.
|
||||
|
@ -123,3 +127,5 @@ struct User {
|
|||
permissions: Vec<Py<Permission>>
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
#![cfg(feature = "anyhow")]
|
||||
|
||||
//! A conversion from [anyhow]’s [`Error`][anyhow_error] type to [`PyErr`].
|
||||
//!
|
||||
//! Use of an error handling library like [anyhow] 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.
|
||||
//!
|
||||
//! This implementation always creates a Python [`RuntimeError`]. 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.
|
||||
//! anyhow = "*"
|
||||
// workaround for `extended_key_value_attributes`: https://github.com/rust-lang/rust/issues/82768#issuecomment-803935643
|
||||
#![cfg_attr(docsrs, cfg_attr(docsrs, doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"anyhow\"] }")))]
|
||||
#![cfg_attr(
|
||||
not(docsrs),
|
||||
doc = "pyo3 = { version = \"*\", features = [\"anyhow\"] }"
|
||||
)]
|
||||
//! ```
|
||||
//!
|
||||
//! Note that you must use compatible versions of anyhow and PyO3.
|
||||
//! The required anyhow version may vary based on the version of PyO3.
|
||||
//!
|
||||
//! # Example: Propagating a `PyErr` into [`anyhow::Error`]
|
||||
//!
|
||||
//! ```rust
|
||||
//! use pyo3::prelude::*;
|
||||
//! use pyo3::wrap_pyfunction;
|
||||
//! 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) -> anyhow::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 `anyhow` in general
|
||||
//!
|
||||
//! Note that you don't need this feature to convert a [`PyErr`] into an [`anyhow::Error`], 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>`. `anyhow` is a convenient alternative for this.
|
||||
//! pub fn decompress(bytes: &[u8]) -> anyhow::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() -> anyhow::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(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! [anyhow]: https://docs.rs/anyhow/ "A trait object based error system for easy idiomatic error handling in Rust applications."
|
||||
//! [anyhow_error]: https://docs.rs/anyhow/latest/anyhow/struct.Error.html "Anyhows `Error` type, a wrapper around a dynamic error type"
|
||||
//! [`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;
|
||||
|
||||
impl From<anyhow::Error> for PyErr {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
PyRuntimeError::new_err(format!("{:?}", err))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_anyhow {
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::IntoPyDict;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
|
||||
fn f() -> Result<()> {
|
||||
use std::io;
|
||||
bail!(io::Error::new(io::ErrorKind::PermissionDenied, "oh no!"));
|
||||
}
|
||||
|
||||
fn g() -> Result<()> {
|
||||
f().context("f failed")
|
||||
}
|
||||
|
||||
fn h() -> Result<()> {
|
||||
g().context("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);
|
||||
let pyerr = py.run("raise err", None, Some(locals)).unwrap_err();
|
||||
assert_eq!(pyerr.pvalue(py).to_string(), expected_contents);
|
||||
})
|
||||
}
|
||||
|
||||
fn k() -> Result<()> {
|
||||
Err(anyhow!("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);
|
||||
let pyerr = py.run("raise err", None, Some(locals)).unwrap_err();
|
||||
assert_eq!(pyerr.pvalue(py).to_string(), expected_contents);
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
//! This module contains conversions between various Rust object and their representation in Python.
|
||||
|
||||
pub mod anyhow;
|
||||
mod array;
|
||||
pub mod eyre;
|
||||
pub mod hashbrown;
|
||||
|
|
18
src/lib.rs
18
src/lib.rs
|
@ -79,10 +79,11 @@
|
|||
//! crate, which is not supported on all platforms.
|
||||
//!
|
||||
//! The following features enable interactions with other crates in the Rust ecosystem:
|
||||
//! - [`anyhow`]: Enables a conversion from [anyhow]’s [`Error`][anyhow_error] type to [`PyErr`].
|
||||
//! - [`eyre`]: Enables a conversion from [eyre]’s [`Report`] type to [`PyErr`].
|
||||
//! - [`hashbrown`]: Enables conversions between Python objects and [hashbrown]'s [`HashMap`] and
|
||||
//! [`HashSet`] types.
|
||||
//! - [`indexmap`]: Enables conversions between Python dictionary and [indexmap]'s [`IndexMap`].
|
||||
//! - [`indexmap`][indexmap_feature]: Enables conversions between Python dictionary and [indexmap]'s [`IndexMap`].
|
||||
//! - [`num-bigint`]: Enables conversions between Python objects and [num-bigint]'s [`BigInt`] and
|
||||
//! [`BigUint`] types.
|
||||
//! - [`num-complex`]: Enables conversions between Python objects and [num-complex]'s [`Complex`]
|
||||
|
@ -242,6 +243,9 @@
|
|||
//! There are many projects using PyO3 - see a list of some at
|
||||
//! <https://github.com/PyO3/pyo3#examples>.
|
||||
//!
|
||||
//! [anyhow]: https://docs.rs/anyhow/ "A trait object based error system for easy idiomatic error handling in Rust applications."
|
||||
//! [anyhow_error]: https://docs.rs/anyhow/latest/anyhow/struct.Error.html "Anyhows `Error` type, a wrapper around a dynamic error type"
|
||||
//! [`anyhow`]: ./anyhow/index.html "Documentation about the `anyhow` feature."
|
||||
//! [inventory]: https://docs.rs/inventory
|
||||
//! [`HashMap`]: https://docs.rs/hashbrown/latest/hashbrown/struct.HashMap.html
|
||||
//! [`HashSet`]: https://docs.rs/hashbrown/latest/hashbrown/struct.HashSet.html
|
||||
|
@ -253,14 +257,14 @@
|
|||
//! [`Serialize`]: https://docs.rs/serde/latest/serde/trait.Serialize.html
|
||||
//! [eyre]: https://docs.rs/eyre/ "A library for easy idiomatic error handling and reporting in Rust applications."
|
||||
//! [`Report`]: https://docs.rs/eyre/latest/eyre/struct.Report.html
|
||||
//! [`eyre`]: ./eyre/index.html
|
||||
//! [`hashbrown`]: ./hashbrown/index.html
|
||||
//! [`indexmap`]: <./indexmap/index.html>
|
||||
//! [`eyre`]: ./eyre/index.html "Documentation about the `eyre` feature."
|
||||
//! [`hashbrown`]: ./hashbrown/index.html "Documentation about the `hashbrown` feature."
|
||||
//! [indexmap_feature]: ./indexmap/index.html "Documentation about the `indexmap` feature."
|
||||
//! [`maturin`]: https://github.com/PyO3/maturin "Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as python packages"
|
||||
//! [`num-bigint`]: ./num_bigint/index.html
|
||||
//! [`num-complex`]: ./num_complex/index.html
|
||||
//! [`num-bigint`]: ./num_bigint/index.html "Documentation about the `num-bigint` feature."
|
||||
//! [`num-complex`]: ./num_complex/index.html "Documentation about the `num-complex` feature."
|
||||
//! [`pyo3-build-config`]: https://docs.rs/pyo3-build-config
|
||||
//! [`serde`]: <./serde/index.html>
|
||||
//! [`serde`]: <./serde/index.html> "Documentation about the `serde` feature."
|
||||
//! [calling_rust]: https://pyo3.rs/latest/python_from_rust.html "Calling Python from Rust - PyO3 user guide"
|
||||
//! [examples subdirectory]: https://github.com/PyO3/pyo3/tree/main/examples
|
||||
//! [feature flags]: https://doc.rust-lang.org/cargo/reference/features.html "Features - The Cargo Book"
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
#![cfg(feature = "anyhow")]
|
||||
|
||||
#[test]
|
||||
fn test_anyhow_py_function_ok_result() {
|
||||
use pyo3::{py_run, pyfunction, wrap_pyfunction, Python};
|
||||
|
||||
#[pyfunction]
|
||||
fn produce_ok_result() -> anyhow::Result<String> {
|
||||
Ok(String::from("OK buddy"))
|
||||
}
|
||||
|
||||
Python::with_gil(|py| {
|
||||
let func = wrap_pyfunction!(produce_ok_result)(py).unwrap();
|
||||
|
||||
py_run!(
|
||||
py,
|
||||
func,
|
||||
r#"
|
||||
func()
|
||||
"#
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anyhow_py_function_err_result() {
|
||||
use pyo3::{pyfunction, types::PyDict, wrap_pyfunction, Python};
|
||||
|
||||
#[pyfunction]
|
||||
fn produce_err_result() -> anyhow::Result<String> {
|
||||
anyhow::bail!("error time")
|
||||
}
|
||||
|
||||
Python::with_gil(|py| {
|
||||
let func = wrap_pyfunction!(produce_err_result)(py).unwrap();
|
||||
let locals = PyDict::new(py);
|
||||
locals.set_item("func", func).unwrap();
|
||||
|
||||
py.run(
|
||||
r#"
|
||||
func()
|
||||
"#,
|
||||
None,
|
||||
Some(locals),
|
||||
)
|
||||
.unwrap_err();
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue