test_datetime.py
1 # SPDX-FileCopyrightText: 2001-2021 Python Software Foundation.All rights reserved. 2 # SPDX-FileCopyrightText: 2000 BeOpen.com. All rights reserved. 3 # SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives. 4 # All rights reserved. 5 # SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives. 6 # All rights reserved. 7 # SPDX-FileCopyrightText: 1991-1995 Stichting Mathematisch Centrum. All rights reserved. 8 # SPDX-FileCopyrightText: 2021 Brent Rubell for Adafruit Industries 9 # SPDX-License-Identifier: Python-2.0 10 # Implements a subset of https://github.com/python/cpython/blob/master/Lib/test/datetimetester.py 11 # NOTE: This test is based off CPython and therefore linting is disabled within this file. 12 # pylint:disable=invalid-name, no-member, cell-var-from-loop, unused-argument, no-self-use, too-few-public-methods, raise-missing-from, too-many-statements, too-many-lines, undefined-variable, eval-used, import-outside-toplevel, redefined-outer-name, too-many-locals, reimported, protected-access, wrong-import-position, consider-using-enumerate, wrong-import-order, redefined-builtin, too-many-public-methods 13 import sys 14 15 # CircuitPython subset implementation 16 sys.path.append("..") 17 from adafruit_datetime import datetime as cpy_datetime 18 from adafruit_datetime import timedelta 19 from adafruit_datetime import tzinfo 20 from adafruit_datetime import date 21 from adafruit_datetime import time 22 from adafruit_datetime import timezone 23 24 import unittest 25 from test import support 26 from test_date import TestDate 27 28 # CPython standard implementation 29 from datetime import datetime as cpython_datetime 30 from datetime import MINYEAR, MAXYEAR 31 32 33 # TZinfo test 34 class FixedOffset(tzinfo): 35 def __init__(self, offset, name, dstoffset=42): 36 if isinstance(offset, int): 37 offset = timedelta(minutes=offset) 38 if isinstance(dstoffset, int): 39 dstoffset = timedelta(minutes=dstoffset) 40 self.__offset = offset 41 self.__name = name 42 self.__dstoffset = dstoffset 43 44 def __repr__(self): 45 return self.__name.lower() 46 47 def utcoffset(self, dt): 48 return self.__offset 49 50 def tzname(self, dt): 51 return self.__name 52 53 def dst(self, dt): 54 return self.__dstoffset 55 56 57 # ======================================================================= 58 # Decorator for running a function in a specific timezone, correctly 59 # resetting it afterwards. 60 61 62 def run_with_tz(tz): 63 def decorator(func): 64 def inner(*args, **kwds): 65 try: 66 tzset = time.tzset 67 except AttributeError: 68 raise unittest.SkipTest("tzset required") 69 if "TZ" in os.environ: 70 orig_tz = os.environ["TZ"] 71 else: 72 orig_tz = None 73 os.environ["TZ"] = tz 74 tzset() 75 76 # now run the function, resetting the tz on exceptions 77 try: 78 return func(*args, **kwds) 79 finally: 80 if orig_tz is None: 81 del os.environ["TZ"] 82 else: 83 os.environ["TZ"] = orig_tz 84 time.tzset() 85 86 inner.__name__ = func.__name__ 87 inner.__doc__ = func.__doc__ 88 return inner 89 90 return decorator 91 92 93 class SubclassDatetime(cpy_datetime): 94 sub_var = 1 95 96 97 class TestDateTime(TestDate): 98 theclass = cpy_datetime 99 theclass_cpython = cpython_datetime 100 101 def test_basic_attributes(self): 102 dt = self.theclass(2002, 3, 1, 12, 0) 103 dt2 = self.theclass_cpython(2002, 3, 1, 12, 0) 104 # test circuitpython basic attributes 105 self.assertEqual(dt.year, 2002) 106 self.assertEqual(dt.month, 3) 107 self.assertEqual(dt.day, 1) 108 self.assertEqual(dt.hour, 12) 109 self.assertEqual(dt.minute, 0) 110 self.assertEqual(dt.second, 0) 111 self.assertEqual(dt.microsecond, 0) 112 # test circuitpython basic attributes against cpython basic attributes 113 self.assertEqual(dt.year, dt2.year) 114 self.assertEqual(dt.month, dt2.month) 115 self.assertEqual(dt.day, dt2.day) 116 self.assertEqual(dt.hour, dt2.hour) 117 self.assertEqual(dt.minute, dt2.minute) 118 self.assertEqual(dt.second, dt2.second) 119 self.assertEqual(dt.microsecond, dt2.microsecond) 120 121 def test_basic_attributes_nonzero(self): 122 # Make sure all attributes are non-zero so bugs in 123 # bit-shifting access show up. 124 dt = self.theclass(2002, 3, 1, 12, 59, 59, 8000) 125 self.assertEqual(dt.year, 2002) 126 self.assertEqual(dt.month, 3) 127 self.assertEqual(dt.day, 1) 128 self.assertEqual(dt.hour, 12) 129 self.assertEqual(dt.minute, 59) 130 self.assertEqual(dt.second, 59) 131 self.assertEqual(dt.microsecond, 8000) 132 133 @unittest.skip("issue with startswith and ada lib.") 134 def test_roundtrip(self): 135 for dt in (self.theclass(1, 2, 3, 4, 5, 6, 7), self.theclass.now()): 136 # Verify dt -> string -> datetime identity. 137 s = repr(dt) 138 self.assertTrue(s.startswith("datetime.")) 139 s = s[9:] 140 dt2 = eval(s) 141 self.assertEqual(dt, dt2) 142 143 # Verify identity via reconstructing from pieces. 144 dt2 = self.theclass( 145 dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond 146 ) 147 self.assertEqual(dt, dt2) 148 149 @unittest.skip("isoformat not implemented") 150 def test_isoformat(self): 151 t = self.theclass(1, 2, 3, 4, 5, 1, 123) 152 self.assertEqual(t.isoformat(), "0001-02-03T04:05:01.000123") 153 self.assertEqual(t.isoformat("T"), "0001-02-03T04:05:01.000123") 154 self.assertEqual(t.isoformat(" "), "0001-02-03 04:05:01.000123") 155 self.assertEqual(t.isoformat("\x00"), "0001-02-03\x0004:05:01.000123") 156 # bpo-34482: Check that surrogates are handled properly. 157 self.assertEqual(t.isoformat("\ud800"), "0001-02-03\ud80004:05:01.000123") 158 self.assertEqual(t.isoformat(timespec="hours"), "0001-02-03T04") 159 self.assertEqual(t.isoformat(timespec="minutes"), "0001-02-03T04:05") 160 self.assertEqual(t.isoformat(timespec="seconds"), "0001-02-03T04:05:01") 161 self.assertEqual( 162 t.isoformat(timespec="milliseconds"), "0001-02-03T04:05:01.000" 163 ) 164 self.assertEqual( 165 t.isoformat(timespec="microseconds"), "0001-02-03T04:05:01.000123" 166 ) 167 self.assertEqual(t.isoformat(timespec="auto"), "0001-02-03T04:05:01.000123") 168 self.assertEqual(t.isoformat(sep=" ", timespec="minutes"), "0001-02-03 04:05") 169 self.assertRaises(ValueError, t.isoformat, timespec="foo") 170 # bpo-34482: Check that surrogates are handled properly. 171 self.assertRaises(ValueError, t.isoformat, timespec="\ud800") 172 # str is ISO format with the separator forced to a blank. 173 self.assertEqual(str(t), "0001-02-03 04:05:01.000123") 174 175 t = self.theclass(1, 2, 3, 4, 5, 1, 999500, tzinfo=timezone.utc) 176 self.assertEqual( 177 t.isoformat(timespec="milliseconds"), "0001-02-03T04:05:01.999+00:00" 178 ) 179 180 t = self.theclass(1, 2, 3, 4, 5, 1, 999500) 181 self.assertEqual( 182 t.isoformat(timespec="milliseconds"), "0001-02-03T04:05:01.999" 183 ) 184 185 t = self.theclass(1, 2, 3, 4, 5, 1) 186 self.assertEqual(t.isoformat(timespec="auto"), "0001-02-03T04:05:01") 187 self.assertEqual( 188 t.isoformat(timespec="milliseconds"), "0001-02-03T04:05:01.000" 189 ) 190 self.assertEqual( 191 t.isoformat(timespec="microseconds"), "0001-02-03T04:05:01.000000" 192 ) 193 194 t = self.theclass(2, 3, 2) 195 self.assertEqual(t.isoformat(), "0002-03-02T00:00:00") 196 self.assertEqual(t.isoformat("T"), "0002-03-02T00:00:00") 197 self.assertEqual(t.isoformat(" "), "0002-03-02 00:00:00") 198 # str is ISO format with the separator forced to a blank. 199 self.assertEqual(str(t), "0002-03-02 00:00:00") 200 # ISO format with timezone 201 tz = FixedOffset(timedelta(seconds=16), "XXX") 202 t = self.theclass(2, 3, 2, tzinfo=tz) 203 self.assertEqual(t.isoformat(), "0002-03-02T00:00:00+00:00:16") 204 205 @unittest.skip("isoformat not implemented.") 206 def test_isoformat_timezone(self): 207 tzoffsets = [ 208 ("05:00", timedelta(hours=5)), 209 ("02:00", timedelta(hours=2)), 210 ("06:27", timedelta(hours=6, minutes=27)), 211 ("12:32:30", timedelta(hours=12, minutes=32, seconds=30)), 212 ( 213 "02:04:09.123456", 214 timedelta(hours=2, minutes=4, seconds=9, microseconds=123456), 215 ), 216 ] 217 218 tzinfos = [ 219 ("", None), 220 ("+00:00", timezone.utc), 221 ("+00:00", timezone(timedelta(0))), 222 ] 223 224 tzinfos += [ 225 (prefix + expected, timezone(sign * td)) 226 for expected, td in tzoffsets 227 for prefix, sign in [("-", -1), ("+", 1)] 228 ] 229 230 dt_base = self.theclass(2016, 4, 1, 12, 37, 9) 231 exp_base = "2016-04-01T12:37:09" 232 233 for exp_tz, tzi in tzinfos: 234 dt = dt_base.replace(tzinfo=tzi) 235 exp = exp_base + exp_tz 236 with self.subTest(tzi=tzi): 237 assert dt.isoformat() == exp 238 239 @unittest.skip("strftime not implemented in datetime") 240 def test_format(self): 241 dt = self.theclass(2007, 9, 10, 4, 5, 1, 123) 242 self.assertEqual(dt.__format__(""), str(dt)) 243 244 with self.assertRaisesRegex(TypeError, "must be str, not int"): 245 dt.__format__(123) 246 247 # check that a derived class's __str__() gets called 248 class A(self.theclass): 249 def __str__(self): 250 return "A" 251 252 a = A(2007, 9, 10, 4, 5, 1, 123) 253 self.assertEqual(a.__format__(""), "A") 254 255 # check that a derived class's strftime gets called 256 class B(self.theclass): 257 def strftime(self, format_spec): 258 return "B" 259 260 b = B(2007, 9, 10, 4, 5, 1, 123) 261 self.assertEqual(b.__format__(""), str(dt)) 262 263 for fmt in [ 264 "m:%m d:%d y:%y", 265 "m:%m d:%d y:%y H:%H M:%M S:%S", 266 "%z %Z", 267 ]: 268 self.assertEqual(dt.__format__(fmt), dt.strftime(fmt)) 269 self.assertEqual(a.__format__(fmt), dt.strftime(fmt)) 270 self.assertEqual(b.__format__(fmt), "B") 271 272 @unittest.skip("ctime not implemented") 273 def test_more_ctime(self): 274 # Test fields that TestDate doesn't touch. 275 import time as cpython_time 276 277 t = self.theclass(2002, 3, 2, 18, 3, 5, 123) 278 self.assertEqual(t.ctime(), "Sat Mar 2 18:03:05 2002") 279 # Oops! The next line fails on Win2K under MSVC 6, so it's commented 280 # out. The difference is that t.ctime() produces " 2" for the day, 281 # but platform ctime() produces "02" for the day. According to 282 # C99, t.ctime() is correct here. 283 # self.assertEqual(t.ctime(), time.ctime(time.mktime(t.timetuple()))) 284 285 # So test a case where that difference doesn't matter. 286 t = self.theclass(2002, 3, 22, 18, 3, 5, 123) 287 self.assertEqual( 288 t.ctime(), cpython_time.ctime(cpython_time.mktime(t.timetuple())) 289 ) 290 291 def test_tz_independent_comparing(self): 292 dt1 = self.theclass(2002, 3, 1, 9, 0, 0) 293 dt2 = self.theclass(2002, 3, 1, 10, 0, 0) 294 dt3 = self.theclass(2002, 3, 1, 9, 0, 0) 295 self.assertEqual(dt1, dt3) 296 self.assertTrue(dt2 > dt3) 297 298 # Make sure comparison doesn't forget microseconds, and isn't done 299 # via comparing a float timestamp (an IEEE double doesn't have enough 300 # precision to span microsecond resolution across years 1 through 9999, 301 # so comparing via timestamp necessarily calls some distinct values 302 # equal). 303 dt1 = self.theclass(MAXYEAR, 12, 31, 23, 59, 59, 999998) 304 us = timedelta(microseconds=1) 305 dt2 = dt1 + us 306 self.assertEqual(dt2 - dt1, us) 307 self.assertTrue(dt1 < dt2) 308 309 @unittest.skip("not implemented - strftime") 310 def test_strftime_with_bad_tzname_replace(self): 311 # verify ok if tzinfo.tzname().replace() returns a non-string 312 class MyTzInfo(FixedOffset): 313 def tzname(self, dt): 314 class MyStr(str): 315 def replace(self, *args): 316 return None 317 318 return MyStr("name") 319 320 t = self.theclass(2005, 3, 2, 0, 0, 0, 0, MyTzInfo(3, "name")) 321 self.assertRaises(TypeError, t.strftime, "%Z") 322 323 def test_bad_constructor_arguments(self): 324 # bad years 325 self.theclass(MINYEAR, 1, 1) # no exception 326 self.theclass(MAXYEAR, 1, 1) # no exception 327 self.assertRaises(ValueError, self.theclass, MINYEAR - 1, 1, 1) 328 self.assertRaises(ValueError, self.theclass, MAXYEAR + 1, 1, 1) 329 # bad months 330 self.theclass(2000, 1, 1) # no exception 331 self.theclass(2000, 12, 1) # no exception 332 self.assertRaises(ValueError, self.theclass, 2000, 0, 1) 333 self.assertRaises(ValueError, self.theclass, 2000, 13, 1) 334 # bad days 335 self.theclass(2000, 2, 29) # no exception 336 self.theclass(2004, 2, 29) # no exception 337 self.theclass(2400, 2, 29) # no exception 338 self.assertRaises(ValueError, self.theclass, 2000, 2, 30) 339 self.assertRaises(ValueError, self.theclass, 2001, 2, 29) 340 self.assertRaises(ValueError, self.theclass, 2100, 2, 29) 341 self.assertRaises(ValueError, self.theclass, 1900, 2, 29) 342 self.assertRaises(ValueError, self.theclass, 2000, 1, 0) 343 self.assertRaises(ValueError, self.theclass, 2000, 1, 32) 344 # bad hours 345 self.theclass(2000, 1, 31, 0) # no exception 346 self.theclass(2000, 1, 31, 23) # no exception 347 self.assertRaises(ValueError, self.theclass, 2000, 1, 31, -1) 348 self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 24) 349 # bad minutes 350 self.theclass(2000, 1, 31, 23, 0) # no exception 351 self.theclass(2000, 1, 31, 23, 59) # no exception 352 self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, -1) 353 self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 60) 354 # bad seconds 355 self.theclass(2000, 1, 31, 23, 59, 0) # no exception 356 self.theclass(2000, 1, 31, 23, 59, 59) # no exception 357 self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, -1) 358 self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, 60) 359 # bad microseconds 360 self.theclass(2000, 1, 31, 23, 59, 59, 0) # no exception 361 self.theclass(2000, 1, 31, 23, 59, 59, 999999) # no exception 362 self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, 59, -1) 363 self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, 59, 1000000) 364 # bad fold 365 self.assertRaises(ValueError, self.theclass, 2000, 1, 31, fold=-1) 366 self.assertRaises(ValueError, self.theclass, 2000, 1, 31, fold=2) 367 # Positional fold: 368 self.assertRaises(TypeError, self.theclass, 2000, 1, 31, 23, 59, 59, 0, None, 1) 369 370 def test_hash_equality(self): 371 d = self.theclass(2000, 12, 31, 23, 30, 17) 372 e = self.theclass(2000, 12, 31, 23, 30, 17) 373 self.assertEqual(d, e) 374 self.assertEqual(hash(d), hash(e)) 375 376 dic = {d: 1} 377 dic[e] = 2 378 self.assertEqual(len(dic), 1) 379 self.assertEqual(dic[d], 2) 380 self.assertEqual(dic[e], 2) 381 382 d = self.theclass(2001, 1, 1, 0, 5, 17) 383 e = self.theclass(2001, 1, 1, 0, 5, 17) 384 self.assertEqual(d, e) 385 self.assertEqual(hash(d), hash(e)) 386 387 dic = {d: 1} 388 dic[e] = 2 389 self.assertEqual(len(dic), 1) 390 self.assertEqual(dic[d], 2) 391 self.assertEqual(dic[e], 2) 392 393 def test_computations(self): 394 a = self.theclass(2002, 1, 31) 395 b = self.theclass(1956, 1, 31) 396 diff = a - b 397 self.assertEqual(diff.days, 46 * 365 + len(range(1956, 2002, 4))) 398 self.assertEqual(diff.seconds, 0) 399 self.assertEqual(diff.microseconds, 0) 400 a = self.theclass(2002, 3, 2, 17, 6) 401 millisec = timedelta(0, 0, 1000) 402 hour = timedelta(0, 3600) 403 day = timedelta(1) 404 week = timedelta(7) 405 self.assertEqual(a + hour, self.theclass(2002, 3, 2, 18, 6)) 406 self.assertEqual(hour + a, self.theclass(2002, 3, 2, 18, 6)) 407 self.assertEqual(a + 10 * hour, self.theclass(2002, 3, 3, 3, 6)) 408 self.assertEqual(a - hour, self.theclass(2002, 3, 2, 16, 6)) 409 self.assertEqual(-hour + a, self.theclass(2002, 3, 2, 16, 6)) 410 self.assertEqual(a - hour, a + -hour) 411 self.assertEqual(a - 20 * hour, self.theclass(2002, 3, 1, 21, 6)) 412 self.assertEqual(a + day, self.theclass(2002, 3, 3, 17, 6)) 413 self.assertEqual(a - day, self.theclass(2002, 3, 1, 17, 6)) 414 self.assertEqual(a + week, self.theclass(2002, 3, 9, 17, 6)) 415 self.assertEqual(a - week, self.theclass(2002, 2, 23, 17, 6)) 416 self.assertEqual(a + 52 * week, self.theclass(2003, 3, 1, 17, 6)) 417 self.assertEqual(a - 52 * week, self.theclass(2001, 3, 3, 17, 6)) 418 self.assertEqual((a + week) - a, week) 419 self.assertEqual((a + day) - a, day) 420 self.assertEqual((a + hour) - a, hour) 421 self.assertEqual((a + millisec) - a, millisec) 422 self.assertEqual((a - week) - a, -week) 423 self.assertEqual((a - day) - a, -day) 424 self.assertEqual((a - hour) - a, -hour) 425 self.assertEqual((a - millisec) - a, -millisec) 426 self.assertEqual(a - (a + week), -week) 427 self.assertEqual(a - (a + day), -day) 428 self.assertEqual(a - (a + hour), -hour) 429 self.assertEqual(a - (a + millisec), -millisec) 430 self.assertEqual(a - (a - week), week) 431 self.assertEqual(a - (a - day), day) 432 self.assertEqual(a - (a - hour), hour) 433 self.assertEqual(a - (a - millisec), millisec) 434 self.assertEqual( 435 a + (week + day + hour + millisec), 436 self.theclass(2002, 3, 10, 18, 6, 0, 1000), 437 ) 438 self.assertEqual( 439 a + (week + day + hour + millisec), (((a + week) + day) + hour) + millisec 440 ) 441 self.assertEqual( 442 a - (week + day + hour + millisec), 443 self.theclass(2002, 2, 22, 16, 5, 59, 999000), 444 ) 445 self.assertEqual( 446 a - (week + day + hour + millisec), (((a - week) - day) - hour) - millisec 447 ) 448 # Add/sub ints or floats should be illegal 449 for i in 1, 1.0: 450 self.assertRaises(TypeError, lambda: a + i) 451 self.assertRaises(TypeError, lambda: a - i) 452 self.assertRaises(TypeError, lambda: i + a) 453 self.assertRaises(TypeError, lambda: i - a) 454 455 # delta - datetime is senseless. 456 self.assertRaises(TypeError, lambda: day - a) 457 # mixing datetime and (delta or datetime) via * or // is senseless 458 self.assertRaises(TypeError, lambda: day * a) 459 self.assertRaises(TypeError, lambda: a * day) 460 self.assertRaises(TypeError, lambda: day // a) 461 self.assertRaises(TypeError, lambda: a // day) 462 self.assertRaises(TypeError, lambda: a * a) 463 self.assertRaises(TypeError, lambda: a // a) 464 # datetime + datetime is senseless 465 self.assertRaises(TypeError, lambda: a + a) 466 467 def test_more_compare(self): 468 # The test_compare() inherited from TestDate covers the error cases. 469 # We just want to test lexicographic ordering on the members datetime 470 # has that date lacks. 471 args = [2000, 11, 29, 20, 58, 16, 999998] 472 t1 = self.theclass(*args) 473 t2 = self.theclass(*args) 474 self.assertEqual(t1, t2) 475 self.assertTrue(t1 <= t2) 476 self.assertTrue(t1 >= t2) 477 self.assertFalse(t1 != t2) 478 self.assertFalse(t1 < t2) 479 self.assertFalse(t1 > t2) 480 481 for i in range(len(args)): 482 newargs = args[:] 483 newargs[i] = args[i] + 1 484 t2 = self.theclass(*newargs) # this is larger than t1 485 self.assertTrue(t1 < t2) 486 self.assertTrue(t2 > t1) 487 self.assertTrue(t1 <= t2) 488 self.assertTrue(t2 >= t1) 489 self.assertTrue(t1 != t2) 490 self.assertTrue(t2 != t1) 491 self.assertFalse(t1 == t2) 492 self.assertFalse(t2 == t1) 493 self.assertFalse(t1 > t2) 494 self.assertFalse(t2 < t1) 495 self.assertFalse(t1 >= t2) 496 self.assertFalse(t2 <= t1) 497 498 # A helper for timestamp constructor tests. 499 def verify_field_equality(self, expected, got): 500 self.assertEqual(expected.tm_year, got.year) 501 self.assertEqual(expected.tm_mon, got.month) 502 self.assertEqual(expected.tm_mday, got.day) 503 self.assertEqual(expected.tm_hour, got.hour) 504 self.assertEqual(expected.tm_min, got.minute) 505 self.assertEqual(expected.tm_sec, got.second) 506 507 def test_fromtimestamp(self): 508 import time 509 510 ts = time.time() 511 expected = time.localtime(ts) 512 got = self.theclass.fromtimestamp(ts) 513 self.verify_field_equality(expected, got) 514 515 @unittest.skip("gmtime not implemented in CircuitPython") 516 def test_utcfromtimestamp(self): 517 import time 518 519 ts = time.time() 520 expected = time.gmtime(ts) 521 got = self.theclass.utcfromtimestamp(ts) 522 self.verify_field_equality(expected, got) 523 524 # Run with US-style DST rules: DST begins 2 a.m. on second Sunday in 525 # March (M3.2.0) and ends 2 a.m. on first Sunday in November (M11.1.0). 526 @support.run_with_tz("EST+05EDT,M3.2.0,M11.1.0") 527 def test_timestamp_naive(self): 528 t = self.theclass(1970, 1, 1) 529 self.assertEqual(t.timestamp(), 18000.0) 530 t = self.theclass(1970, 1, 1, 1, 2, 3, 4) 531 self.assertEqual(t.timestamp(), 18000.0 + 3600 + 2 * 60 + 3 + 4 * 1e-6) 532 # Missing hour 533 t0 = self.theclass(2012, 3, 11, 2, 30) 534 t1 = t0.replace(fold=1) 535 self.assertEqual( 536 self.theclass.fromtimestamp(t1.timestamp()), t0 - timedelta(hours=1) 537 ) 538 self.assertEqual( 539 self.theclass.fromtimestamp(t0.timestamp()), t1 + timedelta(hours=1) 540 ) 541 # Ambiguous hour defaults to DST 542 t = self.theclass(2012, 11, 4, 1, 30) 543 self.assertEqual(self.theclass.fromtimestamp(t.timestamp()), t) 544 545 # Timestamp may raise an overflow error on some platforms 546 for t in [self.theclass(2, 1, 1), self.theclass(9998, 12, 12)]: 547 try: 548 s = t.timestamp() 549 except OverflowError: 550 pass 551 else: 552 self.assertEqual(self.theclass.fromtimestamp(s), t) 553 554 def test_timestamp_aware(self): 555 t = self.theclass(1970, 1, 1, tzinfo=timezone.utc) 556 self.assertEqual(t.timestamp(), 0.0) 557 t = self.theclass(1970, 1, 1, 1, 2, 3, 4, tzinfo=timezone.utc) 558 self.assertEqual(t.timestamp(), 3600 + 2 * 60 + 3 + 4 * 1e-6) 559 t = self.theclass( 560 1970, 1, 1, 1, 2, 3, 4, tzinfo=timezone(timedelta(hours=-5), "EST") 561 ) 562 self.assertEqual(t.timestamp(), 18000 + 3600 + 2 * 60 + 3 + 4 * 1e-6) 563 564 @unittest.skip("Not implemented - gmtime") 565 @support.run_with_tz("MSK-03") # Something east of Greenwich 566 def test_microsecond_rounding(self): 567 for fts in [self.theclass.fromtimestamp, self.theclass.utcfromtimestamp]: 568 zero = fts(0) 569 self.assertEqual(zero.second, 0) 570 self.assertEqual(zero.microsecond, 0) 571 one = fts(1e-6) 572 try: 573 minus_one = fts(-1e-6) 574 except OSError: 575 # localtime(-1) and gmtime(-1) is not supported on Windows 576 pass 577 else: 578 self.assertEqual(minus_one.second, 59) 579 self.assertEqual(minus_one.microsecond, 999999) 580 581 t = fts(-1e-8) 582 self.assertEqual(t, zero) 583 t = fts(-9e-7) 584 self.assertEqual(t, minus_one) 585 t = fts(-1e-7) 586 self.assertEqual(t, zero) 587 t = fts(-1 / 2 ** 7) 588 self.assertEqual(t.second, 59) 589 self.assertEqual(t.microsecond, 992188) 590 591 t = fts(1e-7) 592 self.assertEqual(t, zero) 593 t = fts(9e-7) 594 self.assertEqual(t, one) 595 t = fts(0.99999949) 596 self.assertEqual(t.second, 0) 597 self.assertEqual(t.microsecond, 999999) 598 t = fts(0.9999999) 599 self.assertEqual(t.second, 1) 600 self.assertEqual(t.microsecond, 0) 601 t = fts(1 / 2 ** 7) 602 self.assertEqual(t.second, 0) 603 self.assertEqual(t.microsecond, 7812) 604 605 @unittest.skip("gmtime not implemented in CircuitPython") 606 def test_timestamp_limits(self): 607 # minimum timestamp 608 min_dt = self.theclass.min.replace(tzinfo=timezone.utc) 609 min_ts = min_dt.timestamp() 610 try: 611 # date 0001-01-01 00:00:00+00:00: timestamp=-62135596800 612 self.assertEqual( 613 self.theclass.fromtimestamp(min_ts, tz=timezone.utc), min_dt 614 ) 615 except (OverflowError, OSError) as exc: 616 # the date 0001-01-01 doesn't fit into 32-bit time_t, 617 # or platform doesn't support such very old date 618 self.skipTest(str(exc)) 619 620 # maximum timestamp: set seconds to zero to avoid rounding issues 621 max_dt = self.theclass.max.replace(tzinfo=timezone.utc, second=0, microsecond=0) 622 max_ts = max_dt.timestamp() 623 # date 9999-12-31 23:59:00+00:00: timestamp 253402300740 624 self.assertEqual(self.theclass.fromtimestamp(max_ts, tz=timezone.utc), max_dt) 625 626 # number of seconds greater than 1 year: make sure that the new date 627 # is not valid in datetime.datetime limits 628 delta = 3600 * 24 * 400 629 630 # too small 631 ts = min_ts - delta 632 # converting a Python int to C time_t can raise a OverflowError, 633 # especially on 32-bit platforms. 634 with self.assertRaises((ValueError, OverflowError)): 635 self.theclass.fromtimestamp(ts) 636 with self.assertRaises((ValueError, OverflowError)): 637 self.theclass.utcfromtimestamp(ts) 638 639 # too big 640 ts = max_dt.timestamp() + delta 641 with self.assertRaises((ValueError, OverflowError)): 642 self.theclass.fromtimestamp(ts) 643 with self.assertRaises((ValueError, OverflowError)): 644 self.theclass.utcfromtimestamp(ts) 645 646 def test_insane_fromtimestamp(self): 647 # It's possible that some platform maps time_t to double, 648 # and that this test will fail there. This test should 649 # exempt such platforms (provided they return reasonable 650 # results!). 651 for insane in -1e200, 1e200: 652 self.assertRaises(OverflowError, self.theclass.fromtimestamp, insane) 653 654 @unittest.skip("Not implemented - gmtime") 655 def test_insane_utcfromtimestamp(self): 656 # It's possible that some platform maps time_t to double, 657 # and that this test will fail there. This test should 658 # exempt such platforms (provided they return reasonable 659 # results!). 660 for insane in -1e200, 1e200: 661 self.assertRaises(OverflowError, self.theclass.utcfromtimestamp, insane) 662 663 @unittest.skip("gmtime not implemented in CircuitPython") 664 def test_utcnow(self): 665 import time 666 667 # Call it a success if utcnow() and utcfromtimestamp() are within 668 # a second of each other. 669 tolerance = timedelta(seconds=1) 670 for dummy in range(3): 671 from_now = self.theclass.utcnow() 672 from_timestamp = self.theclass.utcfromtimestamp(time.time()) 673 if abs(from_timestamp - from_now) <= tolerance: 674 break 675 # Else try again a few times. 676 self.assertLessEqual(abs(from_timestamp - from_now), tolerance) 677 678 @unittest.skip("gmtime not implemented in CircuitPython") 679 def test_strptime(self): 680 string = "2004-12-01 13:02:47.197" 681 format = "%Y-%m-%d %H:%M:%S.%f" 682 expected = _strptime._strptime_datetime(self.theclass, string, format) 683 got = self.theclass.strptime(string, format) 684 self.assertEqual(expected, got) 685 self.assertIs(type(expected), self.theclass) 686 self.assertIs(type(got), self.theclass) 687 688 # bpo-34482: Check that surrogates are handled properly. 689 inputs = [ 690 ("2004-12-01\ud80013:02:47.197", "%Y-%m-%d\ud800%H:%M:%S.%f"), 691 ("2004\ud80012-01 13:02:47.197", "%Y\ud800%m-%d %H:%M:%S.%f"), 692 ("2004-12-01 13:02\ud80047.197", "%Y-%m-%d %H:%M\ud800%S.%f"), 693 ] 694 for string, format in inputs: 695 with self.subTest(string=string, format=format): 696 expected = _strptime._strptime_datetime(self.theclass, string, format) 697 got = self.theclass.strptime(string, format) 698 self.assertEqual(expected, got) 699 700 strptime = self.theclass.strptime 701 702 self.assertEqual(strptime("+0002", "%z").utcoffset(), 2 * MINUTE) 703 self.assertEqual(strptime("-0002", "%z").utcoffset(), -2 * MINUTE) 704 self.assertEqual( 705 strptime("-00:02:01.000003", "%z").utcoffset(), 706 -timedelta(minutes=2, seconds=1, microseconds=3), 707 ) 708 # Only local timezone and UTC are supported 709 for tzseconds, tzname in ( 710 (0, "UTC"), 711 (0, "GMT"), 712 (-_time.timezone, _time.tzname[0]), 713 ): 714 if tzseconds < 0: 715 sign = "-" 716 seconds = -tzseconds 717 else: 718 sign = "+" 719 seconds = tzseconds 720 hours, minutes = divmod(seconds // 60, 60) 721 dtstr = "{}{:02d}{:02d} {}".format(sign, hours, minutes, tzname) 722 dt = strptime(dtstr, "%z %Z") 723 self.assertEqual(dt.utcoffset(), timedelta(seconds=tzseconds)) 724 self.assertEqual(dt.tzname(), tzname) 725 # Can produce inconsistent datetime 726 dtstr, fmt = "+1234 UTC", "%z %Z" 727 dt = strptime(dtstr, fmt) 728 self.assertEqual(dt.utcoffset(), 12 * HOUR + 34 * MINUTE) 729 self.assertEqual(dt.tzname(), "UTC") 730 # yet will roundtrip 731 self.assertEqual(dt.strftime(fmt), dtstr) 732 733 # Produce naive datetime if no %z is provided 734 self.assertEqual(strptime("UTC", "%Z").tzinfo, None) 735 736 with self.assertRaises(ValueError): 737 strptime("-2400", "%z") 738 with self.assertRaises(ValueError): 739 strptime("-000", "%z") 740 741 @unittest.skip("gmtime not implemented in CircuitPython") 742 def test_strptime_single_digit(self): 743 # bpo-34903: Check that single digit dates and times are allowed. 744 745 strptime = self.theclass.strptime 746 747 with self.assertRaises(ValueError): 748 # %y does require two digits. 749 newdate = strptime("01/02/3 04:05:06", "%d/%m/%y %H:%M:%S") 750 dt1 = self.theclass(2003, 2, 1, 4, 5, 6) 751 dt2 = self.theclass(2003, 1, 2, 4, 5, 6) 752 dt3 = self.theclass(2003, 2, 1, 0, 0, 0) 753 dt4 = self.theclass(2003, 1, 25, 0, 0, 0) 754 inputs = [ 755 ("%d", "1/02/03 4:5:6", "%d/%m/%y %H:%M:%S", dt1), 756 ("%m", "01/2/03 4:5:6", "%d/%m/%y %H:%M:%S", dt1), 757 ("%H", "01/02/03 4:05:06", "%d/%m/%y %H:%M:%S", dt1), 758 ("%M", "01/02/03 04:5:06", "%d/%m/%y %H:%M:%S", dt1), 759 ("%S", "01/02/03 04:05:6", "%d/%m/%y %H:%M:%S", dt1), 760 ("%j", "2/03 04am:05:06", "%j/%y %I%p:%M:%S", dt2), 761 ("%I", "02/03 4am:05:06", "%j/%y %I%p:%M:%S", dt2), 762 ("%w", "6/04/03", "%w/%U/%y", dt3), 763 # %u requires a single digit. 764 ("%W", "6/4/2003", "%u/%W/%Y", dt3), 765 ("%V", "6/4/2003", "%u/%V/%G", dt4), 766 ] 767 for reason, string, format, target in inputs: 768 reason = "test single digit " + reason 769 with self.subTest( 770 reason=reason, string=string, format=format, target=target 771 ): 772 newdate = strptime(string, format) 773 self.assertEqual(newdate, target, msg=reason) 774 775 def test_more_timetuple(self): 776 # This tests fields beyond those tested by the TestDate.test_timetuple. 777 t = self.theclass(2004, 12, 31, 6, 22, 33) 778 self.assertEqual(t.timetuple(), (2004, 12, 31, 6, 22, 33, 4, 366, -1)) 779 self.assertEqual( 780 t.timetuple(), 781 ( 782 t.year, 783 t.month, 784 t.day, 785 t.hour, 786 t.minute, 787 t.second, 788 t.weekday(), 789 t.toordinal() - date(t.year, 1, 1).toordinal() + 1, 790 -1, 791 ), 792 ) 793 tt = t.timetuple() 794 self.assertEqual(tt.tm_year, t.year) 795 self.assertEqual(tt.tm_mon, t.month) 796 self.assertEqual(tt.tm_mday, t.day) 797 self.assertEqual(tt.tm_hour, t.hour) 798 self.assertEqual(tt.tm_min, t.minute) 799 self.assertEqual(tt.tm_sec, t.second) 800 self.assertEqual(tt.tm_wday, t.weekday()) 801 self.assertEqual(tt.tm_yday, t.toordinal() - date(t.year, 1, 1).toordinal() + 1) 802 self.assertEqual(tt.tm_isdst, -1) 803 804 @unittest.skip("gmtime not implemented in CircuitPython") 805 def test_more_strftime(self): 806 # This tests fields beyond those tested by the TestDate.test_strftime. 807 t = self.theclass(2004, 12, 31, 6, 22, 33, 47) 808 self.assertEqual( 809 t.strftime("%m %d %y %f %S %M %H %j"), "12 31 04 000047 33 22 06 366" 810 ) 811 for (s, us), z in [ 812 ((33, 123), "33.000123"), 813 ((33, 0), "33"), 814 ]: 815 tz = timezone(-timedelta(hours=2, seconds=s, microseconds=us)) 816 t = t.replace(tzinfo=tz) 817 self.assertEqual(t.strftime("%z"), "-0200" + z) 818 819 # bpo-34482: Check that surrogates don't cause a crash. 820 try: 821 t.strftime("%y\ud800%m %H\ud800%M") 822 except UnicodeEncodeError: 823 pass 824 825 def test_extract(self): 826 dt = self.theclass(2002, 3, 4, 18, 45, 3, 1234) 827 self.assertEqual(dt.date(), date(2002, 3, 4)) 828 self.assertEqual(dt.time(), time(18, 45, 3, 1234)) 829 830 # TODO 831 @unittest.skip("not implemented - timezone") 832 def test_combine(self): 833 d = date(2002, 3, 4) 834 t = time(18, 45, 3, 1234) 835 expected = self.theclass(2002, 3, 4, 18, 45, 3, 1234) 836 combine = self.theclass.combine 837 dt = combine(d, t) 838 self.assertEqual(dt, expected) 839 840 dt = combine(time=t, date=d) 841 self.assertEqual(dt, expected) 842 843 self.assertEqual(d, dt.date()) 844 self.assertEqual(t, dt.time()) 845 self.assertEqual(dt, combine(dt.date(), dt.time())) 846 847 self.assertRaises(TypeError, combine) # need an arg 848 self.assertRaises(TypeError, combine, d) # need two args 849 self.assertRaises(TypeError, combine, t, d) # args reversed 850 self.assertRaises(TypeError, combine, d, t, 1) # wrong tzinfo type 851 self.assertRaises(TypeError, combine, d, t, 1, 2) # too many args 852 self.assertRaises(TypeError, combine, "date", "time") # wrong types 853 self.assertRaises(TypeError, combine, d, "time") # wrong type 854 self.assertRaises(TypeError, combine, "date", t) # wrong type 855 856 # tzinfo= argument 857 dt = combine(d, t, timezone.utc) 858 self.assertIs(dt.tzinfo, timezone.utc) 859 dt = combine(d, t, tzinfo=timezone.utc) 860 self.assertIs(dt.tzinfo, timezone.utc) 861 t = time() 862 dt = combine(dt, t) 863 self.assertEqual(dt.date(), d) 864 self.assertEqual(dt.time(), t) 865 866 def test_replace(self): 867 cls = self.theclass 868 args = [1, 2, 3, 4, 5, 6, 7] 869 base = cls(*args) 870 self.assertEqual(base, base.replace()) 871 872 i = 0 873 for name, newval in ( 874 ("year", 2), 875 ("month", 3), 876 ("day", 4), 877 ("hour", 5), 878 ("minute", 6), 879 ("second", 7), 880 ("microsecond", 8), 881 ): 882 newargs = args[:] 883 newargs[i] = newval 884 expected = cls(*newargs) 885 got = base.replace(**{name: newval}) 886 self.assertEqual(expected, got) 887 i += 1 888 889 # Out of bounds. 890 base = cls(2000, 2, 29) 891 self.assertRaises(ValueError, base.replace, year=2001) 892 893 @unittest.skip("astimezone not impld") 894 @support.run_with_tz("EDT4") 895 def test_astimezone(self): 896 dt = self.theclass.now() 897 f = FixedOffset(44, "0044") 898 dt_utc = dt.replace(tzinfo=timezone(timedelta(hours=-4), "EDT")) 899 self.assertEqual(dt.astimezone(), dt_utc) # naive 900 self.assertRaises(TypeError, dt.astimezone, f, f) # too many args 901 self.assertRaises(TypeError, dt.astimezone, dt) # arg wrong type 902 dt_f = dt.replace(tzinfo=f) + timedelta(hours=4, minutes=44) 903 self.assertEqual(dt.astimezone(f), dt_f) # naive 904 self.assertEqual(dt.astimezone(tz=f), dt_f) # naive 905 906 class Bogus(tzinfo): 907 def utcoffset(self, dt): 908 return None 909 910 def dst(self, dt): 911 return timedelta(0) 912 913 bog = Bogus() 914 self.assertRaises(ValueError, dt.astimezone, bog) # naive 915 self.assertEqual(dt.replace(tzinfo=bog).astimezone(f), dt_f) 916 917 class AlsoBogus(tzinfo): 918 def utcoffset(self, dt): 919 return timedelta(0) 920 921 def dst(self, dt): 922 return None 923 924 alsobog = AlsoBogus() 925 self.assertRaises(ValueError, dt.astimezone, alsobog) # also naive 926 927 class Broken(tzinfo): 928 def utcoffset(self, dt): 929 return 1 930 931 def dst(self, dt): 932 return 1 933 934 broken = Broken() 935 dt_broken = dt.replace(tzinfo=broken) 936 with self.assertRaises(TypeError): 937 dt_broken.astimezone() 938 939 def test_subclass_datetime(self): 940 class C(self.theclass): 941 theAnswer = 42 942 943 def __new__(cls, *args, **kws): 944 temp = kws.copy() 945 extra = temp.pop("extra") 946 result = self.theclass.__new__(cls, *args, **temp) 947 result.extra = extra 948 return result 949 950 def newmeth(self, start): 951 return start + self.year + self.month + self.second 952 953 args = 2003, 4, 14, 12, 13, 41 954 955 dt1 = self.theclass(*args) 956 dt2 = C(*args, **{"extra": 7}) 957 958 self.assertEqual(dt2.__class__, C) 959 self.assertEqual(dt2.theAnswer, 42) 960 self.assertEqual(dt2.extra, 7) 961 self.assertEqual(dt1.toordinal(), dt2.toordinal()) 962 self.assertEqual(dt2.newmeth(-7), dt1.year + dt1.month + dt1.second - 7) 963 964 # TODO 965 @unittest.skip("timezone not implemented") 966 def test_subclass_alternate_constructors_datetime(self): 967 # Test that alternate constructors call the constructor 968 class DateTimeSubclass(self.theclass): 969 def __new__(cls, *args, **kwargs): 970 result = self.theclass.__new__(cls, *args, **kwargs) 971 result.extra = 7 972 973 return result 974 975 args = (2003, 4, 14, 12, 30, 15, 123456) 976 d_isoformat = "2003-04-14T12:30:15.123456" # Equivalent isoformat() 977 utc_ts = 1050323415.123456 # UTC timestamp 978 979 base_d = DateTimeSubclass(*args) 980 self.assertIsInstance(base_d, DateTimeSubclass) 981 self.assertEqual(base_d.extra, 7) 982 983 # Timestamp depends on time zone, so we'll calculate the equivalent here 984 ts = base_d.timestamp() 985 986 test_cases = [ 987 ("fromtimestamp", (ts,), base_d), 988 # See https://bugs.python.org/issue32417 989 ("fromtimestamp", (ts, timezone.utc), base_d.astimezone(timezone.utc)), 990 ("utcfromtimestamp", (utc_ts,), base_d), 991 ("fromisoformat", (d_isoformat,), base_d), 992 ("strptime", (d_isoformat, "%Y-%m-%dT%H:%M:%S.%f"), base_d), 993 ("combine", (date(*args[0:3]), time(*args[3:])), base_d), 994 ] 995 996 for constr_name, constr_args, expected in test_cases: 997 for base_obj in (DateTimeSubclass, base_d): 998 # Test both the classmethod and method 999 with self.subTest( 1000 base_obj_type=type(base_obj), constr_name=constr_name 1001 ): 1002 constructor = getattr(base_obj, constr_name) 1003 1004 dt = constructor(*constr_args) 1005 1006 # Test that it creates the right subclass 1007 self.assertIsInstance(dt, DateTimeSubclass) 1008 1009 # Test that it's equal to the base object 1010 self.assertEqual(dt, expected) 1011 1012 # Test that it called the constructor 1013 self.assertEqual(dt.extra, 7) 1014 1015 # TODO 1016 @unittest.skip("timezone not implemented") 1017 def test_subclass_now(self): 1018 # Test that alternate constructors call the constructor 1019 class DateTimeSubclass(self.theclass): 1020 def __new__(cls, *args, **kwargs): 1021 result = self.theclass.__new__(cls, *args, **kwargs) 1022 result.extra = 7 1023 1024 return result 1025 1026 test_cases = [ 1027 ("now", "now", {}), 1028 ("utcnow", "utcnow", {}), 1029 ("now_utc", "now", {"tz": timezone.utc}), 1030 ("now_fixed", "now", {"tz": timezone(timedelta(hours=-5), "EST")}), 1031 ] 1032 1033 for name, meth_name, kwargs in test_cases: 1034 with self.subTest(name): 1035 constr = getattr(DateTimeSubclass, meth_name) 1036 dt = constr(**kwargs) 1037 1038 self.assertIsInstance(dt, DateTimeSubclass) 1039 self.assertEqual(dt.extra, 7) 1040 1041 def test_fromisoformat_datetime(self): 1042 # Test that isoformat() is reversible 1043 base_dates = [(1, 1, 1), (1900, 1, 1), (2004, 11, 12), (2017, 5, 30)] 1044 1045 base_times = [ 1046 (0, 0, 0, 0), 1047 (0, 0, 0, 241000), 1048 (0, 0, 0, 234567), 1049 (12, 30, 45, 234567), 1050 ] 1051 1052 separators = [" ", "T"] 1053 1054 tzinfos = [ 1055 None, 1056 timezone.utc, 1057 timezone(timedelta(hours=-5)), 1058 timezone(timedelta(hours=2)), 1059 ] 1060 1061 dts = [ 1062 self.theclass(*date_tuple, *time_tuple, tzinfo=tzi) 1063 for date_tuple in base_dates 1064 for time_tuple in base_times 1065 for tzi in tzinfos 1066 ] 1067 1068 for dt in dts: 1069 for sep in separators: 1070 dtstr = dt.isoformat(sep=sep) 1071 1072 with self.subTest(dtstr=dtstr): 1073 dt_rt = self.theclass.fromisoformat(dtstr) 1074 self.assertEqual(dt, dt_rt) 1075 1076 # TODO 1077 @unittest.skip("not implemented timezone") 1078 def test_fromisoformat_timezone(self): 1079 base_dt = self.theclass(2014, 12, 30, 12, 30, 45, 217456) 1080 1081 tzoffsets = [ 1082 timedelta(hours=5), 1083 timedelta(hours=2), 1084 timedelta(hours=6, minutes=27), 1085 timedelta(hours=12, minutes=32, seconds=30), 1086 timedelta(hours=2, minutes=4, seconds=9, microseconds=123456), 1087 ] 1088 1089 tzoffsets += [-1 * td for td in tzoffsets] 1090 1091 tzinfos = [None, timezone.utc, timezone(timedelta(hours=0))] 1092 1093 tzinfos += [timezone(td) for td in tzoffsets] 1094 1095 for tzi in tzinfos: 1096 dt = base_dt.replace(tzinfo=tzi) 1097 dtstr = dt.isoformat() 1098 1099 with self.subTest(tstr=dtstr): 1100 dt_rt = self.theclass.fromisoformat(dtstr) 1101 assert dt == dt_rt, dt_rt 1102 1103 def test_fromisoformat_separators(self): 1104 separators = [ 1105 " ", 1106 "T", 1107 "\u007f", # 1-bit widths 1108 "\u0080", 1109 "ʁ", # 2-bit widths 1110 "ᛇ", 1111 "時", # 3-bit widths 1112 "🐍", # 4-bit widths 1113 "\ud800", # bpo-34454: Surrogate code point 1114 ] 1115 1116 for sep in separators: 1117 dt = self.theclass(2018, 1, 31, 23, 59, 47, 124789) 1118 dtstr = dt.isoformat(sep=sep) 1119 1120 with self.subTest(dtstr=dtstr): 1121 dt_rt = self.theclass.fromisoformat(dtstr) 1122 self.assertEqual(dt, dt_rt) 1123 1124 def test_fromisoformat_ambiguous(self): 1125 # Test strings like 2018-01-31+12:15 (where +12:15 is not a time zone) 1126 separators = ["+", "-"] 1127 for sep in separators: 1128 dt = self.theclass(2018, 1, 31, 12, 15) 1129 dtstr = dt.isoformat(sep=sep) 1130 1131 with self.subTest(dtstr=dtstr): 1132 dt_rt = self.theclass.fromisoformat(dtstr) 1133 self.assertEqual(dt, dt_rt) 1134 1135 # TODO 1136 @unittest.skip("_format_time not fully implemented") 1137 def test_fromisoformat_timespecs(self): 1138 datetime_bases = [(2009, 12, 4, 8, 17, 45, 123456), (2009, 12, 4, 8, 17, 45, 0)] 1139 1140 tzinfos = [ 1141 None, 1142 timezone.utc, 1143 timezone(timedelta(hours=-5)), 1144 timezone(timedelta(hours=2)), 1145 timezone(timedelta(hours=6, minutes=27)), 1146 ] 1147 1148 timespecs = ["hours", "minutes", "seconds", "milliseconds", "microseconds"] 1149 1150 for ip, ts in enumerate(timespecs): 1151 for tzi in tzinfos: 1152 for dt_tuple in datetime_bases: 1153 if ts == "milliseconds": 1154 new_microseconds = 1000 * (dt_tuple[6] // 1000) 1155 dt_tuple = dt_tuple[0:6] + (new_microseconds,) 1156 1157 dt = self.theclass(*(dt_tuple[0 : (4 + ip)]), tzinfo=tzi) 1158 dtstr = dt.isoformat(timespec=ts) 1159 with self.subTest(dtstr=dtstr): 1160 dt_rt = self.theclass.fromisoformat(dtstr) 1161 self.assertEqual(dt, dt_rt) 1162 1163 def test_fromisoformat_fails_datetime(self): 1164 # Test that fromisoformat() fails on invalid values 1165 bad_strs = [ 1166 "", # Empty string 1167 "\ud800", # bpo-34454: Surrogate code point 1168 "2009.04-19T03", # Wrong first separator 1169 "2009-04.19T03", # Wrong second separator 1170 "2009-04-19T0a", # Invalid hours 1171 "2009-04-19T03:1a:45", # Invalid minutes 1172 "2009-04-19T03:15:4a", # Invalid seconds 1173 "2009-04-19T03;15:45", # Bad first time separator 1174 "2009-04-19T03:15;45", # Bad second time separator 1175 "2009-04-19T03:15:4500:00", # Bad time zone separator 1176 "2009-04-19T03:15:45.2345", # Too many digits for milliseconds 1177 "2009-04-19T03:15:45.1234567", # Too many digits for microseconds 1178 "2009-04-19T03:15:45.123456+24:30", # Invalid time zone offset 1179 "2009-04-19T03:15:45.123456-24:30", # Invalid negative offset 1180 "2009-04-10ᛇᛇᛇᛇᛇ12:15", # Too many unicode separators 1181 "2009-04\ud80010T12:15", # Surrogate char in date 1182 "2009-04-10T12\ud80015", # Surrogate char in time 1183 "2009-04-19T1", # Incomplete hours 1184 "2009-04-19T12:3", # Incomplete minutes 1185 "2009-04-19T12:30:4", # Incomplete seconds 1186 "2009-04-19T12:", # Ends with time separator 1187 "2009-04-19T12:30:", # Ends with time separator 1188 "2009-04-19T12:30:45.", # Ends with time separator 1189 "2009-04-19T12:30:45.123456+", # Ends with timzone separator 1190 "2009-04-19T12:30:45.123456-", # Ends with timzone separator 1191 "2009-04-19T12:30:45.123456-05:00a", # Extra text 1192 "2009-04-19T12:30:45.123-05:00a", # Extra text 1193 "2009-04-19T12:30:45-05:00a", # Extra text 1194 ] 1195 1196 for bad_str in bad_strs: 1197 with self.subTest(bad_str=bad_str): 1198 with self.assertRaises(ValueError): 1199 self.theclass.fromisoformat(bad_str) 1200 1201 def test_fromisoformat_fails_surrogate(self): 1202 # Test that when fromisoformat() fails with a surrogate character as 1203 # the separator, the error message contains the original string 1204 dtstr = "2018-01-03\ud80001:0113" 1205 1206 with self.assertRaisesRegex(ValueError, repr(dtstr)): 1207 self.theclass.fromisoformat(dtstr) 1208 1209 # TODO 1210 @unittest.skip("fromisoformat not implemented") 1211 def test_fromisoformat_utc(self): 1212 dt_str = "2014-04-19T13:21:13+00:00" 1213 dt = self.theclass.fromisoformat(dt_str) 1214 1215 self.assertIs(dt.tzinfo, timezone.utc) 1216 1217 def test_fromisoformat_subclass(self): 1218 class DateTimeSubclass(self.theclass): 1219 pass 1220 1221 dt = DateTimeSubclass( 1222 2014, 1223 12, 1224 14, 1225 9, 1226 30, 1227 45, 1228 457390, 1229 tzinfo=timezone(timedelta(hours=10, minutes=45)), 1230 ) 1231 1232 dt_rt = DateTimeSubclass.fromisoformat(dt.isoformat()) 1233 1234 self.assertEqual(dt, dt_rt) 1235 self.assertIsInstance(dt_rt, DateTimeSubclass)