diff --git a/.gitignore b/.gitignore index ea8c4bf..70fe7aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +.history /target diff --git a/Cargo.lock b/Cargo.lock index 2e7f101..3f56119 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1745,6 +1745,8 @@ name = "python-utils-macros" version = "0.1.0" dependencies = [ "python-utils-macros-impl", + "quote", + "syn", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 284fc1c..41eb436 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,11 +26,14 @@ derive_more = "2.1.0" ext-trait = "2.0.1" mitsein = "0.8" palette = "0.7" +proc-macro2 = "1.0.106" pyo3 = "0.27" pyo3-async-runtimes = "0.27" +quote = "1.0.46" serde = "1.0.228" snafu = "0.8.9" strum = "0.27.2" +syn = "2.0.118" tokio = "1.48.0" tracing = "0.1.43" typed-builder = "0.22" diff --git a/entrypoint/src/lib.rs b/entrypoint/src/lib.rs index 342f22f..d902ebc 100644 --- a/entrypoint/src/lib.rs +++ b/entrypoint/src/lib.rs @@ -146,7 +146,7 @@ async fn real_main( async { let jacob_phone_id = "galaxy_s21_ultra_1"; let jacob_phone_object_id = ObjectId::from_str(jacob_phone_id).unwrap(); - + let services = Python::attach(|py| home_assistant.services(py)).unwrap(); let mut interval = interval(Duration::from_secs(15)); @@ -163,7 +163,7 @@ async fn real_main( // filter: command::DoNotDisturbFilter::Off, // }, context, target, false).await; // dbg!(turn_off_result); - + // let instant = interval.tick().await; // let context: Option> = None; // let target: Option<()> = None; @@ -172,7 +172,7 @@ async fn real_main( // filter: command::DoNotDisturbFilter::AlarmsOnly, // }, context, target, false).await; // dbg!(alarms_only_result); - + // let instant = interval.tick().await; // let context: Option> = None; // let target: Option<()> = None; @@ -181,7 +181,7 @@ async fn real_main( // filter: command::DoNotDisturbFilter::PriorityOnly, // }, context, target, false).await; // dbg!(priority_only_result); - + // let instant = interval.tick().await; // let context: Option> = None; // let target: Option<()> = None; @@ -192,7 +192,8 @@ async fn real_main( // dbg!(total_silence_result); // } } - ).0 + ) + .0 } #[pyfunction] diff --git a/home-assistant/Cargo.toml b/home-assistant/Cargo.toml index 5205286..762c0ea 100644 --- a/home-assistant/Cargo.toml +++ b/home-assistant/Cargo.toml @@ -24,7 +24,7 @@ once_cell = "1.21.3" protocol = { path = "../protocol" } pyo3 = { workspace = true } pyo3-async-runtimes = { workspace = true, features = ["tokio-runtime"] } -python-utils = { path = "../python-utils" } +python-utils = { path = "../python-utils", features = ["macros"] } snafu = { workspace = true } strum = { workspace = true, features = ["derive"] } tokio = { workspace = true } diff --git a/home-assistant/src/entity_id.rs b/home-assistant/src/entity_id.rs index a3bf661..787c377 100644 --- a/home-assistant/src/entity_id.rs +++ b/home-assistant/src/entity_id.rs @@ -1,18 +1,13 @@ -use std::{convert::Infallible, fmt::Display, str::FromStr}; - -use pyo3::{ - exceptions::{PyException, PyValueError}, - prelude::*, - types::PyString, -}; +use python_utils::{FromPyFromStr, ToStrToPy}; use snafu::{ResultExt, Snafu}; +use std::{fmt::Display, str::FromStr}; use super::{ domain::Domain, object_id::{ObjectId, ObjectIdParsingError}, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, FromPyFromStr, ToStrToPy)] pub struct EntityId(pub Domain, pub ObjectId); #[derive(Debug, Clone, Snafu)] @@ -47,47 +42,3 @@ impl Display for EntityId { write!(f, "{domain}.{object_id}") } } - -#[derive(Debug, Snafu)] -pub enum ExtractEntityIdError { - /// couldn't extract the object as a string - ExtractStringError { source: PyErr }, - - /// couldn't parse the string as an [`EntityId`] - ParseError { source: EntityIdParsingError }, -} - -impl From for PyErr { - fn from(error: ExtractEntityIdError) -> Self { - match &error { - ExtractEntityIdError::ExtractStringError { .. } => { - PyException::new_err(error.to_string()) - } - ExtractEntityIdError::ParseError { .. } => PyValueError::new_err(error.to_string()), - } - } -} - -// TODO: replace with a derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one -impl<'a, 'py> FromPyObject<'a, 'py> for EntityId { - type Error = ExtractEntityIdError; - - fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { - let s = ob.extract().context(ExtractStringSnafu)?; - let entity_id = EntityId::from_str(s).context(ParseSnafu)?; - - Ok(entity_id) - } -} - -// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one -impl<'py> IntoPyObject<'py> for EntityId { - type Target = PyString; - type Output = Bound<'py, Self::Target>; - type Error = Infallible; - - fn into_pyobject(self, py: Python<'py>) -> Result { - let s = self.to_string(); - s.into_pyobject(py) - } -} diff --git a/home-assistant/src/event/context/id.rs b/home-assistant/src/event/context/id.rs index b23ab9a..f7d0168 100644 --- a/home-assistant/src/event/context/id.rs +++ b/home-assistant/src/event/context/id.rs @@ -1,36 +1,28 @@ -use std::{convert::Infallible, sync::Arc}; +use std::{convert::Infallible, fmt::Display, str::FromStr, sync::Arc}; -use pyo3::{exceptions::PyTypeError, prelude::*, types::PyString}; -use snafu::{ResultExt, Snafu}; +use python_utils::{FromPyFromStr, ToStrToPy}; use ulid::Ulid; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, FromPyFromStr, ToStrToPy)] pub enum Id { Ulid(Ulid), Other(Arc), } -#[derive(Debug, Snafu)] -pub enum ExtractIdError { - /// couldn't extract the given object as a string - ExtractStringError { source: PyErr }, -} - -impl From for PyErr { - fn from(error: ExtractIdError) -> Self { - match &error { - ExtractIdError::ExtractStringError { .. } => PyTypeError::new_err(error.to_string()), +impl From for Id { + fn from(s: String) -> Self { + if let Ok(ulid) = s.parse() { + Id::Ulid(ulid) + } else { + Id::Other(s.into()) } } } -// TODO: replace with a derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one -impl<'a, 'py> FromPyObject<'a, 'py> for Id { - type Error = ExtractIdError; - - fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { - let s = ob.extract::<&str>().context(ExtractStringSnafu)?; +impl FromStr for Id { + type Err = Infallible; + fn from_str(s: &str) -> Result { if let Ok(ulid) = s.parse() { Ok(Id::Ulid(ulid)) } else { @@ -39,18 +31,11 @@ impl<'a, 'py> FromPyObject<'a, 'py> for Id { } } -// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one -impl<'py> IntoPyObject<'py> for Id { - type Target = PyString; - - type Output = Bound<'py, Self::Target>; - - type Error = Infallible; - - fn into_pyobject(self, py: Python<'py>) -> Result { +impl Display for Id { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Id::Ulid(ulid) => ulid.to_string().into_pyobject(py), - Id::Other(id) => id.into_pyobject(py), + Id::Ulid(ulid) => write!(f, "{ulid}"), + Id::Other(other) => write!(f, "{other}"), } } } diff --git a/home-assistant/src/event/specific/state_changed.rs b/home-assistant/src/event/specific/state_changed.rs index 7b55f81..c4d14ff 100644 --- a/home-assistant/src/event/specific/state_changed.rs +++ b/home-assistant/src/event/specific/state_changed.rs @@ -1,42 +1,39 @@ -use pyo3::exceptions::{PyTypeError, PyValueError}; -use pyo3::prelude::*; -use snafu::{ResultExt, Snafu}; +use std::fmt::Display; +use std::str::FromStr; + +use pyo3::FromPyObject; +use python_utils::{FromPyFromStr, ToStrToPy}; +use snafu::Snafu; use crate::{entity_id::EntityId, state_object::StateObject}; -#[derive(Debug, Clone)] +// TODO: replace with a derive(PyFromStrLiteral) / #[literal = "state_changed"] once I learn how to make something like that and see about serde or strum integration or inspiration +#[derive(Debug, Clone, FromPyFromStr, ToStrToPy)] pub struct Type; +/// expected a string of value "state_changed", but got {actual} #[derive(Debug, Snafu)] -pub enum ExtractTypeError { - /// couldn't extract this object as a string - ExtractStringError { source: PyErr }, - - /// expected a string of value "state_changed", but got {actual} - UnexpectedValue { actual: String }, +pub struct ParseTypeError { + actual: String, } -impl From for PyErr { - fn from(error: ExtractTypeError) -> Self { - match &error { - ExtractTypeError::ExtractStringError { .. } => PyTypeError::new_err(error.to_string()), - ExtractTypeError::UnexpectedValue { .. } => PyValueError::new_err(error.to_string()), +impl FromStr for Type { + type Err = ParseTypeError; + + fn from_str(s: &str) -> Result { + if s == "state_changed" { + Ok(Self) + } else { + Err(ParseTypeError { + actual: s.to_owned(), + }) } } } -// TODO: replace with a derive(PyFromStrLiteral) / #[literal = "state_changed"] once I learn how to make something like that and see about serde or strum integration or inspiration -impl<'a, 'py> FromPyObject<'a, 'py> for Type { - type Error = ExtractTypeError; - - fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { - let s = ob.extract::<&str>().context(ExtractStringSnafu)?; - - if s == "state_changed" { - Ok(Type) - } else { - Err(ExtractTypeError::UnexpectedValue { actual: s.into() }) - } +impl Display for Type { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "state_changed") } } diff --git a/home-assistant/src/notify/service/mobile_app/mod.rs b/home-assistant/src/notify/service/mobile_app/mod.rs index 4944eec..8c062d6 100644 --- a/home-assistant/src/notify/service/mobile_app/mod.rs +++ b/home-assistant/src/notify/service/mobile_app/mod.rs @@ -1,7 +1,10 @@ use std::{convert::Infallible, str::FromStr}; -use pyo3::{types::{PyDict, PyString, PyDictMethods}, Bound, IntoPyObject, Python, PyErr}; -use python_utils::IntoPyObjectViaDisplay; +use pyo3::{ + types::{PyDict, PyDictMethods, PyString}, + Bound, IntoPyObject, PyErr, Python, +}; +use python_utils::{FromPyFromStr, IntoPyObjectViaDisplay, ToStrToPy}; use snafu::Snafu; use strum::EnumString; use url::Url; @@ -84,7 +87,7 @@ impl TryFrom for NonSpecialMessage { } /// How much of a notification is visible on the lock screen -#[derive(Debug, Clone, Default, EnumString, strum::Display)] +#[derive(Debug, Clone, Default, EnumString, strum::Display, FromPyFromStr, ToStrToPy)] #[strum(serialize_all = "snake_case")] pub enum Visibility { /// always show all notification content @@ -98,38 +101,14 @@ pub enum Visibility { Secret, } -// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one -impl<'py> IntoPyObject<'py> for Visibility { - type Target = PyString; - type Output = Bound<'py, Self::Target>; - type Error = Infallible; - - fn into_pyobject(self, py: Python<'py>) -> Result { - let s = self.to_string(); - s.into_pyobject(py) - } -} - -#[derive(Debug, Clone, EnumString, strum::Display)] +#[derive(Debug, Clone, EnumString, strum::Display, FromPyFromStr, ToStrToPy)] pub enum Behavior { /// prompt for text to return with the event #[strum(serialize = "textInput")] TextInput, } -// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one -impl<'py> IntoPyObject<'py> for Behavior { - type Target = PyString; - type Output = Bound<'py, Self::Target>; - type Error = Infallible; - - fn into_pyobject(self, py: Python<'py>) -> Result { - let s = self.to_string(); - s.into_pyobject(py) - } -} - -#[derive(Debug, Clone, Default, EnumString, strum::Display)] +#[derive(Debug, Clone, Default, EnumString, strum::Display, FromPyFromStr, ToStrToPy)] #[strum(serialize_all = "camelCase")] pub enum ActivationMode { /// launch the app when tapped @@ -139,18 +118,6 @@ pub enum ActivationMode { Background, } -// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one -impl<'py> IntoPyObject<'py> for ActivationMode { - type Target = PyString; - type Output = Bound<'py, Self::Target>; - type Error = Infallible; - - fn into_pyobject(self, py: Python<'py>) -> Result { - let s = self.to_string(); - s.into_pyobject(py) - } -} - // TODO: better typed versions like `CallNumber` or `OpenWebpage` where Action: From and Action: From #[derive(Debug, Clone, IntoPyObject, typed_builder::TypedBuilder)] #[builder(field_defaults(default, setter(strip_option(fallback_suffix = "_option"))))] @@ -190,7 +157,7 @@ pub struct Action { pub icon: Option, } -#[derive(Debug, Clone, Default, EnumString, strum::Display)] +#[derive(Debug, Clone, Default, EnumString, strum::Display, FromPyFromStr, ToStrToPy)] #[strum(serialize_all = "snake_case", suffix = "_stream")] pub enum MediaStream { Alarm, @@ -203,18 +170,6 @@ pub enum MediaStream { System, } -// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one -impl<'py> IntoPyObject<'py> for MediaStream { - type Target = PyString; - type Output = Bound<'py, Self::Target>; - type Error = Infallible; - - fn into_pyobject(self, py: Python<'py>) -> Result { - let s = self.to_string(); - s.into_pyobject(py) - } -} - #[derive(Debug, Default, Clone, typed_builder::TypedBuilder)] #[builder(field_defaults(default, setter(strip_option(fallback_suffix = "_option"))))] pub struct NotifyMobileAppServiceDataData { @@ -232,7 +187,13 @@ impl<'py> IntoPyObject<'py> for NotifyMobileAppServiceDataData { type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - let NotifyMobileAppServiceDataData { actions, command, media_stream, tts_text, visibility } = self; + let NotifyMobileAppServiceDataData { + actions, + command, + media_stream, + tts_text, + visibility, + } = self; let dict = PyDict::new(py); @@ -279,7 +240,11 @@ impl<'py> IntoPyObject<'py> for NotifyMobileAppServiceData { type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - let NotifyMobileAppServiceData { message, title, data } = self; + let NotifyMobileAppServiceData { + message, + title, + data, + } = self; let dict = PyDict::new(py); @@ -293,4 +258,4 @@ impl<'py> IntoPyObject<'py> for NotifyMobileAppServiceData { Ok(dict) } -} \ No newline at end of file +} diff --git a/home-assistant/src/notify/service/mobile_app/standard.rs b/home-assistant/src/notify/service/mobile_app/standard.rs index c2c7a89..0d716bd 100644 --- a/home-assistant/src/notify/service/mobile_app/standard.rs +++ b/home-assistant/src/notify/service/mobile_app/standard.rs @@ -1,7 +1,6 @@ use std::str::FromStr; use mitsein::vec1::Vec1; -use pyo3::{types::PyAnyMethods, IntoPyObject, Python}; use crate::{ object_id::ObjectId, diff --git a/home-assistant/src/sensor/device_classes/power.rs b/home-assistant/src/sensor/device_classes/power.rs index 581b097..5be534c 100644 --- a/home-assistant/src/sensor/device_classes/power.rs +++ b/home-assistant/src/sensor/device_classes/power.rs @@ -1,4 +1,4 @@ -use std::{future::Future, str::FromStr, sync::Arc}; +use std::{future::Future, sync::Arc}; use emitter_and_signal::{Signal, SignalExt}; use pyo3::{ diff --git a/home-assistant/src/slug.rs b/home-assistant/src/slug.rs index 2b668f4..baeee53 100644 --- a/home-assistant/src/slug.rs +++ b/home-assistant/src/slug.rs @@ -1,10 +1,10 @@ use std::{str::FromStr, sync::Arc}; use pyo3::{exceptions::PyValueError, PyErr}; +use python_utils::{FromPyFromStr, ToStrToPy}; use snafu::Snafu; -// TODO: derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one -#[derive(Debug, Clone, derive_more::Display)] +#[derive(Debug, Clone, derive_more::Display, FromPyFromStr, ToStrToPy)] pub struct Slug(Arc); #[derive(Debug, Clone, Snafu)] diff --git a/home-assistant/src/state/error_state.rs b/home-assistant/src/state/error_state.rs index 2c50931..1037256 100644 --- a/home-assistant/src/state/error_state.rs +++ b/home-assistant/src/state/error_state.rs @@ -1,55 +1,12 @@ -use std::str::FromStr; - -use pyo3::{ - exceptions::{PyException, PyValueError}, - prelude::*, -}; -use snafu::{ResultExt, Snafu}; +use python_utils::{FromPyFromStr, ToStrToPy}; use strum::EnumString; /// A state in Home Assistant that is known to represent an error of some kind: /// * `unavailable` (the device is likely offline or unreachable from the Home Assistant instance) /// * `unknown` (I don't know how to explain this one) -#[derive(Debug, Clone, EnumString, strum::Display)] +#[derive(Debug, Clone, EnumString, strum::Display, FromPyFromStr, ToStrToPy)] #[strum(serialize_all = "snake_case")] pub enum ErrorState { Unavailable, Unknown, } - -#[derive(Debug, Snafu)] -pub enum ExtractErrorStateError { - /// couldn't extract the object as a string - ExtractStringError { source: PyErr }, - - /// the string had an unexpected value - UnexpectedValue { - source: ::Err, - }, -} - -impl From for PyErr { - fn from(error: ExtractErrorStateError) -> Self { - match &error { - ExtractErrorStateError::ExtractStringError { .. } => { - PyException::new_err(error.to_string()) - } - ExtractErrorStateError::UnexpectedValue { .. } => { - PyValueError::new_err(error.to_string()) - } - } - } -} - -// TODO: replace with a derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one -impl<'a, 'py> FromPyObject<'a, 'py> for ErrorState { - type Error = ExtractErrorStateError; - - fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { - let s = ob.extract::().context(ExtractStringSnafu)?; - - let state = ErrorState::from_str(&s).context(UnexpectedValueSnafu)?; - - Ok(state) - } -} diff --git a/home-assistant/src/state/mod.rs b/home-assistant/src/state/mod.rs index 185eab0..ce74519 100644 --- a/home-assistant/src/state/mod.rs +++ b/home-assistant/src/state/mod.rs @@ -1,7 +1,6 @@ use std::{convert::Infallible, str::FromStr}; -use pyo3::{exceptions::PyException, prelude::*}; -use snafu::{ResultExt, Snafu}; +use python_utils::{FromPyFromStr, ToStrToPy}; pub mod error_state; pub mod unexpected_state; @@ -9,13 +8,30 @@ pub mod unexpected_state; pub use error_state::ErrorState; pub use unexpected_state::UnexpectedState; -#[derive(Debug, Clone, derive_more::Display)] +#[derive(Debug, Clone, derive_more::Display, FromPyFromStr, ToStrToPy)] pub enum HomeAssistantState { Ok(State), Err(ErrorState), UnexpectedErr(UnexpectedState), } +impl From for HomeAssistantState +where + State: FromStr, +{ + fn from(s: String) -> Self { + if let Ok(ok) = State::from_str(&s) { + return HomeAssistantState::Ok(ok); + } + + if let Ok(error) = ErrorState::from_str(&s) { + return HomeAssistantState::Err(error); + } + + HomeAssistantState::UnexpectedErr(UnexpectedState(s.into())) + } +} + impl FromStr for HomeAssistantState { type Err = Infallible; @@ -31,34 +47,3 @@ impl FromStr for HomeAssistantState { Ok(HomeAssistantState::UnexpectedErr(UnexpectedState(s.into()))) } } - -#[derive(Debug, Snafu)] -pub enum ExtractHomeAssistantStateError { - /// couldn't extract the object as a string - ExtractStringError { source: PyErr }, -} - -impl From for PyErr { - fn from(error: ExtractHomeAssistantStateError) -> Self { - match &error { - ExtractHomeAssistantStateError::ExtractStringError { .. } => { - PyException::new_err(error.to_string()) - } - } - } -} - -// TODO: replace with a derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one -impl<'a, 'py, State: FromStr + FromPyObject<'a, 'py>> FromPyObject<'a, 'py> - for HomeAssistantState -{ - type Error = ExtractHomeAssistantStateError; - - fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { - let s = ob.extract::<&str>().context(ExtractStringSnafu)?; - - let Ok(state) = s.parse(); - - Ok(state) - } -} diff --git a/home-assistant/src/unit_of_measurement/power.rs b/home-assistant/src/unit_of_measurement/power.rs index 852f6b2..ef9de98 100644 --- a/home-assistant/src/unit_of_measurement/power.rs +++ b/home-assistant/src/unit_of_measurement/power.rs @@ -1,10 +1,6 @@ use std::str::FromStr; -use pyo3::{ - exceptions::{PyException, PyValueError}, - prelude::*, -}; -use snafu::{ResultExt, Snafu}; +use python_utils::{FromPyFromStr, ToStrToPy}; use strum::EnumString; use uom::{ si::{ @@ -18,7 +14,7 @@ use uom::{ }; /// Power units -#[derive(Debug, Clone, Copy, EnumString, strum::Display)] +#[derive(Debug, Clone, Copy, EnumString, strum::Display, FromPyFromStr, ToStrToPy)] #[strum(serialize_all = "snake_case")] pub enum UnitOfMeasurement { #[strum(serialize = "mW")] @@ -37,42 +33,6 @@ pub enum UnitOfMeasurement { BtuPerhour, } -#[derive(Debug, Snafu)] -pub enum ExtractUnitOfMeasurementError { - /// couldn't extract the object as a string - ExtractStringError { source: PyErr }, - - /// couldn't parse the string as a [`UnitOfMeasurement`] - ParseError { - source: ::Err, - }, -} - -impl From for PyErr { - fn from(error: ExtractUnitOfMeasurementError) -> Self { - match &error { - ExtractUnitOfMeasurementError::ExtractStringError { .. } => { - PyException::new_err(error.to_string()) - } - ExtractUnitOfMeasurementError::ParseError { .. } => { - PyValueError::new_err(error.to_string()) - } - } - } -} - -// TODO: replace with a derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one -impl<'a, 'py> FromPyObject<'a, 'py> for UnitOfMeasurement { - type Error = ExtractUnitOfMeasurementError; - - fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { - let s = obj.extract().context(ExtractStringSnafu)?; - let unit_of_measurement = UnitOfMeasurement::from_str(s).context(ParseSnafu)?; - - Ok(unit_of_measurement) - } -} - impl UnitOfMeasurement { pub fn into_uom(&self, amount: V) -> Power where diff --git a/python-utils-macros-impl/Cargo.toml b/python-utils-macros-impl/Cargo.toml index 846b0dd..ed7c77f 100644 --- a/python-utils-macros-impl/Cargo.toml +++ b/python-utils-macros-impl/Cargo.toml @@ -4,6 +4,6 @@ version = "0.1.0" edition = "2024" [dependencies] -proc-macro2 = "1.0.106" -quote = "1.0.46" -syn = "2.0.118" +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true } diff --git a/python-utils-macros-impl/src/from_py_from_str.rs b/python-utils-macros-impl/src/from_py_from_str.rs new file mode 100644 index 0000000..759714c --- /dev/null +++ b/python-utils-macros-impl/src/from_py_from_str.rs @@ -0,0 +1,56 @@ +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; +use syn::{ + DeriveInput, GenericParam, + parse::{Parse, ParseStream}, + parse_quote, +}; + +pub struct FromPyFromStr { + derive_input: DeriveInput, +} + +impl Parse for FromPyFromStr { + fn parse(input: ParseStream) -> syn::Result { + let derive_input = input.parse()?; + + let this = Self { derive_input }; + + Ok(this) + } +} + +impl ToTokens for FromPyFromStr { + fn to_tokens(&self, tokens: &mut TokenStream) { + let name = &self.derive_input.ident; + + let (_, ty_generics, where_clause) = self.derive_input.generics.split_for_impl(); + let mut generics = self.derive_input.generics.clone(); + + let a_lifetime: GenericParam = parse_quote!('a); + let py_lifetime: GenericParam = parse_quote!('py); + + generics.params.push(a_lifetime.clone()); + generics.params.push(py_lifetime.clone()); + + let (impl_generics, _, _) = generics.split_for_impl(); + + let output = quote! { + impl #impl_generics ::pyo3::conversion::FromPyObject<#a_lifetime, #py_lifetime> for #name #ty_generics #where_clause + where + #name #ty_generics: ::std::str::FromStr, + <#name #ty_generics as ::std::str::FromStr>::Err: ::std::fmt::Display, + { + type Error = ::python_utils::ExtractPyObjectViaParseError<<#name #ty_generics as ::std::str::FromStr>::Err>; + + fn extract(obj: ::pyo3::Borrowed<#a_lifetime, #py_lifetime, ::pyo3::types::PyAny>) -> ::core::result::Result { + ::python_utils::FromPyObjectViaParse::extract(obj).map(|wrapper| wrapper.0) + } + } + }; + + dbg!(&output.to_string()); + + tokens.extend(output); + } +} diff --git a/python-utils-macros-impl/src/lib.rs b/python-utils-macros-impl/src/lib.rs index 08e9809..c1db754 100644 --- a/python-utils-macros-impl/src/lib.rs +++ b/python-utils-macros-impl/src/lib.rs @@ -1,17 +1,5 @@ -use proc_macro2::TokenStream; -use quote::quote; -use syn::{DeriveInput, parse_macro_input}; +mod from_py_from_str; +mod to_str_to_py; -pub fn py_from_str(input: TokenStream) -> TokenStream { - let derive_input: DeriveInput = parse_macro_input!(input); - - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - - let expanded = quote! { - impl #impl_generics MyTrait for #name #ty_generics #where_clause { - // ... - } - }; - - expanded -} +pub use from_py_from_str::FromPyFromStr; +pub use to_str_to_py::ToStrToPy; diff --git a/python-utils-macros-impl/src/to_str_to_py.rs b/python-utils-macros-impl/src/to_str_to_py.rs new file mode 100644 index 0000000..0ad95ba --- /dev/null +++ b/python-utils-macros-impl/src/to_str_to_py.rs @@ -0,0 +1,56 @@ +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; +use syn::{ + DeriveInput, GenericParam, + parse::{Parse, ParseStream}, + parse_quote, +}; + +pub struct ToStrToPy { + derive_input: DeriveInput, +} + +impl Parse for ToStrToPy { + fn parse(input: ParseStream) -> syn::Result { + let derive_input = input.parse()?; + + let this = Self { derive_input }; + + Ok(this) + } +} + +impl ToTokens for ToStrToPy { + fn to_tokens(&self, tokens: &mut TokenStream) { + let name = &self.derive_input.ident; + + let (_, ty_generics, where_clause) = self.derive_input.generics.split_for_impl(); + let mut generics = self.derive_input.generics.clone(); + + let py_lifetime: GenericParam = parse_quote!('py); + + generics.params.push(py_lifetime.clone()); + + let (impl_generics, _, _) = generics.split_for_impl(); + + let output = quote! { + impl #impl_generics ::pyo3::conversion::IntoPyObject<#py_lifetime> for #name #ty_generics #where_clause + where + #name #ty_generics: ::std::fmt::Display + { + type Target = ::pyo3::types::PyString; + type Output = ::pyo3::Bound<#py_lifetime, Self::Target>; + type Error = ::std::convert::Infallible; + + fn into_pyobject(self, py: ::pyo3::Python<#py_lifetime>) -> ::core::result::Result { + let s = ::std::string::ToString::to_string(&self); + ::pyo3::conversion::IntoPyObject::into_pyobject(s, py) + } + } + }; + + dbg!(&output.to_string()); + + tokens.extend(output); + } +} diff --git a/python-utils-macros/Cargo.toml b/python-utils-macros/Cargo.toml index 4f0319c..2037af7 100644 --- a/python-utils-macros/Cargo.toml +++ b/python-utils-macros/Cargo.toml @@ -9,3 +9,5 @@ proc-macro = true [dependencies] python-utils-macros-impl = { path = "../python-utils-macros-impl" } +quote = { workspace = true } +syn = { workspace = true } diff --git a/python-utils-macros/src/lib.rs b/python-utils-macros/src/lib.rs index cd12e85..6dcf636 100644 --- a/python-utils-macros/src/lib.rs +++ b/python-utils-macros/src/lib.rs @@ -1,6 +1,15 @@ -use proc_macro::proc_macro_derive; +use proc_macro::TokenStream; +use quote::quote; +use syn::parse_macro_input; -#[proc_macro_derive(PyFromStr)] -pub fn py_from_str(input: TokenStream) -> TokenStream { - python_utils_macros_impl::py_from_str(input.into()).into() +#[proc_macro_derive(FromPyFromStr)] +pub fn from_py_from_str(input: TokenStream) -> TokenStream { + let item: python_utils_macros_impl::FromPyFromStr = parse_macro_input!(input); + quote! { #item }.into() +} + +#[proc_macro_derive(ToStrToPy)] +pub fn to_str_to_py(input: TokenStream) -> TokenStream { + let item: python_utils_macros_impl::ToStrToPy = parse_macro_input!(input); + quote! { #item }.into() } diff --git a/python-utils/Cargo.toml b/python-utils/Cargo.toml index 45b279f..9a4f715 100644 --- a/python-utils/Cargo.toml +++ b/python-utils/Cargo.toml @@ -4,8 +4,11 @@ version = "0.1.0" edition = "2021" license = { workspace = true } +[features] +macros = ["dep:python-utils-macros"] + [dependencies] derive_more = { workspace = true } pyo3 = { workspace = true } -python-utils-macros = { path = "../python-utils-macros" } +python-utils-macros = { optional = true, path = "../python-utils-macros" } snafu = { workspace = true } diff --git a/python-utils/src/from_pyobject_via_parse.rs b/python-utils/src/from_pyobject_via_parse.rs new file mode 100644 index 0000000..efeb36d --- /dev/null +++ b/python-utils/src/from_pyobject_via_parse.rs @@ -0,0 +1,73 @@ +use std::{ + fmt::{Debug, Display}, + str::FromStr, + sync::Arc, +}; + +use pyo3::{ + exceptions::{PyException, PyValueError}, + Borrowed, FromPyObject, PyAny, PyErr, +}; + +#[derive(Debug, Clone, derive_more::FromStr)] +pub struct FromPyObjectViaParse(pub T); + +#[derive(Debug, Clone)] +pub enum ExtractPyObjectViaParseError { + /// couldn't extract the object as a string + ExtractStringError { source: Arc }, + + /// couldn't parse the string as an instance of this Rust type + ParseError { source: ParseError }, +} + +impl Display for ExtractPyObjectViaParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ExtractPyObjectViaParseError::ExtractStringError { source } => write!(f, "{}", source), + ExtractPyObjectViaParseError::ParseError { source } => write!(f, "{}", source), + } + } +} + +impl std::error::Error for ExtractPyObjectViaParseError where + ExtractPyObjectViaParseError: Debug + Display +{ +} + +impl From> for PyErr +where + ExtractPyObjectViaParseError: Display, +{ + fn from(error: ExtractPyObjectViaParseError) -> Self { + match &error { + ExtractPyObjectViaParseError::ExtractStringError { .. } => { + PyException::new_err(error.to_string()) + } + ExtractPyObjectViaParseError::ParseError { .. } => { + PyValueError::new_err(error.to_string()) + } + } + } +} + +impl<'a, 'py, T> FromPyObject<'a, 'py> for FromPyObjectViaParse +where + T: FromStr, + PyErr: From::Err>>, +{ + type Error = ExtractPyObjectViaParseError<::Err>; + + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { + let s = obj + .extract::<&str>() + .map_err(Arc::new) + .map_err(|e| ExtractPyObjectViaParseError::ExtractStringError { source: e })?; + + let t = s + .parse() + .map_err(|e| ExtractPyObjectViaParseError::ParseError { source: e })?; + + Ok(FromPyObjectViaParse(t)) + } +} diff --git a/python-utils/src/into_pyobject_via_display.rs b/python-utils/src/into_pyobject_via_display.rs new file mode 100644 index 0000000..8ee641c --- /dev/null +++ b/python-utils/src/into_pyobject_via_display.rs @@ -0,0 +1,20 @@ +use std::{convert::Infallible, fmt::Display}; + +use pyo3::{types::PyString, Bound, IntoPyObject, Python}; + +#[derive(Debug, Clone, derive_more::Display)] +pub struct IntoPyObjectViaDisplay(pub T); + +impl<'py, T> IntoPyObject<'py> for IntoPyObjectViaDisplay +where + T: Display, +{ + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let s = self.to_string(); + s.into_pyobject(py) + } +} diff --git a/python-utils/src/lib.rs b/python-utils/src/lib.rs index 9e07b86..7a087b9 100644 --- a/python-utils/src/lib.rs +++ b/python-utils/src/lib.rs @@ -1,15 +1,18 @@ -use std::{convert::Infallible, fmt::Display, str::FromStr, sync::Arc}; - use pyo3::{ - exceptions::{PyException, PyTypeError, PyValueError}, + exceptions::{PyException, PyTypeError}, prelude::*, - types::PyString, }; use snafu::{ResultExt, Snafu}; -pub use python_utils_macros::PyFromStr; +#[cfg(feature = "macros")] +pub use python_utils_macros::{FromPyFromStr, ToStrToPy}; +pub mod from_pyobject_via_parse; +pub mod into_pyobject_via_display; pub mod none; + +pub use from_pyobject_via_parse::{ExtractPyObjectViaParseError, FromPyObjectViaParse}; +pub use into_pyobject_via_display::IntoPyObjectViaDisplay; pub use none::IsNone; /// Create a GIL-independent reference @@ -83,70 +86,3 @@ pub fn validate_type_by_name( return Ok(()); } - -#[derive(Debug, Clone, derive_more::Display)] -pub struct IntoPyObjectViaDisplay(pub T); - -impl<'py, T> IntoPyObject<'py> for IntoPyObjectViaDisplay -where - T: Display, -{ - type Target = PyString; - type Output = Bound<'py, Self::Target>; - type Error = Infallible; - - fn into_pyobject(self, py: Python<'py>) -> Result { - let s = self.to_string(); - s.into_pyobject(py) - } -} - -#[derive(Debug, Clone, derive_more::FromStr)] -pub struct FromPyObjectViaParse(pub T); - -#[derive(Debug, Clone, Snafu)] -pub enum ExtractPyObjectViaParseError -where - ParseError: 'static + snafu::Error, -{ - /// couldn't extract the object as a string - ExtractStringError { source: Arc }, - - /// couldn't parse the string as an instance of this Rust type - ParseError { source: ParseError }, -} - -impl From> for PyErr -where - E: 'static + snafu::Error, -{ - fn from(error: ExtractPyObjectViaParseError) -> Self { - match &error { - ExtractPyObjectViaParseError::ExtractStringError { .. } => { - PyException::new_err(error.to_string()) - } - ExtractPyObjectViaParseError::ParseError { .. } => { - PyValueError::new_err(error.to_string()) - } - } - } -} - -impl<'a, 'py, T> FromPyObject<'a, 'py> for FromPyObjectViaParse -where - T: FromStr, - ::Err: 'static + snafu::Error, - PyErr: From::Err>>, -{ - type Error = ExtractPyObjectViaParseError<::Err>; - - fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { - let s = obj - .extract::<&str>() - .map_err(Arc::new) - .context(ExtractStringSnafu)?; - let t = T::from_str(s).context(ParseSnafu)?; - - Ok(FromPyObjectViaParse(t)) - } -}