diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4257ffc3fc8..fe0591f2f4f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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)
@@ -293,7 +293,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
diff --git a/.github/workflows/guide.yml b/.github/workflows/guide.yml
index 980420fe2df..923fedd1f5f 100644
--- a/.github/workflows/guide.yml
+++ b/.github/workflows/guide.yml
@@ -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 "" > gh-pages-build/doc/index.html
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b98789367e3..c74c2a71b7e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/Cargo.toml b/Cargo.toml
index d1bc099375f..1eab39d94ea 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
diff --git a/guide/src/features.md b/guide/src/features.md
index 393b7088f85..86447c9dcd9 100644
--- a/guide/src/features.md
+++ b/guide/src/features.md
@@ -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>
}
```
+
+
diff --git a/src/conversions/anyhow.rs b/src/conversions/anyhow.rs
new file mode 100644
index 00000000000..7dd733d744c
--- /dev/null
+++ b/src/conversions/anyhow.rs
@@ -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> {
+//! let data = std::fs::read(filename)?;
+//! Ok(data)
+//! }
+//!
+//! fn main() {
+//! let error = Python::with_gil(|py| -> PyResult> {
+//! let fun = wrap_pyfunction!(py_open, py)?;
+//! let text = fun.call1(("foo.txt",))?.extract::>()?;
+//! 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`. `anyhow` is a convenient alternative for this.
+//! pub fn decompress(bytes: &[u8]) -> anyhow::Result {
+//! // 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::>()
+//! })?;
+//!
+//! // 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 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);
+ })
+ }
+}
diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs
index 3ae8fb9c1a2..b82f123e5cc 100644
--- a/src/conversions/mod.rs
+++ b/src/conversions/mod.rs
@@ -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;
diff --git a/src/lib.rs b/src/lib.rs
index 722f3fffe5c..83a3ebbbe27 100644
--- a/src/lib.rs
+++ b/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
//! .
//!
+//! [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"
diff --git a/tests/test_anyhow.rs b/tests/test_anyhow.rs
new file mode 100644
index 00000000000..3376890763c
--- /dev/null
+++ b/tests/test_anyhow.rs
@@ -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 {
+ 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 {
+ 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();
+ });
+}