diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 647ef9c032b..4577a288632 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"
- PYO3_BUILD_CONFIG=$(pwd)/config.txt cargo check --all-targets --features "extension-module abi3 macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods"
+ 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"
done
build:
@@ -163,7 +163,7 @@ jobs:
id: settings
shell: bash
run: |
- echo "::set-output name=all_additive_features::macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods"
+ echo "::set-output name=all_additive_features::macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods eyre"
- if: matrix.msrv == 'MSRV'
name: Prepare minimal package versions (MSRV only)
diff --git a/.github/workflows/guide.yml b/.github/workflows/guide.yml
index 9f67ea72d46..980420fe2df 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" -- --cfg docsrs
+ cargo +nightly rustdoc --lib --no-default-features --features="macros num-bigint num-complex hashbrown indexmap serde multiple-pymethods eyre" -- --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 5bd6ad3ad83..b98789367e3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Packaging
- 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
diff --git a/Cargo.toml b/Cargo.toml
index 7a8f4ed5d27..d1bc099375f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,7 +16,8 @@ links = "python"
[dependencies]
cfg-if = { version = "1.0" }
-# must stay at 0.3.x for Rust 1.41 compatibility
+eyre = {version = ">= 0.4, < 0.7" , optional = true}
+# indoc must stay at 0.3.x for Rust 1.41 compatibility
indoc = { version = "0.3.6", optional = true }
inventory = { version = "0.1.4", optional = true }
libc = "0.2.62"
@@ -126,5 +127,5 @@ members = [
[package.metadata.docs.rs]
no-default-features = true
-features = ["macros", "num-bigint", "num-complex", "hashbrown", "serde", "multiple-pymethods", "indexmap"]
+features = ["macros", "num-bigint", "num-complex", "hashbrown", "serde", "multiple-pymethods", "indexmap", "eyre"]
rustdoc-args = ["--cfg", "docsrs"]
diff --git a/Makefile b/Makefile
index e2b5e13e992..fdd116c4b02 100644
--- a/Makefile
+++ b/Makefile
@@ -11,8 +11,8 @@ fmt:
black . --check
clippy:
- cargo clippy --features="num-bigint num-complex hashbrown serde" --tests -- -Dwarnings
- cargo clippy --features="abi3 num-bigint num-complex hashbrown serde" --tests -- -Dwarnings
+ cargo clippy --features="num-bigint num-complex hashbrown serde indexmap eyre " --tests -- -Dwarnings
+ cargo clippy --features="abi3 num-bigint num-complex hashbrown serde indexmap eyre" --tests -- -Dwarnings
for example in examples/*/; do cargo clippy --manifest-path $$example/Cargo.toml -- -Dwarnings || exit 1; done
lint: fmt clippy
diff --git a/guide/src/features.md b/guide/src/features.md
index 4be93e6d550..393b7088f85 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.
+### `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.
+
### `hashbrown`
Adds a dependency on [hashbrown](https://docs.rs/hashbrown) and enables conversions into its [`HashMap`](https://docs.rs/hashbrown/latest/hashbrown/struct.HashMap.html) and [`HashSet`](https://docs.rs/hashbrown/latest/hashbrown/struct.HashSet.html) types.
diff --git a/src/conversions/eyre.rs b/src/conversions/eyre.rs
new file mode 100644
index 00000000000..445045f7c40
--- /dev/null
+++ b/src/conversions/eyre.rs
@@ -0,0 +1,151 @@
+#![cfg(feature = "eyre")]
+#![cfg_attr(docsrs, doc(cfg(feature = "eyre")))]
+//! A conversion from [eyre]’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.
+//!
+//! 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.
+//! eyre = "*"
+// 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 = [\"eyre\"] }")))]
+#![cfg_attr(not(docsrs), doc = "pyo3 = { 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 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) -> eyre::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 `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`. `eyre` is a convenient alternative for this.
+//! pub fn decompress(bytes: &[u8]) -> eyre::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() -> 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 for PyErr {
+ fn from(error: Report) -> Self {
+ PyRuntimeError::new_err(format!("{:?}", error))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use pyo3::prelude::*;
+ use pyo3::types::IntoPyDict;
+
+ use eyre::{bail, 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);
+ 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 78825a06e33..3ae8fb9c1a2 100644
--- a/src/conversions/mod.rs
+++ b/src/conversions/mod.rs
@@ -1,6 +1,7 @@
//! This module contains conversions between various Rust object and their representation in Python.
mod array;
+pub mod eyre;
pub mod hashbrown;
pub mod indexmap;
pub mod num_bigint;
diff --git a/src/lib.rs b/src/lib.rs
index 0db782174f8..c6ff81ae713 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -76,7 +76,7 @@
//! crate, which is not supported on all platforms.
//!
//! The following features enable interactions with other crates in the Rust ecosystem:
-//
+//! - [`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`].
@@ -254,6 +254,9 @@
//! [`Complex`]: https://docs.rs/num-complex/latest/num_complex/struct.Complex.html
//! [`Deserialize`]: https://docs.rs/serde/latest/serde/trait.Deserialize.html
//! [`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>
//! [`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"