Skip to content

Commit

Permalink
add conversion support for either::Either
Browse files Browse the repository at this point in the history
  • Loading branch information
aldanor committed Sep 16, 2023
1 parent 8f4a26a commit 16be081
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 1 deletion.
4 changes: 3 additions & 1 deletion Cargo.toml
Expand Up @@ -34,6 +34,7 @@ inventory = { version = "0.3.0", optional = true }
# crate integrations that can be added using the eponymous features
anyhow = { version = "1.0", optional = true }
chrono = { version = "0.4", default-features = false, optional = true }
either = { version = "1.9", optional = true }
eyre = { version = ">= 0.4, < 0.7", optional = true }
hashbrown = { version = ">= 0.9, < 0.15", optional = true }
indexmap = { version = ">= 1.6, < 3", optional = true }
Expand Down Expand Up @@ -106,6 +107,7 @@ full = [
"hashbrown",
"serde",
"indexmap",
"either",
"eyre",
"anyhow",
"experimental-inspect",
Expand All @@ -124,5 +126,5 @@ members = [

[package.metadata.docs.rs]
no-default-features = true
features = ["macros", "num-bigint", "num-complex", "hashbrown", "serde", "multiple-pymethods", "indexmap", "eyre", "chrono", "rust_decimal"]
features = ["macros", "num-bigint", "num-complex", "hashbrown", "serde", "multiple-pymethods", "indexmap", "eyre", "either", "chrono", "rust_decimal"]
rustdoc-args = ["--cfg", "docsrs"]
146 changes: 146 additions & 0 deletions src/conversions/either.rs
@@ -0,0 +1,146 @@
#![cfg(feature = "either")]

//! Conversion to/from
//! [either](https://docs.rs/either/ "A library for easy idiomatic error handling and reporting in Rust applications")’s
//! [`Either`] type to a union of two Python types.
//!
//! Use of a generic sum type like [either] is common when you want to either accept one of two possible
//! types as an argument or return one of two possible types from a function, without having to define
//! a helper type manually yourself.
//!
//! # Setup
//!
//! To use this feature, add this to your **`Cargo.toml`**:
//!
//! ```toml
//! [dependencies]
//! ## change * to the version you want to use, ideally the latest.
//! either = "*"
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"either\"] }")]
//! ```
//!
//! Note that you must use compatible versions of either and PyO3.
//! The required eyre version may vary based on the version of PyO3.
//!
//! # Example: Convert a `int | str` to `Either<i32, String>`.
//!
//! ```rust
//! use either::{Either};
//! use pyo3::{Python, ToPyObject};
//!
//! fn main() {
//! pyo3::prepare_freethreaded_python();
//! Python::with_gil(|py| {
//! // Create a string and an int in Python.
//! let py_str = "crab".to_object(py);
//! let py_int = 42.to_object(py);
//! // Now convert it to an Either<i32, String>.
//! let either_str: Either<i32, String> = py_str.extract().unwrap();
//! let either_int: Either<i32, String> = py_int.extract().unwrap();
//! });
//! }
//! ```
//!
//! [either](https://docs.rs/either/ "A library for easy idiomatic error handling and reporting in Rust applications")’s

use crate::{
exceptions::PyTypeError, inspect::types::TypeInfo, FromPyObject, IntoPy, PyAny, PyObject,
PyResult, Python, ToPyObject,
};
use either::Either;

#[cfg_attr(docsrs, doc(cfg(feature = "either")))]
impl<L, R> IntoPy<PyObject> for Either<L, R>
where
L: IntoPy<PyObject>,
R: IntoPy<PyObject>,
{
#[inline]
fn into_py(self, py: Python<'_>) -> PyObject {
match self {
Either::Left(l) => l.into_py(py),
Either::Right(r) => r.into_py(py),
}
}
}

#[cfg_attr(docsrs, doc(cfg(feature = "either")))]
impl<L, R> ToPyObject for Either<L, R>
where
L: ToPyObject,
R: ToPyObject,
{
#[inline]
fn to_object(&self, py: Python<'_>) -> PyObject {
match self {
Either::Left(l) => l.to_object(py),
Either::Right(r) => r.to_object(py),
}
}
}

#[cfg_attr(docsrs, doc(cfg(feature = "either")))]
impl<'source, L, R> FromPyObject<'source> for Either<L, R>
where
L: FromPyObject<'source>,
R: FromPyObject<'source>,
{
#[inline]
fn extract(obj: &'source PyAny) -> PyResult<Self> {
if let Ok(l) = obj.extract::<L>() {
Ok(Either::Left(l))
} else if let Ok(r) = obj.extract::<R>() {
Ok(Either::Right(r))
} else {
let err_msg = format!("failed to convert the value to '{}'", Self::type_input());
Err(PyTypeError::new_err(err_msg))
}
}

fn type_input() -> TypeInfo {
TypeInfo::union_of(&[L::type_input(), R::type_input()])
}
}

#[cfg(test)]
mod tests {
use crate::exceptions::PyTypeError;
use crate::{Python, ToPyObject};

use either::Either;

#[test]
fn test_either_conversion() {
type E = Either<i32, String>;
type E1 = Either<i32, f32>;
type E2 = Either<f32, i32>;

Python::with_gil(|py| {
let l = E::Left(42);
let obj_l = l.to_object(py);
assert_eq!(obj_l.extract::<i32>(py).unwrap(), 42);
assert_eq!(obj_l.extract::<E>(py).unwrap(), l);

let r = E::Right("foo".to_owned());
let obj_r = r.to_object(py);
assert_eq!(obj_r.extract::<&str>(py).unwrap(), "foo");
assert_eq!(obj_r.extract::<E>(py).unwrap(), r);

let obj_s = "foo".to_object(py);
let err = obj_s.extract::<E1>(py).unwrap_err();
assert!(err.is_instance_of::<PyTypeError>(py));
assert_eq!(
err.to_string(),
"TypeError: failed to convert the value to 'Union[int, float]'"
);

let obj_i = 42.to_object(py);
assert_eq!(obj_i.extract::<E1>(py).unwrap(), E1::Left(42));
assert_eq!(obj_i.extract::<E2>(py).unwrap(), E2::Left(42.0));

let obj_f = 42.0.to_object(py);
assert_eq!(obj_f.extract::<E1>(py).unwrap(), E1::Right(42.0));
assert_eq!(obj_f.extract::<E2>(py).unwrap(), E2::Left(42.0));
});
}
}
1 change: 1 addition & 0 deletions src/conversions/mod.rs
Expand Up @@ -2,6 +2,7 @@

pub mod anyhow;
pub mod chrono;
pub mod either;
pub mod eyre;
pub mod hashbrown;
pub mod indexmap;
Expand Down

0 comments on commit 16be081

Please sign in to comment.