Skip to main content

pyo3/conversions/
either.rs

1#![cfg(feature = "either")]
2
3//! Conversion to/from
4//! [either](https://docs.rs/either/ "A library for easy idiomatic error handling and reporting in Rust applications")’s
5//! [`Either`] type to a union of two Python types.
6//!
7//! Use of a generic sum type like [either] is common when you want to either accept one of two possible
8//! types as an argument or return one of two possible types from a function, without having to define
9//! a helper type manually yourself.
10//!
11//! # Setup
12//!
13//! To use this feature, add this to your **`Cargo.toml`**:
14//!
15//! ```toml
16//! [dependencies]
17//! ## change * to the version you want to use, ideally the latest.
18//! either = "*"
19#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"),  "\", features = [\"either\"] }")]
20//! ```
21//!
22//! Note that you must use compatible versions of either and PyO3.
23//! The required either version may vary based on the version of PyO3.
24//!
25//! # Example: Convert a `int | str` to `Either<i32, String>`.
26//!
27//! ```rust
28//! use either::Either;
29//! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods};
30//!
31//! fn main() -> PyResult<()> {
32//!     Python::initialize();
33//!     Python::attach(|py| {
34//!         // Create a string and an int in Python.
35//!         let py_str = "crab".into_pyobject(py)?;
36//!         let py_int = 42i32.into_pyobject(py)?;
37//!         // Now convert it to an Either<i32, String>.
38//!         let either_str: Either<i32, String> = py_str.extract()?;
39//!         let either_int: Either<i32, String> = py_int.extract()?;
40//!         Ok(())
41//!     })
42//! }
43//! ```
44//!
45//! [either](https://docs.rs/either/ "A library for easy idiomatic error handling and reporting in Rust applications")’s
46
47#[cfg(feature = "experimental-inspect")]
48use crate::inspect::types::TypeInfo;
49#[cfg(feature = "experimental-inspect")]
50use crate::inspect::PyStaticExpr;
51#[cfg(feature = "experimental-inspect")]
52use crate::type_hint_union;
53use crate::{
54    exceptions::PyTypeError, Borrowed, Bound, FromPyObject, IntoPyObject, IntoPyObjectExt, PyAny,
55    PyErr, Python,
56};
57use either::Either;
58
59#[cfg_attr(docsrs, doc(cfg(feature = "either")))]
60impl<'py, L, R> IntoPyObject<'py> for Either<L, R>
61where
62    L: IntoPyObject<'py>,
63    R: IntoPyObject<'py>,
64{
65    type Target = PyAny;
66    type Output = Bound<'py, Self::Target>;
67    type Error = PyErr;
68
69    #[cfg(feature = "experimental-inspect")]
70    const OUTPUT_TYPE: PyStaticExpr = type_hint_union!(L::OUTPUT_TYPE, R::OUTPUT_TYPE);
71
72    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
73        match self {
74            Either::Left(l) => l.into_bound_py_any(py),
75            Either::Right(r) => r.into_bound_py_any(py),
76        }
77    }
78}
79
80#[cfg_attr(docsrs, doc(cfg(feature = "either")))]
81impl<'a, 'py, L, R> IntoPyObject<'py> for &'a Either<L, R>
82where
83    &'a L: IntoPyObject<'py>,
84    &'a R: IntoPyObject<'py>,
85{
86    type Target = PyAny;
87    type Output = Bound<'py, Self::Target>;
88    type Error = PyErr;
89
90    #[cfg(feature = "experimental-inspect")]
91    const OUTPUT_TYPE: PyStaticExpr = type_hint_union!(<&L>::OUTPUT_TYPE, <&R>::OUTPUT_TYPE);
92
93    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
94        match self {
95            Either::Left(l) => l.into_bound_py_any(py),
96            Either::Right(r) => r.into_bound_py_any(py),
97        }
98    }
99}
100
101#[cfg_attr(docsrs, doc(cfg(feature = "either")))]
102impl<'a, 'py, L, R> FromPyObject<'a, 'py> for Either<L, R>
103where
104    L: FromPyObject<'a, 'py>,
105    R: FromPyObject<'a, 'py>,
106{
107    type Error = PyErr;
108
109    #[cfg(feature = "experimental-inspect")]
110    const INPUT_TYPE: PyStaticExpr = type_hint_union!(L::INPUT_TYPE, R::INPUT_TYPE);
111
112    #[inline]
113    fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
114        if let Ok(l) = obj.extract::<L>() {
115            Ok(Either::Left(l))
116        } else if let Ok(r) = obj.extract::<R>() {
117            Ok(Either::Right(r))
118        } else {
119            // TODO: it might be nice to use the `type_input()` name here once `type_input`
120            // is not experimental, rather than the Rust type names.
121            let err_msg = format!(
122                "failed to convert the value to 'Union[{}, {}]'",
123                std::any::type_name::<L>(),
124                std::any::type_name::<R>()
125            );
126            Err(PyTypeError::new_err(err_msg))
127        }
128    }
129
130    #[cfg(feature = "experimental-inspect")]
131    fn type_input() -> TypeInfo {
132        TypeInfo::union_of(&[L::type_input(), R::type_input()])
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use std::borrow::Cow;
139
140    use crate::exceptions::PyTypeError;
141    use crate::{IntoPyObject, Python};
142
143    use crate::types::PyAnyMethods;
144    use either::Either;
145
146    #[test]
147    fn test_either_conversion() {
148        type E = Either<i32, String>;
149        type E1 = Either<i32, f32>;
150        type E2 = Either<f32, i32>;
151
152        Python::attach(|py| {
153            let l = E::Left(42);
154            let obj_l = (&l).into_pyobject(py).unwrap();
155            assert_eq!(obj_l.extract::<i32>().unwrap(), 42);
156            assert_eq!(obj_l.extract::<E>().unwrap(), l);
157
158            let r = E::Right("foo".to_owned());
159            let obj_r = (&r).into_pyobject(py).unwrap();
160            assert_eq!(obj_r.extract::<Cow<'_, str>>().unwrap(), "foo");
161            assert_eq!(obj_r.extract::<E>().unwrap(), r);
162
163            let obj_s = "foo".into_pyobject(py).unwrap();
164            let err = obj_s.extract::<E1>().unwrap_err();
165            assert!(err.is_instance_of::<PyTypeError>(py));
166            assert_eq!(
167                err.to_string(),
168                "TypeError: failed to convert the value to 'Union[i32, f32]'"
169            );
170
171            let obj_i = 42i32.into_pyobject(py).unwrap();
172            assert_eq!(obj_i.extract::<E1>().unwrap(), E1::Left(42));
173            assert_eq!(obj_i.extract::<E2>().unwrap(), E2::Left(42.0));
174
175            let obj_f = 42.0f64.into_pyobject(py).unwrap();
176            assert_eq!(obj_f.extract::<E1>().unwrap(), E1::Right(42.0));
177            assert_eq!(obj_f.extract::<E2>().unwrap(), E2::Left(42.0));
178        });
179    }
180}