@@ -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