Skip to content

Commit 909c0bc

Browse files
committed
Pack datetime subclasses as timestamps unless strict_types is set
1 parent 3650629 commit 909c0bc

2 files changed

Lines changed: 76 additions & 5 deletions

File tree

‎msgpack/_packer.pyx‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from cpython cimport *
22
from cpython.bytearray cimport PyByteArray_Check, PyByteArray_CheckExact
33
from cpython.datetime cimport (
4-
PyDateTime_CheckExact, PyDelta_CheckExact,
4+
PyDateTime_Check, PyDateTime_CheckExact, PyDelta_Check,
55
datetime_tzinfo, timedelta_days, timedelta_seconds, timedelta_microseconds,
66
)
77
from cpython.frozendict cimport PyAnyDict_Check, PyAnyDict_CheckExact
@@ -241,16 +241,20 @@ cdef class Packer:
241241
msgpack_pack_raw_body(&self.pk, <char*>view.buf, L)
242242
finally:
243243
PyBuffer_Release(&view);
244-
elif self.datetime and PyDateTime_CheckExact(o) and datetime_tzinfo(o) is not None:
244+
elif (
245+
self.datetime
246+
and (PyDateTime_CheckExact(o) if strict else PyDateTime_Check(o))
247+
and datetime_tzinfo(o) is not None
248+
):
245249
delta = o - epoch
246-
if not PyDelta_CheckExact(delta):
247-
raise ValueError("failed to calculate delta")
250+
if not PyDelta_Check(delta):
251+
raise TypeError("failed to calculate delta")
248252
llval = timedelta_days(delta) * <long long>(24*60*60) + timedelta_seconds(delta)
249253
ulval = timedelta_microseconds(delta) * 1000
250254
msgpack_pack_timestamp(&self.pk, llval, ulval)
251255
elif will_default:
252256
return -2
253-
elif self.datetime and PyDateTime_CheckExact(o):
257+
elif self.datetime and (PyDateTime_CheckExact(o) if strict else PyDateTime_Check(o)):
254258
# this should be later than will_default
255259
PyErr_Format(ValueError, b"can not serialize '%.200s' object where tzinfo=None", Py_TYPE(o).tp_name)
256260
else:

‎test/test_timestamp.py‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,70 @@ def test_too_large_timestamp():
225225
# When timestamp64 is too large, conversion to datetime fails due to int64 -> int32 conversion.
226226
# https://github.com/msgpack/msgpack-python/issues/696
227227
print(msgpack.unpackb(b"\xd7\xff" + b"\x00" * 8, timestamp=3))
228+
229+
230+
class DatetimeSubclass(datetime.datetime):
231+
pass
232+
233+
234+
class TimedeltaSubclass(datetime.timedelta):
235+
pass
236+
237+
238+
class DatetimeSubclassTimedeltaSubclass(datetime.datetime):
239+
# Subtraction returns a timedelta subclass instead of datetime.timedelta.
240+
def __sub__(self, other):
241+
delta = super().__sub__(other)
242+
if type(delta) is datetime.timedelta:
243+
return TimedeltaSubclass(delta.days, delta.seconds, delta.microseconds)
244+
return delta
245+
246+
247+
class DatetimeSubclassNotTimedelta(datetime.datetime):
248+
# Subtraction does not return a timedelta.
249+
def __sub__(self, other):
250+
return 0
251+
252+
253+
@pytest.mark.parametrize("cls", [DatetimeSubclass, DatetimeSubclassTimedeltaSubclass])
254+
def test_pack_datetime_subclass(cls):
255+
# Without strict_types, an aware datetime subclass packs like datetime.
256+
utc = datetime.timezone.utc
257+
dt = datetime.datetime(2024, 4, 16, 8, 43, 9, 420317, tzinfo=utc)
258+
subdt = cls(2024, 4, 16, 8, 43, 9, 420317, tzinfo=utc)
259+
called = []
260+
packed = msgpack.packb(subdt, datetime=True, default=called.append)
261+
assert packed == msgpack.packb(dt, datetime=True)
262+
assert called == []
263+
assert msgpack.unpackb(packed, timestamp=3) == dt
264+
265+
266+
def test_pack_naive_datetime_subclass():
267+
dt = DatetimeSubclass(1970, 1, 1, 0, 0, 42, 14)
268+
with pytest.raises(ValueError, match="where tzinfo=None"):
269+
msgpack.packb(dt, datetime=True)
270+
271+
272+
@pytest.mark.parametrize(
273+
"options",
274+
[{"datetime": True, "strict_types": True}, {"datetime": False}],
275+
)
276+
def test_pack_datetime_subclass_unconverted(options):
277+
# strict_types=True and datetime=False leave a subclass to default.
278+
utc = datetime.timezone.utc
279+
subdt = DatetimeSubclass(2024, 4, 16, 8, 43, 9, 420317, tzinfo=utc)
280+
with pytest.raises(TypeError):
281+
msgpack.packb(subdt, **options)
282+
called = []
283+
assert msgpack.packb(subdt, default=called.append, **options) == msgpack.packb(None)
284+
assert called == [subdt]
285+
286+
287+
@pytest.mark.skipif(
288+
msgpack.Packer.__module__ == "msgpack.fallback",
289+
reason="subtraction result check is implemented in C extension only",
290+
)
291+
def test_pack_datetime_subclass_subtraction_not_timedelta():
292+
subdt = DatetimeSubclassNotTimedelta(2024, 4, 16, tzinfo=datetime.timezone.utc)
293+
with pytest.raises(TypeError):
294+
msgpack.packb(subdt, datetime=True)

0 commit comments

Comments
 (0)