Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: date/time deserialization with fractional seconds #138

Merged
merged 12 commits into from
Oct 1, 2024
6 changes: 3 additions & 3 deletions serializable/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,9 @@ def deserialize(cls, o: Any) -> datetime:
# Remove any leading hyphen
v = v[1:]

# Ensure any milliseconds are 6 digits
# Background: py<3.11 supports six or less digits for milliseconds
v = re_sub(r'\.(\d{1,6})', lambda m: f'.{int(m.group()[1:]):06}', v)
jkowalleck marked this conversation as resolved.
Show resolved Hide resolved
# Ensure either 0 or exactly 6 decimal places for seconds
# Background: py<3.11 supports either 6 or 0 digits for milliseconds
v = re_sub(r'(\.\d{1,6})\d*', lambda m: f'{(float(m.group(0))):.6f}'[1:], v)
jkowalleck marked this conversation as resolved.
Show resolved Hide resolved

if v.endswith('Z'):
# Replace ZULU time with 00:00 offset
Expand Down
65 changes: 60 additions & 5 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# Copyright (c) Paul Horton. All Rights Reserved.

from datetime import date, datetime, timedelta, timezone
from unittest import TestCase
from unittest import TestCase, mock

from serializable import logger
from serializable.helpers import Iso8601Date, XsdDate, XsdDateTime
Expand Down Expand Up @@ -145,10 +145,65 @@ def test_deserialize_valid_5(self) -> None:
)

def test_deserialize_valid_6(self) -> None:
self.assertEqual(
XsdDateTime.deserialize('2001-10-26T21:32:52.12679'),
datetime(year=2001, month=10, day=26, hour=21, minute=32, second=52, microsecond=12679, tzinfo=None)
)
"""Test that less than 6 decimal places in the seconds field is padded to 6 and parsed correctly."""

# Use mock wrapping datetime so we can check that it was given a correctly padded string
with mock.patch('serializable.helpers.datetime', wraps=datetime) as datetime_mock:

# Make sure the parsed datetime is what we expect
self.assertEqual(
XsdDateTime.deserialize('2001-10-26T21:32:52.12679'),
datetime(year=2001, month=10, day=26, hour=21, minute=32, second=52, microsecond=126790, tzinfo=None)
)

# Make sure the string provided to fromisoformat was correctly padded (pre 3.11 it needs 0 or 6 decimals)
datetime_mock.fromisoformat.assert_called_with('2001-10-26T21:32:52.126790')
jkowalleck marked this conversation as resolved.
Show resolved Hide resolved

def test_deserialize_valid_7(self) -> None:
jkowalleck marked this conversation as resolved.
Show resolved Hide resolved
"""Test that exactly 6 decimal places in the seconds field is not altered and parsed correctly."""

# Use mock wrapping datetime so we can check that the string was not altered
with mock.patch('serializable.helpers.datetime', wraps=datetime) as datetime_mock:

# Make sure the parsed datetime is what we expect
self.assertEqual(
XsdDateTime.deserialize('2024-09-23T08:06:09.185596Z'),
datetime(year=2024, month=9, day=23, hour=8, minute=6,
second=9, microsecond=185596, tzinfo=timezone.utc)
)

# Make sure the string provided to fromisoformat was not altered (pre 3.11 it needs 0 or 6 decimals)
datetime_mock.fromisoformat.assert_called_with('2024-09-23T08:06:09.185596+00:00')

def test_deserialize_valid_8(self) -> None:
"""Test that more than 6 decimal places in the seconds field is truncated and parsed correctly."""

# Use mock wrapping datetime so we can check that the string was truncated
with mock.patch('serializable.helpers.datetime', wraps=datetime) as datetime_mock:

# Make sure the parsed datetime is what we expect
self.assertEqual(
XsdDateTime.deserialize('2024-09-23T08:06:09.185596536Z'),
datetime(year=2024, month=9, day=23, hour=8, minute=6,
second=9, microsecond=185597, tzinfo=timezone.utc)
)

# Make sure the string provided to fromisoformat was truncated (pre 3.11 it needs 0 or 6 decimals)
datetime_mock.fromisoformat.assert_called_with('2024-09-23T08:06:09.185597+00:00')

def test_deserialize_valid_9(self) -> None:
"""Test that a lot more than 6 decimal places in the seconds field is truncated and parsed correctly."""

# Use mock wrapping datetime so we can check that the string was truncated
with mock.patch('serializable.helpers.datetime', wraps=datetime) as datetime_mock:

self.assertEqual(
XsdDateTime.deserialize('2024-09-23T08:06:09.18559653666666666666666666666666'),
datetime(year=2024, month=9, day=23, hour=8, minute=6, second=9, microsecond=185597, tzinfo=None)
)

# Make sure the string provided to fromisoformat was truncated (pre 3.11 it needs 0 or 6 decimals)
datetime_mock.fromisoformat.assert_called_with('2024-09-23T08:06:09.185597')

def test_serialize_1(self) -> None:
serialized = XsdDateTime.serialize(
Expand Down