Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add anyhow-integration feature which implements From<anyhow::Error> for PyErr #1822

Merged
merged 10 commits into from Oct 17, 2021
Merged
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
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 @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/guide.yml
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
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add `PyAny::py` as a convenience for `PyNativeType::py`. [#1751](https://github.com/PyO3/pyo3/pull/1751)
- Add implementation of `std::ops::Index<usize>` for `PyList`, `PyTuple` and `PySequence`. [#1825](https://github.com/PyO3/pyo3/pull/1825)
- Add range indexing implementations of `std::ops::Index` for `PyList`, `PyTuple` and `PySequence`. [#1829](https://github.com/PyO3/pyo3/pull/1829)
- Add `anyhow` feature which provides `impl From<anyhow::Error> for PyErr`. [#1822](https://github.com/PyO3/pyo3/pull/1822)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put this in the Packaging section too and make sure this and the eyre entry are written the same way?

- Add `PyMapping` type to represent the Python mapping protocol. [#1844](https://github.com/PyO3/pyo3/pull/1844)
- Add commonly-used sequence methods to `PyList` and `PyTuple`. [#1849](https://github.com/PyO3/pyo3/pull/1849)
- Add `as_sequence` methods to `PyList` and `PyTuple`. [#1860](https://github.com/PyO3/pyo3/pull/1860)
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
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
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`

This feature makes it possible to return [`anyhow::Result<T>`](https://docs.rs/anyhow/1.0.43/anyhow/type.Result.html) from functions and methods exposed to Python. It does so by adding `impl From<anyhow::Error> for PyErr`. Currently, the conversion simply stringifies the `anyhow::Error` and shoves it into a `PyRuntimeError`. As a consequence, there is no way to convert a `PyErr` back to the original `anyhow::Error`. It is mostly intended as a developer convenience.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly I think it'd be nice for this paragraph and the eyre one to read pretty much identically, given that the features are almost equivalent. I don't mind whether it's this one or the eyre one that changes; depends how much detail you think is appropriate.


### `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
@@ -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
@@ -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
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