From d74b30902d89c868181472a51c135a9a96295a45 Mon Sep 17 00:00:00 2001 From: Oliver Parker <46482091+ollz272@users.noreply.github.com> Date: Sat, 21 Jun 2025 10:38:39 +0100 Subject: [PATCH 1/2] refactor: flatten timedelta mode handling --- src/serializers/config.rs | 38 +++++++----- src/serializers/infer.rs | 25 +------- .../type_serializers/datetime_etc.rs | 13 ++-- src/serializers/type_serializers/timedelta.rs | 60 ++++--------------- 4 files changed, 43 insertions(+), 93 deletions(-) diff --git a/src/serializers/config.rs b/src/serializers/config.rs index 7dbba396f..f32beb96e 100644 --- a/src/serializers/config.rs +++ b/src/serializers/config.rs @@ -14,7 +14,6 @@ use crate::serializers::type_serializers::datetime_etc::{ date_to_milliseconds, date_to_seconds, date_to_string, datetime_to_milliseconds, datetime_to_seconds, datetime_to_string, time_to_milliseconds, time_to_seconds, time_to_string, }; -use crate::serializers::type_serializers::timedelta::EffectiveDeltaMode; use crate::tools::SchemaDict; use super::errors::py_err_se_err; @@ -24,7 +23,6 @@ use super::errors::py_err_se_err; pub(crate) struct SerializationConfig { pub timedelta_mode: TimedeltaMode, pub temporal_mode: TemporalMode, - prefer_timedelta_mode: bool, pub bytes_mode: BytesMode, pub inf_nan_mode: InfNanMode, } @@ -32,16 +30,19 @@ pub(crate) struct SerializationConfig { impl SerializationConfig { pub fn from_config(config: Option<&Bound<'_, PyDict>>) -> PyResult { let timedelta_mode = TimedeltaMode::from_config(config)?; - let temporal_mode = TemporalMode::from_config(config)?; - let prefer_timedelta_mode = config - .and_then(|cfg| cfg.contains(intern!(cfg.py(), "ser_json_timedelta")).ok()) + let temporal_set = config + .and_then(|cfg| cfg.contains(intern!(cfg.py(), "ser_json_temporal")).ok()) .unwrap_or(false); + let temporal_mode = if temporal_set { + TemporalMode::from_config(config)? + } else { + TimedeltaMode::from_config(config)?.into() + }; let bytes_mode = BytesMode::from_config(config)?; let inf_nan_mode = InfNanMode::from_config(config)?; Ok(Self { timedelta_mode, temporal_mode, - prefer_timedelta_mode, bytes_mode, inf_nan_mode, }) @@ -53,22 +54,18 @@ impl SerializationConfig { bytes_mode: &str, inf_nan_mode: &str, ) -> PyResult { + let temporal_mode = if datetime_mode != "iso8601" { + TemporalMode::from_str(datetime_mode)? + } else { + TimedeltaMode::from_str(timedelta_mode)?.into() + }; Ok(Self { timedelta_mode: TimedeltaMode::from_str(timedelta_mode)?, - temporal_mode: TemporalMode::from_str(datetime_mode)?, - prefer_timedelta_mode: true, // This is not settable via args + temporal_mode, bytes_mode: BytesMode::from_str(bytes_mode)?, inf_nan_mode: InfNanMode::from_str(inf_nan_mode)?, }) } - - pub fn effective_delta_mode(&self) -> EffectiveDeltaMode { - if self.prefer_timedelta_mode { - EffectiveDeltaMode::Timedelta(self.timedelta_mode) - } else { - EffectiveDeltaMode::Temporal(self.temporal_mode) - } - } } pub trait FromConfig { @@ -199,6 +196,15 @@ impl TimedeltaMode { } } +impl From for TemporalMode { + fn from(value: TimedeltaMode) -> Self { + match value { + TimedeltaMode::Iso8601 => TemporalMode::Iso8601, + TimedeltaMode::Float => TemporalMode::Seconds, + } + } +} + impl TemporalMode { pub fn datetime_to_json(self, py: Python, datetime: &Bound<'_, PyDateTime>) -> PyResult { match self { diff --git a/src/serializers/infer.rs b/src/serializers/infer.rs index 0b970788b..285c2f99a 100644 --- a/src/serializers/infer.rs +++ b/src/serializers/infer.rs @@ -12,7 +12,6 @@ use serde::ser::{Error, Serialize, SerializeMap, SerializeSeq, Serializer}; use crate::input::{EitherTimedelta, Int}; use crate::serializers::type_serializers; -use crate::serializers::type_serializers::timedelta::EffectiveDeltaMode; use crate::tools::{extract_int, py_err, safe_repr}; use crate::url::{PyMultiHostUrl, PyUrl}; @@ -193,15 +192,7 @@ pub(crate) fn infer_to_python_known( } ObType::Timedelta => { let either_delta = EitherTimedelta::try_from(value)?; - let x = match extra.config.effective_delta_mode() { - EffectiveDeltaMode::Temporal(temporal_mode) => { - temporal_mode.timedelta_to_json(value.py(), either_delta) - } - EffectiveDeltaMode::Timedelta(timedelta_mode) => { - timedelta_mode.either_delta_to_json(value.py(), either_delta) - } - }?; - x + extra.config.temporal_mode.timedelta_to_json(value.py(), either_delta)? } ObType::Url => { let py_url: PyUrl = value.extract()?; @@ -480,14 +471,7 @@ pub(crate) fn infer_serialize_known( } ObType::Timedelta => { let either_delta = EitherTimedelta::try_from(value).map_err(py_err_se_err)?; - match extra.config.effective_delta_mode() { - EffectiveDeltaMode::Temporal(temporal_mode) => { - temporal_mode.timedelta_serialize(either_delta, serializer) - } - EffectiveDeltaMode::Timedelta(timedelta_mode) => { - timedelta_mode.timedelta_serialize(value.py(), either_delta, serializer) - } - } + extra.config.temporal_mode.timedelta_serialize(either_delta, serializer) } ObType::Url => { let py_url: PyUrl = value.extract().map_err(py_err_se_err)?; @@ -654,10 +638,7 @@ pub(crate) fn infer_json_key_known<'a>( } ObType::Timedelta => { let either_delta = EitherTimedelta::try_from(key)?; - match extra.config.effective_delta_mode() { - EffectiveDeltaMode::Temporal(temporal_mode) => temporal_mode.timedelta_json_key(&either_delta), - EffectiveDeltaMode::Timedelta(timedelta_mode) => timedelta_mode.json_key(key.py(), either_delta), - } + extra.config.temporal_mode.timedelta_json_key(&either_delta) } ObType::Url => { let py_url: PyUrl = key.extract()?; diff --git a/src/serializers/type_serializers/datetime_etc.rs b/src/serializers/type_serializers/datetime_etc.rs index c780d2fea..798b89ef5 100644 --- a/src/serializers/type_serializers/datetime_etc.rs +++ b/src/serializers/type_serializers/datetime_etc.rs @@ -17,23 +17,23 @@ pub(crate) fn datetime_to_string(py_dt: &Bound<'_, PyDateTime>) -> PyResult) -> PyResult { - pydatetime_as_datetime(py_dt).map(|dt| + pydatetime_as_datetime(py_dt).map(|dt| { dt.date.timestamp() as f64 + dt.time.hour as f64 * 3600.0 + dt.time.minute as f64 * 60.0 + dt.time.second as f64 + dt.time.microsecond as f64 / 1_000_000.0 - ) + }) } pub(crate) fn datetime_to_milliseconds(py_dt: &Bound<'_, PyDateTime>) -> PyResult { - pydatetime_as_datetime(py_dt).map(|dt| + pydatetime_as_datetime(py_dt).map(|dt| { dt.date.timestamp_ms() as f64 + dt.time.hour as f64 * 3_600_000.0 + dt.time.minute as f64 * 60_000.0 + dt.time.second as f64 * 1_000.0 + dt.time.microsecond as f64 / 1_000.0 - ) + }) } pub(crate) fn date_to_seconds(py_date: &Bound<'_, PyDate>) -> PyResult { @@ -52,9 +52,8 @@ pub(crate) fn time_to_string(py_time: &Bound<'_, PyTime>) -> PyResult { } pub(crate) fn time_to_seconds(py_time: &Bound<'_, PyTime>) -> PyResult { - pytime_as_time(py_time, None).map(|t| { - t.hour as f64 * 3600.0 + t.minute as f64 * 60.0 + t.second as f64 + t.microsecond as f64 / 1_000_000.0 - }) + pytime_as_time(py_time, None) + .map(|t| t.hour as f64 * 3600.0 + t.minute as f64 * 60.0 + t.second as f64 + t.microsecond as f64 / 1_000_000.0) } pub(crate) fn time_to_milliseconds(py_time: &Bound<'_, PyTime>) -> PyResult { diff --git a/src/serializers/type_serializers/timedelta.rs b/src/serializers/type_serializers/timedelta.rs index d970cf9ff..2cac5345f 100644 --- a/src/serializers/type_serializers/timedelta.rs +++ b/src/serializers/type_serializers/timedelta.rs @@ -14,14 +14,7 @@ use super::{ #[derive(Debug)] pub struct TimeDeltaSerializer { - timedelta_mode: TimedeltaMode, temporal_mode: TemporalMode, - prefer_timedelta: bool, -} - -pub enum EffectiveDeltaMode { - Timedelta(TimedeltaMode), - Temporal(TemporalMode), } impl BuildSerializer for TimeDeltaSerializer { @@ -32,29 +25,17 @@ impl BuildSerializer for TimeDeltaSerializer { config: Option<&Bound<'_, PyDict>>, _definitions: &mut DefinitionsBuilder, ) -> PyResult { - let timedelta_mode = TimedeltaMode::from_config(config)?; - let temporal_mode = TemporalMode::from_config(config)?; - - let prefer_timedelta_mode = config - .and_then(|cfg| cfg.contains(intern!(cfg.py(), "ser_json_timedelta")).ok()) + let temporal_set = config + .and_then(|cfg| cfg.contains(intern!(cfg.py(), "ser_json_temporal")).ok()) .unwrap_or(false); - - Ok(Self { - timedelta_mode, - temporal_mode, - prefer_timedelta: prefer_timedelta_mode, - } - .into()) - } -} - -impl TimeDeltaSerializer { - pub fn effective_delta_mode(&self) -> EffectiveDeltaMode { - if self.prefer_timedelta { - EffectiveDeltaMode::Timedelta(self.timedelta_mode) + let temporal_mode = if temporal_set { + TemporalMode::from_config(config)? } else { - EffectiveDeltaMode::Temporal(self.temporal_mode) - } + let td_mode = TimedeltaMode::from_config(config)?; + td_mode.into() + }; + + Ok(Self { temporal_mode }.into()) } } @@ -70,14 +51,7 @@ impl TypeSerializer for TimeDeltaSerializer { ) -> PyResult { match extra.mode { SerMode::Json => match EitherTimedelta::try_from(value) { - Ok(either_timedelta) => match self.effective_delta_mode() { - EffectiveDeltaMode::Timedelta(timedelta_mode) => { - Ok(timedelta_mode.either_delta_to_json(value.py(), either_timedelta)?) - } - EffectiveDeltaMode::Temporal(temporal_mode) => { - Ok(temporal_mode.timedelta_to_json(value.py(), either_timedelta)?) - } - }, + Ok(either_timedelta) => Ok(self.temporal_mode.timedelta_to_json(value.py(), either_timedelta)?), Err(_) => { extra.warnings.on_fallback_py(self.get_name(), value, extra)?; infer_to_python(value, include, exclude, extra) @@ -89,10 +63,7 @@ impl TypeSerializer for TimeDeltaSerializer { fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult> { match EitherTimedelta::try_from(key) { - Ok(either_timedelta) => match self.effective_delta_mode() { - EffectiveDeltaMode::Timedelta(timedelta_mode) => timedelta_mode.json_key(key.py(), either_timedelta), - EffectiveDeltaMode::Temporal(temporal_mode) => temporal_mode.timedelta_json_key(&either_timedelta), - }, + Ok(either_timedelta) => self.temporal_mode.timedelta_json_key(&either_timedelta), Err(_) => { extra.warnings.on_fallback_py(self.get_name(), key, extra)?; infer_json_key(key, extra) @@ -109,14 +80,7 @@ impl TypeSerializer for TimeDeltaSerializer { extra: &Extra, ) -> Result { match EitherTimedelta::try_from(value) { - Ok(either_timedelta) => match self.effective_delta_mode() { - EffectiveDeltaMode::Timedelta(timedelta_mode) => { - timedelta_mode.timedelta_serialize(value.py(), either_timedelta, serializer) - } - EffectiveDeltaMode::Temporal(temporal_mode) => { - temporal_mode.timedelta_serialize(either_timedelta, serializer) - } - }, + Ok(either_timedelta) => self.temporal_mode.timedelta_serialize(either_timedelta, serializer), Err(_) => { extra.warnings.on_fallback_ser::(self.get_name(), value, extra)?; infer_serialize(value, serializer, include, exclude, extra) From 98d4a0ab361c50cc6e77cbc4fb7cfaf359456aef Mon Sep 17 00:00:00 2001 From: Oliver Parker <46482091+ollz272@users.noreply.github.com> Date: Sat, 21 Jun 2025 10:51:54 +0100 Subject: [PATCH 2/2] chore: fix lint --- src/serializers/config.rs | 70 ++----------------- .../type_serializers/datetime_etc.rs | 32 +++++---- 2 files changed, 24 insertions(+), 78 deletions(-) diff --git a/src/serializers/config.rs b/src/serializers/config.rs index f32beb96e..61e5b0697 100644 --- a/src/serializers/config.rs +++ b/src/serializers/config.rs @@ -3,7 +3,7 @@ use std::str::{from_utf8, FromStr, Utf8Error}; use base64::Engine; use pyo3::prelude::*; -use pyo3::types::{PyDate, PyDateTime, PyDelta, PyDict, PyString, PyTime}; +use pyo3::types::{PyDate, PyDateTime, PyDict, PyString, PyTime}; use pyo3::{intern, IntoPyObjectExt}; use serde::ser::Error; @@ -21,7 +21,6 @@ use super::errors::py_err_se_err; #[derive(Debug, Clone)] #[allow(clippy::struct_field_names)] pub(crate) struct SerializationConfig { - pub timedelta_mode: TimedeltaMode, pub temporal_mode: TemporalMode, pub bytes_mode: BytesMode, pub inf_nan_mode: InfNanMode, @@ -29,7 +28,6 @@ pub(crate) struct SerializationConfig { impl SerializationConfig { pub fn from_config(config: Option<&Bound<'_, PyDict>>) -> PyResult { - let timedelta_mode = TimedeltaMode::from_config(config)?; let temporal_set = config .and_then(|cfg| cfg.contains(intern!(cfg.py(), "ser_json_temporal")).ok()) .unwrap_or(false); @@ -41,7 +39,6 @@ impl SerializationConfig { let bytes_mode = BytesMode::from_config(config)?; let inf_nan_mode = InfNanMode::from_config(config)?; Ok(Self { - timedelta_mode, temporal_mode, bytes_mode, inf_nan_mode, @@ -50,18 +47,17 @@ impl SerializationConfig { pub fn from_args( timedelta_mode: &str, - datetime_mode: &str, + temporal_mode: &str, bytes_mode: &str, inf_nan_mode: &str, ) -> PyResult { - let temporal_mode = if datetime_mode != "iso8601" { - TemporalMode::from_str(datetime_mode)? + let resolved_temporal_mode = if temporal_mode != "iso8601" { + TemporalMode::from_str(temporal_mode)? } else { TimedeltaMode::from_str(timedelta_mode)?.into() }; Ok(Self { - timedelta_mode: TimedeltaMode::from_str(timedelta_mode)?, - temporal_mode, + temporal_mode: resolved_temporal_mode, bytes_mode: BytesMode::from_str(bytes_mode)?, inf_nan_mode: InfNanMode::from_str(inf_nan_mode)?, }) @@ -140,61 +136,7 @@ serialization_mode! { Strings => "strings", } -impl TimedeltaMode { - fn total_seconds<'py>(py_timedelta: &Bound<'py, PyDelta>) -> PyResult> { - py_timedelta.call_method0(intern!(py_timedelta.py(), "total_seconds")) - } - - pub fn either_delta_to_json(self, py: Python, either_delta: EitherTimedelta) -> PyResult { - match self { - Self::Iso8601 => { - let d = either_delta.to_duration()?; - d.to_string().into_py_any(py) - } - Self::Float => { - // convert to int via a py timedelta not duration since we know this this case the input would have - // been a py timedelta - let py_timedelta = either_delta.into_pyobject(py)?; - let seconds = Self::total_seconds(&py_timedelta)?; - Ok(seconds.unbind()) - } - } - } - - pub fn json_key<'py>(self, py: Python, either_delta: EitherTimedelta) -> PyResult> { - match self { - Self::Iso8601 => { - let d = either_delta.to_duration()?; - Ok(d.to_string().into()) - } - Self::Float => { - let py_timedelta = either_delta.into_pyobject(py)?; - let seconds: f64 = Self::total_seconds(&py_timedelta)?.extract()?; - Ok(seconds.to_string().into()) - } - } - } - - pub fn timedelta_serialize( - self, - py: Python, - either_delta: EitherTimedelta, - serializer: S, - ) -> Result { - match self { - Self::Iso8601 => { - let d = either_delta.to_duration().map_err(py_err_se_err)?; - serializer.serialize_str(&d.to_string()) - } - Self::Float => { - let py_timedelta = either_delta.into_pyobject(py).map_err(py_err_se_err)?; - let seconds = Self::total_seconds(&py_timedelta).map_err(py_err_se_err)?; - let seconds: f64 = seconds.extract().map_err(py_err_se_err)?; - serializer.serialize_f64(seconds) - } - } - } -} +impl TimedeltaMode {} impl From for TemporalMode { fn from(value: TimedeltaMode) -> Self { diff --git a/src/serializers/type_serializers/datetime_etc.rs b/src/serializers/type_serializers/datetime_etc.rs index 798b89ef5..84c712d6a 100644 --- a/src/serializers/type_serializers/datetime_etc.rs +++ b/src/serializers/type_serializers/datetime_etc.rs @@ -19,20 +19,20 @@ pub(crate) fn datetime_to_string(py_dt: &Bound<'_, PyDateTime>) -> PyResult) -> PyResult { pydatetime_as_datetime(py_dt).map(|dt| { dt.date.timestamp() as f64 - + dt.time.hour as f64 * 3600.0 - + dt.time.minute as f64 * 60.0 - + dt.time.second as f64 - + dt.time.microsecond as f64 / 1_000_000.0 + + f64::from(dt.time.hour) * 3600.0 + + f64::from(dt.time.minute) * 60.0 + + f64::from(dt.time.second) + + f64::from(dt.time.microsecond) / 1_000_000.0 }) } pub(crate) fn datetime_to_milliseconds(py_dt: &Bound<'_, PyDateTime>) -> PyResult { pydatetime_as_datetime(py_dt).map(|dt| { dt.date.timestamp_ms() as f64 - + dt.time.hour as f64 * 3_600_000.0 - + dt.time.minute as f64 * 60_000.0 - + dt.time.second as f64 * 1_000.0 - + dt.time.microsecond as f64 / 1_000.0 + + f64::from(dt.time.hour) * 3_600_000.0 + + f64::from(dt.time.minute) * 60_000.0 + + f64::from(dt.time.second) * 1_000.0 + + f64::from(dt.time.microsecond) / 1_000.0 }) } @@ -52,16 +52,20 @@ pub(crate) fn time_to_string(py_time: &Bound<'_, PyTime>) -> PyResult { } pub(crate) fn time_to_seconds(py_time: &Bound<'_, PyTime>) -> PyResult { - pytime_as_time(py_time, None) - .map(|t| t.hour as f64 * 3600.0 + t.minute as f64 * 60.0 + t.second as f64 + t.microsecond as f64 / 1_000_000.0) + pytime_as_time(py_time, None).map(|t| { + f64::from(t.hour) * 3600.0 + + f64::from(t.minute) * 60.0 + + f64::from(t.second) + + f64::from(t.microsecond) / 1_000_000.0 + }) } pub(crate) fn time_to_milliseconds(py_time: &Bound<'_, PyTime>) -> PyResult { pytime_as_time(py_time, None).map(|t| { - t.hour as f64 * 3_600_000.0 - + t.minute as f64 * 60_000.0 - + t.second as f64 * 1_000.0 - + t.microsecond as f64 / 1_000.0 + f64::from(t.hour) * 3_600_000.0 + + f64::from(t.minute) * 60_000.0 + + f64::from(t.second) * 1_000.0 + + f64::from(t.microsecond) / 1_000.0 }) }