Skip to content

Commit

Permalink
Add anyhow-integration feature which implements From<anyhow::Error>…
Browse files Browse the repository at this point in the history
… 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 <[email protected]>

* Document `anyhow` feature in the guide

Signed-off-by: Chris Laplante <[email protected]>

* 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 <[email protected]>
Co-authored-by: mejrs <[email protected]>
  • Loading branch information
3 people authored Oct 17, 2021
1 parent 6cc53d2 commit 3b94f4b
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 12 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/guide.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions guide/src/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -123,3 +127,5 @@ struct User {
permissions: Vec<Py<Permission>>
}
```


167 changes: 167 additions & 0 deletions src/conversions/anyhow.rs
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions src/conversions/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
18 changes: 11 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`]
Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand Down
48 changes: 48 additions & 0 deletions tests/test_anyhow.rs
Original file line number Diff line number Diff line change
@@ -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();
});
}

0 comments on commit 3b94f4b

Please sign in to comment.