diff --git a/Cargo.toml b/Cargo.toml index 3e2a016..19d8213 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,11 +13,12 @@ rust-version = "1.62.1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] default = ["complex", "time"] -complex = ["num-complex", "pyo3/num-complex"] +complex = ["num-complex", "num-traits", "pyo3/num-complex"] extension-module = ["pyo3/extension-module"] [dependencies] -num-complex = { version = "0.4", optional = true } +num-complex = { version = "0.4.0", optional = true } +num-traits = { version = "0.2.15", optional = true } paste = "1.0" pyo3 = { version = "0.17", default-features = false, features = ["macros", "multiple-pymethods"] } time = { version = "0.3", optional = true } diff --git a/Makefile.toml b/Makefile.toml index fe0ea46..8708401 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -85,43 +85,36 @@ args = ["deadlinks", "--check-http", "--check-intra-doc-links"] [tasks.deny] - dependencies = ["merge-deny-config"] install_crate = "cargo-deny" command = "cargo" args = ["deny", "check", "-c", "${CARGO_MAKE_WORKING_DIRECTORY}/deny.toml", "all"] [tasks.deny-advisories] - dependencies = ["merge-deny-config"] install_crate = "cargo-deny" command = "cargo" args = ["deny", "check", "-c", "${CARGO_MAKE_WORKING_DIRECTORY}/deny.toml", "advisories"] [tasks.deny-ban] - dependencies = ["merge-deny-config"] install_crate = "cargo-deny" command = "cargo" args = ["deny", "check", "-c", "${CARGO_MAKE_WORKING_DIRECTORY}/deny.toml", "ban"] [tasks.deny-bans] - dependencies = ["merge-deny-config"] install_crate = "cargo-deny" command = "cargo" args = ["deny", "check", "-c", "${CARGO_MAKE_WORKING_DIRECTORY}/deny.toml", "bans"] [tasks.deny-license] - dependencies = ["merge-deny-config"] install_crate = "cargo-deny" command = "cargo" args = ["deny", "check", "-c", "${CARGO_MAKE_WORKING_DIRECTORY}/deny.toml", "license"] [tasks.deny-licenses] - dependencies = ["merge-deny-config"] install_crate = "cargo-deny" command = "cargo" args = ["deny", "check", "-c", "${CARGO_MAKE_WORKING_DIRECTORY}/deny.toml", "licenses"] [tasks.deny-sources] - dependencies = ["merge-deny-config"] install_crate = "cargo-deny" command = "cargo" args = ["deny", "check", "-c", "${CARGO_MAKE_WORKING_DIRECTORY}/deny.toml", "sources"] diff --git a/examples/yak_shaving.rs b/examples/yak_shaving.rs index 7e5a0c2..fd9552c 100644 --- a/examples/yak_shaving.rs +++ b/examples/yak_shaving.rs @@ -132,16 +132,16 @@ pub mod python { py_wrap_struct! { PyYak(Yak) as "Yak" { py -> rs { - py_dict: PyDict => Yak { - let is_shaved: &PyBool = py_dict.as_mapping().get_item("is_shaved")?.downcast()?; + py_dict: Py => Yak { + let is_shaved: &PyBool = py_dict.as_ref(py).as_mapping().get_item("is_shaved")?.downcast()?; if is_shaved.is_true() { Ok::<_, PyErr>(Yak::new_shaved()) } else { Ok(Yak::new()) } }, - py_bool: PyBool => bool { - bool::py_try_from_ref(py, py_bool) + py_bool: Py => bool { + bool::py_try_from(py, &py_bool) } }, rs -> py { @@ -256,7 +256,7 @@ try: yak5.shave_with(clippers) except CloggedClippersError: pass - + clippers.unclog() yak5.shave_with(clippers) "#; diff --git a/src/lib.rs b/src/lib.rs index 5a983e9..8144f74 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,6 +80,7 @@ mod wrappers; #[cfg(feature = "complex")] pub use num_complex; +pub use paste; pub use py_try_from::PyTryFrom; pub use pyo3; pub use to_python::ToPython; @@ -206,6 +207,7 @@ impl PyWrapperMut for T where T: PyWrapper + AsMut {} macro_rules! create_init_submodule { ( $(classes: [ $($class: ty),+ ],)? + $(consts: [ $($const: ident),+ ],)? $(errors: [ $($error: ty),+ ],)? $(funcs: [ $($func: path),+ ],)? $(submodules: [ $($mod_name: literal: $init_submod: path),+ ],)? @@ -215,6 +217,9 @@ macro_rules! create_init_submodule { m.add_class::<$class>()?; )+)? $($( + m.add(::std::stringify!($const), $crate::ToPython::<$crate::pyo3::Py<$crate::pyo3::PyAny>>::to_python(&$const, _py)?)?; + )+)? + $($( m.add(std::stringify!($error), _py.get_type::<$error>())?; )+)? $($( diff --git a/src/py_try_from.rs b/src/py_try_from.rs index 947c7a5..4eb6b89 100644 --- a/src/py_try_from.rs +++ b/src/py_try_from.rs @@ -16,46 +16,62 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use pyo3::types::PyComplex; +use pyo3::{ + exceptions::PyValueError, + types::{PyDate, PyDateTime, PyDelta, PyTime, PyTzInfo}, +}; use pyo3::{ types::{ PyBool, PyByteArray, PyBytes, PyDict, PyFloat, PyFrozenSet, PyInt, PyList, PySet, PyString, }, - Py, PyAny, PyResult, Python, + FromPyObject, IntoPy, Py, PyAny, PyResult, Python, }; #[cfg(feature = "complex")] use num_complex::Complex; #[cfg(feature = "complex")] -use pyo3::types::PyComplex; +use num_traits::{Float, FloatConst}; +#[cfg(feature = "complex")] +use pyo3::exceptions::PyFloatingPointError; +#[cfg(feature = "complex")] +use std::fmt::Display; #[cfg(feature = "complex")] use std::os::raw::c_double; #[cfg(feature = "time")] use crate::datetime::DateTime; #[cfg(feature = "time")] -use pyo3::{ - exceptions::PyValueError, - types::{PyDate, PyDateTime, PyDelta, PyTime, PyTuple, PyTzInfo}, - ToPyObject, -}; +use pyo3::{types::PyTuple, ToPyObject}; #[cfg(feature = "time")] use time::{Date, Duration, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset}; /// Convert from a Python type to a Rust type. -pub trait PyTryFrom: Sized { +pub trait PyTryFrom

: Sized { /// Convert from a `Py`. Defaults to delegating to `py_from_ref`. /// /// # Errors /// /// Any errors that may occur during conversion. - fn py_try_from(py: Python, item: Py) -> PyResult; + fn py_try_from(py: Python, item: &P) -> PyResult; +} - /// Convert from a reference to the Python data. - /// - /// # Errors - /// - /// Any errors that may occur during conversion. - fn py_try_from_ref(py: Python, item: &T) -> PyResult; +impl

PyTryFrom for Py

+where + Self: for<'a> FromPyObject<'a>, +{ + fn py_try_from(_py: Python, item: &PyAny) -> PyResult { + item.extract() + } +} + +impl PyTryFrom

for Box +where + T: PyTryFrom

, +{ + fn py_try_from(py: Python, item: &P) -> PyResult { + T::py_try_from(py, item).map(Self::new) + } } /// Provides a body for `py_try_from`, delegating to the implementation for the given Python type. @@ -65,40 +81,21 @@ pub trait PyTryFrom: Sized { #[macro_export] macro_rules! private_py_try_from_py_pyany_inner { ($item: ident, $py: ident, $py_type: ty) => {{ - let actual: $crate::pyo3::Py<$py_type> = $item.extract($py)?; + let actual: &$py_type = $item.extract()?; >::py_try_from($py, actual) }}; } -/// Provides a body for `py_try_from`, delegating to the related `py_try_from_ref` implementation. -/// -/// This should be used in other macros and for generic/container types that can't be implemented -/// entirely with a macro. -#[macro_export] -macro_rules! private_py_try_from_py_inner { - ($item: ident, $py: ident, $py_type: ty) => {{ - let item: &$py_type = $item.as_ref($py); - >::py_try_from_ref($py, item) - }}; -} - /// Generate `PyTryFrom` implementations for `PyAny` that require the `PyAny` to contain a specific Python type. #[macro_export] macro_rules! private_impl_py_try_from_pyany { ($py_type: ty => $rs_type: ty) => { impl $crate::PyTryFrom<$crate::pyo3::PyAny> for $rs_type { fn py_try_from( - py: $crate::pyo3::Python, - item: $crate::pyo3::Py<$crate::pyo3::PyAny>, - ) -> $crate::pyo3::PyResult { - $crate::private_py_try_from_py_pyany_inner!(item, py, $py_type) - } - - fn py_try_from_ref( py: $crate::pyo3::Python, item: &$crate::pyo3::PyAny, ) -> $crate::pyo3::PyResult { - let actual: $crate::pyo3::Py<$py_type> = item.extract()?; + let actual: &$py_type = item.extract()?; >::py_try_from(py, actual) } } @@ -110,14 +107,9 @@ macro_rules! private_impl_py_try_from_pyany { #[allow(clippy::module_name_repetitions)] macro_rules! private_impl_py_try_from { (&$item: ident, $py: ident, $py_type: ty => $rs_type: ty $convert: block) => { - impl PyTryFrom<$py_type> for $rs_type { + #[allow(clippy::use_self)] + impl $crate::PyTryFrom<$py_type> for $rs_type { fn py_try_from( - py: $crate::pyo3::Python, - item: $crate::pyo3::Py<$py_type>, - ) -> $crate::pyo3::PyResult { - $crate::private_py_try_from_py_inner!(item, py, $py_type) - } - fn py_try_from_ref( $py: $crate::pyo3::Python, $item: &$py_type, ) -> $crate::pyo3::PyResult { @@ -137,10 +129,42 @@ macro_rules! private_impl_py_try_from_with_pyany { }; } +/// Implements [`PyTryFrom>`] for `T` where `T: PyTryFrom

` and `P` is a native Python type. +macro_rules! impl_try_from_py_native { + ($py_type: ty => $rs_type: ty) => { + private_impl_py_try_from!(&item, py, $crate::pyo3::Py<$py_type> => $rs_type { + let item: &$py_type = item.as_ref(py); + >::py_try_from(py, item) + }); + } +} + +/// Implements [`PyTryFrom`] for a `T` that is a native Python type. +macro_rules! impl_try_from_self_python { + ($py_type: ty) => { + private_impl_py_try_from!(&item, py, $py_type => $crate::pyo3::Py<$py_type> { + Ok(item.into_py(py)) + }); + private_impl_py_try_from!(&item, _py, $crate::pyo3::Py<$py_type> => $crate::pyo3::Py<$py_type> { + Ok(item.clone()) + }); + } +} + +/// Implements [`PyTryFrom`] for a `T` that is a native Rust type. +macro_rules! impl_try_from_self_rust { + ($rs_type: ty) => { + private_impl_py_try_from!(&item, _py, $rs_type => $rs_type { + Ok(item.clone()) + }); + } +} + /// Implements [`PyTryFrom`] for primitive types by just calling `extract()`. macro_rules! impl_try_from_primitive { ($py_type: ty => $rs_type: ty) => { private_impl_py_try_from_with_pyany!(&item, _py, $py_type => $rs_type { item.extract() }); + impl_try_from_py_native!($py_type => $rs_type); }; } @@ -148,36 +172,79 @@ macro_rules! impl_try_from_primitive { // ==== Bool ==== -private_impl_py_try_from_with_pyany!(&item, _py, PyBool => bool { - Ok(item.is_true()) -}); +impl_try_from_primitive!(PyBool => bool); +impl_try_from_self_python!(PyBool); +impl_try_from_self_rust!(bool); // ==== ByteArray ==== +impl_try_from_self_python!(PyByteArray); +impl_try_from_py_native!(PyByteArray => Vec); private_impl_py_try_from!(&item, _py, PyByteArray => Vec { Ok(item.to_vec()) }); +impl_try_from_py_native!(PyByteArray => Box<[u8]>); +private_impl_py_try_from!(&item, py, PyByteArray => Box<[u8]> { + Vec::::py_try_from(py, item).map(From::from) +}); + // ==== Bytes ==== +impl_try_from_self_python!(PyBytes); +impl_try_from_py_native!(PyBytes => Vec); private_impl_py_try_from!(&item, _py, PyBytes => Vec { Ok(item.as_bytes().to_vec()) }); +impl_try_from_py_native!(PyBytes => Box<[u8]>); +private_impl_py_try_from!(&item, py, PyBytes => Box<[u8]> { + Vec::::py_try_from(py, item).map(From::from) +}); + // ==== Complex ==== +impl_try_from_self_python!(PyComplex); + #[cfg(feature = "complex")] -impl PyTryFrom for Complex +impl PyTryFrom for Complex +where + F: Copy + Float + FloatConst + Into + Display, +{ + fn py_try_from(_py: Python, item: &Self) -> PyResult { + Ok(*item) + } +} + +#[cfg(feature = "complex")] +impl PyTryFrom> for Complex where - F: Copy + From, + F: Copy + Float + FloatConst + Into + Display, { - fn py_try_from(py: Python, item: Py) -> PyResult { - private_py_try_from_py_inner!(item, py, PyComplex) + fn py_try_from(py: Python, item: &Py) -> PyResult { + Self::py_try_from(py, item.as_ref(py)) } - fn py_try_from_ref(_py: Python, item: &PyComplex) -> PyResult { +} + +#[cfg(feature = "complex")] +impl PyTryFrom for Complex +where + // `Display` seems like an odd trait to require, but it is used to make a more useful + // error message. The types realistically used for this are `f32` and `f64` both of which + // impl `Display`, so there's no issue there. + F: Copy + Float + FloatConst + Into + Display, +{ + fn py_try_from(_py: Python, item: &PyComplex) -> PyResult { + let make_error = |val: c_double| { + PyFloatingPointError::new_err(format!( + "expected {val} to be between {} and {}, inclusive", + F::min_value(), + F::max_value(), + )) + }; Ok(Self { - re: F::from(item.real()), - im: F::from(item.imag()), + re: F::from(item.real()).ok_or_else(|| make_error(item.real()))?, + im: F::from(item.imag()).ok_or_else(|| make_error(item.imag()))?, }) } } @@ -185,28 +252,32 @@ where #[cfg(feature = "complex")] impl PyTryFrom for Complex where - F: Copy + From, + F: Copy + Float + FloatConst + Into + Display, { - fn py_try_from(py: Python, item: Py) -> PyResult { - private_py_try_from_py_pyany_inner!(item, py, PyComplex) - } - fn py_try_from_ref(py: Python, item: &PyAny) -> PyResult { + fn py_try_from(py: Python, item: &PyAny) -> PyResult { let dict: &PyComplex = item.downcast()?; - Self::py_try_from_ref(py, dict) + Self::py_try_from(py, dict) } } // ==== Date ==== +impl_try_from_self_python!(PyDate); + +#[cfg(feature = "time")] +impl_try_from_self_rust!(Date); +#[cfg(feature = "time")] +impl_try_from_py_native!(PyDate => Date); + #[cfg(feature = "time")] private_impl_py_try_from_with_pyany!(&item, py, PyDate => Date { - let year = item.getattr("year").map(|any| i32::py_try_from_ref(py, any))??; - let month: u8 = item.getattr("month").map(|any| u8::py_try_from_ref(py, any))??; // 1-12 + let year = item.getattr("year").map(|any| i32::py_try_from(py, any))??; + let month: u8 = item.getattr("month").map(|any| u8::py_try_from(py, any))??; // 1-12 let month: Month = month.try_into() .map_err(|_| { PyValueError::new_err(format!("Expected date month to be within 0-12, got {month}")) })?; - let day = item.getattr("day").map(|any| u8::py_try_from_ref(py, any))??; // 1-X + let day = item.getattr("day").map(|any| u8::py_try_from(py, any))??; // 1-X Self::from_calendar_date(year, month, day).map_err(|err| { PyValueError::new_err(format!("Failed to create Date object: {err}")) @@ -215,11 +286,18 @@ private_impl_py_try_from_with_pyany!(&item, py, PyDate => Date { // ==== DateTime ==== +impl_try_from_self_python!(PyDateTime); + +#[cfg(feature = "time")] +impl_try_from_self_rust!(DateTime); +#[cfg(feature = "time")] +impl_try_from_py_native!(PyDateTime => DateTime); + #[cfg(feature = "time")] private_impl_py_try_from_with_pyany!(&item, py, PyDateTime => DateTime { - let date = item.call_method0("date").map(|date| Date::py_try_from_ref(py, date))??; + let date = item.call_method0("date").map(|date| Date::py_try_from(py, date))??; let (time, offset) = item.call_method0("timetz") - .map(|time| <(Time, Option)>::py_try_from_ref(py, time))??; + .map(|time| <(Time, Option)>::py_try_from(py, time))??; let datetime = PrimitiveDateTime::new(date, time); let datetime = offset.map_or(Self::Primitive(datetime), |offset| { // Cannot create an OffsetDateTime from parts, for some reason. @@ -234,6 +312,14 @@ private_impl_py_try_from_with_pyany!(&item, py, PyDateTime => DateTime { // ==== Delta ==== +impl_try_from_self_python!(PyDelta); + +#[cfg(feature = "time")] +impl_try_from_self_rust!(Duration); + +#[cfg(feature = "time")] +impl_try_from_py_native!(PyDelta => Duration); + #[cfg(feature = "time")] private_impl_py_try_from_with_pyany!(&item, _py, PyDelta => Duration { let days: i64 = item.getattr("days")?.extract()?; @@ -251,22 +337,68 @@ private_impl_py_try_from_with_pyany!(&item, _py, PyDelta => Duration { Ok(Self::new(seconds, nanoseconds)) }); +impl_try_from_self_rust!(std::time::Duration); +impl_try_from_py_native!(PyDelta => std::time::Duration); + +private_impl_py_try_from!(&item, _py, PyDelta => std::time::Duration { + let days: u64 = item.getattr("days")?.extract()?; + let seconds: u64 = item.getattr("seconds")?.extract()?; + let microseconds: u32 = item.getattr("microseconds")?.extract()?; + let nanoseconds = microseconds.checked_mul(1000).ok_or_else(|| { + PyValueError::new_err("Could not fit {microseconds} microseconds as nanoseconds into a 32-bit signed integer") + })?; + let day_seconds = days.checked_mul(24 * 60 * 60).ok_or_else(|| { + PyValueError::new_err("Could not fit {days} days as seconds into a 64-bit signed integer") + })?; + let seconds = seconds.checked_add(day_seconds).ok_or_else(|| { + PyValueError::new_err("Could not add {days} days and {seconds} seconds into a 64-bit signed integer") + })?; + Ok(Self::new(seconds, nanoseconds)) +}); + // ==== Dict ==== -impl PyTryFrom for HashMap +impl_try_from_self_python!(PyDict); + +impl PyTryFrom> for HashMap +where + K2: Eq + std::hash::Hash + PyTryFrom, + V2: PyTryFrom, + Hasher: std::hash::BuildHasher + Default, +{ + fn py_try_from(py: Python, item: &HashMap) -> PyResult { + item.iter() + .map(|(key, val)| { + let key = K2::py_try_from(py, key)?; + let val = V2::py_try_from(py, val)?; + Ok((key, val)) + }) + .collect() + } +} + +impl PyTryFrom> for HashMap where K: Eq + std::hash::Hash + PyTryFrom, V: PyTryFrom, Hasher: std::hash::BuildHasher + Default, { - fn py_try_from(py: Python, item: Py) -> PyResult { - private_py_try_from_py_inner!(item, py, PyDict) + fn py_try_from(py: Python, item: &Py) -> PyResult { + Self::py_try_from(py, item.as_ref(py)) } - fn py_try_from_ref(py: Python, item: &PyDict) -> PyResult { +} + +impl PyTryFrom for HashMap +where + K: Eq + std::hash::Hash + PyTryFrom, + V: PyTryFrom, + Hasher: std::hash::BuildHasher + Default, +{ + fn py_try_from(py: Python, item: &PyDict) -> PyResult { let mut map = Self::with_capacity_and_hasher(item.len(), Hasher::default()); for (key, val) in item.iter() { - let key = K::py_try_from_ref(py, key)?; - let val = V::py_try_from_ref(py, val)?; + let key = K::py_try_from(py, key)?; + let val = V::py_try_from(py, val)?; map.insert(key, val); } Ok(map) @@ -279,28 +411,48 @@ where V: PyTryFrom, Hasher: std::hash::BuildHasher + Default, { - fn py_try_from(py: Python, item: Py) -> PyResult { - private_py_try_from_py_pyany_inner!(item, py, PyDict) - } - fn py_try_from_ref(py: Python, item: &PyAny) -> PyResult { + fn py_try_from(py: Python, item: &PyAny) -> PyResult { let dict: &PyDict = item.downcast()?; - Self::py_try_from_ref(py, dict) + Self::py_try_from(py, dict) } } -impl PyTryFrom for BTreeMap +impl PyTryFrom> for BTreeMap +where + K2: Ord + PyTryFrom, + V2: PyTryFrom, +{ + fn py_try_from(py: Python, item: &BTreeMap) -> PyResult { + item.iter() + .map(|(key, val)| { + let key = K2::py_try_from(py, key)?; + let val = V2::py_try_from(py, val)?; + Ok((key, val)) + }) + .collect() + } +} + +impl PyTryFrom> for BTreeMap where K: Ord + PyTryFrom, V: PyTryFrom, { - fn py_try_from(py: Python, item: Py) -> PyResult { - private_py_try_from_py_inner!(item, py, PyDict) + fn py_try_from(py: Python, item: &Py) -> PyResult { + Self::py_try_from(py, item.as_ref(py)) } - fn py_try_from_ref(py: Python, item: &PyDict) -> PyResult { +} + +impl PyTryFrom for BTreeMap +where + K: Ord + PyTryFrom, + V: PyTryFrom, +{ + fn py_try_from(py: Python, item: &PyDict) -> PyResult { let mut map = Self::new(); for (key, val) in item.iter() { - let key = K::py_try_from_ref(py, key)?; - let val = V::py_try_from_ref(py, val)?; + let key = K::py_try_from(py, key)?; + let val = V::py_try_from(py, val)?; map.insert(key, val); } Ok(map) @@ -312,51 +464,66 @@ where K: Ord + PyTryFrom, V: PyTryFrom, { - fn py_try_from(py: Python, item: Py) -> PyResult { - private_py_try_from_py_pyany_inner!(item, py, PyDict) - } - fn py_try_from_ref(py: Python, item: &PyAny) -> PyResult { + fn py_try_from(py: Python, item: &PyAny) -> PyResult { let dict: &PyDict = item.downcast()?; - >::py_try_from_ref(py, dict) + >::py_try_from(py, dict) } } // ==== Float ==== +impl_try_from_self_python!(PyFloat); +impl_try_from_self_rust!(f32); +impl_try_from_self_rust!(f64); impl_try_from_primitive!(PyFloat => f32); impl_try_from_primitive!(PyFloat => f64); // ==== FrozenSet ==== -impl PyTryFrom for HashSet +impl_try_from_self_python!(PyFrozenSet); + +impl PyTryFrom> for HashSet where T: Eq + std::hash::Hash + PyTryFrom, Hasher: std::hash::BuildHasher + Default, { - fn py_try_from(py: Python, item: Py) -> PyResult { - private_py_try_from_py_inner!(item, py, PyFrozenSet) + fn py_try_from(py: Python, set: &Py) -> PyResult { + Self::py_try_from(py, set.as_ref(py)) } - fn py_try_from_ref(py: Python, set: &PyFrozenSet) -> PyResult { +} + +impl PyTryFrom for HashSet +where + T: Eq + std::hash::Hash + PyTryFrom, + Hasher: std::hash::BuildHasher + Default, +{ + fn py_try_from(py: Python, set: &PyFrozenSet) -> PyResult { let mut map = Self::with_capacity_and_hasher(set.len(), Hasher::default()); for item in set.iter() { - let item = T::py_try_from_ref(py, item)?; + let item = T::py_try_from(py, item)?; map.insert(item); } Ok(map) } } -impl PyTryFrom for BTreeSet +impl PyTryFrom> for BTreeSet where T: Ord + PyTryFrom, { - fn py_try_from(py: Python, item: Py) -> PyResult { - private_py_try_from_py_inner!(item, py, PyFrozenSet) + fn py_try_from(py: Python, set: &Py) -> PyResult { + Self::py_try_from(py, set.as_ref(py)) } - fn py_try_from_ref(py: Python, set: &PyFrozenSet) -> PyResult { +} + +impl PyTryFrom for BTreeSet +where + T: Ord + PyTryFrom, +{ + fn py_try_from(py: Python, set: &PyFrozenSet) -> PyResult { let mut map = Self::new(); for item in set.iter() { - let item = T::py_try_from_ref(py, item)?; + let item = T::py_try_from(py, item)?; map.insert(item); } Ok(map) @@ -365,6 +532,17 @@ where // ==== Integer ==== +impl_try_from_self_python!(PyInt); +impl_try_from_self_rust!(i8); +impl_try_from_self_rust!(i16); +impl_try_from_self_rust!(i32); +impl_try_from_self_rust!(i64); +impl_try_from_self_rust!(i128); +impl_try_from_self_rust!(u8); +impl_try_from_self_rust!(u16); +impl_try_from_self_rust!(u32); +impl_try_from_self_rust!(u64); +impl_try_from_self_rust!(u128); impl_try_from_primitive!(PyInt => i8); impl_try_from_primitive!(PyInt => i16); impl_try_from_primitive!(PyInt => i32); @@ -378,19 +556,35 @@ impl_try_from_primitive!(PyInt => u128); // ==== List ==== -impl PyTryFrom for Vec +impl_try_from_self_python!(PyList); + +impl PyTryFrom> for Vec +where + T: PyTryFrom

, +{ + fn py_try_from(py: Python, item: &Vec

) -> PyResult { + item.iter().map(|item| T::py_try_from(py, item)).collect() + } +} + +impl PyTryFrom> for Vec where T: PyTryFrom, { - fn py_try_from(py: Python, item: Py) -> PyResult { - private_py_try_from_py_inner!(item, py, PyList) + fn py_try_from(py: Python, py_list: &Py) -> PyResult { + Self::py_try_from(py, py_list.as_ref(py)) } +} - fn py_try_from_ref(py: Python, py_list: &PyList) -> PyResult { +impl PyTryFrom for Vec +where + T: PyTryFrom, +{ + fn py_try_from(py: Python, py_list: &PyList) -> PyResult { let mut list = Self::with_capacity(py_list.len()); for item in py_list.iter() { - let item = T::py_try_from_ref(py, item)?; + let item = T::py_try_from(py, item)?; list.push(item); } @@ -402,30 +596,85 @@ impl PyTryFrom for Vec where T: PyTryFrom, { - fn py_try_from(py: Python, item: Py) -> PyResult { - private_py_try_from_py_pyany_inner!(item, py, PyList) + fn py_try_from(py: Python, item: &PyAny) -> PyResult { + let actual: &PyList = item.downcast()?; + Self::py_try_from(py, actual) } +} - fn py_try_from_ref(py: Python, item: &PyAny) -> PyResult { +impl PyTryFrom> for Box<[T]> +where + T: PyTryFrom, +{ + fn py_try_from(py: Python, py_list: &Py) -> PyResult { + Self::py_try_from(py, py_list.as_ref(py)) + } +} + +impl PyTryFrom for Box<[T]> +where + T: PyTryFrom, +{ + fn py_try_from(py: Python, py_list: &PyList) -> PyResult { + Vec::py_try_from(py, py_list).map(From::from) + } +} + +impl PyTryFrom for Box<[T]> +where + T: PyTryFrom, +{ + fn py_try_from(py: Python, item: &PyAny) -> PyResult { let actual: &PyList = item.downcast()?; - Self::py_try_from_ref(py, actual) + Self::py_try_from(py, actual) + } +} + +// ==== Optional[T] ==== + +impl PyTryFrom> for Option +where + T: PyTryFrom

, +{ + fn py_try_from(py: Python, item: &Option

) -> PyResult { + item.as_ref() + .map_or_else(|| Ok(None), |item| T::py_try_from(py, item).map(Some)) } } // ==== Set ==== -impl PyTryFrom for HashSet +impl_try_from_self_python!(PySet); + +impl PyTryFrom> for HashSet +where + T: Eq + std::hash::Hash + PyTryFrom

, + Hasher: std::hash::BuildHasher + Default, +{ + fn py_try_from(py: Python, set: &HashSet) -> PyResult { + set.iter().map(|item| T::py_try_from(py, item)).collect() + } +} + +impl PyTryFrom> for HashSet where T: Eq + std::hash::Hash + PyTryFrom, Hasher: std::hash::BuildHasher + Default, { - fn py_try_from(py: Python, item: Py) -> PyResult { - private_py_try_from_py_inner!(item, py, PySet) + fn py_try_from(py: Python, set: &Py) -> PyResult { + Self::py_try_from(py, set.as_ref(py)) } - fn py_try_from_ref(py: Python, set: &PySet) -> PyResult { +} + +impl PyTryFrom for HashSet +where + T: Eq + std::hash::Hash + PyTryFrom, + Hasher: std::hash::BuildHasher + Default, +{ + fn py_try_from(py: Python, set: &PySet) -> PyResult { let mut map = Self::with_capacity_and_hasher(set.len(), Hasher::default()); for item in set.iter() { - let item = T::py_try_from_ref(py, item)?; + let item = T::py_try_from(py, item)?; map.insert(item); } Ok(map) @@ -437,26 +686,38 @@ where T: Eq + std::hash::Hash + PyTryFrom, Hasher: std::hash::BuildHasher + Default, { - fn py_try_from(py: Python, item: Py) -> PyResult { - private_py_try_from_py_pyany_inner!(item, py, PySet) - } - fn py_try_from_ref(py: Python, item: &PyAny) -> PyResult { + fn py_try_from(py: Python, item: &PyAny) -> PyResult { let set: &PySet = item.downcast()?; - Self::py_try_from_ref(py, set) + Self::py_try_from(py, set) } } -impl PyTryFrom for BTreeSet +impl PyTryFrom> for BTreeSet +where + T: Ord + PyTryFrom

, +{ + fn py_try_from(py: Python, set: &BTreeSet

) -> PyResult { + set.iter().map(|item| T::py_try_from(py, item)).collect() + } +} + +impl PyTryFrom> for BTreeSet where T: Ord + PyTryFrom, { - fn py_try_from(py: Python, item: Py) -> PyResult { - private_py_try_from_py_inner!(item, py, PySet) + fn py_try_from(py: Python, set: &Py) -> PyResult { + Self::py_try_from(py, set.as_ref(py)) } - fn py_try_from_ref(py: Python, set: &PySet) -> PyResult { +} + +impl PyTryFrom for BTreeSet +where + T: Ord + PyTryFrom, +{ + fn py_try_from(py: Python, set: &PySet) -> PyResult { let mut map = Self::new(); for item in set.iter() { - let item = T::py_try_from_ref(py, item)?; + let item = T::py_try_from(py, item)?; map.insert(item); } Ok(map) @@ -467,23 +728,31 @@ impl PyTryFrom for BTreeSet where T: Ord + PyTryFrom, { - fn py_try_from(py: Python, item: Py) -> PyResult { - private_py_try_from_py_pyany_inner!(item, py, PySet) - } - fn py_try_from_ref(py: Python, set: &PyAny) -> PyResult { + fn py_try_from(py: Python, set: &PyAny) -> PyResult { let set: &PySet = set.downcast()?; - >::py_try_from_ref(py, set) + >::py_try_from(py, set) } } // ==== String ==== +impl_try_from_self_python!(PyString); +impl_try_from_self_rust!(String); +impl_try_from_py_native!(PyString => String); + private_impl_py_try_from_with_pyany!(&item, _py, PyString => String { item.to_str().map(ToString::to_string) }); // ==== Time ==== +impl_try_from_self_python!(PyTime); + +#[cfg(feature = "time")] +impl_try_from_self_rust!((Time, Option)); +#[cfg(feature = "time")] +impl_try_from_py_native!(PyTime => (Time, Option)); + #[cfg(feature = "time")] private_impl_py_try_from_with_pyany!(&item, py, PyTime => (Time, Option) { let hour: u8 = item.getattr("hour")?.downcast::()?.extract()?; @@ -491,7 +760,7 @@ private_impl_py_try_from_with_pyany!(&item, py, PyTime => (Time, Option()?.extract()?; let microseconds: u32 = item.getattr("microsecond")?.downcast::()?.extract()?; let tzinfo: Option<&PyTzInfo> = item.getattr("tzinfo")?.extract()?; - let offset = tzinfo.map(|tzinfo| UtcOffset::py_try_from_ref(py, tzinfo)).transpose()?; + let offset = tzinfo.map(|tzinfo| UtcOffset::py_try_from(py, tzinfo)).transpose()?; let timestamp = Time::from_hms_micro(hour, minute, seconds, microseconds).map_err(|err| { PyValueError::new_err(format!("Could not create a Rust Time from {hour}:{minute}:{seconds}.{microseconds}: {err}")) })?; @@ -500,11 +769,18 @@ private_impl_py_try_from_with_pyany!(&item, py, PyTime => (Time, Option UtcOffset); + #[cfg(feature = "time")] private_impl_py_try_from_with_pyany!(&item, py, PyTzInfo => UtcOffset { let args: Py = (py.None(),).to_object(py); let args: &PyTuple = args.extract(py)?; - let duration = item.call_method1("utcoffset", args).map(|any| Duration::py_try_from_ref(py, any))??; + let duration = item.call_method1("utcoffset", args).map(|any| Duration::py_try_from(py, any))??; let seconds = duration.whole_seconds(); let seconds = seconds.try_into().map_err(|_| { PyValueError::new_err(format!("Cannot create a Rust UtcOffset from {seconds} seconds -- too many seconds!")) diff --git a/src/to_python.rs b/src/to_python.rs index 3d1a250..d225163 100644 --- a/src/to_python.rs +++ b/src/to_python.rs @@ -15,57 +15,60 @@ //! Unifying conversion traits from Rust data to Python. use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::hash::BuildHasher; use pyo3::conversion::IntoPy; +use pyo3::types::PyComplex; use pyo3::types::{ PyBool, PyByteArray, PyBytes, PyDict, PyFloat, PyFrozenSet, PyList, PyLong, PySet, PyString, }; +use pyo3::{ + exceptions::PyValueError, + types::{PyDate, PyDateTime, PyDelta, PyTime, PyTzInfo}, +}; use pyo3::{Py, PyAny, PyResult, Python, ToPyObject}; #[cfg(feature = "complex")] use num_complex::Complex; #[cfg(feature = "complex")] -use pyo3::types::PyComplex; +use num_traits::{Float, FloatConst}; #[cfg(feature = "complex")] use std::os::raw::c_double; #[cfg(feature = "time")] use crate::datetime::DateTime; #[cfg(feature = "time")] -use pyo3::{ - exceptions::PyValueError, - types::{PyDate, PyDateTime, PyDelta, PyTime, PyTuple, PyTzInfo}, -}; +use pyo3::types::PyTuple; #[cfg(feature = "time")] use time::{Date, Duration, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset}; /// Convert from a Rust type into a Python type. -pub trait ToPython { +pub trait ToPython { /// Convert from Rust `self` into a Python type. /// /// # Errors /// /// Any failure while converting to Python. - fn to_python(&self, py: Python) -> PyResult>; + fn to_python(&self, py: Python) -> PyResult

; } -// Simple blanket impl based on pyo3::IntoPy does not work because, e.g., `String` -// does not impl `IntoPy>`, only `IntoPy>`. -// -// Using the `impl IntoPy>` is not viable either, because that is liable to -// errors (panics, because this trait doesn't return `Result`). We'd have to clarify -// which `std` types can convert to which `pyo3` types, and at that point we might as -// well implement conversion without using `expect()`. - -/// Implement [`ToPython`] for the given Rust type by delegating to its implementation for -/// the given Python type. Arguments are the same as for -/// [`private_impl_to_python_for`](crate::private_impl_to_python_for). -#[macro_export] -macro_rules! private_impl_to_python_to_pyany { - (&$($lt: lifetime)? $self: ident, $py: ident, $rs_type: ty => $py_type: ty) => { - $crate::private_impl_to_python_for!(&$($lt)? $self, $py, $rs_type => $crate::pyo3::PyAny { - >::to_python($self, $py).map(|item| item.into_py($py)) - }); +impl<'a, T, P> ToPython

for &'a Box +where + T: ToPython

, + P: ToPyObject, +{ + fn to_python(&self, py: Python) -> PyResult

{ + T::to_python(self, py) + } +} + +impl ToPython

for Box +where + T: ToPython

, + P: ToPyObject, +{ + fn to_python(&self, py: Python) -> PyResult

{ + T::to_python(self, py) } } @@ -74,8 +77,9 @@ macro_rules! private_impl_to_python_to_pyany { #[macro_export] macro_rules! private_impl_to_python_for { (&$($lt: lifetime)? $self: ident, $py: ident, $rs_type: ty => $py_type: ty $convert: block) => { + #[allow(clippy::use_self)] impl$(<$lt>)? $crate::ToPython<$py_type> for $(&$lt)? $rs_type { - fn to_python(&$self, $py: $crate::pyo3::Python<'_>) -> $crate::pyo3::PyResult<$crate::pyo3::Py<$py_type>> { + fn to_python(&$self, $py: $crate::pyo3::Python<'_>) -> $crate::pyo3::PyResult<$py_type> { $convert } } @@ -93,14 +97,14 @@ macro_rules! private_impl_to_python_with_reference { }; } -/// Implement [`ToPython`] multiple times for the given types, accounting for owned/reference and [`PyAny`](crate::pyo3::PyAny). +/// Implement [`ToPython>`] for a type using its implementation for `ToPython

` where `P: ToPyObject`. #[macro_export] -macro_rules! private_impl_to_python_with_reference_and_pyany { - (&$self: ident, $py: ident, $rs_type: ty => $py_type: ty $convert: block) => { - $crate::private_impl_to_python_with_reference!(&$self, $py, $rs_type => $py_type $convert); - $crate::private_impl_to_python_to_pyany!(&$self, $py, $rs_type => $py_type); - $crate::private_impl_to_python_to_pyany!(&'a $self, $py, $rs_type => $py_type); - }; +macro_rules! private_impl_to_python_pyany { + ($rs_type: ty => $py_type: ty) => { + private_impl_to_python_with_reference!(&self, py, $rs_type => $crate::pyo3::Py<$crate::pyo3::PyAny> { + $crate::ToPython::<$py_type>::to_python(self, py).map(|item| $crate::pyo3::ToPyObject::to_object(&item, py)) + }); + } } /// Implements [`IntoPython`] by converting to `Py` and extracting `Py` from that. @@ -108,47 +112,66 @@ macro_rules! private_impl_to_python_with_reference_and_pyany { /// For types like integers, this is only way to convert. macro_rules! impl_for_primitive { ($rs_type: ty => $py_type: ty) => { - private_impl_to_python_with_reference_and_pyany!(&self, py, $rs_type => $py_type { + private_impl_to_python_with_reference!(&self, py, $rs_type => $py_type { // No way to convert except via ToPyObject and downcasting. self.into_py(py).extract(py) }); }; } +/// Implement `ToPython` for a given type. +macro_rules! impl_for_self { + ($type: ty) => { + private_impl_to_python_with_reference!(&self, _py, $type => $type { + Ok(self.clone()) + }); + private_impl_to_python_pyany!($type => $type); + } +} + // ============ Begin Implementations ============== // ==== Bool ==== -private_impl_to_python_with_reference_and_pyany!(&self, py, bool => PyBool { +impl_for_self!(bool); +impl_for_self!(Py); + +private_impl_to_python_with_reference!(&self, py, bool => Py { Ok(PyBool::new(py, *self).into_py(py)) }); // ==== ByteArray ==== -private_impl_to_python_for!(&'a self, py, [u8] => PyByteArray { +impl_for_self!(Py); + +private_impl_to_python_with_reference!(&self, py, [u8] => Py { Ok(PyByteArray::new(py, self).into_py(py)) }); -private_impl_to_python_with_reference!(&self, py, Vec => PyByteArray { +private_impl_to_python_with_reference!(&self, py, Vec => Py { self.as_slice().to_python(py) }); // ==== Bytes ==== -private_impl_to_python_for!(&'a self, py, [u8] => PyBytes { +impl_for_self!(Py); + +private_impl_to_python_with_reference!(&self, py, [u8] => Py { Ok(PyBytes::new(py, self).into_py(py)) }); -private_impl_to_python_with_reference!(&self, py, Vec => PyBytes { +private_impl_to_python_with_reference!(&self, py, Vec => Py { self.as_slice().to_python(py) }); // ==== Complex ==== +impl_for_self!(Py); + #[cfg(feature = "complex")] -impl<'a, F> ToPython for &'a Complex +impl<'a, F> ToPython> for &'a Complex where - F: Copy + Into, + F: Copy + Float + FloatConst + Into, { fn to_python(&self, py: Python) -> PyResult> { Ok(PyComplex::from_complex(py, **self).into_py(py)) @@ -156,49 +179,56 @@ where } #[cfg(feature = "complex")] -impl ToPython for Complex +impl ToPython> for Complex where - F: Copy + Into, + F: Copy + Float + FloatConst + Into, { fn to_python(&self, py: Python) -> PyResult> { - <&Self as ToPython>::to_python(&self, py) + <&Self as ToPython>>::to_python(&self, py) } } #[cfg(feature = "complex")] -impl<'a, F> ToPython for &'a Complex +impl<'a, F> ToPython> for &'a Complex where - F: Copy + Into, + F: Copy + Float + FloatConst + Into, { fn to_python(&self, py: Python) -> PyResult> { - >::to_python(self, py).map(|c| c.into_py(py)) + Ok(PyComplex::from_complex(py, **self).into_py(py)) } } #[cfg(feature = "complex")] -impl ToPython for Complex +impl ToPython> for Complex where - F: Copy + Into, + F: Copy + Float + FloatConst + Into, { fn to_python(&self, py: Python) -> PyResult> { - >::to_python(self, py).map(|c| c.into_py(py)) + <&Self as ToPython>>::to_python(&self, py) } } // ==== Date ==== +impl_for_self!(Py); + #[cfg(feature = "time")] -private_impl_to_python_with_reference_and_pyany!(&self, py, Date => PyDate { +private_impl_to_python_with_reference!(&self, py, Date => Py { let year: i32 = self.year(); let month: u8 = self.month().into(); let day: u8 = self.day(); PyDate::new(py, year, month, day).map(|date| date.into_py(py)) }); +#[cfg(feature = "time")] +private_impl_to_python_pyany!(Date => Py); + // ==== DateTime ==== +impl_for_self!(Py); + #[cfg(feature = "time")] -private_impl_to_python_with_reference_and_pyany!(&self, py, DateTime => PyDateTime { +private_impl_to_python_with_reference!(&self, py, DateTime => Py { match self { Self::Primitive(datetime) => datetime.to_python(py), Self::Offset(datetime) => datetime.to_python(py), @@ -206,7 +236,10 @@ private_impl_to_python_with_reference_and_pyany!(&self, py, DateTime => PyDateTi }); #[cfg(feature = "time")] -private_impl_to_python_with_reference_and_pyany!(&self, py, PrimitiveDateTime => PyDateTime { +private_impl_to_python_pyany!(DateTime => Py); + +#[cfg(feature = "time")] +private_impl_to_python_with_reference!(&self, py, PrimitiveDateTime => Py { let date = self.date(); let time = self.time(); let year: i32 = date.year(); @@ -220,7 +253,10 @@ private_impl_to_python_with_reference_and_pyany!(&self, py, PrimitiveDateTime => }); #[cfg(feature = "time")] -private_impl_to_python_with_reference_and_pyany!(&self, py, OffsetDateTime => PyDateTime { +private_impl_to_python_pyany!(PrimitiveDateTime => Py); + +#[cfg(feature = "time")] +private_impl_to_python_with_reference!(&self, py, OffsetDateTime => Py { let date = self.date(); let time = self.time(); let offset = self.offset(); @@ -236,10 +272,15 @@ private_impl_to_python_with_reference_and_pyany!(&self, py, OffsetDateTime => Py PyDateTime::new(py, year, month, day, hour, minute, second, microsecond, Some(tzinfo)).map(|dt| dt.into_py(py)) }); +#[cfg(feature = "time")] +private_impl_to_python_pyany!(OffsetDateTime => Py); + // ==== Delta ==== +impl_for_self!(Py); + #[cfg(feature = "time")] -private_impl_to_python_with_reference_and_pyany!(&self, py, Duration => PyDelta { +private_impl_to_python_with_reference!(&self, py, Duration => Py { let days: i32 = self.whole_days().try_into().map_err(|_| { PyValueError::new_err(format!("Cannot fit {} days into a 32-bit signed integer", self.whole_days())) })?; @@ -252,12 +293,59 @@ private_impl_to_python_with_reference_and_pyany!(&self, py, Duration => PyDelta PyDelta::new(py, days, seconds, microseconds, true).map(|delta| delta.into_py(py)) }); +#[cfg(feature = "time")] +private_impl_to_python_pyany!(Duration => Py); + +private_impl_to_python_with_reference!(&self, py, std::time::Duration => Py { + /// The number of seconds in a day. + const DAY_FACTOR: u64 = 60 * 60 * 24; + let microseconds = self.as_micros() % 1_000_000; + let seconds = self.as_secs() % DAY_FACTOR; + let days = self.as_secs() / DAY_FACTOR; + + let microseconds: i32 = microseconds.try_into().map_err(|_| { + PyValueError::new_err(format!("Cannot fit {microseconds} microseconds into a 32-bit signed integer")) + })?; + + let seconds: i32 = seconds.try_into().map_err(|_| { + PyValueError::new_err(format!("Cannot fit {seconds} seconds into a 32-bit signed integer")) + })?; + + let days: i32 = days.try_into().map_err(|_| { + PyValueError::new_err(format!("Cannot fit {days} days into a 32-bit signed integer")) + })?; + + PyDelta::new(py, days, seconds, microseconds, true).map(|delta| delta.into_py(py)) +}); + +private_impl_to_python_pyany!(std::time::Duration => Py); + // ==== Dict ==== -impl<'a, K, V, Hasher> ToPython for &'a HashMap +impl_for_self!(Py); + +impl<'a, K1, K2, V1, V2, Hasher> ToPython> for &'a HashMap +where + K1: ToPython, + V1: ToPython, + K2: ToPyObject + Eq + std::hash::Hash, + V2: ToPyObject, +{ + fn to_python(&self, py: Python) -> PyResult> { + self.iter() + .map(|(key, val)| { + let key = key.to_python(py)?; + let val = val.to_python(py)?; + Ok((key, val)) + }) + .collect::>() + } +} + +impl<'a, K, V, Hasher> ToPython> for &'a HashMap where - K: ToPython + std::fmt::Debug, - V: ToPython, + K: ToPython> + std::fmt::Debug, + V: ToPython>, { fn to_python(&self, py: Python) -> PyResult> { let dict = PyDict::new(py); @@ -270,40 +358,70 @@ where } } -impl ToPython for HashMap +impl<'a, K, V, Hasher> ToPython> for &'a HashMap where - K: ToPython + std::fmt::Debug, - V: ToPython, + K: ToPython> + std::fmt::Debug, + V: ToPython>, +{ + fn to_python(&self, py: Python) -> PyResult> { + >>::to_python(self, py).map(|dict| dict.into_py(py)) + } +} + +impl ToPython> for HashMap +where + K1: ToPython, + V1: ToPython, + K2: ToPyObject + Eq + std::hash::Hash, + V2: ToPyObject, +{ + fn to_python(&self, py: Python) -> PyResult> { + <&Self as ToPython>>::to_python(&self, py) + } +} + +impl ToPython> for HashMap +where + K: ToPython> + std::fmt::Debug, + V: ToPython>, { fn to_python(&self, py: Python) -> PyResult> { - <&Self as ToPython>::to_python(&self, py) + <&Self as ToPython>>::to_python(&self, py) } } -impl ToPython for HashMap +impl ToPython> for HashMap where - K: ToPython + std::fmt::Debug, - V: ToPython, + K: ToPython> + std::fmt::Debug, + V: ToPython>, { fn to_python(&self, py: Python) -> PyResult> { - >::to_python(self, py).map(|dict| dict.into_py(py)) + >>::to_python(self, py).map(|dict| dict.into_py(py)) } } -impl<'a, K, V, Hasher> ToPython for &'a HashMap +impl<'a, K1, K2, V1, V2> ToPython> for &'a BTreeMap where - K: ToPython + std::fmt::Debug, - V: ToPython, + K1: ToPython + std::fmt::Debug, + V1: ToPython, + K2: ToPyObject + Ord, + V2: ToPyObject, { - fn to_python(&self, py: Python) -> PyResult> { - >::to_python(self, py).map(|dict| dict.into_py(py)) + fn to_python(&self, py: Python) -> PyResult> { + let mut map = BTreeMap::new(); + for (key, val) in *self { + let pykey = key.to_python(py)?; + let pyval = val.to_python(py)?; + map.insert(pykey, pyval); + } + Ok(map) } } -impl<'a, K, V> ToPython for &'a BTreeMap +impl<'a, K, V> ToPython> for &'a BTreeMap where - K: ToPython + std::fmt::Debug, - V: ToPython, + K: ToPython> + std::fmt::Debug, + V: ToPython>, { fn to_python(&self, py: Python) -> PyResult> { let dict = PyDict::new(py); @@ -316,46 +434,62 @@ where } } -impl ToPython for BTreeMap +impl<'a, K, V> ToPython> for &'a BTreeMap where - K: ToPython + std::fmt::Debug, - V: ToPython, + K: ToPython> + std::fmt::Debug, + V: ToPython>, { - fn to_python(&self, py: Python) -> PyResult> { - <&Self as ToPython>::to_python(&self, py) + fn to_python(&self, py: Python) -> PyResult> { + >>::to_python(self, py).map(|dict| dict.into_py(py)) } } -impl<'a, K, V> ToPython for &'a BTreeMap +impl ToPython> for BTreeMap where - K: ToPython + std::fmt::Debug, - V: ToPython, + K1: ToPython + std::fmt::Debug, + V1: ToPython, + K2: ToPyObject + Ord, + V2: ToPyObject, { - fn to_python(&self, py: Python) -> PyResult> { - >::to_python(self, py).map(|dict| dict.into_py(py)) + fn to_python(&self, py: Python) -> PyResult> { + <&Self as ToPython>>::to_python(&self, py) + } +} + +impl ToPython> for BTreeMap +where + K: ToPython> + std::fmt::Debug, + V: ToPython>, +{ + fn to_python(&self, py: Python) -> PyResult> { + <&Self as ToPython>>::to_python(&self, py) } } -impl ToPython for BTreeMap +impl ToPython> for BTreeMap where - K: ToPython + std::fmt::Debug, - V: ToPython, + K: ToPython> + std::fmt::Debug, + V: ToPython>, { fn to_python(&self, py: Python) -> PyResult> { - >::to_python(self, py).map(|dict| dict.into_py(py)) + >>::to_python(self, py).map(|dict| dict.into_py(py)) } } // ==== Float ==== -impl_for_primitive!(f32 => PyFloat); -impl_for_primitive!(f64 => PyFloat); +impl_for_self!(Py); +impl_for_self!(f32); +impl_for_self!(f64); + +impl_for_primitive!(f32 => Py); +impl_for_primitive!(f64 => Py); // ==== FrozenSet ==== -impl<'a, T, Hasher> ToPython for &'a HashSet +impl<'a, T, Hasher> ToPython> for &'a HashSet where - T: ToPython + Clone, + T: ToPython> + Clone, { fn to_python(&self, py: Python) -> PyResult> { let elements = self @@ -366,18 +500,18 @@ where } } -impl ToPython for HashSet +impl ToPython> for HashSet where - T: ToPython + Clone, + T: ToPython> + Clone, { fn to_python(&self, py: Python) -> PyResult> { - <&Self as ToPython>::to_python(&self, py) + <&Self as ToPython>>::to_python(&self, py) } } -impl<'a, T> ToPython for &'a BTreeSet +impl<'a, T> ToPython> for &'a BTreeSet where - T: ToPython + Clone, + T: ToPython> + Clone, { fn to_python(&self, py: Python) -> PyResult> { let elements = self @@ -388,33 +522,81 @@ where } } -impl ToPython for BTreeSet +impl ToPython> for BTreeSet where - T: ToPython + Clone, + T: ToPython> + Clone, { fn to_python(&self, py: Python) -> PyResult> { - <&Self as ToPython>::to_python(&self, py) + <&Self as ToPython>>::to_python(&self, py) } } // ==== Integer ==== -impl_for_primitive!(i8 => PyLong); -impl_for_primitive!(i16 => PyLong); -impl_for_primitive!(i32 => PyLong); -impl_for_primitive!(i64 => PyLong); -impl_for_primitive!(i128 => PyLong); -impl_for_primitive!(u8 => PyLong); -impl_for_primitive!(u16 => PyLong); -impl_for_primitive!(u32 => PyLong); -impl_for_primitive!(u64 => PyLong); -impl_for_primitive!(u128 => PyLong); +impl_for_self!(Py); +impl_for_self!(i8); +impl_for_self!(i16); +impl_for_self!(i32); +impl_for_self!(i64); +impl_for_self!(i128); +impl_for_self!(u8); +impl_for_self!(u16); +impl_for_self!(u32); +impl_for_self!(u64); +impl_for_self!(u128); + +impl_for_primitive!(i8 => Py); +impl_for_primitive!(i16 => Py); +impl_for_primitive!(i32 => Py); +impl_for_primitive!(i64 => Py); +impl_for_primitive!(i128 => Py); +impl_for_primitive!(u8 => Py); +impl_for_primitive!(u16 => Py); +impl_for_primitive!(u32 => Py); +impl_for_primitive!(u64 => Py); +impl_for_primitive!(u128 => Py); + +// ==== Optional[T] ==== + +impl<'a, T, P> ToPython> for &'a Option +where + T: ToPython

, + P: ToPyObject, +{ + fn to_python(&self, py: Python) -> PyResult> { + self.as_ref().map(|inner| inner.to_python(py)).transpose() + } +} + +impl ToPython> for Option +where + T: ToPython

, + P: ToPyObject, +{ + fn to_python(&self, py: Python) -> PyResult> { + <&Self as ToPython>>::to_python(&self, py) + } +} // ==== List ==== -impl<'a, T> ToPython for &'a [T] +impl_for_self!(Py); + +impl<'a, T, P> ToPython> for &'a [T] where - T: ToPython + Clone, + T: ToPython

+ Clone, + P: ToPyObject, +{ + fn to_python(&self, py: Python) -> PyResult> { + self.iter() + .map(|item| item.to_python(py)) + .collect::>>() + } +} + +impl<'a, T> ToPython> for &'a [T] +where + T: ToPython> + Clone, { fn to_python(&self, py: Python) -> PyResult> { let elements = self @@ -425,45 +607,93 @@ where } } -impl<'a, T> ToPython for &'a [T] +impl<'a, T> ToPython> for &'a [T] +where + T: ToPython> + Clone, +{ + fn to_python(&self, py: Python) -> PyResult> { + >>::to_python(self, py).map(|item| item.into_py(py)) + } +} + +impl ToPython> for [T] +where + T: ToPython

+ Clone, + P: ToPyObject, +{ + fn to_python(&self, py: Python) -> PyResult> { + <&Self as ToPython>>::to_python(&self, py) + } +} + +impl ToPython> for [T] +where + T: ToPython> + Clone, +{ + fn to_python(&self, py: Python) -> PyResult> { + <&Self as ToPython>>::to_python(&self, py) + } +} + +impl ToPython> for [T] where - T: ToPython + Clone, + T: ToPython> + Clone, { fn to_python(&self, py: Python) -> PyResult> { - >::to_python(self, py).map(|list| list.into_py(py)) + <&Self as ToPython>>::to_python(&self, py) + } +} + +impl ToPython> for Vec +where + T: ToPython

+ Clone, + P: ToPyObject, +{ + fn to_python(&self, py: Python) -> PyResult> { + self.as_slice().to_python(py) } } -impl ToPython for Vec +impl<'a, T, P> ToPython> for &'a Vec where - T: ToPython + Clone, + T: ToPython

+ Clone, + P: ToPyObject, +{ + fn to_python(&self, py: Python) -> PyResult> { + self.as_slice().to_python(py) + } +} + +impl ToPython> for Vec +where + T: ToPython> + Clone, { fn to_python(&self, py: Python) -> PyResult> { self.as_slice().to_python(py) } } -impl<'a, T> ToPython for &'a Vec +impl<'a, T> ToPython> for &'a Vec where - T: ToPython + Clone, + T: ToPython> + Clone, { fn to_python(&self, py: Python) -> PyResult> { self.as_slice().to_python(py) } } -impl ToPython for Vec +impl ToPython> for Vec where - T: ToPython + Clone, + T: ToPython> + Clone, { fn to_python(&self, py: Python) -> PyResult> { self.as_slice().to_python(py) } } -impl<'a, T> ToPython for &'a Vec +impl<'a, T> ToPython> for &'a Vec where - T: ToPython + Clone, + T: ToPython> + Clone, { fn to_python(&self, py: Python) -> PyResult> { self.as_slice().to_python(py) @@ -472,9 +702,35 @@ where // ==== Set ==== -impl<'a, T, Hasher> ToPython for &'a HashSet +impl_for_self!(Py); + +impl<'a, T, P, Hasher> ToPython> for &'a HashSet where - T: ToPython + Clone, + T: ToPython

+ Clone, + P: ToPyObject + std::hash::Hash + Eq, + Hasher: Default + BuildHasher, +{ + fn to_python(&self, py: Python) -> PyResult> { + self.iter() + .map(|item| item.to_python(py)) + .collect::>() + } +} + +impl ToPython> for HashSet +where + T: ToPython

+ Clone, + P: ToPyObject + std::hash::Hash + Eq, + Hasher: Default + BuildHasher, +{ + fn to_python(&self, py: Python) -> PyResult> { + <&Self as ToPython>>::to_python(&self, py) + } +} + +impl<'a, T, Hasher> ToPython> for &'a HashSet +where + T: ToPython> + Clone, { fn to_python(&self, py: Python) -> PyResult> { // Using PySet::new seems to do extra cloning, so build manually. @@ -486,36 +742,59 @@ where } } -impl ToPython for HashSet +impl ToPython> for HashSet where - T: ToPython + Clone, + T: ToPython> + Clone, { fn to_python(&self, py: Python) -> PyResult> { - <&Self as ToPython>::to_python(&self, py) + <&Self as ToPython>>::to_python(&self, py) } } -impl<'a, T, Hasher> ToPython for &'a HashSet +impl<'a, T, Hasher> ToPython> for &'a HashSet where - T: ToPython + Clone, + T: ToPython> + Clone, { fn to_python(&self, py: Python) -> PyResult> { - >::to_python(self, py).map(|set| set.into_py(py)) + >>::to_python(self, py).map(|item| item.into_py(py)) } } -impl ToPython for HashSet +impl ToPython> for HashSet where - T: ToPython + Clone, + T: ToPython> + Clone, { fn to_python(&self, py: Python) -> PyResult> { - >::to_python(self, py).map(|set| set.into_py(py)) + <&Self as ToPython>>::to_python(&self, py) + } +} + +impl<'a, T, P> ToPython> for &'a BTreeSet +where + T: ToPython

+ Clone, + // Hash is required for the ToPyObject impl + P: ToPyObject + Ord + std::hash::Hash, +{ + fn to_python(&self, py: Python) -> PyResult> { + self.iter() + .map(|item| item.to_python(py)) + .collect::>() + } +} + +impl ToPython> for BTreeSet +where + T: ToPython

+ Clone, + P: ToPyObject + Ord + std::hash::Hash, +{ + fn to_python(&self, py: Python) -> PyResult> { + <&Self as ToPython>>::to_python(&self, py) } } -impl<'a, T> ToPython for &'a BTreeSet +impl<'a, T> ToPython> for &'a BTreeSet where - T: ToPython + Clone, + T: ToPython> + Clone, { fn to_python(&self, py: Python) -> PyResult> { // Using PySet::new seems to do extra cloning, so build manually. @@ -527,48 +806,54 @@ where } } -impl ToPython for BTreeSet +impl ToPython> for BTreeSet where - T: ToPython + Clone, + T: ToPython> + Clone, { fn to_python(&self, py: Python) -> PyResult> { - <&Self as ToPython>::to_python(&self, py) + <&Self as ToPython>>::to_python(&self, py) } } -impl<'a, T> ToPython for &'a BTreeSet +impl<'a, T> ToPython> for &'a BTreeSet where - T: ToPython + Clone, + T: ToPython> + Clone, { fn to_python(&self, py: Python) -> PyResult> { - >::to_python(self, py).map(|set| set.into_py(py)) + >>::to_python(self, py).map(|item| item.into_py(py)) } } -impl ToPython for BTreeSet +impl ToPython> for BTreeSet where - T: ToPython + Clone, + T: ToPython> + Clone, { fn to_python(&self, py: Python) -> PyResult> { - >::to_python(self, py).map(|set| set.into_py(py)) + <&Self as ToPython>>::to_python(&self, py) } } // ==== String ==== -private_impl_to_python_for!(&'a self, py, str => PyString { +impl_for_self!(Py); +impl_for_self!(String); + +private_impl_to_python_with_reference!(&self, py, str => Py { Ok(PyString::new(py, self).into_py(py)) }); -private_impl_to_python_to_pyany!(&'a self, py, str => PyString); -private_impl_to_python_with_reference_and_pyany!(&self, py, String => PyString { +private_impl_to_python_pyany!(str => Py); + +private_impl_to_python_with_reference!(&self, py, String => Py { self.as_str().to_python(py) }); // ==== Time ==== +impl_for_self!(Py); + #[cfg(feature = "time")] -private_impl_to_python_with_reference_and_pyany!(&self, py, (Time, Option) => PyTime { +private_impl_to_python_with_reference!(&self, py, (Time, Option) => Py { let (time, offset) = self; let hour = time.hour(); let minute = time.minute(); @@ -581,10 +866,15 @@ private_impl_to_python_with_reference_and_pyany!(&self, py, (Time, Option) => Py); + // ==== TzInfo ==== +impl_for_self!(Py); + #[cfg(feature = "time")] -private_impl_to_python_with_reference_and_pyany!(&self, py, UtcOffset => PyTzInfo { +private_impl_to_python_with_reference!(&self, py, UtcOffset => Py { let datetime = py.import("datetime")?; let timezone = datetime.getattr("timezone")?; let (hour, minute, second) = self.as_hms(); @@ -597,4 +887,7 @@ private_impl_to_python_with_reference_and_pyany!(&self, py, UtcOffset => PyTzInf tzinfo.extract() }); +#[cfg(feature = "time")] +private_impl_to_python_pyany!(UtcOffset => Py); + // ============ End Implementations ============== diff --git a/src/traits.rs b/src/traits.rs index dac0fbc..154979e 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -61,7 +61,7 @@ macro_rules! impl_hash { impl $name { pub fn __hash__(&self) -> isize { let mut hasher = ::std::collections::hash_map::DefaultHasher::new(); - ::std::hash::Hash::hash(self, &mut hasher); + ::std::hash::Hash::hash($crate::PyWrapper::as_inner(self), &mut hasher); let bytes = ::std::hash::Hasher::finish(&hasher).to_ne_bytes(); isize::from_ne_bytes(bytes) } diff --git a/src/wrappers.rs b/src/wrappers.rs index b622727..0575ae7 100644 --- a/src/wrappers.rs +++ b/src/wrappers.rs @@ -102,22 +102,36 @@ macro_rules! py_wrap_type { impl $crate::PyTryFrom<$name> for $from { fn py_try_from( py: $crate::pyo3::Python, - item: $crate::pyo3::Py<$name>, + item: &$name, + ) -> $crate::pyo3::PyResult { + Ok(item.0.clone()) + } + } + + impl $crate::PyTryFrom<$crate::pyo3::PyAny> for $name { + fn py_try_from( + py: $crate::pyo3::Python, + item: &$crate::pyo3::PyAny, ) -> $crate::pyo3::PyResult { - let cell: &$crate::pyo3::PyCell<$name> = item.into_ref(py); - let item: &$name = &*cell.borrow(); - Self::py_try_from_ref(py, item) + item.extract() } - fn py_try_from_ref( + } + + impl $crate::PyTryFrom<$name> for $name { + fn py_try_from( py: $crate::pyo3::Python, item: &$name, ) -> $crate::pyo3::PyResult { - Ok(item.0.clone()) + Ok(item.clone()) } } $crate::private_impl_to_python_with_reference!(&self, py, $from => $name { - $crate::pyo3::Py::new(py, $name::from(self.clone())) + Ok($name::from(self.clone())) + }); + + $crate::private_impl_to_python_with_reference!(&self, py, $name => $crate::pyo3::Py<$crate::pyo3::PyAny> { + Ok(::to_object(self, py)) }); impl From<$name> for $from { @@ -317,15 +331,15 @@ macro_rules! py_wrap_simple_enum { /// // Fallible transformation from Python type `P` to Rust type `T` where `Foo: From`. /// // Used to implement `TryFrom

for PyFoo`. Any errors returned must be `PyErr`. /// py -> rs { -/// py_dict: PyDict => Foo { -/// let bar = py_dict.get_item("bar").unwrap().extract().unwrap(); -/// let baz = py_dict.get_item("baz").unwrap().extract().unwrap(); +/// py_dict: Py => Foo { +/// let bar = py_dict.as_ref(py).get_item("bar").unwrap().extract().unwrap(); +/// let baz = py_dict.as_ref(py).get_item("baz").unwrap().extract().unwrap(); /// Ok::<_, PyErr>(Foo { bar, baz }) /// }, -/// py_tuple: PyTuple => (String, f32) { +/// py_tuple: Py => (String, f32) { /// Ok::<_, PyErr>(( -/// py_tuple.get_item(0).unwrap().extract().unwrap(), -/// py_tuple.get_item(1).unwrap().extract().unwrap(), +/// py_tuple.as_ref(py).get_item(0).unwrap().extract().unwrap(), +/// py_tuple.as_ref(py).get_item(1).unwrap().extract().unwrap(), /// )) /// } /// }, @@ -353,16 +367,16 @@ macro_rules! py_wrap_struct { /// Used to implement `TryFrom

for PyFoo`. Any errors returned must be `PyErr`. /// /// $py_for_from should conventionally be `py` -- it is the name of the `Python<'_>` parameter. - $py_for_from: ident -> rs { + $($py_for_from: ident -> rs { $($py_ident: ident: $py_src: ty => $rs_dest: ty $to_rs: block),+ - }, + },)? /// Fallible transformation from Rust type `T` to Python type `P` where `T: From` /// Used to implement `TryFrom for P`. Any errors returned must be `PyErr`. /// /// $py_for_to should conventionally be `py` -- it is the name of the `Python<'_>` parameter. - rs -> $py_for_to: ident { + $(rs -> $py_for_to: ident { $($rs_ident: ident: $rs_src: ty => $py_dest: ty $to_py: block),+ - } + })? } ) => { $crate::py_wrap_type! { @@ -372,23 +386,22 @@ macro_rules! py_wrap_struct { $name($rs_from) $(as $py_class)?; } - $( - impl TryFrom<$crate::pyo3::Py<$py_src>> for $name { + $($( + impl TryFrom<$py_src> for $name { #[allow(unused_qualifications)] type Error = pyo3::PyErr; - fn try_from($py_ident: $crate::pyo3::Py<$py_src>) -> Result { + fn try_from($py_ident: $py_src) -> Result { $crate::pyo3::Python::with_gil(|$py_for_from| { let rust = { - let $py_ident: &$py_src = $py_ident.as_ref($py_for_from); $to_rs }?; Ok(Self::from(<$rs_from>::from(rust))) }) } } - )+ + )+)? - $( + $($( impl TryFrom<$name> for $py_dest { #[allow(unused_qualifications)] type Error = pyo3::PyErr; @@ -400,7 +413,7 @@ macro_rules! py_wrap_struct { }) } } - )+ + )+)? $crate::impl_as_mut_for_wrapper!($name); @@ -412,20 +425,52 @@ macro_rules! py_wrap_struct { pub fn new(py: $crate::pyo3::Python, input: $crate::pyo3::Py<$crate::pyo3::PyAny>) -> $crate::pyo3::PyResult { use $crate::pyo3::FromPyObject; - $( - if let Ok(item) = input.extract::<$crate::pyo3::Py<$py_src>>(py) { + $($( + if let Ok(item) = input.extract::<$py_src>(py) { return Self::try_from(item); } - )+ + )+)? Err($crate::pyo3::exceptions::PyValueError::new_err( - concat!("expected one of:" $(, " ", std::stringify!($py_src))+) + concat!("expected one of:" $($(, " ", std::stringify!($py_src))+)?) )) } } } } +/// (Internal) Helper macro to get the final type in a chain of conversions. +/// +/// Necessary because the pattern `$(=> $foo: ty)* => $bar: ty` is ambiguous. +#[macro_export] +macro_rules! private_ultimate_type { + ($type: ty) => { $type }; + ($type: ty, $($others: ty),+) => { $crate::private_ultimate_type!($($others),+) } +} + +/// (Internal) Helper macro to implement chained conversion through intermediate types, +/// where the type system cannot determine a path from the first to last item. +#[macro_export] +macro_rules! private_intermediate_to_python { + ($py: ident, &$item: ident $(=> $convert: ty)+) => {{ + $( + let $item: $convert = $crate::ToPython::<$convert>::to_python(&$item, $py)?; + )+ + Ok::<_, $crate::pyo3::PyErr>($item) + }} +} + +/// (Internal) Helper macro to implement chained conversion through intermediate types, +/// where the type system cannot determine a path from the last to first item. +#[macro_export] +macro_rules! private_intermediate_try_from_python { + ($py: ident, &$item: ident => $convert: ty $($(=> $delayed: ty)+)?) => {{ + $(let $item: $convert = $crate::private_intermediate_try_from_python!($py, &$item $(=> $delayed)+)?; + let $item = &$item;)? + <_ as $crate::PyTryFrom<$convert>>::py_try_from($py, $item) + }}; +} + /// Create a newtype wrapper for a Rust enum with unique 1-tuple variants. /// /// # Implements @@ -447,26 +492,44 @@ macro_rules! py_wrap_struct { /// use rigetti_pyo3::py_wrap_union_enum; /// use rigetti_pyo3::pyo3::prelude::*; /// use rigetti_pyo3::pyo3::types::*; +/// use std::collections::HashSet; /// /// #[derive(Clone)] /// pub enum TestEnum { +/// Unit, /// String(String), /// Integer(i32), /// UInteger(u32), +/// List(Vec>), /// Mapping(std::collections::HashMap), /// } /// /// py_wrap_union_enum! { /// PyTestEnum(TestEnum) as "TestEnum" { -/// // Syntax is (1): (2) => (3), where: +/// // Syntax is (1): (2) [=> (3)] [=> (4)] [...], where: /// // 1: The name used in generated methods /// // 2: The name of the Rust enum variant -/// // 3: The Python type the inner item must convert to -/// string: String => PyString, -/// int: Integer => PyInt, -/// uint: UInteger => PyInt, +/// // 3: The (Python) type the inner item must convert to (if it has associated data) +/// // 4: The (Python) type the type from (3) must convert to, etc. +/// unit: Unit, +/// // Read as "give the name `string` to variant `String`, which must convert (from +/// // a `String`) to a `String` and then to a `Py`." +/// // +/// // That is, `string` is used to generate methods `is_string`, `from_string`, etc.; +/// // the first `String` is the name of the variant, not the type (which is elided); +/// // the second `String` is the type to convert the elided type into, and `Py` is +/// // the final type to convert into. +/// // +/// // This specifies an unnecessary conversion from String => String to illustrate +/// // conversion chaining. +/// string: String => String => Py, +/// int: Integer => Py, +/// uint: UInteger => Py, +/// list: List => Py, +/// // Alternatively, in the case of `Vec` where `T` does not have conversion to `PyAny`. +/// // list: List => Vec> => Py, /// // Generates `from_dict`, `is_dict`, `as_dict`, `to_dict` -/// dict: Mapping => PyDict +/// dict: Mapping => Py /// } /// } /// ``` @@ -475,7 +538,7 @@ macro_rules! py_wrap_union_enum { ( $(#[$meta: meta])* $name: ident($rs_inner: ident) $(as $py_class: literal)? { - $($variant_name: ident: $variant: ident => $py_variant: ty),+ + $($variant_name: ident: $variant: ident $($(=> $convert: ty)+)?),+ } ) => { $crate::py_wrap_type! { @@ -485,66 +548,86 @@ macro_rules! py_wrap_union_enum { $crate::impl_as_mut_for_wrapper!($name); - ::paste::paste! { + $crate::paste::paste! { #[$crate::pyo3::pymethods] impl $name { #[new] - pub fn new(py: $crate::pyo3::Python, input: $crate::pyo3::Py<$crate::pyo3::PyAny>) -> $crate::pyo3::PyResult { + pub fn new(py: $crate::pyo3::Python, input: &$crate::pyo3::PyAny) -> $crate::pyo3::PyResult { $( - if let Ok(inner) = input.extract::<$crate::pyo3::Py<$py_variant>>(py) { - if let Ok(item) = $crate::PyTryFrom::py_try_from(py, inner) { - return Ok(Self($rs_inner::$variant(item))); - } - } + $( + if let Ok(inner) = <_ as $crate::PyTryFrom<$crate::pyo3::PyAny>>::py_try_from(py, input) { + let inner = &inner; + let converted = $crate::private_intermediate_try_from_python!(py, &inner $(=> $convert)+); + if let Ok(item) = converted { + return Ok(Self::from($rs_inner::$variant(item))); + } + } + )? )+ Err($crate::pyo3::exceptions::PyValueError::new_err( format!( "could not create {} from {}", stringify!($name), - input.as_ref(py).repr()? + input.repr()? ) )) } + #[allow(unreachable_code, unreachable_pattern)] pub fn inner(&self, py: $crate::pyo3::Python) -> $crate::pyo3::PyResult<$crate::pyo3::Py<$crate::pyo3::PyAny>> { match &self.0 { $( - $rs_inner::$variant(inner) => { - Ok($crate::pyo3::conversion::IntoPy::<$crate::pyo3::Py<$crate::pyo3::PyAny>>::into_py( - <_ as $crate::ToPython<$py_variant>>::to_python(&inner, py)?, - py, - )) - }, + $($rs_inner::$variant(inner) => { + Ok($crate::pyo3::conversion::IntoPy::<$crate::pyo3::Py<$crate::pyo3::PyAny>>::into_py( + $crate::private_intermediate_to_python!(py, &inner $(=> $convert)+)?, + py, + )) + },)? )+ + _ => { + use $crate::pyo3::exceptions::PyRuntimeError; + Err(PyRuntimeError::new_err("Enum variant has no inner data or is unimplemented")) + } } } $( - #[staticmethod] - pub fn [< from_ $variant_name >](py: $crate::pyo3::Python, inner: Py<$py_variant>) -> $crate::pyo3::PyResult { - $crate::PyTryFrom::<$py_variant>::py_try_from(py, inner) - .map($rs_inner::$variant) - .map(Self) - } - const fn [< is_ $variant_name >](&self) -> bool { - matches!(self.0, $rs_inner::$variant(_)) - } + match &self.0 { + $($rs_inner::$variant(_) => { + // Hacky stuff to enable the correct level of repetition in the macro. + let _: Option<$crate::private_ultimate_type!($($convert),+)> = None; - fn [< as_ $variant_name >](&self, py: $crate::pyo3::Python) -> Option> { - self.[< to_ $variant_name >](py).ok() + true + }, + )? + _ => false + } } + $( + #[staticmethod] + pub fn [< from_ $variant_name >](py: $crate::pyo3::Python, inner: $crate::private_ultimate_type!($($convert),+)) -> $crate::pyo3::PyResult { + let inner = &inner; + $crate::private_intermediate_try_from_python!(py, &inner $(=> $convert)+) + .map($rs_inner::$variant) + .map(Self) + } - fn [< to_ $variant_name >](&self, py: $crate::pyo3::Python) -> $crate::pyo3::PyResult> { - if let $rs_inner::$variant(inner) = &self.0 { - <_ as $crate::ToPython<$py_variant>>::to_python(&inner, py) - } else { - Err($crate::pyo3::exceptions::PyValueError::new_err( - concat!("expected self to be a ", stringify!($variant_name)) - )) + fn [< as_ $variant_name >](&self, py: $crate::pyo3::Python) -> Option<$crate::private_ultimate_type!($($convert),+)> { + self.[< to_ $variant_name >](py).ok() } - } + + fn [< to_ $variant_name >](&self, py: $crate::pyo3::Python) -> $crate::pyo3::PyResult<$crate::private_ultimate_type!($($convert),+)> { + if let $rs_inner::$variant(inner) = &self.0 { + $crate::private_intermediate_to_python!(py, &inner $(=> $convert)+) + } else { + Err($crate::pyo3::exceptions::PyValueError::new_err( + concat!("expected self to be a ", stringify!($variant_name)) + )) + } + } + )? )+ } } @@ -599,3 +682,120 @@ macro_rules! wrap_error { impl ::std::error::Error for $name {} }; } + +/// Wraps a data struct and makes (some of) its fields available to Python. +/// +/// # Implements +/// +/// - Everything implemented by [`py_wrap_type`]. +/// - [`PyWrapperMut`](crate::PyWrapperMut). +/// - `get_foo` and `set_foo` methods for field `foo`, which translate to `@property` and +/// `@foo.setter` in Python, i.e. allowing access to the field as a property. +/// +/// # Warning! +/// +/// The mutability of exposed fields may not work as you expect. +/// +/// Since objects are converted back and forth along the FFI boundary using `Clone`s, +/// pointers are not shared like in native Python. In native Python, this code runs +/// without issue: +/// +/// ```python +/// class Test: +/// def __init__(self): +/// self.inner = {} +/// +/// @property +/// def foo(self): +/// return self.inner +/// +/// @foo.setter +/// def foo(self, value): +/// self.inner = value +/// +/// c = Test() +/// d = c.inner +/// +/// d["a"] = ["a"] +/// assert "a" in c.inner +/// +/// d = c.foo +/// d["b"] = "b" +/// assert "b" in c.inner +/// ``` +/// +/// Using these bindings, assuming that this macro was used to create `Test`, the +/// equivalent would be: +/// +/// ```python +/// c = Test() +/// d = c.foo +/// +/// d["a"] = ["a"] +/// assert "a" not in c.foo +/// c.foo = d +/// assert "a" in c.foo +/// ``` +/// +/// # Example +/// +/// ``` +/// use rigetti_pyo3::pyo3::{Py, types::{PyInt, PyString}}; +/// use rigetti_pyo3::py_wrap_data_struct; +/// +/// #[derive(Clone)] +/// pub struct Person { +/// pub name: String, +/// pub age: u8, +/// } +/// +/// py_wrap_data_struct! { +/// PyPerson(Person) as "Person" { +/// name: String => Py, +/// age: u8 => Py +/// } +/// } +/// ``` +#[macro_export] +macro_rules! py_wrap_data_struct { + ( + $(#[$meta: meta])* + $name: ident($rs_inner: ty) $(as $class_name: literal)? { + $( + $field_name: ident: $field_rs_type: ty $(=> $convert: ty)+ + ),+ + } + ) => { + $crate::py_wrap_type! { + $( + #[$meta] + )* + $name($rs_inner) $(as $class_name)?; + } + + $crate::impl_as_mut_for_wrapper!($name); + + $crate::paste::paste! { + #[rigetti_pyo3::pyo3::pymethods] + impl $name { + $( + #[getter] + fn [< get_ $field_name >](&self, py: $crate::pyo3::Python<'_>) -> $crate::pyo3::PyResult<$crate::private_ultimate_type!($($convert),+)> { + use $crate::{PyWrapper, ToPython}; + let inner = &self.as_inner().$field_name; + $crate::private_intermediate_to_python!(py, &inner $(=> $convert)+) + } + + #[setter] + fn [< set_ $field_name >](&mut self, py: $crate::pyo3::Python<'_>, from: $crate::private_ultimate_type!($($convert),+)) -> $crate::pyo3::PyResult<()> { + use $crate::{PyTryFrom, PyWrapperMut}; + let from = &from; + let new_val: $field_rs_type = $crate::private_intermediate_try_from_python!(py, &from $(=> $convert)+)?; + self.as_inner_mut().$field_name = new_val; + Ok(()) + } + )+ + } + } + }; +}