1232 lines
48 KiB
Python
1232 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
|
|
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)
|