@@ -1252,54 +1252,116 @@ def test_class_conversions(self):
12521252
12531253 def test_clamping (self ):
12541254 # Test clamping from below and above.
1255- opts1 = CodecOptions (
1255+ opts = CodecOptions (
12561256 datetime_conversion = DatetimeConversion .DATETIME_CLAMP ,
12571257 tz_aware = True ,
12581258 tzinfo = datetime .timezone .utc ,
12591259 )
12601260 below = encode ({"x" : DatetimeMS (_datetime_to_millis (datetime .datetime .min ) - 1 )})
1261- dec_below = decode (below , opts1 )
1261+ dec_below = decode (below , opts )
12621262 self .assertEqual (
12631263 dec_below ["x" ], datetime .datetime .min .replace (tzinfo = datetime .timezone .utc )
12641264 )
12651265
12661266 above = encode ({"x" : DatetimeMS (_datetime_to_millis (datetime .datetime .max ) + 1 )})
1267- dec_above = decode (above , opts1 )
1267+ dec_above = decode (above , opts )
12681268 self .assertEqual (
12691269 dec_above ["x" ],
12701270 datetime .datetime .max .replace (tzinfo = datetime .timezone .utc , microsecond = 999000 ),
12711271 )
12721272
1273- def test_tz_clamping (self ):
1273+ def test_tz_clamping_local (self ):
12741274 # Naive clamping to local tz.
1275- opts1 = CodecOptions (datetime_conversion = DatetimeConversion .DATETIME_CLAMP , tz_aware = False )
1275+ opts = CodecOptions (datetime_conversion = DatetimeConversion .DATETIME_CLAMP , tz_aware = False )
12761276 below = encode ({"x" : DatetimeMS (_datetime_to_millis (datetime .datetime .min ) - 24 * 60 * 60 )})
12771277
1278- dec_below = decode (below , opts1 )
1278+ dec_below = decode (below , opts )
12791279 self .assertEqual (dec_below ["x" ], datetime .datetime .min )
12801280
12811281 above = encode ({"x" : DatetimeMS (_datetime_to_millis (datetime .datetime .max ) + 24 * 60 * 60 )})
1282- dec_above = decode (above , opts1 )
1282+ dec_above = decode (above , opts )
12831283 self .assertEqual (
12841284 dec_above ["x" ],
12851285 datetime .datetime .max .replace (microsecond = 999000 ),
12861286 )
12871287
1288- # Aware clamping.
1289- opts2 = CodecOptions (datetime_conversion = DatetimeConversion .DATETIME_CLAMP , tz_aware = True )
1288+ def test_tz_clamping_utc (self ):
1289+ # Aware clamping default utc.
1290+ opts = CodecOptions (datetime_conversion = DatetimeConversion .DATETIME_CLAMP , tz_aware = True )
12901291 below = encode ({"x" : DatetimeMS (_datetime_to_millis (datetime .datetime .min ) - 24 * 60 * 60 )})
1291- dec_below = decode (below , opts2 )
1292+ dec_below = decode (below , opts )
12921293 self .assertEqual (
12931294 dec_below ["x" ], datetime .datetime .min .replace (tzinfo = datetime .timezone .utc )
12941295 )
12951296
12961297 above = encode ({"x" : DatetimeMS (_datetime_to_millis (datetime .datetime .max ) + 24 * 60 * 60 )})
1297- dec_above = decode (above , opts2 )
1298+ dec_above = decode (above , opts )
12981299 self .assertEqual (
12991300 dec_above ["x" ],
13001301 datetime .datetime .max .replace (tzinfo = datetime .timezone .utc , microsecond = 999000 ),
13011302 )
13021303
1304+ def test_tz_clamping_non_utc (self ):
1305+ for tz in [FixedOffset (60 , "+1H" ), FixedOffset (- 60 , "-1H" )]:
1306+ opts = CodecOptions (
1307+ datetime_conversion = DatetimeConversion .DATETIME_CLAMP , tz_aware = True , tzinfo = tz
1308+ )
1309+ # Min/max values in this timezone which can be represented in both BSON and datetime UTC.
1310+ try :
1311+ min_tz = datetime .datetime .min .replace (tzinfo = utc ).astimezone (tz )
1312+ except OverflowError :
1313+ min_tz = datetime .datetime .min .replace (tzinfo = tz )
1314+ try :
1315+ max_tz = datetime .datetime .max .replace (tzinfo = utc , microsecond = 999000 ).astimezone (
1316+ tz
1317+ )
1318+ except OverflowError :
1319+ max_tz = datetime .datetime .max .replace (tzinfo = tz , microsecond = 999000 )
1320+
1321+ for in_range in [
1322+ min_tz ,
1323+ min_tz + datetime .timedelta (milliseconds = 1 ),
1324+ max_tz - datetime .timedelta (milliseconds = 1 ),
1325+ max_tz ,
1326+ ]:
1327+ doc = decode (encode ({"x" : in_range }), opts )
1328+ self .assertEqual (doc ["x" ], in_range )
1329+
1330+ for too_low in [
1331+ DatetimeMS (_datetime_to_millis (min_tz ) - 1 ),
1332+ DatetimeMS (_datetime_to_millis (min_tz ) - 60 * 60 * 1000 ),
1333+ DatetimeMS (_datetime_to_millis (min_tz ) - 1 - 60 * 60 * 1000 ),
1334+ DatetimeMS (_datetime_to_millis (datetime .datetime .min ) - 1 ),
1335+ DatetimeMS (_datetime_to_millis (datetime .datetime .min ) - 60 * 60 * 1000 ),
1336+ DatetimeMS (_datetime_to_millis (datetime .datetime .min ) - 1 - 60 * 60 * 1000 ),
1337+ ]:
1338+ doc = decode (encode ({"x" : too_low }), opts )
1339+ self .assertEqual (doc ["x" ], min_tz )
1340+
1341+ for too_high in [
1342+ DatetimeMS (_datetime_to_millis (max_tz ) + 1 ),
1343+ DatetimeMS (_datetime_to_millis (max_tz ) + 60 * 60 * 1000 ),
1344+ DatetimeMS (_datetime_to_millis (max_tz ) + 1 + 60 * 60 * 1000 ),
1345+ DatetimeMS (_datetime_to_millis (datetime .datetime .max ) + 1 ),
1346+ DatetimeMS (_datetime_to_millis (datetime .datetime .max ) + 60 * 60 * 1000 ),
1347+ DatetimeMS (_datetime_to_millis (datetime .datetime .max ) + 1 + 60 * 60 * 1000 ),
1348+ ]:
1349+ doc = decode (encode ({"x" : too_high }), opts )
1350+ self .assertEqual (doc ["x" ], max_tz )
1351+
1352+ def test_tz_clamping_non_utc_simple (self ):
1353+ dtm = datetime .datetime (2024 , 8 , 23 )
1354+ encoded = encode ({"d" : dtm })
1355+ self .assertEqual (decode (encoded )["d" ], dtm )
1356+ for conversion in [
1357+ DatetimeConversion .DATETIME ,
1358+ DatetimeConversion .DATETIME_CLAMP ,
1359+ DatetimeConversion .DATETIME_AUTO ,
1360+ ]:
1361+ for tz in [FixedOffset (60 , "+1H" ), FixedOffset (- 60 , "-1H" )]:
1362+ opts = CodecOptions (datetime_conversion = conversion , tz_aware = True , tzinfo = tz )
1363+ self .assertEqual (decode (encoded , opts )["d" ], dtm .replace (tzinfo = utc ).astimezone (tz ))
1364+
13031365 def test_datetime_auto (self ):
13041366 # Naive auto, in range.
13051367 opts1 = CodecOptions (datetime_conversion = DatetimeConversion .DATETIME_AUTO )
0 commit comments