@@ -1104,6 +1104,99 @@ def __divmod__(self, other):
11041104#############################################################################
11051105# date tests
11061106
1107+ class TestStrptime (unittest .TestCase ):
1108+ def test_numeric_fields (self ):
1109+ cases = (
1110+ ('2024-02-29 12:34:56.123' , '%Y-%m-%d %H:%M:%S.%f' ,
1111+ datetime (2024 , 2 , 29 , 12 , 34 , 56 , 123000 )),
1112+ ('2024-02-29T12:34:56.123+0530' , '%Y-%m-%dT%H:%M:%S.%f%z' ,
1113+ datetime (2024 , 2 , 29 , 12 , 34 , 56 , 123000 ,
1114+ timezone (timedelta (hours = 5 , minutes = 30 )))),
1115+ ('12:34:56Z' , '%H:%M:%S%z' , datetime (1900 , 1 , 1 , 12 , 34 , 56 , tzinfo = UTC )),
1116+ ('12:34:56-03:30' , '%H:%M:%S%z' ,
1117+ datetime (1900 , 1 , 1 , 12 , 34 , 56 ,
1118+ tzinfo = timezone (- timedelta (hours = 3 , minutes = 30 )))),
1119+ ('12:34:56' , '%H:%M:%S%z' , datetime (1900 , 1 , 1 , 12 , 34 , 56 )),
1120+ ('' , '' , datetime (1900 , 1 , 1 )),
1121+ ('2024111' , '%Y%m%d' , datetime (2024 , 11 , 1 )),
1122+ ('2024131' , '%Y%m%d' , datetime (2024 , 1 , 31 )),
1123+ ('2024\0 -02-29' , '%Y\0 -%m-%d' , datetime (2024 , 2 , 29 )),
1124+ ('٢٠٢٤-02-29' , '%Y-%m-%d' , datetime (2024 , 2 , 29 )),
1125+ ('2024\u2003 02\u2003 29' , '%Y %m %d' , datetime (2024 , 2 , 29 )),
1126+ ('2024t02t29' , '%YT%mt%d' , datetime (2024 , 2 , 29 )),
1127+ ('24 2025-02-01' , '%y %Y-%m-%d' , datetime (2025 , 2 , 1 )),
1128+ )
1129+ for text , fmt , expected in cases :
1130+ for cls , result in ((datetime , expected ), (date , expected .date ()),
1131+ (time , expected .timetz ())):
1132+ with self .subTest (text = text , fmt = fmt , cls = cls ):
1133+ actual = cls .strptime (text , fmt )
1134+ self .assertEqual (actual , result )
1135+ self .assertIs (type (actual ), cls )
1136+ with self .subTest (text = text , fmt = fmt , cls = _time .struct_time ):
1137+ actual = _time .strptime (text , fmt )
1138+ self .assertEqual (actual , expected .timetuple ())
1139+ self .assertIsNone (actual .tm_zone )
1140+ offset = expected .utcoffset ()
1141+ self .assertEqual (actual .tm_gmtoff ,
1142+ None if offset is None else offset .total_seconds ())
1143+
1144+ def test_invalid_fields (self ):
1145+ cases = (
1146+ ('2024-02-30' , '%Y-%m-%d' ),
1147+ ('1900-02-29' , '%Y-%m-%d' ),
1148+ ('0000-01-01' , '%Y-%m-%d' ),
1149+ ('2024-01- 12' , '%Y-%m-%d' ),
1150+ ('2024\0 ignored' , '%Y' ),
1151+ ('24:00:00' , '%H:%M:%S' ),
1152+ ('23:60:00' , '%H:%M:%S' ),
1153+ ('23:59:62' , '%H:%M:%S' ),
1154+ )
1155+ for text , fmt in cases :
1156+ for parse in (datetime .strptime , date .strptime , time .strptime ,
1157+ _time .strptime ):
1158+ with self .subTest (text = text , fmt = fmt , parse = parse ):
1159+ with self .assertRaises (ValueError ):
1160+ parse (text , fmt )
1161+
1162+ def test_leap_seconds (self ):
1163+ for second in (60 , 61 ):
1164+ text = f'2024-02-29 23:59:{ second } '
1165+ fmt = '%Y-%m-%d %H:%M:%S'
1166+ with self .subTest (second = second ):
1167+ self .assertEqual (date .strptime (text , fmt ), date (2024 , 2 , 29 ))
1168+ self .assertEqual (_time .strptime (text , fmt ),
1169+ (2024 , 2 , 29 , 23 , 59 , second , 3 , 60 , - 1 ))
1170+ for cls in (datetime , time ):
1171+ with self .assertRaises (ValueError ):
1172+ cls .strptime (text , fmt )
1173+
1174+ def test_offset_outside_datetime_range (self ):
1175+ # date discards the offset; struct_time doesn't construct a timezone.
1176+ self .assertEqual (date .strptime ('+2400' , '%z' ), date (1900 , 1 , 1 ))
1177+ self .assertEqual (_time .strptime ('+2400' , '%z' ).tm_gmtoff , 86400 )
1178+ for cls in (datetime , time ):
1179+ with self .subTest (cls = cls ):
1180+ with self .assertRaises (ValueError ):
1181+ cls .strptime ('+2400' , '%z' )
1182+
1183+ def test_subclass_constructor (self ):
1184+ for cls , args in ((date , (2024 , 2 , 29 )),
1185+ (time , (12 , 34 , 56 , 123000 )),
1186+ (datetime , (2024 , 2 , 29 , 12 , 34 , 56 , 123000 ))):
1187+ class Capture (cls ):
1188+ def __new__ (cls , * args , ** kwargs ):
1189+ return args , kwargs
1190+
1191+ text = '2024-02-29 12:34:56.123'
1192+ fmt = '%Y-%m-%d %H:%M:%S.%f'
1193+ with self .subTest (cls = cls ):
1194+ self .assertEqual (Capture .strptime (text , fmt ), (args , {}))
1195+ aware_args = args if cls is date else args + (UTC ,)
1196+ self .assertEqual (Capture .strptime (text + 'Z' , fmt + '%z' ),
1197+ (aware_args , {}))
1198+
1199+
11071200class TestDateOnly (unittest .TestCase ):
11081201 # Tests here won't pass if also run on datetime objects, so don't
11091202 # subclass this to test datetimes too.
@@ -1178,6 +1271,23 @@ def test_strptime(self):
11781271 self .assertEqual (expected , got )
11791272 self .assertIs (type (got ), date )
11801273
1274+ def test_strptime_whitespace (self ):
1275+ whitespace = (' ' , '\t \n \r \v \f ' , '\x1c ' , '\xa0 ' , '\u2003 ' , '\u202f ' )
1276+ for input_space in whitespace :
1277+ for format_space in whitespace :
1278+ with self .subTest (input_space = input_space ,
1279+ format_space = format_space ):
1280+ text = input_space .join (('2024' , '02' , '29' ))
1281+ fmt = format_space .join (('%Y' , '%m' , '%d' ))
1282+ self .assertEqual (date .strptime (text , fmt ), date (2024 , 2 , 29 ))
1283+
1284+ def test_strptime_whitespace_before_adjacent_fields (self ):
1285+ for separator in ('\x1c ' , '\x1d ' , '\x1e ' , '\x1f ' ):
1286+ with self .subTest (separator = separator ):
1287+ text = f'2024{ separator } 112'
1288+ fmt = f'%Y{ separator } %d%m'
1289+ self .assertEqual (date .strptime (text , fmt ), date (2024 , 2 , 11 ))
1290+
11811291 def test_strptime_single_digit (self ):
11821292 # bpo-34903: Check that single digit dates are allowed.
11831293 strptime = date .strptime
@@ -3089,6 +3199,70 @@ def test_strptime(self):
30893199 with self .assertRaises (ValueError ): strptime ("-000" , "%z" )
30903200 with self .assertRaises (ValueError ): strptime ("z" , "%z" )
30913201
3202+ def test_strptime_whitespace (self ):
3203+ whitespace = (' ' , '\t \n \r \v \f ' , '\x1c ' , '\xa0 ' , '\u2003 ' , '\u202f ' )
3204+ for input_space in whitespace :
3205+ for format_space in whitespace :
3206+ with self .subTest (input_space = input_space ,
3207+ format_space = format_space ):
3208+ text = input_space .join (('2024-02-29' , '12:34:56' ))
3209+ fmt = format_space .join (('%Y-%m-%d' , '%H:%M:%S' ))
3210+ self .assertEqual (self .theclass .strptime (text , fmt ),
3211+ self .theclass (2024 , 2 , 29 , 12 , 34 , 56 ))
3212+
3213+ def test_strptime_whitespace_before_adjacent_fields (self ):
3214+ for separator in ('\x1c ' , '\x1d ' , '\x1e ' , '\x1f ' ):
3215+ with self .subTest (separator = separator ):
3216+ text = f'2024{ separator } 112 34{ separator } 123'
3217+ fmt = f'%Y{ separator } %d%m %M{ separator } %H%S'
3218+ self .assertEqual (self .theclass .strptime (text , fmt ),
3219+ self .theclass (2024 , 2 , 11 , 12 , 34 , 3 ))
3220+
3221+ def test_strptime_numeric_fallback (self ):
3222+ cases = (
3223+ ('2024\0 -02-29' , '%Y\0 -%m-%d' , (2024 , 2 , 29 )),
3224+ ('٢٠٢٤-02-29' , '%Y-%m-%d' , (2024 , 2 , 29 )),
3225+ ('2024\u2003 02\u2003 29' , '%Y %m %d' , (2024 , 2 , 29 )),
3226+ ('2024t02t29' , '%YT%mt%d' , (2024 , 2 , 29 )),
3227+ ('2024111' , '%Y%m%d' , (2024 , 11 , 1 )),
3228+ ('2024131' , '%Y%m%d' , (2024 , 1 , 31 )),
3229+ ('24 2025-02-01' , '%y %Y-%m-%d' , (2025 , 2 , 1 )),
3230+ ('2025 24-02-01' , '%Y %y-%m-%d' , (2024 , 2 , 1 )),
3231+ )
3232+ for text , fmt , expected in cases :
3233+ with self .subTest (text = text , fmt = fmt ):
3234+ self .assertEqual (self .theclass .strptime (text , fmt ),
3235+ self .theclass (* expected ))
3236+
3237+ def test_strptime_numeric_invalid (self ):
3238+ cases = (
3239+ ('2024\0 ignored' , '%Y' ),
3240+ ('2024' , '%Y\0 ignored' ),
3241+ ('2024' , '%4Y' ),
3242+ ('2024-02-30' , '%Y-%m-%d' ),
3243+ ('1900-02-29' , '%Y-%m-%d' ),
3244+ ('0000-01-01' , '%Y-%m-%d' ),
3245+ ('2024-01- 12' , '%Y-%m-%d' ),
3246+ )
3247+ for text , fmt in cases :
3248+ with self .subTest (text = text , fmt = fmt ):
3249+ with self .assertRaises (ValueError ):
3250+ self .theclass .strptime (text , fmt )
3251+ with self .assertRaises (re .PatternError ):
3252+ self .theclass .strptime ('2024 2025' , '%Y %Y' )
3253+
3254+ def test_strptime_subclass_constructor (self ):
3255+ class Capture (self .theclass ):
3256+ def __new__ (cls , * args , ** kwargs ):
3257+ return args , kwargs
3258+
3259+ args = (2024 , 2 , 29 , 12 , 34 , 56 , 123000 )
3260+ fmt = '%Y-%m-%d %H:%M:%S.%f'
3261+ text = '2024-02-29 12:34:56.123'
3262+ self .assertEqual (Capture .strptime (text , fmt ), (args , {}))
3263+ self .assertEqual (Capture .strptime (text + '+0530' , fmt + '%z' ),
3264+ (args + (timezone (timedelta (hours = 5 , minutes = 30 )),), {}))
3265+
30923266 def test_strptime_ampm (self ):
30933267 dt = datetime (1999 , 3 , 17 , 0 , 44 , 55 , 2 )
30943268 for hour in range (0 , 24 ):
@@ -4286,6 +4460,25 @@ def test_strptime(self):
42864460 with self .assertRaises (ValueError ):
42874461 self .theclass .strptime ('13:02:47.1975' , '%H:%M:%S.%3f' )
42884462
4463+ def test_strptime_whitespace (self ):
4464+ whitespace = (' ' , '\t \n \r \v \f ' , '\x1c ' , '\xa0 ' , '\u2003 ' , '\u202f ' )
4465+ for input_space in whitespace :
4466+ for format_space in whitespace :
4467+ with self .subTest (input_space = input_space ,
4468+ format_space = format_space ):
4469+ text = input_space .join (('12' , '34' , '56' ))
4470+ fmt = format_space .join (('%H' , '%M' , '%S' ))
4471+ self .assertEqual (self .theclass .strptime (text , fmt ),
4472+ self .theclass (12 , 34 , 56 ))
4473+
4474+ def test_strptime_whitespace_before_adjacent_fields (self ):
4475+ for separator in ('\x1c ' , '\x1d ' , '\x1e ' , '\x1f ' ):
4476+ with self .subTest (separator = separator ):
4477+ text = f'34{ separator } 123'
4478+ fmt = f'%M{ separator } %H%S'
4479+ self .assertEqual (self .theclass .strptime (text , fmt ),
4480+ self .theclass (12 , 34 , 3 ))
4481+
42894482 def test_strptime_tz (self ):
42904483 strptime = self .theclass .strptime
42914484 self .assertEqual (strptime ("+0002" , "%z" ).utcoffset (), 2 * MINUTE )
0 commit comments