Skip to content

Commit

Permalink
fix: date/time deserialization with fractional seconds (#138)
Browse files Browse the repository at this point in the history
fix multiple where fractional seconds were not properly deserialized or deserialization caused unexpected crashes in py<3.11

---------

Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Co-authored-by: Jan Kowalleck <jan.kowalleck@gmail.com>
  • Loading branch information
ILikeToFixThings and jkowalleck authored Oct 1, 2024
1 parent d75df5b commit f4b1c27
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 8 deletions.
27 changes: 20 additions & 7 deletions serializable/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from datetime import date, datetime
from logging import getLogger
from re import sub as re_sub
from re import compile as re_compile
from typing import TYPE_CHECKING, Any, Optional, Type, TypeVar, Union

if TYPE_CHECKING: # pragma: no cover
Expand Down Expand Up @@ -195,21 +195,34 @@ def serialize(cls, o: Any) -> str:

raise ValueError(f'Attempt to serialize a non-date: {o.__class__}')

# region fixup_microseconds
# see https://github.com/madpah/serializable/pull/138

__PATTERN_FRACTION = re_compile(r'\.\d+')

@classmethod
def __fix_microseconds(cls, v: str) -> str:
"""
Fix for Python's violation of ISO8601 for :py:meth:`datetime.fromisoformat`.
1. Ensure either 0 or exactly 6 decimal places for seconds.
Background: py<3.11 supports either 6 or 0 digits for milliseconds when parsing.
2. Ensure correct rounding of microseconds on the 6th digit.
"""
return cls.__PATTERN_FRACTION.sub(lambda m: f'{(float(m.group(0))):.6f}'[1:], v)

# endregion fixup_microseconds

@classmethod
def deserialize(cls, o: Any) -> datetime:
try:
v = str(o)
if v.startswith('-'):
# 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)

if v.endswith('Z'):
# Replace ZULU time with 00:00 offset
v = f'{v[:-1]}+00:00'
return datetime.fromisoformat(v)
return datetime.fromisoformat(
cls.__fix_microseconds(v))
except ValueError:
raise ValueError(f'Date-Time string supplied ({o}) is not a supported ISO Format')
28 changes: 27 additions & 1 deletion tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,35 @@ def test_deserialize_valid_5(self) -> None:
)

def test_deserialize_valid_6(self) -> None:
"""Test that less than 6 decimal places in the seconds field is parsed correctly."""
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)
datetime(year=2001, month=10, day=26, hour=21, minute=32, second=52, microsecond=126790, tzinfo=None)
)

def test_deserialize_valid_7(self) -> None:
"""Test that exactly 6 decimal places in the seconds field is parsed correctly."""
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)
)

def test_deserialize_valid_8(self) -> None:
"""Test that more than 6 decimal places in the seconds field is parsed correctly."""
self.assertEqual(
# values are chosen to showcase rounding on microseconds
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)
)

def test_deserialize_valid_9(self) -> None:
"""Test that a lot more than 6 decimal places in the seconds is parsed correctly."""
self.assertEqual(
# values are chosen to showcase rounding on microseconds
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)
)

def test_serialize_1(self) -> None:
Expand Down

0 comments on commit f4b1c27

Please sign in to comment.