feat: FromPyFromStr and ToStrToPy macros YAY

This commit is contained in:
J / Jacob Babich
2026-07-09 01:35:49 -04:00
parent c637ad2d76
commit 9a9cefee37
25 changed files with 343 additions and 394 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
.history
/target /target

2
Cargo.lock generated
View File

@@ -1745,6 +1745,8 @@ name = "python-utils-macros"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"python-utils-macros-impl", "python-utils-macros-impl",
"quote",
"syn",
] ]
[[package]] [[package]]

View File

@@ -26,11 +26,14 @@ derive_more = "2.1.0"
ext-trait = "2.0.1" ext-trait = "2.0.1"
mitsein = "0.8" mitsein = "0.8"
palette = "0.7" palette = "0.7"
proc-macro2 = "1.0.106"
pyo3 = "0.27" pyo3 = "0.27"
pyo3-async-runtimes = "0.27" pyo3-async-runtimes = "0.27"
quote = "1.0.46"
serde = "1.0.228" serde = "1.0.228"
snafu = "0.8.9" snafu = "0.8.9"
strum = "0.27.2" strum = "0.27.2"
syn = "2.0.118"
tokio = "1.48.0" tokio = "1.48.0"
tracing = "0.1.43" tracing = "0.1.43"
typed-builder = "0.22" typed-builder = "0.22"

View File

@@ -146,7 +146,7 @@ async fn real_main(
async { async {
let jacob_phone_id = "galaxy_s21_ultra_1"; let jacob_phone_id = "galaxy_s21_ultra_1";
let jacob_phone_object_id = ObjectId::from_str(jacob_phone_id).unwrap(); let jacob_phone_object_id = ObjectId::from_str(jacob_phone_id).unwrap();
let services = Python::attach(|py| home_assistant.services(py)).unwrap(); let services = Python::attach(|py| home_assistant.services(py)).unwrap();
let mut interval = interval(Duration::from_secs(15)); let mut interval = interval(Duration::from_secs(15));
@@ -163,7 +163,7 @@ async fn real_main(
// filter: command::DoNotDisturbFilter::Off, // filter: command::DoNotDisturbFilter::Off,
// }, context, target, false).await; // }, context, target, false).await;
// dbg!(turn_off_result); // dbg!(turn_off_result);
// let instant = interval.tick().await; // let instant = interval.tick().await;
// let context: Option<Context<()>> = None; // let context: Option<Context<()>> = None;
// let target: Option<()> = None; // let target: Option<()> = None;
@@ -172,7 +172,7 @@ async fn real_main(
// filter: command::DoNotDisturbFilter::AlarmsOnly, // filter: command::DoNotDisturbFilter::AlarmsOnly,
// }, context, target, false).await; // }, context, target, false).await;
// dbg!(alarms_only_result); // dbg!(alarms_only_result);
// let instant = interval.tick().await; // let instant = interval.tick().await;
// let context: Option<Context<()>> = None; // let context: Option<Context<()>> = None;
// let target: Option<()> = None; // let target: Option<()> = None;
@@ -181,7 +181,7 @@ async fn real_main(
// filter: command::DoNotDisturbFilter::PriorityOnly, // filter: command::DoNotDisturbFilter::PriorityOnly,
// }, context, target, false).await; // }, context, target, false).await;
// dbg!(priority_only_result); // dbg!(priority_only_result);
// let instant = interval.tick().await; // let instant = interval.tick().await;
// let context: Option<Context<()>> = None; // let context: Option<Context<()>> = None;
// let target: Option<()> = None; // let target: Option<()> = None;
@@ -192,7 +192,8 @@ async fn real_main(
// dbg!(total_silence_result); // dbg!(total_silence_result);
// } // }
} }
).0 )
.0
} }
#[pyfunction] #[pyfunction]

View File

@@ -24,7 +24,7 @@ once_cell = "1.21.3"
protocol = { path = "../protocol" } protocol = { path = "../protocol" }
pyo3 = { workspace = true } pyo3 = { workspace = true }
pyo3-async-runtimes = { workspace = true, features = ["tokio-runtime"] } pyo3-async-runtimes = { workspace = true, features = ["tokio-runtime"] }
python-utils = { path = "../python-utils" } python-utils = { path = "../python-utils", features = ["macros"] }
snafu = { workspace = true } snafu = { workspace = true }
strum = { workspace = true, features = ["derive"] } strum = { workspace = true, features = ["derive"] }
tokio = { workspace = true } tokio = { workspace = true }

View File

@@ -1,18 +1,13 @@
use std::{convert::Infallible, fmt::Display, str::FromStr}; use python_utils::{FromPyFromStr, ToStrToPy};
use pyo3::{
exceptions::{PyException, PyValueError},
prelude::*,
types::PyString,
};
use snafu::{ResultExt, Snafu}; use snafu::{ResultExt, Snafu};
use std::{fmt::Display, str::FromStr};
use super::{ use super::{
domain::Domain, domain::Domain,
object_id::{ObjectId, ObjectIdParsingError}, object_id::{ObjectId, ObjectIdParsingError},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone, FromPyFromStr, ToStrToPy)]
pub struct EntityId(pub Domain, pub ObjectId); pub struct EntityId(pub Domain, pub ObjectId);
#[derive(Debug, Clone, Snafu)] #[derive(Debug, Clone, Snafu)]
@@ -47,47 +42,3 @@ impl Display for EntityId {
write!(f, "{domain}.{object_id}") 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<ExtractEntityIdError> 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<Self, Self::Error> {
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<Self::Output, Self::Error> {
let s = self.to_string();
s.into_pyobject(py)
}
}

View File

@@ -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 python_utils::{FromPyFromStr, ToStrToPy};
use snafu::{ResultExt, Snafu};
use ulid::Ulid; use ulid::Ulid;
#[derive(Debug, Clone)] #[derive(Debug, Clone, FromPyFromStr, ToStrToPy)]
pub enum Id { pub enum Id {
Ulid(Ulid), Ulid(Ulid),
Other(Arc<str>), Other(Arc<str>),
} }
#[derive(Debug, Snafu)] impl From<String> for Id {
pub enum ExtractIdError { fn from(s: String) -> Self {
/// couldn't extract the given object as a string if let Ok(ulid) = s.parse() {
ExtractStringError { source: PyErr }, Id::Ulid(ulid)
} } else {
Id::Other(s.into())
impl From<ExtractIdError> for PyErr {
fn from(error: ExtractIdError) -> Self {
match &error {
ExtractIdError::ExtractStringError { .. } => PyTypeError::new_err(error.to_string()),
} }
} }
} }
// TODO: replace with a derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one impl FromStr for Id {
impl<'a, 'py> FromPyObject<'a, 'py> for Id { type Err = Infallible;
type Error = ExtractIdError;
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
let s = ob.extract::<&str>().context(ExtractStringSnafu)?;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(ulid) = s.parse() { if let Ok(ulid) = s.parse() {
Ok(Id::Ulid(ulid)) Ok(Id::Ulid(ulid))
} else { } 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 Display for Id {
impl<'py> IntoPyObject<'py> for Id { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
type Target = PyString;
type Output = Bound<'py, Self::Target>;
type Error = Infallible;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
match self { match self {
Id::Ulid(ulid) => ulid.to_string().into_pyobject(py), Id::Ulid(ulid) => write!(f, "{ulid}"),
Id::Other(id) => id.into_pyobject(py), Id::Other(other) => write!(f, "{other}"),
} }
} }
} }

View File

@@ -1,42 +1,39 @@
use pyo3::exceptions::{PyTypeError, PyValueError}; use std::fmt::Display;
use pyo3::prelude::*; use std::str::FromStr;
use snafu::{ResultExt, Snafu};
use pyo3::FromPyObject;
use python_utils::{FromPyFromStr, ToStrToPy};
use snafu::Snafu;
use crate::{entity_id::EntityId, state_object::StateObject}; 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; pub struct Type;
/// expected a string of value "state_changed", but got {actual}
#[derive(Debug, Snafu)] #[derive(Debug, Snafu)]
pub enum ExtractTypeError { pub struct ParseTypeError {
/// couldn't extract this object as a string actual: String,
ExtractStringError { source: PyErr },
/// expected a string of value "state_changed", but got {actual}
UnexpectedValue { actual: String },
} }
impl From<ExtractTypeError> for PyErr { impl FromStr for Type {
fn from(error: ExtractTypeError) -> Self { type Err = ParseTypeError;
match &error {
ExtractTypeError::ExtractStringError { .. } => PyTypeError::new_err(error.to_string()), fn from_str(s: &str) -> Result<Self, Self::Err> {
ExtractTypeError::UnexpectedValue { .. } => PyValueError::new_err(error.to_string()), 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 Display for Type {
impl<'a, 'py> FromPyObject<'a, 'py> for Type { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
type Error = ExtractTypeError; write!(f, "state_changed")
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
let s = ob.extract::<&str>().context(ExtractStringSnafu)?;
if s == "state_changed" {
Ok(Type)
} else {
Err(ExtractTypeError::UnexpectedValue { actual: s.into() })
}
} }
} }

View File

@@ -1,7 +1,10 @@
use std::{convert::Infallible, str::FromStr}; use std::{convert::Infallible, str::FromStr};
use pyo3::{types::{PyDict, PyString, PyDictMethods}, Bound, IntoPyObject, Python, PyErr}; use pyo3::{
use python_utils::IntoPyObjectViaDisplay; types::{PyDict, PyDictMethods, PyString},
Bound, IntoPyObject, PyErr, Python,
};
use python_utils::{FromPyFromStr, IntoPyObjectViaDisplay, ToStrToPy};
use snafu::Snafu; use snafu::Snafu;
use strum::EnumString; use strum::EnumString;
use url::Url; use url::Url;
@@ -84,7 +87,7 @@ impl TryFrom<String> for NonSpecialMessage {
} }
/// How much of a notification is visible on the lock screen /// 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")] #[strum(serialize_all = "snake_case")]
pub enum Visibility { pub enum Visibility {
/// always show all notification content /// always show all notification content
@@ -98,38 +101,14 @@ pub enum Visibility {
Secret, Secret,
} }
// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one #[derive(Debug, Clone, EnumString, strum::Display, FromPyFromStr, ToStrToPy)]
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<Self::Output, Self::Error> {
let s = self.to_string();
s.into_pyobject(py)
}
}
#[derive(Debug, Clone, EnumString, strum::Display)]
pub enum Behavior { pub enum Behavior {
/// prompt for text to return with the event /// prompt for text to return with the event
#[strum(serialize = "textInput")] #[strum(serialize = "textInput")]
TextInput, TextInput,
} }
// TODO: replace with a derive(DisplayToPy) (analogous to serde_with::SerializeDisplay) once I make one #[derive(Debug, Clone, Default, EnumString, strum::Display, FromPyFromStr, ToStrToPy)]
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<Self::Output, Self::Error> {
let s = self.to_string();
s.into_pyobject(py)
}
}
#[derive(Debug, Clone, Default, EnumString, strum::Display)]
#[strum(serialize_all = "camelCase")] #[strum(serialize_all = "camelCase")]
pub enum ActivationMode { pub enum ActivationMode {
/// launch the app when tapped /// launch the app when tapped
@@ -139,18 +118,6 @@ pub enum ActivationMode {
Background, 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<Self::Output, Self::Error> {
let s = self.to_string();
s.into_pyobject(py)
}
}
// TODO: better typed versions like `CallNumber` or `OpenWebpage` where Action: From<CallNumber> and Action: From<OpenWebPage> // TODO: better typed versions like `CallNumber` or `OpenWebpage` where Action: From<CallNumber> and Action: From<OpenWebPage>
#[derive(Debug, Clone, IntoPyObject, typed_builder::TypedBuilder)] #[derive(Debug, Clone, IntoPyObject, typed_builder::TypedBuilder)]
#[builder(field_defaults(default, setter(strip_option(fallback_suffix = "_option"))))] #[builder(field_defaults(default, setter(strip_option(fallback_suffix = "_option"))))]
@@ -190,7 +157,7 @@ pub struct Action {
pub icon: Option<String>, pub icon: Option<String>,
} }
#[derive(Debug, Clone, Default, EnumString, strum::Display)] #[derive(Debug, Clone, Default, EnumString, strum::Display, FromPyFromStr, ToStrToPy)]
#[strum(serialize_all = "snake_case", suffix = "_stream")] #[strum(serialize_all = "snake_case", suffix = "_stream")]
pub enum MediaStream { pub enum MediaStream {
Alarm, Alarm,
@@ -203,18 +170,6 @@ pub enum MediaStream {
System, 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<Self::Output, Self::Error> {
let s = self.to_string();
s.into_pyobject(py)
}
}
#[derive(Debug, Default, Clone, typed_builder::TypedBuilder)] #[derive(Debug, Default, Clone, typed_builder::TypedBuilder)]
#[builder(field_defaults(default, setter(strip_option(fallback_suffix = "_option"))))] #[builder(field_defaults(default, setter(strip_option(fallback_suffix = "_option"))))]
pub struct NotifyMobileAppServiceDataData { pub struct NotifyMobileAppServiceDataData {
@@ -232,7 +187,13 @@ impl<'py> IntoPyObject<'py> for NotifyMobileAppServiceDataData {
type Error = PyErr; type Error = PyErr;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
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); let dict = PyDict::new(py);
@@ -279,7 +240,11 @@ impl<'py> IntoPyObject<'py> for NotifyMobileAppServiceData {
type Error = PyErr; type Error = PyErr;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
let NotifyMobileAppServiceData { message, title, data } = self; let NotifyMobileAppServiceData {
message,
title,
data,
} = self;
let dict = PyDict::new(py); let dict = PyDict::new(py);
@@ -293,4 +258,4 @@ impl<'py> IntoPyObject<'py> for NotifyMobileAppServiceData {
Ok(dict) Ok(dict)
} }
} }

View File

@@ -1,7 +1,6 @@
use std::str::FromStr; use std::str::FromStr;
use mitsein::vec1::Vec1; use mitsein::vec1::Vec1;
use pyo3::{types::PyAnyMethods, IntoPyObject, Python};
use crate::{ use crate::{
object_id::ObjectId, object_id::ObjectId,

View File

@@ -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 emitter_and_signal::{Signal, SignalExt};
use pyo3::{ use pyo3::{

View File

@@ -1,10 +1,10 @@
use std::{str::FromStr, sync::Arc}; use std::{str::FromStr, sync::Arc};
use pyo3::{exceptions::PyValueError, PyErr}; use pyo3::{exceptions::PyValueError, PyErr};
use python_utils::{FromPyFromStr, ToStrToPy};
use snafu::Snafu; use snafu::Snafu;
// TODO: derive(PyFromStr) (analogous to serde_with::DeserializeFromStr) once I make one #[derive(Debug, Clone, derive_more::Display, FromPyFromStr, ToStrToPy)]
#[derive(Debug, Clone, derive_more::Display)]
pub struct Slug(Arc<str>); pub struct Slug(Arc<str>);
#[derive(Debug, Clone, Snafu)] #[derive(Debug, Clone, Snafu)]

View File

@@ -1,55 +1,12 @@
use std::str::FromStr; use python_utils::{FromPyFromStr, ToStrToPy};
use pyo3::{
exceptions::{PyException, PyValueError},
prelude::*,
};
use snafu::{ResultExt, Snafu};
use strum::EnumString; use strum::EnumString;
/// A state in Home Assistant that is known to represent an error of some kind: /// 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) /// * `unavailable` (the device is likely offline or unreachable from the Home Assistant instance)
/// * `unknown` (I don't know how to explain this one) /// * `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")] #[strum(serialize_all = "snake_case")]
pub enum ErrorState { pub enum ErrorState {
Unavailable, Unavailable,
Unknown, 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: <ErrorState as FromStr>::Err,
},
}
impl From<ExtractErrorStateError> 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<Self, Self::Error> {
let s = ob.extract::<String>().context(ExtractStringSnafu)?;
let state = ErrorState::from_str(&s).context(UnexpectedValueSnafu)?;
Ok(state)
}
}

View File

@@ -1,7 +1,6 @@
use std::{convert::Infallible, str::FromStr}; use std::{convert::Infallible, str::FromStr};
use pyo3::{exceptions::PyException, prelude::*}; use python_utils::{FromPyFromStr, ToStrToPy};
use snafu::{ResultExt, Snafu};
pub mod error_state; pub mod error_state;
pub mod unexpected_state; pub mod unexpected_state;
@@ -9,13 +8,30 @@ pub mod unexpected_state;
pub use error_state::ErrorState; pub use error_state::ErrorState;
pub use unexpected_state::UnexpectedState; pub use unexpected_state::UnexpectedState;
#[derive(Debug, Clone, derive_more::Display)] #[derive(Debug, Clone, derive_more::Display, FromPyFromStr, ToStrToPy)]
pub enum HomeAssistantState<State> { pub enum HomeAssistantState<State> {
Ok(State), Ok(State),
Err(ErrorState), Err(ErrorState),
UnexpectedErr(UnexpectedState), UnexpectedErr(UnexpectedState),
} }
impl<State> From<String> for HomeAssistantState<State>
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<State: FromStr> FromStr for HomeAssistantState<State> { impl<State: FromStr> FromStr for HomeAssistantState<State> {
type Err = Infallible; type Err = Infallible;
@@ -31,34 +47,3 @@ impl<State: FromStr> FromStr for HomeAssistantState<State> {
Ok(HomeAssistantState::UnexpectedErr(UnexpectedState(s.into()))) 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<ExtractHomeAssistantStateError> 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<State>
{
type Error = ExtractHomeAssistantStateError;
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
let s = ob.extract::<&str>().context(ExtractStringSnafu)?;
let Ok(state) = s.parse();
Ok(state)
}
}

View File

@@ -1,10 +1,6 @@
use std::str::FromStr; use std::str::FromStr;
use pyo3::{ use python_utils::{FromPyFromStr, ToStrToPy};
exceptions::{PyException, PyValueError},
prelude::*,
};
use snafu::{ResultExt, Snafu};
use strum::EnumString; use strum::EnumString;
use uom::{ use uom::{
si::{ si::{
@@ -18,7 +14,7 @@ use uom::{
}; };
/// Power units /// Power units
#[derive(Debug, Clone, Copy, EnumString, strum::Display)] #[derive(Debug, Clone, Copy, EnumString, strum::Display, FromPyFromStr, ToStrToPy)]
#[strum(serialize_all = "snake_case")] #[strum(serialize_all = "snake_case")]
pub enum UnitOfMeasurement { pub enum UnitOfMeasurement {
#[strum(serialize = "mW")] #[strum(serialize = "mW")]
@@ -37,42 +33,6 @@ pub enum UnitOfMeasurement {
BtuPerhour, 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: <UnitOfMeasurement as FromStr>::Err,
},
}
impl From<ExtractUnitOfMeasurementError> 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<Self, Self::Error> {
let s = obj.extract().context(ExtractStringSnafu)?;
let unit_of_measurement = UnitOfMeasurement::from_str(s).context(ParseSnafu)?;
Ok(unit_of_measurement)
}
}
impl UnitOfMeasurement { impl UnitOfMeasurement {
pub fn into_uom<V>(&self, amount: V) -> Power<V> pub fn into_uom<V>(&self, amount: V) -> Power<V>
where where

View File

@@ -4,6 +4,6 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
proc-macro2 = "1.0.106" proc-macro2 = { workspace = true }
quote = "1.0.46" quote = { workspace = true }
syn = "2.0.118" syn = { workspace = true }

View File

@@ -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<Self> {
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<Self, Self::Error> {
::python_utils::FromPyObjectViaParse::extract(obj).map(|wrapper| wrapper.0)
}
}
};
dbg!(&output.to_string());
tokens.extend(output);
}
}

View File

@@ -1,17 +1,5 @@
use proc_macro2::TokenStream; mod from_py_from_str;
use quote::quote; mod to_str_to_py;
use syn::{DeriveInput, parse_macro_input};
pub fn py_from_str(input: TokenStream) -> TokenStream { pub use from_py_from_str::FromPyFromStr;
let derive_input: DeriveInput = parse_macro_input!(input); pub use to_str_to_py::ToStrToPy;
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
}

View File

@@ -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<Self> {
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<Self::Output, Self::Error> {
let s = ::std::string::ToString::to_string(&self);
::pyo3::conversion::IntoPyObject::into_pyobject(s, py)
}
}
};
dbg!(&output.to_string());
tokens.extend(output);
}
}

View File

@@ -9,3 +9,5 @@ proc-macro = true
[dependencies] [dependencies]
python-utils-macros-impl = { path = "../python-utils-macros-impl" } python-utils-macros-impl = { path = "../python-utils-macros-impl" }
quote = { workspace = true }
syn = { workspace = true }

View File

@@ -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)] #[proc_macro_derive(FromPyFromStr)]
pub fn py_from_str(input: TokenStream) -> TokenStream { pub fn from_py_from_str(input: TokenStream) -> TokenStream {
python_utils_macros_impl::py_from_str(input.into()).into() 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()
} }

View File

@@ -4,8 +4,11 @@ version = "0.1.0"
edition = "2021" edition = "2021"
license = { workspace = true } license = { workspace = true }
[features]
macros = ["dep:python-utils-macros"]
[dependencies] [dependencies]
derive_more = { workspace = true } derive_more = { workspace = true }
pyo3 = { workspace = true } pyo3 = { workspace = true }
python-utils-macros = { path = "../python-utils-macros" } python-utils-macros = { optional = true, path = "../python-utils-macros" }
snafu = { workspace = true } snafu = { workspace = true }

View File

@@ -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<T>(pub T);
#[derive(Debug, Clone)]
pub enum ExtractPyObjectViaParseError<ParseError> {
/// couldn't extract the object as a string
ExtractStringError { source: Arc<PyErr> },
/// couldn't parse the string as an instance of this Rust type
ParseError { source: ParseError },
}
impl<ParseError: Display> Display for ExtractPyObjectViaParseError<ParseError> {
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<ParseError> std::error::Error for ExtractPyObjectViaParseError<ParseError> where
ExtractPyObjectViaParseError<ParseError>: Debug + Display
{
}
impl<ParseError> From<ExtractPyObjectViaParseError<ParseError>> for PyErr
where
ExtractPyObjectViaParseError<ParseError>: Display,
{
fn from(error: ExtractPyObjectViaParseError<ParseError>) -> 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<T>
where
T: FromStr,
PyErr: From<ExtractPyObjectViaParseError<<T as FromStr>::Err>>,
{
type Error = ExtractPyObjectViaParseError<<T as FromStr>::Err>;
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
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))
}
}

View File

@@ -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<T>(pub T);
impl<'py, T> IntoPyObject<'py> for IntoPyObjectViaDisplay<T>
where
T: Display,
{
type Target = PyString;
type Output = Bound<'py, Self::Target>;
type Error = Infallible;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
let s = self.to_string();
s.into_pyobject(py)
}
}

View File

@@ -1,15 +1,18 @@
use std::{convert::Infallible, fmt::Display, str::FromStr, sync::Arc};
use pyo3::{ use pyo3::{
exceptions::{PyException, PyTypeError, PyValueError}, exceptions::{PyException, PyTypeError},
prelude::*, prelude::*,
types::PyString,
}; };
use snafu::{ResultExt, Snafu}; 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 mod none;
pub use from_pyobject_via_parse::{ExtractPyObjectViaParseError, FromPyObjectViaParse};
pub use into_pyobject_via_display::IntoPyObjectViaDisplay;
pub use none::IsNone; pub use none::IsNone;
/// Create a GIL-independent reference /// Create a GIL-independent reference
@@ -83,70 +86,3 @@ pub fn validate_type_by_name(
return Ok(()); return Ok(());
} }
#[derive(Debug, Clone, derive_more::Display)]
pub struct IntoPyObjectViaDisplay<T>(pub T);
impl<'py, T> IntoPyObject<'py> for IntoPyObjectViaDisplay<T>
where
T: Display,
{
type Target = PyString;
type Output = Bound<'py, Self::Target>;
type Error = Infallible;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
let s = self.to_string();
s.into_pyobject(py)
}
}
#[derive(Debug, Clone, derive_more::FromStr)]
pub struct FromPyObjectViaParse<T>(pub T);
#[derive(Debug, Clone, Snafu)]
pub enum ExtractPyObjectViaParseError<ParseError>
where
ParseError: 'static + snafu::Error,
{
/// couldn't extract the object as a string
ExtractStringError { source: Arc<PyErr> },
/// couldn't parse the string as an instance of this Rust type
ParseError { source: ParseError },
}
impl<E> From<ExtractPyObjectViaParseError<E>> for PyErr
where
E: 'static + snafu::Error,
{
fn from(error: ExtractPyObjectViaParseError<E>) -> 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<T>
where
T: FromStr,
<T as FromStr>::Err: 'static + snafu::Error,
PyErr: From<ExtractPyObjectViaParseError<<T as FromStr>::Err>>,
{
type Error = ExtractPyObjectViaParseError<<T as FromStr>::Err>;
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
let s = obj
.extract::<&str>()
.map_err(Arc::new)
.context(ExtractStringSnafu)?;
let t = T::from_str(s).context(ParseSnafu)?;
Ok(FromPyObjectViaParse(t))
}
}