/ tests / test_datetime.py
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)