Adafruit_CircuitPython_date.../tests/test_datetime.py
2021-03-17 11:55:00 -04:00

1235 lines
48 KiB
Python

# SPDX-FileCopyrightText: 2001-2021 Python Software Foundation.All rights reserved.
# SPDX-FileCopyrightText: 2000 BeOpen.com. All rights reserved.
# SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives.
# All rights reserved.
# SPDX-FileCopyrightText: 1995-2001 Corporation for National Research Initiatives.
# All rights reserved.
# SPDX-FileCopyrightText: 1991-1995 Stichting Mathematisch Centrum. All rights reserved.
# SPDX-FileCopyrightText: 2021 Brent Rubell for Adafruit Industries
# SPDX-License-Identifier: Python-2.0
# Implements a subset of https://github.com/python/cpython/blob/master/Lib/test/datetimetester.py
# NOTE: This test is based off CPython and therefore linting is disabled within this file.
# 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
import sys
# CircuitPython subset implementation
sys.path.append("..")
from adafruit_datetime import datetime as cpy_datetime
from adafruit_datetime import timedelta
from adafruit_datetime import tzinfo
from adafruit_datetime import date
from adafruit_datetime import time
from adafruit_datetime import timezone
import unittest
from test import support
from test_date import TestDate
# CPython standard implementation
from datetime import datetime as cpython_datetime
from datetime import MINYEAR, MAXYEAR
# TZinfo test
class FixedOffset(tzinfo):
def __init__(self, offset, name, dstoffset=42):
if isinstance(offset, int):
offset = timedelta(minutes=offset)
if isinstance(dstoffset, int):
dstoffset = timedelta(minutes=dstoffset)
self.__offset = offset
self.__name = name
self.__dstoffset = dstoffset
def __repr__(self):
return self.__name.lower()
def utcoffset(self, dt):
return self.__offset
def tzname(self, dt):
return self.__name
def dst(self, dt):
return self.__dstoffset
# =======================================================================
# Decorator for running a function in a specific timezone, correctly
# resetting it afterwards.
def run_with_tz(tz):
def decorator(func):
def inner(*args, **kwds):
try:
tzset = time.tzset
except AttributeError:
raise unittest.SkipTest("tzset required")
if "TZ" in os.environ:
orig_tz = os.environ["TZ"]
else:
orig_tz = None
os.environ["TZ"] = tz
tzset()
# now run the function, resetting the tz on exceptions
try:
return func(*args, **kwds)
finally:
if orig_tz is None:
del os.environ["TZ"]
else:
os.environ["TZ"] = orig_tz
time.tzset()
inner.__name__ = func.__name__
inner.__doc__ = func.__doc__
return inner
return decorator
class SubclassDatetime(cpy_datetime):
sub_var = 1
class TestDateTime(TestDate):
theclass = cpy_datetime
theclass_cpython = cpython_datetime
def test_basic_attributes(self):
dt = self.theclass(2002, 3, 1, 12, 0)
dt2 = self.theclass_cpython(2002, 3, 1, 12, 0)
# test circuitpython basic attributes
self.assertEqual(dt.year, 2002)
self.assertEqual(dt.month, 3)
self.assertEqual(dt.day, 1)
self.assertEqual(dt.hour, 12)
self.assertEqual(dt.minute, 0)
self.assertEqual(dt.second, 0)
self.assertEqual(dt.microsecond, 0)
# test circuitpython basic attributes against cpython basic attributes
self.assertEqual(dt.year, dt2.year)
self.assertEqual(dt.month, dt2.month)
self.assertEqual(dt.day, dt2.day)
self.assertEqual(dt.hour, dt2.hour)
self.assertEqual(dt.minute, dt2.minute)
self.assertEqual(dt.second, dt2.second)
self.assertEqual(dt.microsecond, dt2.microsecond)
def test_basic_attributes_nonzero(self):
# Make sure all attributes are non-zero so bugs in
# bit-shifting access show up.
dt = self.theclass(2002, 3, 1, 12, 59, 59, 8000)
self.assertEqual(dt.year, 2002)
self.assertEqual(dt.month, 3)
self.assertEqual(dt.day, 1)
self.assertEqual(dt.hour, 12)
self.assertEqual(dt.minute, 59)
self.assertEqual(dt.second, 59)
self.assertEqual(dt.microsecond, 8000)
@unittest.skip("issue with startswith and ada lib.")
def test_roundtrip(self):
for dt in (self.theclass(1, 2, 3, 4, 5, 6, 7), self.theclass.now()):
# Verify dt -> string -> datetime identity.
s = repr(dt)
self.assertTrue(s.startswith("datetime."))
s = s[9:]
dt2 = eval(s)
self.assertEqual(dt, dt2)
# Verify identity via reconstructing from pieces.
dt2 = self.theclass(
dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond
)
self.assertEqual(dt, dt2)
@unittest.skip("isoformat not implemented")
def test_isoformat(self):
t = self.theclass(1, 2, 3, 4, 5, 1, 123)
self.assertEqual(t.isoformat(), "0001-02-03T04:05:01.000123")
self.assertEqual(t.isoformat("T"), "0001-02-03T04:05:01.000123")
self.assertEqual(t.isoformat(" "), "0001-02-03 04:05:01.000123")
self.assertEqual(t.isoformat("\x00"), "0001-02-03\x0004:05:01.000123")
# bpo-34482: Check that surrogates are handled properly.
self.assertEqual(t.isoformat("\ud800"), "0001-02-03\ud80004:05:01.000123")
self.assertEqual(t.isoformat(timespec="hours"), "0001-02-03T04")
self.assertEqual(t.isoformat(timespec="minutes"), "0001-02-03T04:05")
self.assertEqual(t.isoformat(timespec="seconds"), "0001-02-03T04:05:01")
self.assertEqual(
t.isoformat(timespec="milliseconds"), "0001-02-03T04:05:01.000"
)
self.assertEqual(
t.isoformat(timespec="microseconds"), "0001-02-03T04:05:01.000123"
)
self.assertEqual(t.isoformat(timespec="auto"), "0001-02-03T04:05:01.000123")
self.assertEqual(t.isoformat(sep=" ", timespec="minutes"), "0001-02-03 04:05")
self.assertRaises(ValueError, t.isoformat, timespec="foo")
# bpo-34482: Check that surrogates are handled properly.
self.assertRaises(ValueError, t.isoformat, timespec="\ud800")
# str is ISO format with the separator forced to a blank.
self.assertEqual(str(t), "0001-02-03 04:05:01.000123")
t = self.theclass(1, 2, 3, 4, 5, 1, 999500, tzinfo=timezone.utc)
self.assertEqual(
t.isoformat(timespec="milliseconds"), "0001-02-03T04:05:01.999+00:00"
)
t = self.theclass(1, 2, 3, 4, 5, 1, 999500)
self.assertEqual(
t.isoformat(timespec="milliseconds"), "0001-02-03T04:05:01.999"
)
t = self.theclass(1, 2, 3, 4, 5, 1)
self.assertEqual(t.isoformat(timespec="auto"), "0001-02-03T04:05:01")
self.assertEqual(
t.isoformat(timespec="milliseconds"), "0001-02-03T04:05:01.000"
)
self.assertEqual(
t.isoformat(timespec="microseconds"), "0001-02-03T04:05:01.000000"
)
t = self.theclass(2, 3, 2)
self.assertEqual(t.isoformat(), "0002-03-02T00:00:00")
self.assertEqual(t.isoformat("T"), "0002-03-02T00:00:00")
self.assertEqual(t.isoformat(" "), "0002-03-02 00:00:00")
# str is ISO format with the separator forced to a blank.
self.assertEqual(str(t), "0002-03-02 00:00:00")
# ISO format with timezone
tz = FixedOffset(timedelta(seconds=16), "XXX")
t = self.theclass(2, 3, 2, tzinfo=tz)
self.assertEqual(t.isoformat(), "0002-03-02T00:00:00+00:00:16")
@unittest.skip("isoformat not implemented.")
def test_isoformat_timezone(self):
tzoffsets = [
("05:00", timedelta(hours=5)),
("02:00", timedelta(hours=2)),
("06:27", timedelta(hours=6, minutes=27)),
("12:32:30", timedelta(hours=12, minutes=32, seconds=30)),
(
"02:04:09.123456",
timedelta(hours=2, minutes=4, seconds=9, microseconds=123456),
),
]
tzinfos = [
("", None),
("+00:00", timezone.utc),
("+00:00", timezone(timedelta(0))),
]
tzinfos += [
(prefix + expected, timezone(sign * td))
for expected, td in tzoffsets
for prefix, sign in [("-", -1), ("+", 1)]
]
dt_base = self.theclass(2016, 4, 1, 12, 37, 9)
exp_base = "2016-04-01T12:37:09"
for exp_tz, tzi in tzinfos:
dt = dt_base.replace(tzinfo=tzi)
exp = exp_base + exp_tz
with self.subTest(tzi=tzi):
assert dt.isoformat() == exp
@unittest.skip("strftime not implemented in datetime")
def test_format(self):
dt = self.theclass(2007, 9, 10, 4, 5, 1, 123)
self.assertEqual(dt.__format__(""), str(dt))
with self.assertRaisesRegex(TypeError, "must be str, not int"):
dt.__format__(123)
# check that a derived class's __str__() gets called
class A(self.theclass):
def __str__(self):
return "A"
a = A(2007, 9, 10, 4, 5, 1, 123)
self.assertEqual(a.__format__(""), "A")
# check that a derived class's strftime gets called
class B(self.theclass):
def strftime(self, format_spec):
return "B"
b = B(2007, 9, 10, 4, 5, 1, 123)
self.assertEqual(b.__format__(""), str(dt))
for fmt in [
"m:%m d:%d y:%y",
"m:%m d:%d y:%y H:%H M:%M S:%S",
"%z %Z",
]:
self.assertEqual(dt.__format__(fmt), dt.strftime(fmt))
self.assertEqual(a.__format__(fmt), dt.strftime(fmt))
self.assertEqual(b.__format__(fmt), "B")
@unittest.skip("ctime not implemented")
def test_more_ctime(self):
# Test fields that TestDate doesn't touch.
import time as cpython_time
t = self.theclass(2002, 3, 2, 18, 3, 5, 123)
self.assertEqual(t.ctime(), "Sat Mar 2 18:03:05 2002")
# Oops! The next line fails on Win2K under MSVC 6, so it's commented
# out. The difference is that t.ctime() produces " 2" for the day,
# but platform ctime() produces "02" for the day. According to
# C99, t.ctime() is correct here.
# self.assertEqual(t.ctime(), time.ctime(time.mktime(t.timetuple())))
# So test a case where that difference doesn't matter.
t = self.theclass(2002, 3, 22, 18, 3, 5, 123)
self.assertEqual(
t.ctime(), cpython_time.ctime(cpython_time.mktime(t.timetuple()))
)
def test_tz_independent_comparing(self):
dt1 = self.theclass(2002, 3, 1, 9, 0, 0)
dt2 = self.theclass(2002, 3, 1, 10, 0, 0)
dt3 = self.theclass(2002, 3, 1, 9, 0, 0)
self.assertEqual(dt1, dt3)
self.assertTrue(dt2 > dt3)
# Make sure comparison doesn't forget microseconds, and isn't done
# via comparing a float timestamp (an IEEE double doesn't have enough
# precision to span microsecond resolution across years 1 through 9999,
# so comparing via timestamp necessarily calls some distinct values
# equal).
dt1 = self.theclass(MAXYEAR, 12, 31, 23, 59, 59, 999998)
us = timedelta(microseconds=1)
dt2 = dt1 + us
self.assertEqual(dt2 - dt1, us)
self.assertTrue(dt1 < dt2)
@unittest.skip("not implemented - strftime")
def test_strftime_with_bad_tzname_replace(self):
# verify ok if tzinfo.tzname().replace() returns a non-string
class MyTzInfo(FixedOffset):
def tzname(self, dt):
class MyStr(str):
def replace(self, *args):
return None
return MyStr("name")
t = self.theclass(2005, 3, 2, 0, 0, 0, 0, MyTzInfo(3, "name"))
self.assertRaises(TypeError, t.strftime, "%Z")
def test_bad_constructor_arguments(self):
# bad years
self.theclass(MINYEAR, 1, 1) # no exception
self.theclass(MAXYEAR, 1, 1) # no exception
self.assertRaises(ValueError, self.theclass, MINYEAR - 1, 1, 1)
self.assertRaises(ValueError, self.theclass, MAXYEAR + 1, 1, 1)
# bad months
self.theclass(2000, 1, 1) # no exception
self.theclass(2000, 12, 1) # no exception
self.assertRaises(ValueError, self.theclass, 2000, 0, 1)
self.assertRaises(ValueError, self.theclass, 2000, 13, 1)
# bad days
self.theclass(2000, 2, 29) # no exception
self.theclass(2004, 2, 29) # no exception
self.theclass(2400, 2, 29) # no exception
self.assertRaises(ValueError, self.theclass, 2000, 2, 30)
self.assertRaises(ValueError, self.theclass, 2001, 2, 29)
self.assertRaises(ValueError, self.theclass, 2100, 2, 29)
self.assertRaises(ValueError, self.theclass, 1900, 2, 29)
self.assertRaises(ValueError, self.theclass, 2000, 1, 0)
self.assertRaises(ValueError, self.theclass, 2000, 1, 32)
# bad hours
self.theclass(2000, 1, 31, 0) # no exception
self.theclass(2000, 1, 31, 23) # no exception
self.assertRaises(ValueError, self.theclass, 2000, 1, 31, -1)
self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 24)
# bad minutes
self.theclass(2000, 1, 31, 23, 0) # no exception
self.theclass(2000, 1, 31, 23, 59) # no exception
self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, -1)
self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 60)
# bad seconds
self.theclass(2000, 1, 31, 23, 59, 0) # no exception
self.theclass(2000, 1, 31, 23, 59, 59) # no exception
self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, -1)
self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, 60)
# bad microseconds
self.theclass(2000, 1, 31, 23, 59, 59, 0) # no exception
self.theclass(2000, 1, 31, 23, 59, 59, 999999) # no exception
self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, 59, -1)
self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, 59, 1000000)
# bad fold
self.assertRaises(ValueError, self.theclass, 2000, 1, 31, fold=-1)
self.assertRaises(ValueError, self.theclass, 2000, 1, 31, fold=2)
# Positional fold:
self.assertRaises(TypeError, self.theclass, 2000, 1, 31, 23, 59, 59, 0, None, 1)
def test_hash_equality(self):
d = self.theclass(2000, 12, 31, 23, 30, 17)
e = self.theclass(2000, 12, 31, 23, 30, 17)
self.assertEqual(d, e)
self.assertEqual(hash(d), hash(e))
dic = {d: 1}
dic[e] = 2
self.assertEqual(len(dic), 1)
self.assertEqual(dic[d], 2)
self.assertEqual(dic[e], 2)
d = self.theclass(2001, 1, 1, 0, 5, 17)
e = self.theclass(2001, 1, 1, 0, 5, 17)
self.assertEqual(d, e)
self.assertEqual(hash(d), hash(e))
dic = {d: 1}
dic[e] = 2
self.assertEqual(len(dic), 1)
self.assertEqual(dic[d], 2)
self.assertEqual(dic[e], 2)
def test_computations(self):
a = self.theclass(2002, 1, 31)
b = self.theclass(1956, 1, 31)
diff = a - b
self.assertEqual(diff.days, 46 * 365 + len(range(1956, 2002, 4)))
self.assertEqual(diff.seconds, 0)
self.assertEqual(diff.microseconds, 0)
a = self.theclass(2002, 3, 2, 17, 6)
millisec = timedelta(0, 0, 1000)
hour = timedelta(0, 3600)
day = timedelta(1)
week = timedelta(7)
self.assertEqual(a + hour, self.theclass(2002, 3, 2, 18, 6))
self.assertEqual(hour + a, self.theclass(2002, 3, 2, 18, 6))
self.assertEqual(a + 10 * hour, self.theclass(2002, 3, 3, 3, 6))
self.assertEqual(a - hour, self.theclass(2002, 3, 2, 16, 6))
self.assertEqual(-hour + a, self.theclass(2002, 3, 2, 16, 6))
self.assertEqual(a - hour, a + -hour)
self.assertEqual(a - 20 * hour, self.theclass(2002, 3, 1, 21, 6))
self.assertEqual(a + day, self.theclass(2002, 3, 3, 17, 6))
self.assertEqual(a - day, self.theclass(2002, 3, 1, 17, 6))
self.assertEqual(a + week, self.theclass(2002, 3, 9, 17, 6))
self.assertEqual(a - week, self.theclass(2002, 2, 23, 17, 6))
self.assertEqual(a + 52 * week, self.theclass(2003, 3, 1, 17, 6))
self.assertEqual(a - 52 * week, self.theclass(2001, 3, 3, 17, 6))
self.assertEqual((a + week) - a, week)
self.assertEqual((a + day) - a, day)
self.assertEqual((a + hour) - a, hour)
self.assertEqual((a + millisec) - a, millisec)
self.assertEqual((a - week) - a, -week)
self.assertEqual((a - day) - a, -day)
self.assertEqual((a - hour) - a, -hour)
self.assertEqual((a - millisec) - a, -millisec)
self.assertEqual(a - (a + week), -week)
self.assertEqual(a - (a + day), -day)
self.assertEqual(a - (a + hour), -hour)
self.assertEqual(a - (a + millisec), -millisec)
self.assertEqual(a - (a - week), week)
self.assertEqual(a - (a - day), day)
self.assertEqual(a - (a - hour), hour)
self.assertEqual(a - (a - millisec), millisec)
self.assertEqual(
a + (week + day + hour + millisec),
self.theclass(2002, 3, 10, 18, 6, 0, 1000),
)
self.assertEqual(
a + (week + day + hour + millisec), (((a + week) + day) + hour) + millisec
)
self.assertEqual(
a - (week + day + hour + millisec),
self.theclass(2002, 2, 22, 16, 5, 59, 999000),
)
self.assertEqual(
a - (week + day + hour + millisec), (((a - week) - day) - hour) - millisec
)
# Add/sub ints or floats should be illegal
for i in 1, 1.0:
self.assertRaises(TypeError, lambda: a + i)
self.assertRaises(TypeError, lambda: a - i)
self.assertRaises(TypeError, lambda: i + a)
self.assertRaises(TypeError, lambda: i - a)
# delta - datetime is senseless.
self.assertRaises(TypeError, lambda: day - a)
# mixing datetime and (delta or datetime) via * or // is senseless
self.assertRaises(TypeError, lambda: day * a)
self.assertRaises(TypeError, lambda: a * day)
self.assertRaises(TypeError, lambda: day // a)
self.assertRaises(TypeError, lambda: a // day)
self.assertRaises(TypeError, lambda: a * a)
self.assertRaises(TypeError, lambda: a // a)
# datetime + datetime is senseless
self.assertRaises(TypeError, lambda: a + a)
def test_more_compare(self):
# The test_compare() inherited from TestDate covers the error cases.
# We just want to test lexicographic ordering on the members datetime
# has that date lacks.
args = [2000, 11, 29, 20, 58, 16, 999998]
t1 = self.theclass(*args)
t2 = self.theclass(*args)
self.assertEqual(t1, t2)
self.assertTrue(t1 <= t2)
self.assertTrue(t1 >= t2)
self.assertFalse(t1 != t2)
self.assertFalse(t1 < t2)
self.assertFalse(t1 > t2)
for i in range(len(args)):
newargs = args[:]
newargs[i] = args[i] + 1
t2 = self.theclass(*newargs) # this is larger than t1
self.assertTrue(t1 < t2)
self.assertTrue(t2 > t1)
self.assertTrue(t1 <= t2)
self.assertTrue(t2 >= t1)
self.assertTrue(t1 != t2)
self.assertTrue(t2 != t1)
self.assertFalse(t1 == t2)
self.assertFalse(t2 == t1)
self.assertFalse(t1 > t2)
self.assertFalse(t2 < t1)
self.assertFalse(t1 >= t2)
self.assertFalse(t2 <= t1)
# A helper for timestamp constructor tests.
def verify_field_equality(self, expected, got):
self.assertEqual(expected.tm_year, got.year)
self.assertEqual(expected.tm_mon, got.month)
self.assertEqual(expected.tm_mday, got.day)
self.assertEqual(expected.tm_hour, got.hour)
self.assertEqual(expected.tm_min, got.minute)
self.assertEqual(expected.tm_sec, got.second)
def test_fromtimestamp(self):
import time
ts = time.time()
expected = time.localtime(ts)
got = self.theclass.fromtimestamp(ts)
self.verify_field_equality(expected, got)
@unittest.skip("gmtime not implemented in CircuitPython")
def test_utcfromtimestamp(self):
import time
ts = time.time()
expected = time.gmtime(ts)
got = self.theclass.utcfromtimestamp(ts)
self.verify_field_equality(expected, got)
# Run with US-style DST rules: DST begins 2 a.m. on second Sunday in
# March (M3.2.0) and ends 2 a.m. on first Sunday in November (M11.1.0).
@support.run_with_tz("EST+05EDT,M3.2.0,M11.1.0")
def test_timestamp_naive(self):
t = self.theclass(1970, 1, 1)
self.assertEqual(t.timestamp(), 18000.0)
t = self.theclass(1970, 1, 1, 1, 2, 3, 4)
self.assertEqual(t.timestamp(), 18000.0 + 3600 + 2 * 60 + 3 + 4 * 1e-6)
# Missing hour
t0 = self.theclass(2012, 3, 11, 2, 30)
t1 = t0.replace(fold=1)
self.assertEqual(
self.theclass.fromtimestamp(t1.timestamp()), t0 - timedelta(hours=1)
)
self.assertEqual(
self.theclass.fromtimestamp(t0.timestamp()), t1 + timedelta(hours=1)
)
# Ambiguous hour defaults to DST
t = self.theclass(2012, 11, 4, 1, 30)
self.assertEqual(self.theclass.fromtimestamp(t.timestamp()), t)
# Timestamp may raise an overflow error on some platforms
for t in [self.theclass(2, 1, 1), self.theclass(9998, 12, 12)]:
try:
s = t.timestamp()
except OverflowError:
pass
else:
self.assertEqual(self.theclass.fromtimestamp(s), t)
def test_timestamp_aware(self):
t = self.theclass(1970, 1, 1, tzinfo=timezone.utc)
self.assertEqual(t.timestamp(), 0.0)
t = self.theclass(1970, 1, 1, 1, 2, 3, 4, tzinfo=timezone.utc)
self.assertEqual(t.timestamp(), 3600 + 2 * 60 + 3 + 4 * 1e-6)
t = self.theclass(
1970, 1, 1, 1, 2, 3, 4, tzinfo=timezone(timedelta(hours=-5), "EST")
)
self.assertEqual(t.timestamp(), 18000 + 3600 + 2 * 60 + 3 + 4 * 1e-6)
@unittest.skip("Not implemented - gmtime")
@support.run_with_tz("MSK-03") # Something east of Greenwich
def test_microsecond_rounding(self):
for fts in [self.theclass.fromtimestamp, self.theclass.utcfromtimestamp]:
zero = fts(0)
self.assertEqual(zero.second, 0)
self.assertEqual(zero.microsecond, 0)
one = fts(1e-6)
try:
minus_one = fts(-1e-6)
except OSError:
# localtime(-1) and gmtime(-1) is not supported on Windows
pass
else:
self.assertEqual(minus_one.second, 59)
self.assertEqual(minus_one.microsecond, 999999)
t = fts(-1e-8)
self.assertEqual(t, zero)
t = fts(-9e-7)
self.assertEqual(t, minus_one)
t = fts(-1e-7)
self.assertEqual(t, zero)
t = fts(-1 / 2 ** 7)
self.assertEqual(t.second, 59)
self.assertEqual(t.microsecond, 992188)
t = fts(1e-7)
self.assertEqual(t, zero)
t = fts(9e-7)
self.assertEqual(t, one)
t = fts(0.99999949)
self.assertEqual(t.second, 0)
self.assertEqual(t.microsecond, 999999)
t = fts(0.9999999)
self.assertEqual(t.second, 1)
self.assertEqual(t.microsecond, 0)
t = fts(1 / 2 ** 7)
self.assertEqual(t.second, 0)
self.assertEqual(t.microsecond, 7812)
@unittest.skip("gmtime not implemented in CircuitPython")
def test_timestamp_limits(self):
# minimum timestamp
min_dt = self.theclass.min.replace(tzinfo=timezone.utc)
min_ts = min_dt.timestamp()
try:
# date 0001-01-01 00:00:00+00:00: timestamp=-62135596800
self.assertEqual(
self.theclass.fromtimestamp(min_ts, tz=timezone.utc), min_dt
)
except (OverflowError, OSError) as exc:
# the date 0001-01-01 doesn't fit into 32-bit time_t,
# or platform doesn't support such very old date
self.skipTest(str(exc))
# maximum timestamp: set seconds to zero to avoid rounding issues
max_dt = self.theclass.max.replace(tzinfo=timezone.utc, second=0, microsecond=0)
max_ts = max_dt.timestamp()
# date 9999-12-31 23:59:00+00:00: timestamp 253402300740
self.assertEqual(self.theclass.fromtimestamp(max_ts, tz=timezone.utc), max_dt)
# number of seconds greater than 1 year: make sure that the new date
# is not valid in datetime.datetime limits
delta = 3600 * 24 * 400
# too small
ts = min_ts - delta
# converting a Python int to C time_t can raise a OverflowError,
# especially on 32-bit platforms.
with self.assertRaises((ValueError, OverflowError)):
self.theclass.fromtimestamp(ts)
with self.assertRaises((ValueError, OverflowError)):
self.theclass.utcfromtimestamp(ts)
# too big
ts = max_dt.timestamp() + delta
with self.assertRaises((ValueError, OverflowError)):
self.theclass.fromtimestamp(ts)
with self.assertRaises((ValueError, OverflowError)):
self.theclass.utcfromtimestamp(ts)
def test_insane_fromtimestamp(self):
# It's possible that some platform maps time_t to double,
# and that this test will fail there. This test should
# exempt such platforms (provided they return reasonable
# results!).
for insane in -1e200, 1e200:
self.assertRaises(OverflowError, self.theclass.fromtimestamp, insane)
@unittest.skip("Not implemented - gmtime")
def test_insane_utcfromtimestamp(self):
# It's possible that some platform maps time_t to double,
# and that this test will fail there. This test should
# exempt such platforms (provided they return reasonable
# results!).
for insane in -1e200, 1e200:
self.assertRaises(OverflowError, self.theclass.utcfromtimestamp, insane)
@unittest.skip("gmtime not implemented in CircuitPython")
def test_utcnow(self):
import time
# Call it a success if utcnow() and utcfromtimestamp() are within
# a second of each other.
tolerance = timedelta(seconds=1)
for dummy in range(3):
from_now = self.theclass.utcnow()
from_timestamp = self.theclass.utcfromtimestamp(time.time())
if abs(from_timestamp - from_now) <= tolerance:
break
# Else try again a few times.
self.assertLessEqual(abs(from_timestamp - from_now), tolerance)
@unittest.skip("gmtime not implemented in CircuitPython")
def test_strptime(self):
string = "2004-12-01 13:02:47.197"
format = "%Y-%m-%d %H:%M:%S.%f"
expected = _strptime._strptime_datetime(self.theclass, string, format)
got = self.theclass.strptime(string, format)
self.assertEqual(expected, got)
self.assertIs(type(expected), self.theclass)
self.assertIs(type(got), self.theclass)
# bpo-34482: Check that surrogates are handled properly.
inputs = [
("2004-12-01\ud80013:02:47.197", "%Y-%m-%d\ud800%H:%M:%S.%f"),
("2004\ud80012-01 13:02:47.197", "%Y\ud800%m-%d %H:%M:%S.%f"),
("2004-12-01 13:02\ud80047.197", "%Y-%m-%d %H:%M\ud800%S.%f"),
]
for string, format in inputs:
with self.subTest(string=string, format=format):
expected = _strptime._strptime_datetime(self.theclass, string, format)
got = self.theclass.strptime(string, format)
self.assertEqual(expected, got)
strptime = self.theclass.strptime
self.assertEqual(strptime("+0002", "%z").utcoffset(), 2 * MINUTE)
self.assertEqual(strptime("-0002", "%z").utcoffset(), -2 * MINUTE)
self.assertEqual(
strptime("-00:02:01.000003", "%z").utcoffset(),
-timedelta(minutes=2, seconds=1, microseconds=3),
)
# Only local timezone and UTC are supported
for tzseconds, tzname in (
(0, "UTC"),
(0, "GMT"),
(-_time.timezone, _time.tzname[0]),
):
if tzseconds < 0:
sign = "-"
seconds = -tzseconds
else:
sign = "+"
seconds = tzseconds
hours, minutes = divmod(seconds // 60, 60)
dtstr = "{}{:02d}{:02d} {}".format(sign, hours, minutes, tzname)
dt = strptime(dtstr, "%z %Z")
self.assertEqual(dt.utcoffset(), timedelta(seconds=tzseconds))
self.assertEqual(dt.tzname(), tzname)
# Can produce inconsistent datetime
dtstr, fmt = "+1234 UTC", "%z %Z"
dt = strptime(dtstr, fmt)
self.assertEqual(dt.utcoffset(), 12 * HOUR + 34 * MINUTE)
self.assertEqual(dt.tzname(), "UTC")
# yet will roundtrip
self.assertEqual(dt.strftime(fmt), dtstr)
# Produce naive datetime if no %z is provided
self.assertEqual(strptime("UTC", "%Z").tzinfo, None)
with self.assertRaises(ValueError):
strptime("-2400", "%z")
with self.assertRaises(ValueError):
strptime("-000", "%z")
@unittest.skip("gmtime not implemented in CircuitPython")
def test_strptime_single_digit(self):
# bpo-34903: Check that single digit dates and times are allowed.
strptime = self.theclass.strptime
with self.assertRaises(ValueError):
# %y does require two digits.
newdate = strptime("01/02/3 04:05:06", "%d/%m/%y %H:%M:%S")
dt1 = self.theclass(2003, 2, 1, 4, 5, 6)
dt2 = self.theclass(2003, 1, 2, 4, 5, 6)
dt3 = self.theclass(2003, 2, 1, 0, 0, 0)
dt4 = self.theclass(2003, 1, 25, 0, 0, 0)
inputs = [
("%d", "1/02/03 4:5:6", "%d/%m/%y %H:%M:%S", dt1),
("%m", "01/2/03 4:5:6", "%d/%m/%y %H:%M:%S", dt1),
("%H", "01/02/03 4:05:06", "%d/%m/%y %H:%M:%S", dt1),
("%M", "01/02/03 04:5:06", "%d/%m/%y %H:%M:%S", dt1),
("%S", "01/02/03 04:05:6", "%d/%m/%y %H:%M:%S", dt1),
("%j", "2/03 04am:05:06", "%j/%y %I%p:%M:%S", dt2),
("%I", "02/03 4am:05:06", "%j/%y %I%p:%M:%S", dt2),
("%w", "6/04/03", "%w/%U/%y", dt3),
# %u requires a single digit.
("%W", "6/4/2003", "%u/%W/%Y", dt3),
("%V", "6/4/2003", "%u/%V/%G", dt4),
]
for reason, string, format, target in inputs:
reason = "test single digit " + reason
with self.subTest(
reason=reason, string=string, format=format, target=target
):
newdate = strptime(string, format)
self.assertEqual(newdate, target, msg=reason)
def test_more_timetuple(self):
# This tests fields beyond those tested by the TestDate.test_timetuple.
t = self.theclass(2004, 12, 31, 6, 22, 33)
self.assertEqual(t.timetuple(), (2004, 12, 31, 6, 22, 33, 4, 366, -1))
self.assertEqual(
t.timetuple(),
(
t.year,
t.month,
t.day,
t.hour,
t.minute,
t.second,
t.weekday(),
t.toordinal() - date(t.year, 1, 1).toordinal() + 1,
-1,
),
)
tt = t.timetuple()
self.assertEqual(tt.tm_year, t.year)
self.assertEqual(tt.tm_mon, t.month)
self.assertEqual(tt.tm_mday, t.day)
self.assertEqual(tt.tm_hour, t.hour)
self.assertEqual(tt.tm_min, t.minute)
self.assertEqual(tt.tm_sec, t.second)
self.assertEqual(tt.tm_wday, t.weekday())
self.assertEqual(tt.tm_yday, t.toordinal() - date(t.year, 1, 1).toordinal() + 1)
self.assertEqual(tt.tm_isdst, -1)
@unittest.skip("gmtime not implemented in CircuitPython")
def test_more_strftime(self):
# This tests fields beyond those tested by the TestDate.test_strftime.
t = self.theclass(2004, 12, 31, 6, 22, 33, 47)
self.assertEqual(
t.strftime("%m %d %y %f %S %M %H %j"), "12 31 04 000047 33 22 06 366"
)
for (s, us), z in [
((33, 123), "33.000123"),
((33, 0), "33"),
]:
tz = timezone(-timedelta(hours=2, seconds=s, microseconds=us))
t = t.replace(tzinfo=tz)
self.assertEqual(t.strftime("%z"), "-0200" + z)
# bpo-34482: Check that surrogates don't cause a crash.
try:
t.strftime("%y\ud800%m %H\ud800%M")
except UnicodeEncodeError:
pass
def test_extract(self):
dt = self.theclass(2002, 3, 4, 18, 45, 3, 1234)
self.assertEqual(dt.date(), date(2002, 3, 4))
self.assertEqual(dt.time(), time(18, 45, 3, 1234))
# TODO
@unittest.skip("not implemented - timezone")
def test_combine(self):
d = date(2002, 3, 4)
t = time(18, 45, 3, 1234)
expected = self.theclass(2002, 3, 4, 18, 45, 3, 1234)
combine = self.theclass.combine
dt = combine(d, t)
self.assertEqual(dt, expected)
dt = combine(time=t, date=d)
self.assertEqual(dt, expected)
self.assertEqual(d, dt.date())
self.assertEqual(t, dt.time())
self.assertEqual(dt, combine(dt.date(), dt.time()))
self.assertRaises(TypeError, combine) # need an arg
self.assertRaises(TypeError, combine, d) # need two args
self.assertRaises(TypeError, combine, t, d) # args reversed
self.assertRaises(TypeError, combine, d, t, 1) # wrong tzinfo type
self.assertRaises(TypeError, combine, d, t, 1, 2) # too many args
self.assertRaises(TypeError, combine, "date", "time") # wrong types
self.assertRaises(TypeError, combine, d, "time") # wrong type
self.assertRaises(TypeError, combine, "date", t) # wrong type
# tzinfo= argument
dt = combine(d, t, timezone.utc)
self.assertIs(dt.tzinfo, timezone.utc)
dt = combine(d, t, tzinfo=timezone.utc)
self.assertIs(dt.tzinfo, timezone.utc)
t = time()
dt = combine(dt, t)
self.assertEqual(dt.date(), d)
self.assertEqual(dt.time(), t)
def test_replace(self):
cls = self.theclass
args = [1, 2, 3, 4, 5, 6, 7]
base = cls(*args)
self.assertEqual(base, base.replace())
i = 0
for name, newval in (
("year", 2),
("month", 3),
("day", 4),
("hour", 5),
("minute", 6),
("second", 7),
("microsecond", 8),
):
newargs = args[:]
newargs[i] = newval
expected = cls(*newargs)
got = base.replace(**{name: newval})
self.assertEqual(expected, got)
i += 1
# Out of bounds.
base = cls(2000, 2, 29)
self.assertRaises(ValueError, base.replace, year=2001)
@unittest.skip("astimezone not impld")
@support.run_with_tz("EDT4")
def test_astimezone(self):
dt = self.theclass.now()
f = FixedOffset(44, "0044")
dt_utc = dt.replace(tzinfo=timezone(timedelta(hours=-4), "EDT"))
self.assertEqual(dt.astimezone(), dt_utc) # naive
self.assertRaises(TypeError, dt.astimezone, f, f) # too many args
self.assertRaises(TypeError, dt.astimezone, dt) # arg wrong type
dt_f = dt.replace(tzinfo=f) + timedelta(hours=4, minutes=44)
self.assertEqual(dt.astimezone(f), dt_f) # naive
self.assertEqual(dt.astimezone(tz=f), dt_f) # naive
class Bogus(tzinfo):
def utcoffset(self, dt):
return None
def dst(self, dt):
return timedelta(0)
bog = Bogus()
self.assertRaises(ValueError, dt.astimezone, bog) # naive
self.assertEqual(dt.replace(tzinfo=bog).astimezone(f), dt_f)
class AlsoBogus(tzinfo):
def utcoffset(self, dt):
return timedelta(0)
def dst(self, dt):
return None
alsobog = AlsoBogus()
self.assertRaises(ValueError, dt.astimezone, alsobog) # also naive
class Broken(tzinfo):
def utcoffset(self, dt):
return 1
def dst(self, dt):
return 1
broken = Broken()
dt_broken = dt.replace(tzinfo=broken)
with self.assertRaises(TypeError):
dt_broken.astimezone()
def test_subclass_datetime(self):
class C(self.theclass):
theAnswer = 42
def __new__(cls, *args, **kws):
temp = kws.copy()
extra = temp.pop("extra")
result = self.theclass.__new__(cls, *args, **temp)
result.extra = extra
return result
def newmeth(self, start):
return start + self.year + self.month + self.second
args = 2003, 4, 14, 12, 13, 41
dt1 = self.theclass(*args)
dt2 = C(*args, **{"extra": 7})
self.assertEqual(dt2.__class__, C)
self.assertEqual(dt2.theAnswer, 42)
self.assertEqual(dt2.extra, 7)
self.assertEqual(dt1.toordinal(), dt2.toordinal())
self.assertEqual(dt2.newmeth(-7), dt1.year + dt1.month + dt1.second - 7)
# TODO
@unittest.skip("timezone not implemented")
def test_subclass_alternate_constructors_datetime(self):
# Test that alternate constructors call the constructor
class DateTimeSubclass(self.theclass):
def __new__(cls, *args, **kwargs):
result = self.theclass.__new__(cls, *args, **kwargs)
result.extra = 7
return result
args = (2003, 4, 14, 12, 30, 15, 123456)
d_isoformat = "2003-04-14T12:30:15.123456" # Equivalent isoformat()
utc_ts = 1050323415.123456 # UTC timestamp
base_d = DateTimeSubclass(*args)
self.assertIsInstance(base_d, DateTimeSubclass)
self.assertEqual(base_d.extra, 7)
# Timestamp depends on time zone, so we'll calculate the equivalent here
ts = base_d.timestamp()
test_cases = [
("fromtimestamp", (ts,), base_d),
# See https://bugs.python.org/issue32417
("fromtimestamp", (ts, timezone.utc), base_d.astimezone(timezone.utc)),
("utcfromtimestamp", (utc_ts,), base_d),
("fromisoformat", (d_isoformat,), base_d),
("strptime", (d_isoformat, "%Y-%m-%dT%H:%M:%S.%f"), base_d),
("combine", (date(*args[0:3]), time(*args[3:])), base_d),
]
for constr_name, constr_args, expected in test_cases:
for base_obj in (DateTimeSubclass, base_d):
# Test both the classmethod and method
with self.subTest(
base_obj_type=type(base_obj), constr_name=constr_name
):
constructor = getattr(base_obj, constr_name)
dt = constructor(*constr_args)
# Test that it creates the right subclass
self.assertIsInstance(dt, DateTimeSubclass)
# Test that it's equal to the base object
self.assertEqual(dt, expected)
# Test that it called the constructor
self.assertEqual(dt.extra, 7)
# TODO
@unittest.skip("timezone not implemented")
def test_subclass_now(self):
# Test that alternate constructors call the constructor
class DateTimeSubclass(self.theclass):
def __new__(cls, *args, **kwargs):
result = self.theclass.__new__(cls, *args, **kwargs)
result.extra = 7
return result
test_cases = [
("now", "now", {}),
("utcnow", "utcnow", {}),
("now_utc", "now", {"tz": timezone.utc}),
("now_fixed", "now", {"tz": timezone(timedelta(hours=-5), "EST")}),
]
for name, meth_name, kwargs in test_cases:
with self.subTest(name):
constr = getattr(DateTimeSubclass, meth_name)
dt = constr(**kwargs)
self.assertIsInstance(dt, DateTimeSubclass)
self.assertEqual(dt.extra, 7)
def test_fromisoformat_datetime(self):
# Test that isoformat() is reversible
base_dates = [(1, 1, 1), (1900, 1, 1), (2004, 11, 12), (2017, 5, 30)]
base_times = [
(0, 0, 0, 0),
(0, 0, 0, 241000),
(0, 0, 0, 234567),
(12, 30, 45, 234567),
]
separators = [" ", "T"]
tzinfos = [
None,
timezone.utc,
timezone(timedelta(hours=-5)),
timezone(timedelta(hours=2)),
]
dts = [
self.theclass(*date_tuple, *time_tuple, tzinfo=tzi)
for date_tuple in base_dates
for time_tuple in base_times
for tzi in tzinfos
]
for dt in dts:
for sep in separators:
dtstr = dt.isoformat(sep=sep)
with self.subTest(dtstr=dtstr):
dt_rt = self.theclass.fromisoformat(dtstr)
self.assertEqual(dt, dt_rt)
# TODO
@unittest.skip("not implemented timezone")
def test_fromisoformat_timezone(self):
base_dt = self.theclass(2014, 12, 30, 12, 30, 45, 217456)
tzoffsets = [
timedelta(hours=5),
timedelta(hours=2),
timedelta(hours=6, minutes=27),
timedelta(hours=12, minutes=32, seconds=30),
timedelta(hours=2, minutes=4, seconds=9, microseconds=123456),
]
tzoffsets += [-1 * td for td in tzoffsets]
tzinfos = [None, timezone.utc, timezone(timedelta(hours=0))]
tzinfos += [timezone(td) for td in tzoffsets]
for tzi in tzinfos:
dt = base_dt.replace(tzinfo=tzi)
dtstr = dt.isoformat()
with self.subTest(tstr=dtstr):
dt_rt = self.theclass.fromisoformat(dtstr)
assert dt == dt_rt, dt_rt
def test_fromisoformat_separators(self):
separators = [
" ",
"T",
"\u007f", # 1-bit widths
"\u0080",
"ʁ", # 2-bit widths
"",
"", # 3-bit widths
"🐍", # 4-bit widths
"\ud800", # bpo-34454: Surrogate code point
]
for sep in separators:
dt = self.theclass(2018, 1, 31, 23, 59, 47, 124789)
dtstr = dt.isoformat(sep=sep)
with self.subTest(dtstr=dtstr):
dt_rt = self.theclass.fromisoformat(dtstr)
self.assertEqual(dt, dt_rt)
def test_fromisoformat_ambiguous(self):
# Test strings like 2018-01-31+12:15 (where +12:15 is not a time zone)
separators = ["+", "-"]
for sep in separators:
dt = self.theclass(2018, 1, 31, 12, 15)
dtstr = dt.isoformat(sep=sep)
with self.subTest(dtstr=dtstr):
dt_rt = self.theclass.fromisoformat(dtstr)
self.assertEqual(dt, dt_rt)
# TODO
@unittest.skip("_format_time not fully implemented")
def test_fromisoformat_timespecs(self):
datetime_bases = [(2009, 12, 4, 8, 17, 45, 123456), (2009, 12, 4, 8, 17, 45, 0)]
tzinfos = [
None,
timezone.utc,
timezone(timedelta(hours=-5)),
timezone(timedelta(hours=2)),
timezone(timedelta(hours=6, minutes=27)),
]
timespecs = ["hours", "minutes", "seconds", "milliseconds", "microseconds"]
for ip, ts in enumerate(timespecs):
for tzi in tzinfos:
for dt_tuple in datetime_bases:
if ts == "milliseconds":
new_microseconds = 1000 * (dt_tuple[6] // 1000)
dt_tuple = dt_tuple[0:6] + (new_microseconds,)
dt = self.theclass(*(dt_tuple[0 : (4 + ip)]), tzinfo=tzi)
dtstr = dt.isoformat(timespec=ts)
with self.subTest(dtstr=dtstr):
dt_rt = self.theclass.fromisoformat(dtstr)
self.assertEqual(dt, dt_rt)
def test_fromisoformat_fails_datetime(self):
# Test that fromisoformat() fails on invalid values
bad_strs = [
"", # Empty string
"\ud800", # bpo-34454: Surrogate code point
"2009.04-19T03", # Wrong first separator
"2009-04.19T03", # Wrong second separator
"2009-04-19T0a", # Invalid hours
"2009-04-19T03:1a:45", # Invalid minutes
"2009-04-19T03:15:4a", # Invalid seconds
"2009-04-19T03;15:45", # Bad first time separator
"2009-04-19T03:15;45", # Bad second time separator
"2009-04-19T03:15:4500:00", # Bad time zone separator
"2009-04-19T03:15:45.2345", # Too many digits for milliseconds
"2009-04-19T03:15:45.1234567", # Too many digits for microseconds
"2009-04-19T03:15:45.123456+24:30", # Invalid time zone offset
"2009-04-19T03:15:45.123456-24:30", # Invalid negative offset
"2009-04-10ᛇᛇᛇᛇᛇ12:15", # Too many unicode separators
"2009-04\ud80010T12:15", # Surrogate char in date
"2009-04-10T12\ud80015", # Surrogate char in time
"2009-04-19T1", # Incomplete hours
"2009-04-19T12:3", # Incomplete minutes
"2009-04-19T12:30:4", # Incomplete seconds
"2009-04-19T12:", # Ends with time separator
"2009-04-19T12:30:", # Ends with time separator
"2009-04-19T12:30:45.", # Ends with time separator
"2009-04-19T12:30:45.123456+", # Ends with timzone separator
"2009-04-19T12:30:45.123456-", # Ends with timzone separator
"2009-04-19T12:30:45.123456-05:00a", # Extra text
"2009-04-19T12:30:45.123-05:00a", # Extra text
"2009-04-19T12:30:45-05:00a", # Extra text
]
for bad_str in bad_strs:
with self.subTest(bad_str=bad_str):
with self.assertRaises(ValueError):
self.theclass.fromisoformat(bad_str)
def test_fromisoformat_fails_surrogate(self):
# Test that when fromisoformat() fails with a surrogate character as
# the separator, the error message contains the original string
dtstr = "2018-01-03\ud80001:0113"
with self.assertRaisesRegex(ValueError, repr(dtstr)):
self.theclass.fromisoformat(dtstr)
# TODO
@unittest.skip("fromisoformat not implemented")
def test_fromisoformat_utc(self):
dt_str = "2014-04-19T13:21:13+00:00"
dt = self.theclass.fromisoformat(dt_str)
self.assertIs(dt.tzinfo, timezone.utc)
def test_fromisoformat_subclass(self):
class DateTimeSubclass(self.theclass):
pass
dt = DateTimeSubclass(
2014,
12,
14,
9,
30,
45,
457390,
tzinfo=timezone(timedelta(hours=10, minutes=45)),
)
dt_rt = DateTimeSubclass.fromisoformat(dt.isoformat())
self.assertEqual(dt, dt_rt)
self.assertIsInstance(dt_rt, DateTimeSubclass)