Skip to content

Commit e0b4c94

Browse files
ollz272davidhewitt
andauthored
feat: add 'millisecond' option to ser_json_timedelta config parameter (#1427)
Co-authored-by: David Hewitt <[email protected]>
1 parent bc0c97a commit e0b4c94

File tree

10 files changed

+259
-57
lines changed

10 files changed

+259
-57
lines changed

python/pydantic_core/_pydantic_core.pyi

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ def to_json(
357357
by_alias: bool = True,
358358
exclude_none: bool = False,
359359
round_trip: bool = False,
360-
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',
360+
timedelta_mode: Literal['iso8601', 'seconds_float', 'milliseconds_float'] = 'iso8601',
361361
bytes_mode: Literal['utf8', 'base64', 'hex'] = 'utf8',
362362
inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants',
363363
serialize_unknown: bool = False,
@@ -378,7 +378,7 @@ def to_json(
378378
by_alias: Whether to use the alias names of fields.
379379
exclude_none: Whether to exclude fields that have a value of `None`.
380380
round_trip: Whether to enable serialization and validation round-trip support.
381-
timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'` or `'float'`.
381+
timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'`, `'seconds_float'` or `'milliseconds_float'`.
382382
bytes_mode: How to serialize `bytes` objects, either `'utf8'`, `'base64'`, or `'hex'`.
383383
inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`.
384384
serialize_unknown: Attempt to serialize unknown types, `str(value)` will be used, if that fails
@@ -432,7 +432,7 @@ def to_jsonable_python(
432432
by_alias: bool = True,
433433
exclude_none: bool = False,
434434
round_trip: bool = False,
435-
timedelta_mode: Literal['iso8601', 'float'] = 'iso8601',
435+
timedelta_mode: Literal['iso8601', 'seconds_float', 'milliseconds_float'] = 'iso8601',
436436
bytes_mode: Literal['utf8', 'base64', 'hex'] = 'utf8',
437437
inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants',
438438
serialize_unknown: bool = False,
@@ -453,7 +453,7 @@ def to_jsonable_python(
453453
by_alias: Whether to use the alias names of fields.
454454
exclude_none: Whether to exclude fields that have a value of `None`.
455455
round_trip: Whether to enable serialization and validation round-trip support.
456-
timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'` or `'float'`.
456+
timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'`, `'seconds_float'`, or`'milliseconds_float'`.
457457
bytes_mode: How to serialize `bytes` objects, either `'utf8'`, `'base64'`, or `'hex'`.
458458
inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`.
459459
serialize_unknown: Attempt to serialize unknown types, `str(value)` will be used, if that fails

python/pydantic_core/core_schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class CoreConfig(TypedDict, total=False):
105105
# fields related to float fields only
106106
allow_inf_nan: bool # default: True
107107
# the config options are used to customise serialization to JSON
108-
ser_json_timedelta: Literal['iso8601', 'float'] # default: 'iso8601'
108+
ser_json_timedelta: Literal['iso8601', 'seconds_float', 'milliseconds_float'] # default: 'iso8601'
109109
ser_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8'
110110
ser_json_inf_nan: Literal['null', 'constants', 'strings'] # default: 'null'
111111
val_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8'

src/input/datetime.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,93 @@ impl<'a> EitherTimedelta<'a> {
109109
Self::Raw(duration) => duration_as_pytimedelta(py, duration),
110110
}
111111
}
112+
113+
pub fn total_seconds(&self) -> PyResult<f64> {
114+
match self {
115+
Self::Raw(timedelta) => {
116+
let mut days: i64 = i64::from(timedelta.day);
117+
let mut seconds: i64 = i64::from(timedelta.second);
118+
let mut microseconds = i64::from(timedelta.microsecond);
119+
if !timedelta.positive {
120+
days = -days;
121+
seconds = -seconds;
122+
microseconds = -microseconds;
123+
}
124+
125+
let days_seconds = (86_400 * days) + seconds;
126+
if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) {
127+
let total_microseconds = days_seconds_as_micros + microseconds;
128+
Ok(total_microseconds as f64 / 1_000_000.0)
129+
} else {
130+
// Fall back to floating-point operations if the multiplication overflows
131+
let total_seconds = days_seconds as f64 + microseconds as f64 / 1_000_000.0;
132+
Ok(total_seconds)
133+
}
134+
}
135+
Self::PyExact(py_timedelta) => {
136+
let days: i64 = py_timedelta.get_days().into(); // -999999999 to 999999999
137+
let seconds: i64 = py_timedelta.get_seconds().into(); // 0 through 86399
138+
let microseconds = py_timedelta.get_microseconds(); // 0 through 999999
139+
let days_seconds = (86_400 * days) + seconds;
140+
if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) {
141+
let total_microseconds = days_seconds_as_micros + i64::from(microseconds);
142+
Ok(total_microseconds as f64 / 1_000_000.0)
143+
} else {
144+
// Fall back to floating-point operations if the multiplication overflows
145+
let total_seconds = days_seconds as f64 + f64::from(microseconds) / 1_000_000.0;
146+
Ok(total_seconds)
147+
}
148+
}
149+
Self::PySubclass(py_timedelta) => py_timedelta
150+
.call_method0(intern!(py_timedelta.py(), "total_seconds"))?
151+
.extract(),
152+
}
153+
}
154+
155+
pub fn total_milliseconds(&self) -> PyResult<f64> {
156+
match self {
157+
Self::Raw(timedelta) => {
158+
let mut days: i64 = i64::from(timedelta.day);
159+
let mut seconds: i64 = i64::from(timedelta.second);
160+
let mut microseconds = i64::from(timedelta.microsecond);
161+
if !timedelta.positive {
162+
days = -days;
163+
seconds = -seconds;
164+
microseconds = -microseconds;
165+
}
166+
167+
let days_seconds = (86_400 * days) + seconds;
168+
if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) {
169+
let total_microseconds = days_seconds_as_micros + microseconds;
170+
Ok(total_microseconds as f64 / 1_000.0)
171+
} else {
172+
// Fall back to floating-point operations if the multiplication overflows
173+
let total_seconds = days_seconds as f64 + microseconds as f64 / 1_000.0;
174+
Ok(total_seconds)
175+
}
176+
}
177+
Self::PyExact(py_timedelta) => {
178+
let days: i64 = py_timedelta.get_days().into(); // -999999999 to 999999999
179+
let seconds: i64 = py_timedelta.get_seconds().into(); // 0 through 86399
180+
let microseconds = py_timedelta.get_microseconds(); // 0 through 999999
181+
let days_seconds = (86_400 * days) + seconds;
182+
if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) {
183+
let total_microseconds = days_seconds_as_micros + i64::from(microseconds);
184+
Ok(total_microseconds as f64 / 1_000.0)
185+
} else {
186+
// Fall back to floating-point operations if the multiplication overflows
187+
let total_milliseconds = days_seconds as f64 * 1_000.0 + f64::from(microseconds) / 1_000.0;
188+
Ok(total_milliseconds)
189+
}
190+
}
191+
Self::PySubclass(py_timedelta) => {
192+
let total_seconds: f64 = py_timedelta
193+
.call_method0(intern!(py_timedelta.py(), "total_seconds"))?
194+
.extract()?;
195+
Ok(total_seconds / 1000.0)
196+
}
197+
}
198+
}
112199
}
113200

114201
impl<'a> TryFrom<&'_ Bound<'a, PyAny>> for EitherTimedelta<'a> {

src/serializers/config.rs

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::str::{from_utf8, FromStr, Utf8Error};
44
use base64::Engine;
55
use pyo3::intern;
66
use pyo3::prelude::*;
7-
use pyo3::types::{PyDelta, PyDict, PyString};
7+
use pyo3::types::{PyDict, PyString};
88

99
use serde::ser::Error;
1010

@@ -88,7 +88,8 @@ serialization_mode! {
8888
TimedeltaMode,
8989
"ser_json_timedelta",
9090
Iso8601 => "iso8601",
91-
Float => "float",
91+
SecondsFloat => "seconds_float",
92+
MillisecondsFloat => "milliseconds_float"
9293
}
9394

9495
serialization_mode! {
@@ -108,43 +109,42 @@ serialization_mode! {
108109
}
109110

110111
impl TimedeltaMode {
111-
fn total_seconds<'py>(py_timedelta: &Bound<'py, PyDelta>) -> PyResult<Bound<'py, PyAny>> {
112-
py_timedelta.call_method0(intern!(py_timedelta.py(), "total_seconds"))
113-
}
114-
115112
pub fn either_delta_to_json(self, py: Python, either_delta: &EitherTimedelta) -> PyResult<PyObject> {
116113
match self {
117114
Self::Iso8601 => {
118115
let d = either_delta.to_duration()?;
119116
Ok(d.to_string().into_py(py))
120117
}
121-
Self::Float => {
122-
// convert to int via a py timedelta not duration since we know this this case the input would have
123-
// been a py timedelta
124-
let py_timedelta = either_delta.try_into_py(py)?;
125-
let seconds = Self::total_seconds(&py_timedelta)?;
118+
Self::SecondsFloat => {
119+
let seconds: f64 = either_delta.total_seconds()?;
126120
Ok(seconds.into_py(py))
127121
}
122+
Self::MillisecondsFloat => {
123+
let milliseconds: f64 = either_delta.total_milliseconds()?;
124+
Ok(milliseconds.into_py(py))
125+
}
128126
}
129127
}
130128

131-
pub fn json_key<'py>(self, py: Python, either_delta: &EitherTimedelta) -> PyResult<Cow<'py, str>> {
129+
pub fn json_key<'py>(self, either_delta: &EitherTimedelta) -> PyResult<Cow<'py, str>> {
132130
match self {
133131
Self::Iso8601 => {
134132
let d = either_delta.to_duration()?;
135133
Ok(d.to_string().into())
136134
}
137-
Self::Float => {
138-
let py_timedelta = either_delta.try_into_py(py)?;
139-
let seconds: f64 = Self::total_seconds(&py_timedelta)?.extract()?;
135+
Self::SecondsFloat => {
136+
let seconds: f64 = either_delta.total_seconds()?;
140137
Ok(seconds.to_string().into())
141138
}
139+
Self::MillisecondsFloat => {
140+
let milliseconds: f64 = either_delta.total_milliseconds()?;
141+
Ok(milliseconds.to_string().into())
142+
}
142143
}
143144
}
144145

145146
pub fn timedelta_serialize<S: serde::ser::Serializer>(
146147
self,
147-
py: Python,
148148
either_delta: &EitherTimedelta,
149149
serializer: S,
150150
) -> Result<S::Ok, S::Error> {
@@ -153,12 +153,14 @@ impl TimedeltaMode {
153153
let d = either_delta.to_duration().map_err(py_err_se_err)?;
154154
serializer.serialize_str(&d.to_string())
155155
}
156-
Self::Float => {
157-
let py_timedelta = either_delta.try_into_py(py).map_err(py_err_se_err)?;
158-
let seconds = Self::total_seconds(&py_timedelta).map_err(py_err_se_err)?;
159-
let seconds: f64 = seconds.extract().map_err(py_err_se_err)?;
156+
Self::SecondsFloat => {
157+
let seconds: f64 = either_delta.total_seconds().map_err(py_err_se_err)?;
160158
serializer.serialize_f64(seconds)
161159
}
160+
Self::MillisecondsFloat => {
161+
let milliseconds: f64 = either_delta.total_milliseconds().map_err(py_err_se_err)?;
162+
serializer.serialize_f64(milliseconds)
163+
}
162164
}
163165
}
164166
}

src/serializers/infer.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ pub(crate) fn infer_serialize_known<S: Serializer>(
477477
extra
478478
.config
479479
.timedelta_mode
480-
.timedelta_serialize(value.py(), &either_delta, serializer)
480+
.timedelta_serialize(&either_delta, serializer)
481481
}
482482
ObType::Url => {
483483
let py_url: PyUrl = value.extract().map_err(py_err_se_err)?;
@@ -655,7 +655,7 @@ pub(crate) fn infer_json_key_known<'a>(
655655
}
656656
ObType::Timedelta => {
657657
let either_delta = EitherTimedelta::try_from(key)?;
658-
extra.config.timedelta_mode.json_key(key.py(), &either_delta)
658+
extra.config.timedelta_mode.json_key(&either_delta)
659659
}
660660
ObType::Url => {
661661
let py_url: PyUrl = key.extract()?;

src/serializers/type_serializers/timedelta.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ impl TypeSerializer for TimeDeltaSerializer {
5454

5555
fn json_key<'a>(&self, key: &'a Bound<'_, PyAny>, extra: &Extra) -> PyResult<Cow<'a, str>> {
5656
match EitherTimedelta::try_from(key) {
57-
Ok(either_timedelta) => self.timedelta_mode.json_key(key.py(), &either_timedelta),
57+
Ok(either_timedelta) => self.timedelta_mode.json_key(&either_timedelta),
5858
Err(_) => {
5959
extra.warnings.on_fallback_py(self.get_name(), key, extra)?;
6060
infer_json_key(key, extra)
@@ -71,9 +71,7 @@ impl TypeSerializer for TimeDeltaSerializer {
7171
extra: &Extra,
7272
) -> Result<S::Ok, S::Error> {
7373
match EitherTimedelta::try_from(value) {
74-
Ok(either_timedelta) => self
75-
.timedelta_mode
76-
.timedelta_serialize(value.py(), &either_timedelta, serializer),
74+
Ok(either_timedelta) => self.timedelta_mode.timedelta_serialize(&either_timedelta, serializer),
7775
Err(_) => {
7876
extra.warnings.on_fallback_ser::<S>(self.get_name(), value, extra)?;
7977
infer_serialize(value, serializer, include, exclude, extra)

0 commit comments

Comments
 (0)