From 010791546a9f5d6cae316f5b9fa8859c61f7728b Mon Sep 17 00:00:00 2001 From: Oliver Parker Date: Fri, 30 Aug 2024 08:28:19 +0100 Subject: [PATCH 01/19] feat: add 'millisecond' option to ser_json_timedelta config parameter --- python/pydantic_core/core_schema.py | 2 +- src/serializers/config.rs | 24 +++++++++++++++++++++++- tests/serializers/test_any.py | 24 ++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index 8635b63a5..ec53488a7 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -105,7 +105,7 @@ class CoreConfig(TypedDict, total=False): # fields related to float fields only allow_inf_nan: bool # default: True # the config options are used to customise serialization to JSON - ser_json_timedelta: Literal['iso8601', 'float'] # default: 'iso8601' + ser_json_timedelta: Literal['iso8601', 'float', 'millisecond'] # default: 'iso8601' ser_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8' ser_json_inf_nan: Literal['null', 'constants', 'strings'] # default: 'null' val_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8' diff --git a/src/serializers/config.rs b/src/serializers/config.rs index 13a833176..c7d7f4890 100644 --- a/src/serializers/config.rs +++ b/src/serializers/config.rs @@ -4,7 +4,7 @@ use std::str::{from_utf8, FromStr, Utf8Error}; use base64::Engine; use pyo3::intern; use pyo3::prelude::*; -use pyo3::types::{PyDelta, PyDict, PyString}; +use pyo3::types::{PyDelta, PyDict, PyFloat, PyString}; use serde::ser::Error; @@ -89,6 +89,7 @@ serialization_mode! { "ser_json_timedelta", Iso8601 => "iso8601", Float => "float", + Millisecond => "millisecond" } serialization_mode! { @@ -125,6 +126,14 @@ impl TimedeltaMode { let seconds = Self::total_seconds(&py_timedelta)?; Ok(seconds.into_py(py)) } + Self::Millisecond => { + // 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.try_into_py(py)?; + let seconds: f64 = Self::total_seconds(&py_timedelta)?.extract()?; + let object: Bound = PyFloat::new_bound(py, seconds * 1000.0); + Ok(object.into_py(py)) + } } } @@ -139,6 +148,12 @@ impl TimedeltaMode { let seconds: f64 = Self::total_seconds(&py_timedelta)?.extract()?; Ok(seconds.to_string().into()) } + Self::Millisecond => { + let py_timedelta = either_delta.try_into_py(py)?; + let seconds: f64 = Self::total_seconds(&py_timedelta)?.extract()?; + let milliseconds: f64 = seconds * 1000.0; + Ok(milliseconds.to_string().into()) + } } } @@ -159,6 +174,13 @@ impl TimedeltaMode { let seconds: f64 = seconds.extract().map_err(py_err_se_err)?; serializer.serialize_f64(seconds) } + Self::Millisecond => { + let py_timedelta = either_delta.try_into_py(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)?; + let milliseconds: f64 = seconds * 1000.0; + serializer.serialize_f64(milliseconds) + } } } } diff --git a/tests/serializers/test_any.py b/tests/serializers/test_any.py index 344e0ff02..3ba5c7be4 100644 --- a/tests/serializers/test_any.py +++ b/tests/serializers/test_any.py @@ -201,6 +201,30 @@ def test_any_config_timedelta_float_faction(): assert s.to_json({one_half_s: 'foo'}) == b'{"1.5":"foo"}' +def test_any_config_timedelta_millisecond(): + s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': 'millisecond'}) + h2 = timedelta(hours=2) + assert s.to_python(h2) == h2 + assert s.to_python(h2, mode='json') == 7200000.0 + assert s.to_json(h2) == b'7200000.0' + + assert s.to_python({h2: 'foo'}) == {h2: 'foo'} + assert s.to_python({h2: 'foo'}, mode='json') == {'7200000': 'foo'} + assert s.to_json({h2: 'foo'}) == b'{"7200000":"foo"}' + + +def test_any_config_timedelta_millisecond_fraction(): + s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': 'millisecond'}) + h2 = timedelta(seconds=1.5) + assert s.to_python(h2) == h2 + assert s.to_python(h2, mode='json') == 1500.0 + assert s.to_json(h2) == b'1500.0' + + assert s.to_python({h2: 'foo'}) == {h2: 'foo'} + assert s.to_python({h2: 'foo'}, mode='json') == {'1500': 'foo'} + assert s.to_json({h2: 'foo'}) == b'{"1500":"foo"}' + + def test_recursion(any_serializer): v = [1, 2] v.append(v) From 537a4841fb81d249528775e0917c4c147aba50c7 Mon Sep 17 00:00:00 2001 From: Oliver Parker Date: Wed, 11 Sep 2024 07:06:50 +0100 Subject: [PATCH 02/19] fix: add tests --- tests/serializers/test_any.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/serializers/test_any.py b/tests/serializers/test_any.py index 3ba5c7be4..30e6a4bc8 100644 --- a/tests/serializers/test_any.py +++ b/tests/serializers/test_any.py @@ -201,6 +201,18 @@ def test_any_config_timedelta_float_faction(): assert s.to_json({one_half_s: 'foo'}) == b'{"1.5":"foo"}' +def test_any_config_timedelta_float_negative(): + s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': 'float'}) + one_half_s = timedelta(seconds=-1.5) + assert s.to_python(one_half_s) == one_half_s + assert s.to_python(one_half_s, mode='json') == -1.5 + assert s.to_json(one_half_s) == b'-1.5' + + assert s.to_python({one_half_s: 'foo'}) == {one_half_s: 'foo'} + assert s.to_python({one_half_s: 'foo'}, mode='json') == {'-1.5': 'foo'} + assert s.to_json({one_half_s: 'foo'}) == b'{"-1.5":"foo"}' + + def test_any_config_timedelta_millisecond(): s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': 'millisecond'}) h2 = timedelta(hours=2) @@ -225,6 +237,18 @@ def test_any_config_timedelta_millisecond_fraction(): assert s.to_json({h2: 'foo'}) == b'{"1500":"foo"}' +def test_any_config_timedelta_millisecond_negative(): + s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': 'millisecond'}) + h2 = timedelta(seconds=-1.5) + assert s.to_python(h2) == h2 + assert s.to_python(h2, mode='json') == -1500.0 + assert s.to_json(h2) == b'-1500.0' + + assert s.to_python({h2: 'foo'}) == {h2: 'foo'} + assert s.to_python({h2: 'foo'}, mode='json') == {'-1500': 'foo'} + assert s.to_json({h2: 'foo'}) == b'{"-1500":"foo"}' + + def test_recursion(any_serializer): v = [1, 2] v.append(v) From f7a4008f630da1eb6d60f1ba27c5817ba09fd78e Mon Sep 17 00:00:00 2001 From: Oliver Parker Date: Wed, 11 Sep 2024 17:42:23 +0100 Subject: [PATCH 03/19] fix: add support seconds_float --- src/serializers/config.rs | 15 ++++++++------- tests/serializers/test_any.py | 23 +++++++++++++---------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/serializers/config.rs b/src/serializers/config.rs index c7d7f4890..3c1907bcf 100644 --- a/src/serializers/config.rs +++ b/src/serializers/config.rs @@ -89,7 +89,8 @@ serialization_mode! { "ser_json_timedelta", Iso8601 => "iso8601", Float => "float", - Millisecond => "millisecond" + SecondsFloat => "seconds_float", + MillisecondsFloat => "milliseconds_float" } serialization_mode! { @@ -119,14 +120,14 @@ impl TimedeltaMode { let d = either_delta.to_duration()?; Ok(d.to_string().into_py(py)) } - Self::Float => { + Self::Float | Self::SecondsFloat => { // 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.try_into_py(py)?; let seconds = Self::total_seconds(&py_timedelta)?; Ok(seconds.into_py(py)) } - Self::Millisecond => { + Self::MillisecondsFloat => { // 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.try_into_py(py)?; @@ -143,12 +144,12 @@ impl TimedeltaMode { let d = either_delta.to_duration()?; Ok(d.to_string().into()) } - Self::Float => { + Self::Float | Self::SecondsFloat => { let py_timedelta = either_delta.try_into_py(py)?; let seconds: f64 = Self::total_seconds(&py_timedelta)?.extract()?; Ok(seconds.to_string().into()) } - Self::Millisecond => { + Self::MillisecondsFloat => { let py_timedelta = either_delta.try_into_py(py)?; let seconds: f64 = Self::total_seconds(&py_timedelta)?.extract()?; let milliseconds: f64 = seconds * 1000.0; @@ -168,13 +169,13 @@ impl TimedeltaMode { let d = either_delta.to_duration().map_err(py_err_se_err)?; serializer.serialize_str(&d.to_string()) } - Self::Float => { + Self::Float | Self::SecondsFloat => { let py_timedelta = either_delta.try_into_py(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) } - Self::Millisecond => { + Self::MillisecondsFloat => { let py_timedelta = either_delta.try_into_py(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)?; diff --git a/tests/serializers/test_any.py b/tests/serializers/test_any.py index 30e6a4bc8..9e2de3cf0 100644 --- a/tests/serializers/test_any.py +++ b/tests/serializers/test_any.py @@ -9,7 +9,7 @@ from enum import Enum from math import inf, isinf, isnan, nan from pathlib import Path -from typing import ClassVar +from typing import ClassVar, Literal import pytest from dirty_equals import HasRepr, IsList @@ -177,8 +177,9 @@ def test_any_with_timedelta_serializer(): ] -def test_any_config_timedelta_float(): - s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': 'float'}) +@pytest.mark.parametrize('mode', ['float', 'seconds_float']) +def test_any_config_timedelta_float(mode: Literal['float', 'seconds_float']): + s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': mode}) h2 = timedelta(hours=2) assert s.to_python(h2) == h2 assert s.to_python(h2, mode='json') == 7200.0 @@ -189,8 +190,9 @@ def test_any_config_timedelta_float(): assert s.to_json({h2: 'foo'}) == b'{"7200":"foo"}' -def test_any_config_timedelta_float_faction(): - s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': 'float'}) +@pytest.mark.parametrize('mode', ['float', 'seconds_float']) +def test_any_config_timedelta_float_faction(mode): + s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': mode}) one_half_s = timedelta(seconds=1.5) assert s.to_python(one_half_s) == one_half_s assert s.to_python(one_half_s, mode='json') == 1.5 @@ -201,8 +203,9 @@ def test_any_config_timedelta_float_faction(): assert s.to_json({one_half_s: 'foo'}) == b'{"1.5":"foo"}' -def test_any_config_timedelta_float_negative(): - s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': 'float'}) +@pytest.mark.parametrize('mode', ['float', 'seconds_float']) +def test_any_config_timedelta_float_negative(mode): + s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': mode}) one_half_s = timedelta(seconds=-1.5) assert s.to_python(one_half_s) == one_half_s assert s.to_python(one_half_s, mode='json') == -1.5 @@ -214,7 +217,7 @@ def test_any_config_timedelta_float_negative(): def test_any_config_timedelta_millisecond(): - s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': 'millisecond'}) + s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': 'milliseconds_float'}) h2 = timedelta(hours=2) assert s.to_python(h2) == h2 assert s.to_python(h2, mode='json') == 7200000.0 @@ -226,7 +229,7 @@ def test_any_config_timedelta_millisecond(): def test_any_config_timedelta_millisecond_fraction(): - s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': 'millisecond'}) + s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': 'milliseconds_float'}) h2 = timedelta(seconds=1.5) assert s.to_python(h2) == h2 assert s.to_python(h2, mode='json') == 1500.0 @@ -238,7 +241,7 @@ def test_any_config_timedelta_millisecond_fraction(): def test_any_config_timedelta_millisecond_negative(): - s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': 'millisecond'}) + s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': 'milliseconds_float'}) h2 = timedelta(seconds=-1.5) assert s.to_python(h2) == h2 assert s.to_python(h2, mode='json') == -1500.0 From f5df4d4b03bd0971fc224b1112ce2fdcf10b0578 Mon Sep 17 00:00:00 2001 From: Oliver Parker Date: Wed, 11 Sep 2024 17:42:53 +0100 Subject: [PATCH 04/19] fix: add support seconds_float --- python/pydantic_core/core_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index ec53488a7..3eb738f2d 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -105,7 +105,7 @@ class CoreConfig(TypedDict, total=False): # fields related to float fields only allow_inf_nan: bool # default: True # the config options are used to customise serialization to JSON - ser_json_timedelta: Literal['iso8601', 'float', 'millisecond'] # default: 'iso8601' + ser_json_timedelta: Literal['iso8601', 'float', 'seconds_float', 'milliseconds_float'] # default: 'iso8601' ser_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8' ser_json_inf_nan: Literal['null', 'constants', 'strings'] # default: 'null' val_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8' From 367da21f1e25a356bb9fc2f876487e680a71ecd9 Mon Sep 17 00:00:00 2001 From: Oliver Parker Date: Wed, 11 Sep 2024 17:44:37 +0100 Subject: [PATCH 05/19] fix: add support seconds_float --- tests/serializers/test_pickling.py | 2 +- tests/serializers/test_timedelta.py | 2 +- tests/test_garbage_collection.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/serializers/test_pickling.py b/tests/serializers/test_pickling.py index 2ca230313..b6bc105d5 100644 --- a/tests/serializers/test_pickling.py +++ b/tests/serializers/test_pickling.py @@ -42,7 +42,7 @@ def test_schema_serializer_capturing_function(value, expected_python, expected_j def test_schema_serializer_containing_config(): - s = SchemaSerializer(core_schema.timedelta_schema(), config={'ser_json_timedelta': 'float'}) + s = SchemaSerializer(core_schema.timedelta_schema(), config={'ser_json_timedelta': 'seconds_float'}) s = pickle.loads(pickle.dumps(s)) assert s.to_python(timedelta(seconds=4, microseconds=500_000)) == timedelta(seconds=4, microseconds=500_000) diff --git a/tests/serializers/test_timedelta.py b/tests/serializers/test_timedelta.py index 19abb673d..9fcc9d362 100644 --- a/tests/serializers/test_timedelta.py +++ b/tests/serializers/test_timedelta.py @@ -31,7 +31,7 @@ def test_timedelta(): def test_timedelta_float(): - v = SchemaSerializer(core_schema.timedelta_schema(), config={'ser_json_timedelta': 'float'}) + v = SchemaSerializer(core_schema.timedelta_schema(), config={'ser_json_timedelta': 'seconds_float'}) assert v.to_python(timedelta(seconds=4, microseconds=500_000)) == timedelta(seconds=4, microseconds=500_000) assert v.to_python(timedelta(seconds=4, microseconds=500_000), mode='json') == 4.5 diff --git a/tests/test_garbage_collection.py b/tests/test_garbage_collection.py index f4e178f26..803979aa9 100644 --- a/tests/test_garbage_collection.py +++ b/tests/test_garbage_collection.py @@ -28,7 +28,7 @@ class BaseModel: def __init_subclass__(cls) -> None: cls.__schema__ = SchemaSerializer( - core_schema.model_schema(cls, GC_TEST_SCHEMA_INNER), config={'ser_json_timedelta': 'float'} + core_schema.model_schema(cls, GC_TEST_SCHEMA_INNER), config={'ser_json_timedelta': 'seconds_float'} ) cache: 'WeakValueDictionary[int, Any]' = WeakValueDictionary() From 4af46251c1658752f782ac74d6188d0ca4092a26 Mon Sep 17 00:00:00 2001 From: Oliver Parker Date: Wed, 11 Sep 2024 18:48:45 +0100 Subject: [PATCH 06/19] fix: changes --- python/pydantic_core/_pydantic_core.pyi | 10 ++++++---- tests/serializers/test_any.py | 8 ++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/python/pydantic_core/_pydantic_core.pyi b/python/pydantic_core/_pydantic_core.pyi index fb52a5b10..be5c9fb4b 100644 --- a/python/pydantic_core/_pydantic_core.pyi +++ b/python/pydantic_core/_pydantic_core.pyi @@ -356,7 +356,7 @@ def to_json( by_alias: bool = True, exclude_none: bool = False, round_trip: bool = False, - timedelta_mode: Literal['iso8601', 'float'] = 'iso8601', + timedelta_mode: Literal['iso8601', 'float', 'seconds_float', 'milliseconds_float'] = 'iso8601', bytes_mode: Literal['utf8', 'base64', 'hex'] = 'utf8', inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants', serialize_unknown: bool = False, @@ -377,7 +377,8 @@ def to_json( by_alias: Whether to use the alias names of fields. exclude_none: Whether to exclude fields that have a value of `None`. round_trip: Whether to enable serialization and validation round-trip support. - timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'` or `'float'`. + timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'`, `'seconds_float'`, `'milliseconds_float'`, `'float'`. + Note that `'float'` is deprecated in favour of `'seconds_float'` bytes_mode: How to serialize `bytes` objects, either `'utf8'`, `'base64'`, or `'hex'`. inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`. serialize_unknown: Attempt to serialize unknown types, `str(value)` will be used, if that fails @@ -431,7 +432,7 @@ def to_jsonable_python( by_alias: bool = True, exclude_none: bool = False, round_trip: bool = False, - timedelta_mode: Literal['iso8601', 'float'] = 'iso8601', + timedelta_mode: Literal['iso8601', 'float', 'seconds_float', 'milliseconds_float'] = 'iso8601', bytes_mode: Literal['utf8', 'base64', 'hex'] = 'utf8', inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants', serialize_unknown: bool = False, @@ -452,7 +453,8 @@ def to_jsonable_python( by_alias: Whether to use the alias names of fields. exclude_none: Whether to exclude fields that have a value of `None`. round_trip: Whether to enable serialization and validation round-trip support. - timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'` or `'float'`. + timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'`, `'seconds_float'`, `'milliseconds_float'`, `'float'`. + Note that `'float'` is deprecated in favour of `'seconds_float'` bytes_mode: How to serialize `bytes` objects, either `'utf8'`, `'base64'`, or `'hex'`. inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`. serialize_unknown: Attempt to serialize unknown types, `str(value)` will be used, if that fails diff --git a/tests/serializers/test_any.py b/tests/serializers/test_any.py index 9e2de3cf0..0106c8b20 100644 --- a/tests/serializers/test_any.py +++ b/tests/serializers/test_any.py @@ -473,9 +473,13 @@ def test_base64(): (lambda: datetime(2032, 1, 1), {}, b'"2032-01-01T00:00:00"'), (lambda: time(12, 34, 56), {}, b'"12:34:56"'), (lambda: timedelta(days=12, seconds=34, microseconds=56), {}, b'"P12DT34.000056S"'), - (lambda: timedelta(days=12, seconds=34, microseconds=56), dict(timedelta_mode='float'), b'1036834.000056'), + ( + lambda: timedelta(days=12, seconds=34, microseconds=56), + dict(timedelta_mode='seconds_float'), + b'1036834.000056', + ), (lambda: timedelta(seconds=-1), {}, b'"-PT1S"'), - (lambda: timedelta(seconds=-1), dict(timedelta_mode='float'), b'-1.0'), + (lambda: timedelta(seconds=-1), dict(timedelta_mode='seconds_float'), b'-1.0'), (lambda: {1, 2, 3}, {}, b'[1,2,3]'), (lambda: frozenset([1, 2, 3]), {}, b'[1,2,3]'), (lambda: (v for v in range(4)), {}, b'[0,1,2,3]'), From c959789be8ea3075e684779922a5e33c0ad5cbf2 Mon Sep 17 00:00:00 2001 From: Oliver Parker Date: Thu, 12 Sep 2024 19:06:42 +0100 Subject: [PATCH 07/19] fix: wip, added a bunch more tests --- src/input/datetime.rs | 34 +++++++++++++++ src/serializers/config.rs | 24 +++-------- tests/serializers/test_any.py | 78 +++++++++++++++++++++-------------- 3 files changed, 86 insertions(+), 50 deletions(-) diff --git a/src/input/datetime.rs b/src/input/datetime.rs index 577a91014..0ba053159 100644 --- a/src/input/datetime.rs +++ b/src/input/datetime.rs @@ -109,6 +109,40 @@ impl<'a> EitherTimedelta<'a> { Self::Raw(duration) => duration_as_pytimedelta(py, duration), } } + + pub fn total_seconds(&self) -> PyResult { + match self { + Self::Raw(timedelta) => { + let mut days: f64 = timedelta.day as f64; + let mut seconds: f64 = timedelta.second as f64; + let mut microseconds: f64 = timedelta.microsecond as f64; + let mut total_seconds: f64 = if !timedelta.positive { + -1.0 * (86400.0 * days + seconds + microseconds / 1_000_000.0) + } else { + 86400.0 * days + seconds + microseconds / 1_000_000.0 + }; + Ok(total_seconds) + } + Self::PyExact(py_timedelta) => { + let mut days: f64 = py_timedelta.get_days() as f64; // -999999999 to 999999999 + let mut seconds: f64 = py_timedelta.get_seconds() as f64; // 0 through 86399 + let mut microseconds: f64 = py_timedelta.get_microseconds() as f64; // 0 through 999999 + let positive = days >= 0.0; + let total_seconds: f64 = if !positive { + 86400.0 * days + seconds + microseconds / 1_000_000.0 + } else { + 86400.0 * days + seconds + microseconds / 1_000_000.0 + }; + Ok(total_seconds) + } + Self::PySubclass(py_timedelta) => { + let total_seconds: f64 = py_timedelta + .call_method0(intern!(py_timedelta.py(), "total_seconds"))? + .extract()?; + Ok(total_seconds) + } + } + } } impl<'a> TryFrom<&'_ Bound<'a, PyAny>> for EitherTimedelta<'a> { diff --git a/src/serializers/config.rs b/src/serializers/config.rs index 3c1907bcf..d0f0bc6bb 100644 --- a/src/serializers/config.rs +++ b/src/serializers/config.rs @@ -110,10 +110,6 @@ serialization_mode! { } 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 => { @@ -123,15 +119,13 @@ impl TimedeltaMode { Self::Float | Self::SecondsFloat => { // 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.try_into_py(py)?; - let seconds = Self::total_seconds(&py_timedelta)?; + let seconds: f64 = either_delta.total_seconds()?; Ok(seconds.into_py(py)) } Self::MillisecondsFloat => { // 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.try_into_py(py)?; - let seconds: f64 = Self::total_seconds(&py_timedelta)?.extract()?; + let seconds: f64 = either_delta.total_seconds()?; let object: Bound = PyFloat::new_bound(py, seconds * 1000.0); Ok(object.into_py(py)) } @@ -145,13 +139,11 @@ impl TimedeltaMode { Ok(d.to_string().into()) } Self::Float | Self::SecondsFloat => { - let py_timedelta = either_delta.try_into_py(py)?; - let seconds: f64 = Self::total_seconds(&py_timedelta)?.extract()?; + let seconds: f64 = either_delta.total_seconds()?; Ok(seconds.to_string().into()) } Self::MillisecondsFloat => { - let py_timedelta = either_delta.try_into_py(py)?; - let seconds: f64 = Self::total_seconds(&py_timedelta)?.extract()?; + let seconds: f64 = either_delta.total_seconds()?; let milliseconds: f64 = seconds * 1000.0; Ok(milliseconds.to_string().into()) } @@ -170,15 +162,11 @@ impl TimedeltaMode { serializer.serialize_str(&d.to_string()) } Self::Float | Self::SecondsFloat => { - let py_timedelta = either_delta.try_into_py(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)?; + let seconds: f64 = either_delta.total_seconds().map_err(py_err_se_err)?; serializer.serialize_f64(seconds) } Self::MillisecondsFloat => { - let py_timedelta = either_delta.try_into_py(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)?; + let seconds: f64 = either_delta.total_seconds().map_err(py_err_se_err)?; let milliseconds: f64 = seconds * 1000.0; serializer.serialize_f64(milliseconds) } diff --git a/tests/serializers/test_any.py b/tests/serializers/test_any.py index 0106c8b20..01590a455 100644 --- a/tests/serializers/test_any.py +++ b/tests/serializers/test_any.py @@ -216,40 +216,54 @@ def test_any_config_timedelta_float_negative(mode): assert s.to_json({one_half_s: 'foo'}) == b'{"-1.5":"foo"}' -def test_any_config_timedelta_millisecond(): - s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': 'milliseconds_float'}) - h2 = timedelta(hours=2) - assert s.to_python(h2) == h2 - assert s.to_python(h2, mode='json') == 7200000.0 - assert s.to_json(h2) == b'7200000.0' - - assert s.to_python({h2: 'foo'}) == {h2: 'foo'} - assert s.to_python({h2: 'foo'}, mode='json') == {'7200000': 'foo'} - assert s.to_json({h2: 'foo'}) == b'{"7200000":"foo"}' - - -def test_any_config_timedelta_millisecond_fraction(): - s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': 'milliseconds_float'}) - h2 = timedelta(seconds=1.5) - assert s.to_python(h2) == h2 - assert s.to_python(h2, mode='json') == 1500.0 - assert s.to_json(h2) == b'1500.0' - - assert s.to_python({h2: 'foo'}) == {h2: 'foo'} - assert s.to_python({h2: 'foo'}, mode='json') == {'1500': 'foo'} - assert s.to_json({h2: 'foo'}) == b'{"1500":"foo"}' - - -def test_any_config_timedelta_millisecond_negative(): +@pytest.mark.parametrize( + 'td,expected_to_python,expected_to_json,expected_to_python_dict,expected_to_json_dict', + [ + (timedelta(hours=2), 7200000.0, b'7200000.0', {'7200000': 'foo'}, b'{"7200000":"foo"}'), + (timedelta(hours=-2), -7200000.0, b'-7200000.0', {'-7200000': 'foo'}, b'{"-7200000":"foo"}'), + (timedelta(seconds=1.5), 1500.0, b'1500.0', {'1500': 'foo'}, b'{"1500":"foo"}'), + (timedelta(seconds=-1.5), -1500.0, b'-1500.0', {'-1500': 'foo'}, b'{"-1500":"foo"}'), + (timedelta(microseconds=1), 0.001, b'0.001', {'0.001': 'foo'}, b'{"0.001":"foo"}'), + (timedelta(microseconds=-1), -0.001, b'-0.001', {'-0.001': 'foo'}, b'{"-0.001":"foo"}'), + (timedelta(days=1), 86400000.0, b'86400000.0', {'86400000': 'foo'}, b'{"86400000":"foo"}'), + (timedelta(days=-1), -86400000.0, b'-86400000.0', {'-86400000': 'foo'}, b'{"-86400000":"foo"}'), + (timedelta(days=1, seconds=1), 86401000.0, b'86401000.0', {'86401000': 'foo'}, b'{"86401000":"foo"}'), + (timedelta(days=-1, seconds=-1), -86401000.0, b'-86401000.0', {'-86401000': 'foo'}, b'{"-86401000":"foo"}'), + (timedelta(days=1, seconds=-1), 86399000.0, b'86399000.0', {'86399000': 'foo'}, b'{"86399000":"foo"}'), + ( + timedelta(days=1, seconds=1, microseconds=1), + 86401000.001, + b'86401000.001', + {'86401000.001': 'foo'}, + b'{"86401000.001":"foo"}', + ), + ( + timedelta(days=-1, seconds=-1, microseconds=-1), + -86401000.001, + b'-86401000.001', + {'-86401000.001': 'foo'}, + b'{"-86401000.001":"foo"}', + ), + ( + timedelta(days=-1, seconds=-1, microseconds=-1), + -86401000.001, + b'-86401000.001', + {'-86401000.001': 'foo'}, + b'{"-86401000.001":"foo"}', + ), + ], +) +def test_any_config_timedelta_millisecond( + td: timedelta, expected_to_python, expected_to_json, expected_to_python_dict, expected_to_json_dict +): s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': 'milliseconds_float'}) - h2 = timedelta(seconds=-1.5) - assert s.to_python(h2) == h2 - assert s.to_python(h2, mode='json') == -1500.0 - assert s.to_json(h2) == b'-1500.0' + assert s.to_python(td) == td + assert s.to_python(td, mode='json') == expected_to_python + assert s.to_json(td) == expected_to_json - assert s.to_python({h2: 'foo'}) == {h2: 'foo'} - assert s.to_python({h2: 'foo'}, mode='json') == {'-1500': 'foo'} - assert s.to_json({h2: 'foo'}) == b'{"-1500":"foo"}' + assert s.to_python({td: 'foo'}) == {td: 'foo'} + assert s.to_python({td: 'foo'}, mode='json') == expected_to_python_dict + assert s.to_json({td: 'foo'}) == expected_to_json_dict def test_recursion(any_serializer): From 5d80b344f4fcb11055ffa56c3da0e060ae8c89f1 Mon Sep 17 00:00:00 2001 From: Oliver Parker Date: Thu, 12 Sep 2024 19:33:33 +0100 Subject: [PATCH 08/19] fix: fix tests, silly float values... --- tests/serializers/test_any.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/tests/serializers/test_any.py b/tests/serializers/test_any.py index 01590a455..d3c01fc15 100644 --- a/tests/serializers/test_any.py +++ b/tests/serializers/test_any.py @@ -224,7 +224,13 @@ def test_any_config_timedelta_float_negative(mode): (timedelta(seconds=1.5), 1500.0, b'1500.0', {'1500': 'foo'}, b'{"1500":"foo"}'), (timedelta(seconds=-1.5), -1500.0, b'-1500.0', {'-1500': 'foo'}, b'{"-1500":"foo"}'), (timedelta(microseconds=1), 0.001, b'0.001', {'0.001': 'foo'}, b'{"0.001":"foo"}'), - (timedelta(microseconds=-1), -0.001, b'-0.001', {'-0.001': 'foo'}, b'{"-0.001":"foo"}'), + ( + timedelta(microseconds=-1), + -0.0010000000000287557, + b'-0.0010000000000287557', + {'-0.0010000000000287557': 'foo'}, + b'{"-0.0010000000000287557":"foo"}', + ), (timedelta(days=1), 86400000.0, b'86400000.0', {'86400000': 'foo'}, b'{"86400000":"foo"}'), (timedelta(days=-1), -86400000.0, b'-86400000.0', {'-86400000': 'foo'}, b'{"-86400000":"foo"}'), (timedelta(days=1, seconds=1), 86401000.0, b'86401000.0', {'86401000': 'foo'}, b'{"86401000":"foo"}'), @@ -232,24 +238,17 @@ def test_any_config_timedelta_float_negative(mode): (timedelta(days=1, seconds=-1), 86399000.0, b'86399000.0', {'86399000': 'foo'}, b'{"86399000":"foo"}'), ( timedelta(days=1, seconds=1, microseconds=1), - 86401000.001, - b'86401000.001', - {'86401000.001': 'foo'}, - b'{"86401000.001":"foo"}', - ), - ( - timedelta(days=-1, seconds=-1, microseconds=-1), - -86401000.001, - b'-86401000.001', - {'-86401000.001': 'foo'}, - b'{"-86401000.001":"foo"}', + 86401000.00099999, + b'86401000.00099999', + {'86401000.00099999': 'foo'}, + b'{"86401000.00099999":"foo"}', ), ( timedelta(days=-1, seconds=-1, microseconds=-1), - -86401000.001, - b'-86401000.001', - {'-86401000.001': 'foo'}, - b'{"-86401000.001":"foo"}', + -86401000.00099999, + b'-86401000.00099999', + {'-86401000.00099999': 'foo'}, + b'{"-86401000.00099999":"foo"}', ), ], ) From c38fce0de984578082ddb69a238208d7f45e1a32 Mon Sep 17 00:00:00 2001 From: Oliver Parker Date: Thu, 12 Sep 2024 19:44:36 +0100 Subject: [PATCH 09/19] fix: full tests --- tests/serializers/test_any.py | 155 ++++++++++++++++++++++------------ 1 file changed, 101 insertions(+), 54 deletions(-) diff --git a/tests/serializers/test_any.py b/tests/serializers/test_any.py index d3c01fc15..13b982517 100644 --- a/tests/serializers/test_any.py +++ b/tests/serializers/test_any.py @@ -9,7 +9,7 @@ from enum import Enum from math import inf, isinf, isnan, nan from pathlib import Path -from typing import ClassVar, Literal +from typing import ClassVar import pytest from dirty_equals import HasRepr, IsList @@ -177,71 +177,76 @@ def test_any_with_timedelta_serializer(): ] -@pytest.mark.parametrize('mode', ['float', 'seconds_float']) -def test_any_config_timedelta_float(mode: Literal['float', 'seconds_float']): - s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': mode}) - h2 = timedelta(hours=2) - assert s.to_python(h2) == h2 - assert s.to_python(h2, mode='json') == 7200.0 - assert s.to_json(h2) == b'7200.0' - - assert s.to_python({h2: 'foo'}) == {h2: 'foo'} - assert s.to_python({h2: 'foo'}, mode='json') == {'7200': 'foo'} - assert s.to_json({h2: 'foo'}) == b'{"7200":"foo"}' - - -@pytest.mark.parametrize('mode', ['float', 'seconds_float']) -def test_any_config_timedelta_float_faction(mode): - s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': mode}) - one_half_s = timedelta(seconds=1.5) - assert s.to_python(one_half_s) == one_half_s - assert s.to_python(one_half_s, mode='json') == 1.5 - assert s.to_json(one_half_s) == b'1.5' - - assert s.to_python({one_half_s: 'foo'}) == {one_half_s: 'foo'} - assert s.to_python({one_half_s: 'foo'}, mode='json') == {'1.5': 'foo'} - assert s.to_json({one_half_s: 'foo'}) == b'{"1.5":"foo"}' - - -@pytest.mark.parametrize('mode', ['float', 'seconds_float']) -def test_any_config_timedelta_float_negative(mode): - s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': mode}) - one_half_s = timedelta(seconds=-1.5) - assert s.to_python(one_half_s) == one_half_s - assert s.to_python(one_half_s, mode='json') == -1.5 - assert s.to_json(one_half_s) == b'-1.5' - - assert s.to_python({one_half_s: 'foo'}) == {one_half_s: 'foo'} - assert s.to_python({one_half_s: 'foo'}, mode='json') == {'-1.5': 'foo'} - assert s.to_json({one_half_s: 'foo'}) == b'{"-1.5":"foo"}' - - @pytest.mark.parametrize( - 'td,expected_to_python,expected_to_json,expected_to_python_dict,expected_to_json_dict', + 'td,expected_to_python,expected_to_json,expected_to_python_dict,expected_to_json_dict,mode', [ - (timedelta(hours=2), 7200000.0, b'7200000.0', {'7200000': 'foo'}, b'{"7200000":"foo"}'), - (timedelta(hours=-2), -7200000.0, b'-7200000.0', {'-7200000': 'foo'}, b'{"-7200000":"foo"}'), - (timedelta(seconds=1.5), 1500.0, b'1500.0', {'1500': 'foo'}, b'{"1500":"foo"}'), - (timedelta(seconds=-1.5), -1500.0, b'-1500.0', {'-1500': 'foo'}, b'{"-1500":"foo"}'), - (timedelta(microseconds=1), 0.001, b'0.001', {'0.001': 'foo'}, b'{"0.001":"foo"}'), + (timedelta(hours=2), 7200000.0, b'7200000.0', {'7200000': 'foo'}, b'{"7200000":"foo"}', 'milliseconds_float'), + ( + timedelta(hours=-2), + -7200000.0, + b'-7200000.0', + {'-7200000': 'foo'}, + b'{"-7200000":"foo"}', + 'milliseconds_float', + ), + (timedelta(seconds=1.5), 1500.0, b'1500.0', {'1500': 'foo'}, b'{"1500":"foo"}', 'milliseconds_float'), + (timedelta(seconds=-1.5), -1500.0, b'-1500.0', {'-1500': 'foo'}, b'{"-1500":"foo"}', 'milliseconds_float'), + (timedelta(microseconds=1), 0.001, b'0.001', {'0.001': 'foo'}, b'{"0.001":"foo"}', 'milliseconds_float'), ( timedelta(microseconds=-1), -0.0010000000000287557, b'-0.0010000000000287557', {'-0.0010000000000287557': 'foo'}, b'{"-0.0010000000000287557":"foo"}', + 'milliseconds_float', + ), + ( + timedelta(days=1), + 86400000.0, + b'86400000.0', + {'86400000': 'foo'}, + b'{"86400000":"foo"}', + 'milliseconds_float', + ), + ( + timedelta(days=-1), + -86400000.0, + b'-86400000.0', + {'-86400000': 'foo'}, + b'{"-86400000":"foo"}', + 'milliseconds_float', + ), + ( + timedelta(days=1, seconds=1), + 86401000.0, + b'86401000.0', + {'86401000': 'foo'}, + b'{"86401000":"foo"}', + 'milliseconds_float', + ), + ( + timedelta(days=-1, seconds=-1), + -86401000.0, + b'-86401000.0', + {'-86401000': 'foo'}, + b'{"-86401000":"foo"}', + 'milliseconds_float', + ), + ( + timedelta(days=1, seconds=-1), + 86399000.0, + b'86399000.0', + {'86399000': 'foo'}, + b'{"86399000":"foo"}', + 'milliseconds_float', ), - (timedelta(days=1), 86400000.0, b'86400000.0', {'86400000': 'foo'}, b'{"86400000":"foo"}'), - (timedelta(days=-1), -86400000.0, b'-86400000.0', {'-86400000': 'foo'}, b'{"-86400000":"foo"}'), - (timedelta(days=1, seconds=1), 86401000.0, b'86401000.0', {'86401000': 'foo'}, b'{"86401000":"foo"}'), - (timedelta(days=-1, seconds=-1), -86401000.0, b'-86401000.0', {'-86401000': 'foo'}, b'{"-86401000":"foo"}'), - (timedelta(days=1, seconds=-1), 86399000.0, b'86399000.0', {'86399000': 'foo'}, b'{"86399000":"foo"}'), ( timedelta(days=1, seconds=1, microseconds=1), 86401000.00099999, b'86401000.00099999', {'86401000.00099999': 'foo'}, b'{"86401000.00099999":"foo"}', + 'milliseconds_float', ), ( timedelta(days=-1, seconds=-1, microseconds=-1), @@ -249,13 +254,55 @@ def test_any_config_timedelta_float_negative(mode): b'-86401000.00099999', {'-86401000.00099999': 'foo'}, b'{"-86401000.00099999":"foo"}', + 'milliseconds_float', + ), + (timedelta(hours=2), 7200.0, b'7200.0', {'7200': 'foo'}, b'{"7200":"foo"}', 'seconds_float'), + (timedelta(hours=-2), -7200.0, b'-7200.0', {'-7200': 'foo'}, b'{"-7200":"foo"}', 'seconds_float'), + (timedelta(seconds=1.5), 1.5, b'1.5', {'1.5': 'foo'}, b'{"1.5":"foo"}', 'seconds_float'), + (timedelta(seconds=-1.5), -1.5, b'-1.5', {'-1.5': 'foo'}, b'{"-1.5":"foo"}', 'seconds_float'), + (timedelta(microseconds=1), 1e-6, b'1e-6', {'0.000001': 'foo'}, b'{"0.000001":"foo"}', 'seconds_float'), + ( + timedelta(microseconds=-1), + -1.0000000000287557e-6, + b'-1.0000000000287557e-6', + {'-0.0000010000000000287557': 'foo'}, + b'{"-0.0000010000000000287557":"foo"}', + 'seconds_float', + ), + (timedelta(days=1), 86400.0, b'86400.0', {'86400': 'foo'}, b'{"86400":"foo"}', 'seconds_float'), + (timedelta(days=-1), -86400.0, b'-86400.0', {'-86400': 'foo'}, b'{"-86400":"foo"}', 'seconds_float'), + (timedelta(days=1, seconds=1), 86401.0, b'86401.0', {'86401': 'foo'}, b'{"86401":"foo"}', 'seconds_float'), + ( + timedelta(days=-1, seconds=-1), + -86401.0, + b'-86401.0', + {'-86401': 'foo'}, + b'{"-86401":"foo"}', + 'seconds_float', + ), + (timedelta(days=1, seconds=-1), 86399.0, b'86399.0', {'86399': 'foo'}, b'{"86399":"foo"}', 'seconds_float'), + ( + timedelta(days=1, seconds=1, microseconds=1), + 86401.000001, + b'86401.000001', + {'86401.000001': 'foo'}, + b'{"86401.000001":"foo"}', + 'seconds_float', + ), + ( + timedelta(days=-1, seconds=-1, microseconds=-1), + -86401.000001, + b'-86401.000001', + {'-86401.000001': 'foo'}, + b'{"-86401.000001":"foo"}', + 'seconds_float', ), ], ) -def test_any_config_timedelta_millisecond( - td: timedelta, expected_to_python, expected_to_json, expected_to_python_dict, expected_to_json_dict +def test_any_config_timedelta( + td: timedelta, expected_to_python, expected_to_json, expected_to_python_dict, expected_to_json_dict, mode ): - s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': 'milliseconds_float'}) + s = SchemaSerializer(core_schema.any_schema(), config={'ser_json_timedelta': mode}) assert s.to_python(td) == td assert s.to_python(td, mode='json') == expected_to_python assert s.to_json(td) == expected_to_json From abe32e6952814739ef5fbfd7c6c179968be9677f Mon Sep 17 00:00:00 2001 From: Oliver Parker Date: Thu, 12 Sep 2024 19:53:11 +0100 Subject: [PATCH 10/19] fix: final bits, hopefully ok --- src/input/datetime.rs | 22 +++++++------------ src/serializers/config.rs | 5 ++--- src/serializers/infer.rs | 4 ++-- src/serializers/type_serializers/timedelta.rs | 6 ++--- 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/input/datetime.rs b/src/input/datetime.rs index 0ba053159..5dc3e7ee1 100644 --- a/src/input/datetime.rs +++ b/src/input/datetime.rs @@ -113,10 +113,10 @@ impl<'a> EitherTimedelta<'a> { pub fn total_seconds(&self) -> PyResult { match self { Self::Raw(timedelta) => { - let mut days: f64 = timedelta.day as f64; - let mut seconds: f64 = timedelta.second as f64; - let mut microseconds: f64 = timedelta.microsecond as f64; - let mut total_seconds: f64 = if !timedelta.positive { + let days: f64 = f64::from(timedelta.day); + let seconds: f64 = f64::from(timedelta.second); + let microseconds: f64 = f64::from(timedelta.microsecond); + let total_seconds: f64 = if !timedelta.positive { -1.0 * (86400.0 * days + seconds + microseconds / 1_000_000.0) } else { 86400.0 * days + seconds + microseconds / 1_000_000.0 @@ -124,16 +124,10 @@ impl<'a> EitherTimedelta<'a> { Ok(total_seconds) } Self::PyExact(py_timedelta) => { - let mut days: f64 = py_timedelta.get_days() as f64; // -999999999 to 999999999 - let mut seconds: f64 = py_timedelta.get_seconds() as f64; // 0 through 86399 - let mut microseconds: f64 = py_timedelta.get_microseconds() as f64; // 0 through 999999 - let positive = days >= 0.0; - let total_seconds: f64 = if !positive { - 86400.0 * days + seconds + microseconds / 1_000_000.0 - } else { - 86400.0 * days + seconds + microseconds / 1_000_000.0 - }; - Ok(total_seconds) + let days: f64 = f64::from(py_timedelta.get_days()); // -999999999 to 999999999 + let seconds: f64 = f64::from(py_timedelta.get_seconds()); // 0 through 86399 + let microseconds: f64 = f64::from(py_timedelta.get_microseconds()); // 0 through 999999 + Ok(86400.0 * days + seconds + microseconds / 1_000_000.0) } Self::PySubclass(py_timedelta) => { let total_seconds: f64 = py_timedelta diff --git a/src/serializers/config.rs b/src/serializers/config.rs index d0f0bc6bb..e57b836d8 100644 --- a/src/serializers/config.rs +++ b/src/serializers/config.rs @@ -4,7 +4,7 @@ use std::str::{from_utf8, FromStr, Utf8Error}; use base64::Engine; use pyo3::intern; use pyo3::prelude::*; -use pyo3::types::{PyDelta, PyDict, PyFloat, PyString}; +use pyo3::types::{PyDict, PyFloat, PyString}; use serde::ser::Error; @@ -132,7 +132,7 @@ impl TimedeltaMode { } } - pub fn json_key<'py>(self, py: Python, either_delta: &EitherTimedelta) -> PyResult> { + pub fn json_key<'py>(self, either_delta: &EitherTimedelta) -> PyResult> { match self { Self::Iso8601 => { let d = either_delta.to_duration()?; @@ -152,7 +152,6 @@ impl TimedeltaMode { pub fn timedelta_serialize( self, - py: Python, either_delta: &EitherTimedelta, serializer: S, ) -> Result { diff --git a/src/serializers/infer.rs b/src/serializers/infer.rs index 4e6bb504f..f03e890d5 100644 --- a/src/serializers/infer.rs +++ b/src/serializers/infer.rs @@ -477,7 +477,7 @@ pub(crate) fn infer_serialize_known( extra .config .timedelta_mode - .timedelta_serialize(value.py(), &either_delta, serializer) + .timedelta_serialize(&either_delta, serializer) } ObType::Url => { let py_url: PyUrl = value.extract().map_err(py_err_se_err)?; @@ -655,7 +655,7 @@ pub(crate) fn infer_json_key_known<'a>( } ObType::Timedelta => { let either_delta = EitherTimedelta::try_from(key)?; - extra.config.timedelta_mode.json_key(key.py(), &either_delta) + extra.config.timedelta_mode.json_key(&either_delta) } ObType::Url => { let py_url: PyUrl = key.extract()?; diff --git a/src/serializers/type_serializers/timedelta.rs b/src/serializers/type_serializers/timedelta.rs index a62b595d0..acc604732 100644 --- a/src/serializers/type_serializers/timedelta.rs +++ b/src/serializers/type_serializers/timedelta.rs @@ -54,7 +54,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) => self.timedelta_mode.json_key(key.py(), &either_timedelta), + Ok(either_timedelta) => self.timedelta_mode.json_key(&either_timedelta), Err(_) => { extra.warnings.on_fallback_py(self.get_name(), key, extra)?; infer_json_key(key, extra) @@ -71,9 +71,7 @@ impl TypeSerializer for TimeDeltaSerializer { extra: &Extra, ) -> Result { match EitherTimedelta::try_from(value) { - Ok(either_timedelta) => self - .timedelta_mode - .timedelta_serialize(value.py(), &either_timedelta, serializer), + Ok(either_timedelta) => self.timedelta_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 9241bd7576b9d9298d4ece55ed9e100e57bf4eed Mon Sep 17 00:00:00 2001 From: Oliver Parker Date: Thu, 12 Sep 2024 19:55:05 +0100 Subject: [PATCH 11/19] fix: remove float support, will be handled by pydantic for deprecation --- src/serializers/config.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/serializers/config.rs b/src/serializers/config.rs index e57b836d8..0fd8f1a48 100644 --- a/src/serializers/config.rs +++ b/src/serializers/config.rs @@ -88,7 +88,6 @@ serialization_mode! { TimedeltaMode, "ser_json_timedelta", Iso8601 => "iso8601", - Float => "float", SecondsFloat => "seconds_float", MillisecondsFloat => "milliseconds_float" } @@ -116,7 +115,7 @@ impl TimedeltaMode { let d = either_delta.to_duration()?; Ok(d.to_string().into_py(py)) } - Self::Float | Self::SecondsFloat => { + Self::SecondsFloat => { // convert to int via a py timedelta not duration since we know this this case the input would have // been a py timedelta let seconds: f64 = either_delta.total_seconds()?; @@ -138,7 +137,7 @@ impl TimedeltaMode { let d = either_delta.to_duration()?; Ok(d.to_string().into()) } - Self::Float | Self::SecondsFloat => { + Self::SecondsFloat => { let seconds: f64 = either_delta.total_seconds()?; Ok(seconds.to_string().into()) } @@ -160,7 +159,7 @@ impl TimedeltaMode { let d = either_delta.to_duration().map_err(py_err_se_err)?; serializer.serialize_str(&d.to_string()) } - Self::Float | Self::SecondsFloat => { + Self::SecondsFloat => { let seconds: f64 = either_delta.total_seconds().map_err(py_err_se_err)?; serializer.serialize_f64(seconds) } From 61971a0c3829c77532d2e17188cd33d8c702a0bd Mon Sep 17 00:00:00 2001 From: Oliver Parker Date: Thu, 12 Sep 2024 19:57:33 +0100 Subject: [PATCH 12/19] fix: some bits --- python/pydantic_core/_pydantic_core.pyi | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/pydantic_core/_pydantic_core.pyi b/python/pydantic_core/_pydantic_core.pyi index be5c9fb4b..751b15a85 100644 --- a/python/pydantic_core/_pydantic_core.pyi +++ b/python/pydantic_core/_pydantic_core.pyi @@ -356,7 +356,7 @@ def to_json( by_alias: bool = True, exclude_none: bool = False, round_trip: bool = False, - timedelta_mode: Literal['iso8601', 'float', 'seconds_float', 'milliseconds_float'] = 'iso8601', + timedelta_mode: Literal['iso8601', 'seconds_float', 'milliseconds_float'] = 'iso8601', bytes_mode: Literal['utf8', 'base64', 'hex'] = 'utf8', inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants', serialize_unknown: bool = False, @@ -377,7 +377,7 @@ def to_json( by_alias: Whether to use the alias names of fields. exclude_none: Whether to exclude fields that have a value of `None`. round_trip: Whether to enable serialization and validation round-trip support. - timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'`, `'seconds_float'`, `'milliseconds_float'`, `'float'`. + timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'`, `'seconds_float'` or `'milliseconds_float'`. Note that `'float'` is deprecated in favour of `'seconds_float'` bytes_mode: How to serialize `bytes` objects, either `'utf8'`, `'base64'`, or `'hex'`. inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`. @@ -432,7 +432,7 @@ def to_jsonable_python( by_alias: bool = True, exclude_none: bool = False, round_trip: bool = False, - timedelta_mode: Literal['iso8601', 'float', 'seconds_float', 'milliseconds_float'] = 'iso8601', + timedelta_mode: Literal['iso8601', 'seconds_float', 'milliseconds_float'] = 'iso8601', bytes_mode: Literal['utf8', 'base64', 'hex'] = 'utf8', inf_nan_mode: Literal['null', 'constants', 'strings'] = 'constants', serialize_unknown: bool = False, @@ -453,7 +453,7 @@ def to_jsonable_python( by_alias: Whether to use the alias names of fields. exclude_none: Whether to exclude fields that have a value of `None`. round_trip: Whether to enable serialization and validation round-trip support. - timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'`, `'seconds_float'`, `'milliseconds_float'`, `'float'`. + timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'`, `'seconds_float'`, or`'milliseconds_float'`. Note that `'float'` is deprecated in favour of `'seconds_float'` bytes_mode: How to serialize `bytes` objects, either `'utf8'`, `'base64'`, or `'hex'`. inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`. From 9aea492b0152df7c4636a2a8899f4c43f804d6af Mon Sep 17 00:00:00 2001 From: Oliver Parker Date: Thu, 12 Sep 2024 19:58:36 +0100 Subject: [PATCH 13/19] fix: remove a deprecation message --- python/pydantic_core/_pydantic_core.pyi | 2 -- 1 file changed, 2 deletions(-) diff --git a/python/pydantic_core/_pydantic_core.pyi b/python/pydantic_core/_pydantic_core.pyi index 751b15a85..00ff3bd83 100644 --- a/python/pydantic_core/_pydantic_core.pyi +++ b/python/pydantic_core/_pydantic_core.pyi @@ -378,7 +378,6 @@ def to_json( exclude_none: Whether to exclude fields that have a value of `None`. round_trip: Whether to enable serialization and validation round-trip support. timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'`, `'seconds_float'` or `'milliseconds_float'`. - Note that `'float'` is deprecated in favour of `'seconds_float'` bytes_mode: How to serialize `bytes` objects, either `'utf8'`, `'base64'`, or `'hex'`. inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`. serialize_unknown: Attempt to serialize unknown types, `str(value)` will be used, if that fails @@ -454,7 +453,6 @@ def to_jsonable_python( exclude_none: Whether to exclude fields that have a value of `None`. round_trip: Whether to enable serialization and validation round-trip support. timedelta_mode: How to serialize `timedelta` objects, either `'iso8601'`, `'seconds_float'`, or`'milliseconds_float'`. - Note that `'float'` is deprecated in favour of `'seconds_float'` bytes_mode: How to serialize `bytes` objects, either `'utf8'`, `'base64'`, or `'hex'`. inf_nan_mode: How to serialize `Infinity`, `-Infinity` and `NaN` values, either `'null'`, `'constants'`, or `'strings'`. serialize_unknown: Attempt to serialize unknown types, `str(value)` will be used, if that fails From bff8e3baffba03dd778ffdf63f66bfe00218978d Mon Sep 17 00:00:00 2001 From: Oliver Parker Date: Fri, 13 Sep 2024 06:09:54 +0100 Subject: [PATCH 14/19] fix: remove redundant comment --- src/serializers/config.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/serializers/config.rs b/src/serializers/config.rs index 0fd8f1a48..58296c340 100644 --- a/src/serializers/config.rs +++ b/src/serializers/config.rs @@ -116,14 +116,10 @@ impl TimedeltaMode { Ok(d.to_string().into_py(py)) } Self::SecondsFloat => { - // convert to int via a py timedelta not duration since we know this this case the input would have - // been a py timedelta let seconds: f64 = either_delta.total_seconds()?; Ok(seconds.into_py(py)) } Self::MillisecondsFloat => { - // convert to int via a py timedelta not duration since we know this this case the input would have - // been a py timedelta let seconds: f64 = either_delta.total_seconds()?; let object: Bound = PyFloat::new_bound(py, seconds * 1000.0); Ok(object.into_py(py)) From 00e68b76a1cea1b4e7b06a431b87432ea3e564c6 Mon Sep 17 00:00:00 2001 From: Oliver Parker Date: Fri, 13 Sep 2024 06:10:40 +0100 Subject: [PATCH 15/19] fix: remove type hint --- python/pydantic_core/core_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index 3eb738f2d..c5748fd19 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -105,7 +105,7 @@ class CoreConfig(TypedDict, total=False): # fields related to float fields only allow_inf_nan: bool # default: True # the config options are used to customise serialization to JSON - ser_json_timedelta: Literal['iso8601', 'float', 'seconds_float', 'milliseconds_float'] # default: 'iso8601' + ser_json_timedelta: Literal['iso8601', 'seconds_float', 'milliseconds_float'] # default: 'iso8601' ser_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8' ser_json_inf_nan: Literal['null', 'constants', 'strings'] # default: 'null' val_json_bytes: Literal['utf8', 'base64', 'hex'] # default: 'utf8' From b4c86de84ed9c4df959424e64975720b2f88e3ea Mon Sep 17 00:00:00 2001 From: Oliver Parker Date: Fri, 13 Sep 2024 06:12:14 +0100 Subject: [PATCH 16/19] fix: simplify using Davids suggestion --- src/input/datetime.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/input/datetime.rs b/src/input/datetime.rs index 5dc3e7ee1..e6cce1a1a 100644 --- a/src/input/datetime.rs +++ b/src/input/datetime.rs @@ -129,12 +129,9 @@ impl<'a> EitherTimedelta<'a> { let microseconds: f64 = f64::from(py_timedelta.get_microseconds()); // 0 through 999999 Ok(86400.0 * days + seconds + microseconds / 1_000_000.0) } - Self::PySubclass(py_timedelta) => { - let total_seconds: f64 = py_timedelta - .call_method0(intern!(py_timedelta.py(), "total_seconds"))? - .extract()?; - Ok(total_seconds) - } + Self::PySubclass(py_timedelta) => py_timedelta + .call_method0(intern!(py_timedelta.py(), "total_seconds"))? + .extract(), } } } From a3900f60a76c36da79502d4f549c8b3ded90df11 Mon Sep 17 00:00:00 2001 From: Oliver Parker Date: Fri, 13 Sep 2024 06:28:04 +0100 Subject: [PATCH 17/19] fix: some floating point bits --- src/input/datetime.rs | 25 +++++++++++++++++++++++++ src/serializers/config.rs | 13 +++++-------- tests/serializers/test_any.py | 24 ++++++++++++------------ 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/src/input/datetime.rs b/src/input/datetime.rs index e6cce1a1a..c883b7ed4 100644 --- a/src/input/datetime.rs +++ b/src/input/datetime.rs @@ -134,6 +134,31 @@ impl<'a> EitherTimedelta<'a> { .extract(), } } + + pub fn total_milliseconds(&self) -> PyResult { + match self { + Self::Raw(timedelta) => { + let days: f64 = f64::from(timedelta.day); + let seconds: f64 = f64::from(timedelta.second); + let microseconds: f64 = f64::from(timedelta.microsecond); + let total_seconds: f64 = if !timedelta.positive { + -1.0 * (86_400_000.0 * days + seconds * 1_000.0 + microseconds / 1_000.0) + } else { + 86_400_000.0 * days + seconds * 1_000.0 + microseconds / 1_000.0 + }; + Ok(total_seconds) + } + Self::PyExact(py_timedelta) => { + let days: f64 = f64::from(py_timedelta.get_days()); // -999999999 to 999999999 + let seconds: f64 = f64::from(py_timedelta.get_seconds()); // 0 through 86399 + let microseconds: f64 = f64::from(py_timedelta.get_microseconds()); // 0 through 999999 + Ok(86_400_000.0 * days + seconds * 1_000.0 + microseconds / 1_000.0) + } + Self::PySubclass(py_timedelta) => py_timedelta + .call_method0(intern!(py_timedelta.py(), "total_seconds"))? + .extract(), + } + } } impl<'a> TryFrom<&'_ Bound<'a, PyAny>> for EitherTimedelta<'a> { diff --git a/src/serializers/config.rs b/src/serializers/config.rs index 58296c340..8b1367958 100644 --- a/src/serializers/config.rs +++ b/src/serializers/config.rs @@ -4,7 +4,7 @@ use std::str::{from_utf8, FromStr, Utf8Error}; use base64::Engine; use pyo3::intern; use pyo3::prelude::*; -use pyo3::types::{PyDict, PyFloat, PyString}; +use pyo3::types::{PyDict, PyString}; use serde::ser::Error; @@ -120,9 +120,8 @@ impl TimedeltaMode { Ok(seconds.into_py(py)) } Self::MillisecondsFloat => { - let seconds: f64 = either_delta.total_seconds()?; - let object: Bound = PyFloat::new_bound(py, seconds * 1000.0); - Ok(object.into_py(py)) + let milliseconds: f64 = either_delta.total_milliseconds()?; + Ok(milliseconds.into_py(py)) } } } @@ -138,8 +137,7 @@ impl TimedeltaMode { Ok(seconds.to_string().into()) } Self::MillisecondsFloat => { - let seconds: f64 = either_delta.total_seconds()?; - let milliseconds: f64 = seconds * 1000.0; + let milliseconds: f64 = either_delta.total_milliseconds()?; Ok(milliseconds.to_string().into()) } } @@ -160,8 +158,7 @@ impl TimedeltaMode { serializer.serialize_f64(seconds) } Self::MillisecondsFloat => { - let seconds: f64 = either_delta.total_seconds().map_err(py_err_se_err)?; - let milliseconds: f64 = seconds * 1000.0; + let milliseconds: f64 = either_delta.total_milliseconds().map_err(py_err_se_err)?; serializer.serialize_f64(milliseconds) } } diff --git a/tests/serializers/test_any.py b/tests/serializers/test_any.py index 13b982517..21096e475 100644 --- a/tests/serializers/test_any.py +++ b/tests/serializers/test_any.py @@ -194,10 +194,10 @@ def test_any_with_timedelta_serializer(): (timedelta(microseconds=1), 0.001, b'0.001', {'0.001': 'foo'}, b'{"0.001":"foo"}', 'milliseconds_float'), ( timedelta(microseconds=-1), - -0.0010000000000287557, - b'-0.0010000000000287557', - {'-0.0010000000000287557': 'foo'}, - b'{"-0.0010000000000287557":"foo"}', + -0.0009999999999763531, + b'-0.0009999999999763531', + {'-0.0009999999999763531': 'foo'}, + b'{"-0.0009999999999763531":"foo"}', 'milliseconds_float', ), ( @@ -242,18 +242,18 @@ def test_any_with_timedelta_serializer(): ), ( timedelta(days=1, seconds=1, microseconds=1), - 86401000.00099999, - b'86401000.00099999', - {'86401000.00099999': 'foo'}, - b'{"86401000.00099999":"foo"}', + 86401000.001, + b'86401000.001', + {'86401000.001': 'foo'}, + b'{"86401000.001":"foo"}', 'milliseconds_float', ), ( timedelta(days=-1, seconds=-1, microseconds=-1), - -86401000.00099999, - b'-86401000.00099999', - {'-86401000.00099999': 'foo'}, - b'{"-86401000.00099999":"foo"}', + -86401000.001, + b'-86401000.001', + {'-86401000.001': 'foo'}, + b'{"-86401000.001":"foo"}', 'milliseconds_float', ), (timedelta(hours=2), 7200.0, b'7200.0', {'7200': 'foo'}, b'{"7200":"foo"}', 'seconds_float'), From fb80b8335f6f26c6e6f4dfb0947d90a991713a83 Mon Sep 17 00:00:00 2001 From: Oliver Parker <46482091+ollz272@users.noreply.github.com> Date: Tue, 17 Sep 2024 18:48:00 +0100 Subject: [PATCH 18/19] Update src/input/datetime.rs Co-authored-by: David Hewitt --- src/input/datetime.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/input/datetime.rs b/src/input/datetime.rs index c883b7ed4..0952a71ea 100644 --- a/src/input/datetime.rs +++ b/src/input/datetime.rs @@ -154,9 +154,12 @@ impl<'a> EitherTimedelta<'a> { let microseconds: f64 = f64::from(py_timedelta.get_microseconds()); // 0 through 999999 Ok(86_400_000.0 * days + seconds * 1_000.0 + microseconds / 1_000.0) } - Self::PySubclass(py_timedelta) => py_timedelta - .call_method0(intern!(py_timedelta.py(), "total_seconds"))? - .extract(), + Self::PySubclass(py_timedelta) => { + let total_seconds = py_timedelta + .call_method0(intern!(py_timedelta.py(), "total_seconds"))? + .extract()?; + Ok(total_seconds / 1000.0) + }, } } } From d313c90a8df3d7264563766950b6c905386f18f8 Mon Sep 17 00:00:00 2001 From: Oliver Parker Date: Tue, 17 Sep 2024 19:09:34 +0100 Subject: [PATCH 19/19] fix: fix precision issues using davids method --- src/input/datetime.rs | 86 ++++++++++++++++++++++++----------- tests/serializers/test_any.py | 16 +++---- 2 files changed, 68 insertions(+), 34 deletions(-) diff --git a/src/input/datetime.rs b/src/input/datetime.rs index 0952a71ea..db5daff40 100644 --- a/src/input/datetime.rs +++ b/src/input/datetime.rs @@ -113,21 +113,38 @@ impl<'a> EitherTimedelta<'a> { pub fn total_seconds(&self) -> PyResult { match self { Self::Raw(timedelta) => { - let days: f64 = f64::from(timedelta.day); - let seconds: f64 = f64::from(timedelta.second); - let microseconds: f64 = f64::from(timedelta.microsecond); - let total_seconds: f64 = if !timedelta.positive { - -1.0 * (86400.0 * days + seconds + microseconds / 1_000_000.0) + let mut days: i64 = i64::from(timedelta.day); + let mut seconds: i64 = i64::from(timedelta.second); + let mut microseconds = i64::from(timedelta.microsecond); + if !timedelta.positive { + days = -days; + seconds = -seconds; + microseconds = -microseconds; + } + + let days_seconds = (86_400 * days) + seconds; + if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) { + let total_microseconds = days_seconds_as_micros + microseconds; + Ok(total_microseconds as f64 / 1_000_000.0) } else { - 86400.0 * days + seconds + microseconds / 1_000_000.0 - }; - Ok(total_seconds) + // Fall back to floating-point operations if the multiplication overflows + let total_seconds = days_seconds as f64 + microseconds as f64 / 1_000_000.0; + Ok(total_seconds) + } } Self::PyExact(py_timedelta) => { - let days: f64 = f64::from(py_timedelta.get_days()); // -999999999 to 999999999 - let seconds: f64 = f64::from(py_timedelta.get_seconds()); // 0 through 86399 - let microseconds: f64 = f64::from(py_timedelta.get_microseconds()); // 0 through 999999 - Ok(86400.0 * days + seconds + microseconds / 1_000_000.0) + let days: i64 = py_timedelta.get_days().into(); // -999999999 to 999999999 + let seconds: i64 = py_timedelta.get_seconds().into(); // 0 through 86399 + let microseconds = py_timedelta.get_microseconds(); // 0 through 999999 + let days_seconds = (86_400 * days) + seconds; + if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) { + let total_microseconds = days_seconds_as_micros + i64::from(microseconds); + Ok(total_microseconds as f64 / 1_000_000.0) + } else { + // Fall back to floating-point operations if the multiplication overflows + let total_seconds = days_seconds as f64 + f64::from(microseconds) / 1_000_000.0; + Ok(total_seconds) + } } Self::PySubclass(py_timedelta) => py_timedelta .call_method0(intern!(py_timedelta.py(), "total_seconds"))? @@ -138,28 +155,45 @@ impl<'a> EitherTimedelta<'a> { pub fn total_milliseconds(&self) -> PyResult { match self { Self::Raw(timedelta) => { - let days: f64 = f64::from(timedelta.day); - let seconds: f64 = f64::from(timedelta.second); - let microseconds: f64 = f64::from(timedelta.microsecond); - let total_seconds: f64 = if !timedelta.positive { - -1.0 * (86_400_000.0 * days + seconds * 1_000.0 + microseconds / 1_000.0) + let mut days: i64 = i64::from(timedelta.day); + let mut seconds: i64 = i64::from(timedelta.second); + let mut microseconds = i64::from(timedelta.microsecond); + if !timedelta.positive { + days = -days; + seconds = -seconds; + microseconds = -microseconds; + } + + let days_seconds = (86_400 * days) + seconds; + if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) { + let total_microseconds = days_seconds_as_micros + microseconds; + Ok(total_microseconds as f64 / 1_000.0) } else { - 86_400_000.0 * days + seconds * 1_000.0 + microseconds / 1_000.0 - }; - Ok(total_seconds) + // Fall back to floating-point operations if the multiplication overflows + let total_seconds = days_seconds as f64 + microseconds as f64 / 1_000.0; + Ok(total_seconds) + } } Self::PyExact(py_timedelta) => { - let days: f64 = f64::from(py_timedelta.get_days()); // -999999999 to 999999999 - let seconds: f64 = f64::from(py_timedelta.get_seconds()); // 0 through 86399 - let microseconds: f64 = f64::from(py_timedelta.get_microseconds()); // 0 through 999999 - Ok(86_400_000.0 * days + seconds * 1_000.0 + microseconds / 1_000.0) + let days: i64 = py_timedelta.get_days().into(); // -999999999 to 999999999 + let seconds: i64 = py_timedelta.get_seconds().into(); // 0 through 86399 + let microseconds = py_timedelta.get_microseconds(); // 0 through 999999 + let days_seconds = (86_400 * days) + seconds; + if let Some(days_seconds_as_micros) = days_seconds.checked_mul(1_000_000) { + let total_microseconds = days_seconds_as_micros + i64::from(microseconds); + Ok(total_microseconds as f64 / 1_000.0) + } else { + // Fall back to floating-point operations if the multiplication overflows + let total_milliseconds = days_seconds as f64 * 1_000.0 + f64::from(microseconds) / 1_000.0; + Ok(total_milliseconds) + } } Self::PySubclass(py_timedelta) => { - let total_seconds = py_timedelta + let total_seconds: f64 = py_timedelta .call_method0(intern!(py_timedelta.py(), "total_seconds"))? .extract()?; Ok(total_seconds / 1000.0) - }, + } } } } diff --git a/tests/serializers/test_any.py b/tests/serializers/test_any.py index 21096e475..9fbc8ac03 100644 --- a/tests/serializers/test_any.py +++ b/tests/serializers/test_any.py @@ -194,10 +194,10 @@ def test_any_with_timedelta_serializer(): (timedelta(microseconds=1), 0.001, b'0.001', {'0.001': 'foo'}, b'{"0.001":"foo"}', 'milliseconds_float'), ( timedelta(microseconds=-1), - -0.0009999999999763531, - b'-0.0009999999999763531', - {'-0.0009999999999763531': 'foo'}, - b'{"-0.0009999999999763531":"foo"}', + -0.001, + b'-0.001', + {'-0.001': 'foo'}, + b'{"-0.001":"foo"}', 'milliseconds_float', ), ( @@ -263,10 +263,10 @@ def test_any_with_timedelta_serializer(): (timedelta(microseconds=1), 1e-6, b'1e-6', {'0.000001': 'foo'}, b'{"0.000001":"foo"}', 'seconds_float'), ( timedelta(microseconds=-1), - -1.0000000000287557e-6, - b'-1.0000000000287557e-6', - {'-0.0000010000000000287557': 'foo'}, - b'{"-0.0000010000000000287557":"foo"}', + -1e-6, + b'-1e-6', + {'-0.000001': 'foo'}, + b'{"-0.000001":"foo"}', 'seconds_float', ), (timedelta(days=1), 86400.0, b'86400.0', {'86400': 'foo'}, b'{"86400":"foo"}', 'seconds_float'),