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"