"""Test date/time type. See http://www.zope.org/Members/fdrake/DateTimeWiki/TestCases """ import sys import pickle import cPickle import unittest from test import test_support from datetime import MINYEAR, MAXYEAR from datetime import timedelta from datetime import tzinfo from datetime import time from datetime import date, datetime pickle_choices = [ (pickle, pickle, 0), (pickle, pickle, 1), (pickle, pickle, 2), (cPickle, cPickle, 0), (cPickle, cPickle, 1), (cPickle, cPickle, 2), (pickle, cPickle, 0), (pickle, cPickle, 1), (pickle, cPickle, 2), (cPickle, pickle, 0), (cPickle, pickle, 1), (cPickle, pickle, 2), ] # XXX The test suite uncovered a bug in Python 2.2.2: if x and y are # XXX instances of new-style classes (like date and time) that both # XXX define __cmp__, and x is compared to y, and one of the __cmp__ # XXX implementations raises an exception, the exception can get dropped # XXX on the floor when it occurs, and pop up again at some "random" time # XXX later (it depends on when the next opcode gets executed that # XXX bothers to check). There isn't a workaround for this, so instead # XXX we disable the parts of the tests that trigger it unless # XXX CMP_BUG_FIXED is true. The bug is still there, we simply avoid # XXX provoking it here. # XXX Guido checked into a fix that will go into 2.2.3. The bug was # XXX already fixed in 2.3 CVS via a different means. CMP_BUG_FIXED = sys.version_info >= (2, 2, 3) ############################################################################# # module tests class TestModule(unittest.TestCase): def test_constants(self): import datetime self.assertEqual(datetime.MINYEAR, 1) self.assertEqual(datetime.MAXYEAR, 9999) ############################################################################# # tzinfo tests 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 class PicklableFixedOffset(FixedOffset): def __init__(self, offset=None, name=None, dstoffset=None): FixedOffset.__init__(self, offset, name, dstoffset) class TestTZInfo(unittest.TestCase): def test_non_abstractness(self): # In order to allow subclasses to get pickled, the C implementation # wasn't able to get away with having __init__ raise # NotImplementedError. useless = tzinfo() dt = datetime.max self.assertRaises(NotImplementedError, useless.tzname, dt) self.assertRaises(NotImplementedError, useless.utcoffset, dt) self.assertRaises(NotImplementedError, useless.dst, dt) def test_subclass_must_override(self): class NotEnough(tzinfo): def __init__(self, offset, name): self.__offset = offset self.__name = name self.failUnless(issubclass(NotEnough, tzinfo)) ne = NotEnough(3, "NotByALongShot") self.failUnless(isinstance(ne, tzinfo)) dt = datetime.now() self.assertRaises(NotImplementedError, ne.tzname, dt) self.assertRaises(NotImplementedError, ne.utcoffset, dt) self.assertRaises(NotImplementedError, ne.dst, dt) def test_normal(self): fo = FixedOffset(3, "Three") self.failUnless(isinstance(fo, tzinfo)) for dt in datetime.now(), None: self.assertEqual(fo.utcoffset(dt), timedelta(minutes=3)) self.assertEqual(fo.tzname(dt), "Three") self.assertEqual(fo.dst(dt), timedelta(minutes=42)) def test_pickling_base(self): # There's no point to pickling tzinfo objects on their own (they # carry no data), but they need to be picklable anyway else # concrete subclasses can't be pickled. orig = tzinfo.__new__(tzinfo) self.failUnless(type(orig) is tzinfo) for pickler, unpickler, proto in pickle_choices: green = pickler.dumps(orig, proto) derived = unpickler.loads(green) self.failUnless(type(derived) is tzinfo) def test_pickling_subclass(self): # Make sure we can pickle/unpickle an instance of a subclass. offset = timedelta(minutes=-300) orig = PicklableFixedOffset(offset, 'cookie') self.failUnless(isinstance(orig, tzinfo)) self.failUnless(type(orig) is PicklableFixedOffset) self.assertEqual(orig.utcoffset(None), offset) self.assertEqual(orig.tzname(None), 'cookie') for pickler, unpickler, proto in pickle_choices: green = pickler.dumps(orig, proto) derived = unpickler.loads(green) self.failUnless(isinstance(derived, tzinfo)) self.failUnless(type(derived) is PicklableFixedOffset) self.assertEqual(derived.utcoffset(None), offset) self.assertEqual(derived.tzname(None), 'cookie') ############################################################################# # timedelta tests class TestTimeDelta(unittest.TestCase): def test_constructor(self): eq = self.assertEqual td = timedelta # Check keyword args to constructor eq(td(), td(weeks=0, days=0, hours=0, minutes=0, seconds=0, milliseconds=0, microseconds=0)) eq(td(1), td(days=1)) eq(td(0, 1), td(seconds=1)) eq(td(0, 0, 1), td(microseconds=1)) eq(td(weeks=1), td(days=7)) eq(td(days=1), td(hours=24)) eq(td(hours=1), td(minutes=60)) eq(td(minutes=1), td(seconds=60)) eq(td(seconds=1), td(milliseconds=1000)) eq(td(milliseconds=1), td(microseconds=1000)) # Check float args to constructor eq(td(weeks=1.0/7), td(days=1)) eq(td(days=1.0/24), td(hours=1)) eq(td(hours=1.0/60), td(minutes=1)) eq(td(minutes=1.0/60), td(seconds=1)) eq(td(seconds=0.001), td(milliseconds=1)) eq(td(milliseconds=0.001), td(microseconds=1)) def test_computations(self): eq = self.assertEqual td = timedelta a = td(7) # One week b = td(0, 60) # One minute c = td(0, 0, 1000) # One millisecond eq(a+b+c, td(7, 60, 1000)) eq(a-b, td(6, 24*3600 - 60)) eq(-a, td(-7)) eq(+a, td(7)) eq(-b, td(-1, 24*3600 - 60)) eq(-c, td(-1, 24*3600 - 1, 999000)) eq(abs(a), a) eq(abs(-a), a) eq(td(6, 24*3600), a) eq(td(0, 0, 60*1000000), b) eq(a*10, td(70)) eq(a*10, 10*a) eq(a*10L, 10*a) eq(b*10, td(0, 600)) eq(10*b, td(0, 600)) eq(b*10L, td(0, 600)) eq(c*10, td(0, 0, 10000)) eq(10*c, td(0, 0, 10000)) eq(c*10L, td(0, 0, 10000)) eq(a*-1, -a) eq(b*-2, -b-b) eq(c*-2, -c+-c) eq(b*(60*24), (b*60)*24) eq(b*(60*24), (60*b)*24) eq(c*1000, td(0, 1)) eq(1000*c, td(0, 1)) eq(a//7, td(1)) eq(b//10, td(0, 6)) eq(c//1000, td(0, 0, 1)) eq(a//10, td(0, 7*24*360)) eq(a//3600000, td(0, 0, 7*24*1000)) def test_disallowed_computations(self): a = timedelta(42) # Add/sub ints, longs, floats should be illegal for i in 1, 1L, 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) # Mul/div by float isn't supported. x = 2.3 self.assertRaises(TypeError, lambda: a*x) self.assertRaises(TypeError, lambda: x*a) self.assertRaises(TypeError, lambda: a/x) self.assertRaises(TypeError, lambda: x/a) self.assertRaises(TypeError, lambda: a // x) self.assertRaises(TypeError, lambda: x // a) # Divison of int by timedelta doesn't make sense. # Division by zero doesn't make sense. for zero in 0, 0L: self.assertRaises(TypeError, lambda: zero // a) self.assertRaises(ZeroDivisionError, lambda: a // zero) def test_basic_attributes(self): days, seconds, us = 1, 7, 31 td = timedelta(days, seconds, us) self.assertEqual(td.days, days) self.assertEqual(td.seconds, seconds) self.assertEqual(td.microseconds, us) def test_carries(self): t1 = timedelta(days=100, weeks=-7, hours=-24*(100-49), minutes=-3, seconds=12, microseconds=(3*60 - 12) * 1e6 + 1) t2 = timedelta(microseconds=1) self.assertEqual(t1, t2) def test_hash_equality(self): t1 = timedelta(days=100, weeks=-7, hours=-24*(100-49), minutes=-3, seconds=12, microseconds=(3*60 - 12) * 1000000) t2 = timedelta() self.assertEqual(hash(t1), hash(t2)) t1 += timedelta(weeks=7) t2 += timedelta(days=7*7) self.assertEqual(t1, t2) self.assertEqual(hash(t1), hash(t2)) d = {t1: 1} d[t2] = 2 self.assertEqual(len(d), 1) self.assertEqual(d[t1], 2) def test_pickling(self): args = 12, 34, 56 orig = timedelta(*args) for pickler, unpickler, proto in pickle_choices: green = pickler.dumps(orig, proto) derived = unpickler.loads(green) self.assertEqual(orig, derived) def test_compare(self): t1 = timedelta(2, 3, 4) t2 = timedelta(2, 3, 4) self.failUnless(t1 == t2) self.failUnless(t1 <= t2) self.failUnless(t1 >= t2) self.failUnless(not t1 != t2) self.failUnless(not t1 < t2) self.failUnless(not t1 > t2) self.assertEqual(cmp(t1, t2), 0) self.assertEqual(cmp(t2, t1), 0) for args in (3, 3, 3), (2, 4, 4), (2, 3, 5): t2 = timedelta(*args) # this is larger than t1 self.failUnless(t1 < t2) self.failUnless(t2 > t1) self.failUnless(t1 <= t2) self.failUnless(t2 >= t1) self.failUnless(t1 != t2) self.failUnless(t2 != t1) self.failUnless(not t1 == t2) self.failUnless(not t2 == t1) self.failUnless(not t1 > t2) self.failUnless(not t2 < t1) self.failUnless(not t1 >= t2) self.failUnless(not t2 <= t1) self.assertEqual(cmp(t1, t2), -1) self.assertEqual(cmp(t2, t1), 1) for badarg in 10, 10L, 34.5, "abc", {}, [], (): self.assertRaises(TypeError, lambda: t1 == badarg) self.assertRaises(TypeError, lambda: t1 != badarg) self.assertRaises(TypeError, lambda: t1 <= badarg) self.assertRaises(TypeError, lambda: t1 < badarg) self.assertRaises(TypeError, lambda: t1 > badarg) self.assertRaises(TypeError, lambda: t1 >= badarg) self.assertRaises(TypeError, lambda: badarg == t1) self.assertRaises(TypeError, lambda: badarg != t1) self.assertRaises(TypeError, lambda: badarg <= t1) self.assertRaises(TypeError, lambda: badarg < t1) self.assertRaises(TypeError, lambda: badarg > t1) self.assertRaises(TypeError, lambda: badarg >= t1) def test_str(self): td = timedelta eq = self.assertEqual eq(str(td(1)), "1 day, 0:00:00") eq(str(td(-1)), "-1 day, 0:00:00") eq(str(td(2)), "2 days, 0:00:00") eq(str(td(-2)), "-2 days, 0:00:00") eq(str(td(hours=12, minutes=58, seconds=59)), "12:58:59") eq(str(td(hours=2, minutes=3, seconds=4)), "2:03:04") eq(str(td(weeks=-30, hours=23, minutes=12, seconds=34)), "-210 days, 23:12:34") eq(str(td(milliseconds=1)), "0:00:00.001000") eq(str(td(microseconds=3)), "0:00:00.000003") eq(str(td(days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999)), "999999999 days, 23:59:59.999999") def test_roundtrip(self): for td in (timedelta(days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999), timedelta(days=-999999999), timedelta(days=1, seconds=2, microseconds=3)): # Verify td -> string -> td identity. s = repr(td) self.failUnless(s.startswith('datetime.')) s = s[9:] td2 = eval(s) self.assertEqual(td, td2) # Verify identity via reconstructing from pieces. td2 = timedelta(td.days, td.seconds, td.microseconds) self.assertEqual(td, td2) def test_resolution_info(self): self.assert_(isinstance(timedelta.min, timedelta)) self.assert_(isinstance(timedelta.max, timedelta)) self.assert_(isinstance(timedelta.resolution, timedelta)) self.assert_(timedelta.max > timedelta.min) self.assertEqual(timedelta.min, timedelta(-999999999)) self.assertEqual(timedelta.max, timedelta(999999999, 24*3600-1, 1e6-1)) self.assertEqual(timedelta.resolution, timedelta(0, 0, 1)) def test_overflow(self): tiny = timedelta.resolution td = timedelta.min + tiny td -= tiny # no problem self.assertRaises(OverflowError, td.__sub__, tiny) self.assertRaises(OverflowError, td.__add__, -tiny) td = timedelta.max - tiny td += tiny # no problem self.assertRaises(OverflowError, td.__add__, tiny) self.assertRaises(OverflowError, td.__sub__, -tiny) self.assertRaises(OverflowError, lambda: -timedelta.max) def test_microsecond_rounding(self): td = timedelta eq = self.assertEqual # Single-field rounding. eq(td(milliseconds=0.4/1000), td(0)) # rounds to 0 eq(td(milliseconds=-0.4/1000), td(0)) # rounds to 0 eq(td(milliseconds=0.6/1000), td(microseconds=1)) eq(td(milliseconds=-0.6/1000), td(microseconds=-1)) # Rounding due to contributions from more than one field. us_per_hour = 3600e6 us_per_day = us_per_hour * 24 eq(td(days=.4/us_per_day), td(0)) eq(td(hours=.2/us_per_hour), td(0)) eq(td(days=.4/us_per_day, hours=.2/us_per_hour), td(microseconds=1)) eq(td(days=-.4/us_per_day), td(0)) eq(td(hours=-.2/us_per_hour), td(0)) eq(td(days=-.4/us_per_day, hours=-.2/us_per_hour), td(microseconds=-1)) def test_massive_normalization(self): td = timedelta(microseconds=-1) self.assertEqual((td.days, td.seconds, td.microseconds), (-1, 24*3600-1, 999999)) def test_bool(self): self.failUnless(timedelta(1)) self.failUnless(timedelta(0, 1)) self.failUnless(timedelta(0, 0, 1)) self.failUnless(timedelta(microseconds=1)) self.failUnless(not timedelta(0)) ############################################################################# # date tests class TestDateOnly(unittest.TestCase): # Tests here won't pass if also run on datetime objects, so don't # subclass this to test datetimes too. def test_delta_non_days_ignored(self): dt = date(2000, 1, 2) delta = timedelta(days=1, hours=2, minutes=3, seconds=4, microseconds=5) days = timedelta(delta.days) self.assertEqual(days, timedelta(1)) dt2 = dt + delta self.assertEqual(dt2, dt + days) dt2 = delta + dt self.assertEqual(dt2, dt + days) dt2 = dt - delta self.assertEqual(dt2, dt - days) delta = -delta days = timedelta(delta.days) self.assertEqual(days, timedelta(-2)) dt2 = dt + delta self.assertEqual(dt2, dt + days) dt2 = delta + dt self.assertEqual(dt2, dt + days) dt2 = dt - delta self.assertEqual(dt2, dt - days) class TestDate(unittest.TestCase): # Tests here should pass for both dates and datetimes, except for a # few tests that TestDateTime overrides. theclass = date def test_basic_attributes(self): dt = self.theclass(2002, 3, 1) self.assertEqual(dt.year, 2002) self.assertEqual(dt.month, 3) self.assertEqual(dt.day, 1) def test_roundtrip(self): for dt in (self.theclass(1, 2, 3), self.theclass.today()): # Verify dt -> string -> date identity. s = repr(dt) self.failUnless(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) self.assertEqual(dt, dt2) def test_ordinal_conversions(self): # Check some fixed values. for y, m, d, n in [(1, 1, 1, 1), # calendar origin (1, 12, 31, 365), (2, 1, 1, 366), # first example from "Calendrical Calculations" (1945, 11, 12, 710347)]: d = self.theclass(y, m, d) self.assertEqual(n, d.toordinal()) fromord = self.theclass.fromordinal(n) self.assertEqual(d, fromord) if hasattr(fromord, "hour"): # if we're checking something fancier than a date, verify # the extra fields have been zeroed out self.assertEqual(fromord.hour, 0) self.assertEqual(fromord.minute, 0) self.assertEqual(fromord.second, 0) self.assertEqual(fromord.microsecond, 0) # Check first and last days of year spottily across the whole # range of years supported. for year in xrange(MINYEAR, MAXYEAR+1, 7): # Verify (year, 1, 1) -> ordinal -> y, m, d is identity. d = self.theclass(year, 1, 1) n = d.toordinal() d2 = self.theclass.fromordinal(n) self.assertEqual(d, d2) # Verify that moving back a day gets to the end of year-1. if year > 1: d = self.theclass.fromordinal(n-1) d2 = self.theclass(year-1, 12, 31) self.assertEqual(d, d2) self.assertEqual(d2.toordinal(), n-1) # Test every day in a leap-year and a non-leap year. dim = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] for year, isleap in (2000, True), (2002, False): n = self.theclass(year, 1, 1).toordinal() for month, maxday in zip(range(1, 13), dim): if month == 2 and isleap: maxday += 1 for day in range(1, maxday+1): d = self.theclass(year, month, day) self.assertEqual(d.toordinal(), n) self.assertEqual(d, self.theclass.fromordinal(n)) n += 1 def test_extreme_ordinals(self): a = self.theclass.min a = self.theclass(a.year, a.month, a.day) # get rid of time parts aord = a.toordinal() b = a.fromordinal(aord) self.assertEqual(a, b) self.assertRaises(ValueError, lambda: a.fromordinal(aord - 1)) b = a + timedelta(days=1) self.assertEqual(b.toordinal(), aord + 1) self.assertEqual(b, self.theclass.fromordinal(aord + 1)) a = self.theclass.max a = self.theclass(a.year, a.month, a.day) # get rid of time parts aord = a.toordinal() b = a.fromordinal(aord) self.assertEqual(a, b) self.assertRaises(ValueError, lambda: a.fromordinal(aord + 1)) b = a - timedelta(days=1) self.assertEqual(b.toordinal(), aord - 1) self.assertEqual(b, self.theclass.fromordinal(aord - 1)) 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) def test_hash_equality(self): d = self.theclass(2000, 12, 31) # same thing e = self.theclass(2000, 12, 31) 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) # same thing e = self.theclass(2001, 1, 1) 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) day = timedelta(1) week = timedelta(7) a = self.theclass(2002, 3, 2) self.assertEqual(a + day, self.theclass(2002, 3, 3)) self.assertEqual(day + a, self.theclass(2002, 3, 3)) self.assertEqual(a - day, self.theclass(2002, 3, 1)) self.assertEqual(-day + a, self.theclass(2002, 3, 1)) self.assertEqual(a + week, self.theclass(2002, 3, 9)) self.assertEqual(a - week, self.theclass(2002, 2, 23)) self.assertEqual(a + 52*week, self.theclass(2003, 3, 1)) self.assertEqual(a - 52*week, self.theclass(2001, 3, 3)) self.assertEqual((a + week) - a, week) self.assertEqual((a + day) - a, day) self.assertEqual((a - week) - a, -week) self.assertEqual((a - day) - a, -day) self.assertEqual(a - (a + week), -week) self.assertEqual(a - (a + day), -day) self.assertEqual(a - (a - week), week) self.assertEqual(a - (a - day), day) # Add/sub ints, longs, floats should be illegal for i in 1, 1L, 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 - date is senseless. self.assertRaises(TypeError, lambda: day - a) # mixing date and (delta or date) 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) # date + date is senseless self.assertRaises(TypeError, lambda: a + a) def test_overflow(self): tiny = self.theclass.resolution dt = self.theclass.min + tiny dt -= tiny # no problem self.assertRaises(OverflowError, dt.__sub__, tiny) self.assertRaises(OverflowError, dt.__add__, -tiny) dt = self.theclass.max - tiny dt += tiny # no problem self.assertRaises(OverflowError, dt.__add__, tiny) self.assertRaises(OverflowError, dt.__sub__, -tiny) def test_fromtimestamp(self): import time # Try an arbitrary fixed value. year, month, day = 1999, 9, 19 ts = time.mktime((year, month, day, 0, 0, 0, 0, 0, -1)) d = self.theclass.fromtimestamp(ts) self.assertEqual(d.year, year) self.assertEqual(d.month, month) self.assertEqual(d.day, day) def test_today(self): import time # We claim that today() is like fromtimestamp(time.time()), so # prove it. for dummy in range(3): today = self.theclass.today() ts = time.time() todayagain = self.theclass.fromtimestamp(ts) if today == todayagain: break # There are several legit reasons that could fail: # 1. It recently became midnight, between the today() and the # time() calls. # 2. The platform time() has such fine resolution that we'll # never get the same value twice. # 3. The platform time() has poor resolution, and we just # happened to call today() right before a resolution quantum # boundary. # 4. The system clock got fiddled between calls. # In any case, wait a little while and try again. time.sleep(0.1) # It worked or it didn't. If it didn't, assume it's reason #2, and # let the test pass if they're within half a second of each other. self.failUnless(today == todayagain or abs(todayagain - today) < timedelta(seconds=0.5)) def test_weekday(self): for i in range(7): # March 4, 2002 is a Monday self.assertEqual(self.theclass(2002, 3, 4+i).weekday(), i) self.assertEqual(self.theclass(2002, 3, 4+i).isoweekday(), i+1) # January 2, 1956 is a Monday self.assertEqual(self.theclass(1956, 1, 2+i).weekday(), i) self.assertEqual(self.theclass(1956, 1, 2+i).isoweekday(), i+1) def test_isocalendar(self): # Check examples from # http://www.phys.uu.nl/~vgent/calendar/isocalendar.htm for i in range(7): d = self.theclass(2003, 12, 22+i) self.assertEqual(d.isocalendar(), (2003, 52, i+1)) d = self.theclass(2003, 12, 29) + timedelta(i) self.assertEqual(d.isocalendar(), (2004, 1, i+1)) d = self.theclass(2004, 1, 5+i) self.assertEqual(d.isocalendar(), (2004, 2, i+1)) d = self.theclass(2009, 12, 21+i) self.assertEqual(d.isocalendar(), (2009, 52, i+1)) d = self.theclass(2009, 12, 28) + timedelta(i) self.assertEqual(d.isocalendar(), (2009, 53, i+1)) d = self.theclass(2010, 1, 4+i) self.assertEqual(d.isocalendar(), (2010, 1, i+1)) def test_iso_long_years(self): # Calculate long ISO years and compare to table from # http://www.phys.uu.nl/~vgent/calendar/isocalendar.htm ISO_LONG_YEARS_TABLE = """ 4 32 60 88 9 37 65 93 15 43 71 99 20 48 76 26 54 82 105 133 161 189 111 139 167 195 116 144 172 122 150 178 128 156 184 201 229 257 285 207 235 263 291 212 240 268 296 218 246 274 224 252 280 303 331 359 387 308 336 364 392 314 342 370 398 320 348 376 325 353 381 """ iso_long_years = map(int, ISO_LONG_YEARS_TABLE.split()) iso_long_years.sort() L = [] for i in range(400): d = self.theclass(2000+i, 12, 31) d1 = self.theclass(1600+i, 12, 31) self.assertEqual(d.isocalendar()[1:], d1.isocalendar()[1:]) if d.isocalendar()[1] == 53: L.append(i) self.assertEqual(L, iso_long_years) def test_isoformat(self): t = self.theclass(2, 3, 2) self.assertEqual(t.isoformat(), "0002-03-02") def test_ctime(self): t = self.theclass(2002, 3, 2) self.assertEqual(t.ctime(), "Sat Mar 2 00:00:00 2002") def test_strftime(self): t = self.theclass(2005, 3, 2) self.assertEqual(t.strftime("m:%m d:%d y:%y"), "m:03 d:02 y:05") self.assertRaises(TypeError, t.strftime) # needs an arg self.assertRaises(TypeError, t.strftime, "one", "two") # too many args self.assertRaises(TypeError, t.strftime, 42) # arg wrong type # A naive object replaces %z and %Z w/ empty strings. self.assertEqual(t.strftime("'%z' '%Z'"), "'' ''") def test_resolution_info(self): self.assert_(isinstance(self.theclass.min, self.theclass)) self.assert_(isinstance(self.theclass.max, self.theclass)) self.assert_(isinstance(self.theclass.resolution, timedelta)) self.assert_(self.theclass.max > self.theclass.min) def test_extreme_timedelta(self): big = self.theclass.max - self.theclass.min # 3652058 days, 23 hours, 59 minutes, 59 seconds, 999999 microseconds n = (big.days*24*3600 + big.seconds)*1000000 + big.microseconds # n == 315537897599999999 ~= 2**58.13 justasbig = timedelta(0, 0, n) self.assertEqual(big, justasbig) self.assertEqual(self.theclass.min + big, self.theclass.max) self.assertEqual(self.theclass.max - big, self.theclass.min) def test_timetuple(self): for i in range(7): # January 2, 1956 is a Monday (0) d = self.theclass(1956, 1, 2+i) t = d.timetuple() self.assertEqual(t, (1956, 1, 2+i, 0, 0, 0, i, 2+i, -1)) # February 1, 1956 is a Wednesday (2) d = self.theclass(1956, 2, 1+i) t = d.timetuple() self.assertEqual(t, (1956, 2, 1+i, 0, 0, 0, (2+i)%7, 32+i, -1)) # March 1, 1956 is a Thursday (3), and is the 31+29+1 = 61st day # of the year. d = self.theclass(1956, 3, 1+i) t = d.timetuple() self.assertEqual(t, (1956, 3, 1+i, 0, 0, 0, (3+i)%7, 61+i, -1)) self.assertEqual(t.tm_year, 1956) self.assertEqual(t.tm_mon, 3) self.assertEqual(t.tm_mday, 1+i) self.assertEqual(t.tm_hour, 0) self.assertEqual(t.tm_min, 0) self.assertEqual(t.tm_sec, 0) self.assertEqual(t.tm_wday, (3+i)%7) self.assertEqual(t.tm_yday, 61+i) self.assertEqual(t.tm_isdst, -1) def test_pickling(self): args = 6, 7, 23 orig = self.theclass(*args) for pickler, unpickler, proto in pickle_choices: green = pickler.dumps(orig, proto) derived = unpickler.loads(green) self.assertEqual(orig, derived) def test_compare(self): t1 = self.theclass(2, 3, 4) t2 = self.theclass(2, 3, 4) self.failUnless(t1 == t2) self.failUnless(t1 <= t2) self.failUnless(t1 >= t2) self.failUnless(not t1 != t2) self.failUnless(not t1 < t2) self.failUnless(not t1 > t2) self.assertEqual(cmp(t1, t2), 0) self.assertEqual(cmp(t2, t1), 0) for args in (3, 3, 3), (2, 4, 4), (2, 3, 5): t2 = self.theclass(*args) # this is larger than t1 self.failUnless(t1 < t2) self.failUnless(t2 > t1) self.failUnless(t1 <= t2) self.failUnless(t2 >= t1) self.failUnless(t1 != t2) self.failUnless(t2 != t1) self.failUnless(not t1 == t2) self.failUnless(not t2 == t1) self.failUnless(not t1 > t2) self.failUnless(not t2 < t1) self.failUnless(not t1 >= t2) self.failUnless(not t2 <= t1) self.assertEqual(cmp(t1, t2), -1) self.assertEqual(cmp(t2, t1), 1) for badarg in 10, 10L, 34.5, "abc", {}, [], (): self.assertRaises(TypeError, lambda: t1 == badarg) self.assertRaises(TypeError, lambda: t1 != badarg) self.assertRaises(TypeError, lambda: t1 <= badarg) self.assertRaises(TypeError, lambda: t1 < badarg) self.assertRaises(TypeError, lambda: t1 > badarg) self.assertRaises(TypeError, lambda: t1 >= badarg) self.assertRaises(TypeError, lambda: badarg == t1) self.assertRaises(TypeError, lambda: badarg != t1) self.assertRaises(TypeError, lambda: badarg <= t1) self.assertRaises(TypeError, lambda: badarg < t1) self.assertRaises(TypeError, lambda: badarg > t1) self.assertRaises(TypeError, lambda: badarg >= t1) def test_mixed_compare(self): our = self.theclass(2000, 4, 5) self.assertRaises(TypeError, cmp, our, 1) self.assertRaises(TypeError, cmp, 1, our) class AnotherDateTimeClass(object): def __cmp__(self, other): # Return "equal" so calling this can't be confused with # compare-by-address (which never says "equal" for distinct # objects). return 0 # This still errors, because date and datetime comparison raise # TypeError instead of NotImplemented when they don't know what to # do, in order to stop comparison from falling back to the default # compare-by-address. their = AnotherDateTimeClass() self.assertRaises(TypeError, cmp, our, their) # Oops: The next stab raises TypeError in the C implementation, # but not in the Python implementation of datetime. The difference # is due to that the Python implementation defines __cmp__ but # the C implementation defines tp_richcompare. This is more pain # to fix than it's worth, so commenting out the test. # self.assertEqual(cmp(their, our), 0) # But date and datetime comparison return NotImplemented instead if the # other object has a timetuple attr. This gives the other object a # chance to do the comparison. class Comparable(AnotherDateTimeClass): def timetuple(self): return () their = Comparable() self.assertEqual(cmp(our, their), 0) self.assertEqual(cmp(their, our), 0) self.failUnless(our == their) self.failUnless(their == our) def test_bool(self): # All dates are considered true. self.failUnless(self.theclass.min) self.failUnless(self.theclass.max) def test_srftime_out_of_range(self): # For nasty technical reasons, we can't handle years before 1900. cls = self.theclass self.assertEqual(cls(1900, 1, 1).strftime("%Y"), "1900") for y in 1, 49, 51, 99, 100, 1000, 1899: self.assertRaises(ValueError, cls(y, 1, 1).strftime, "%Y") def test_replace(self): cls = self.theclass args = [1, 2, 3] base = cls(*args) self.assertEqual(base, base.replace()) i = 0 for name, newval in (("year", 2), ("month", 3), ("day", 4)): 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) ############################################################################# # datetime tests class TestDateTime(TestDate): theclass = datetime def test_basic_attributes(self): dt = self.theclass(2002, 3, 1, 12, 0) 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) 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) 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.failUnless(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) def test_isoformat(self): t = self.theclass(2, 3, 2, 4, 5, 1, 123) self.assertEqual(t.isoformat(), "0002-03-02T04:05:01.000123") self.assertEqual(t.isoformat('T'), "0002-03-02T04:05:01.000123") self.assertEqual(t.isoformat(' '), "0002-03-02 04:05:01.000123") # str is ISO format with the separator forced to a blank. self.assertEqual(str(t), "0002-03-02 04:05:01.000123") 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") def test_more_ctime(self): # Test fields that TestDate doesn't touch. import 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(), time.ctime(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.assert_(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 thru 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.assert_(dt1 < dt2) 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) 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, longs, floats should be illegal for i in 1, 1L, 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_pickling(self): args = 6, 7, 23, 20, 59, 1, 64**2 orig = self.theclass(*args) for pickler, unpickler, proto in pickle_choices: green = pickler.dumps(orig, proto) derived = unpickler.loads(green) self.assertEqual(orig, derived) 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.failUnless(t1 == t2) self.failUnless(t1 <= t2) self.failUnless(t1 >= t2) self.failUnless(not t1 != t2) self.failUnless(not t1 < t2) self.failUnless(not t1 > t2) self.assertEqual(cmp(t1, t2), 0) self.assertEqual(cmp(t2, t1), 0) for i in range(len(args)): newargs = args[:] newargs[i] = args[i] + 1 t2 = self.theclass(*newargs) # this is larger than t1 self.failUnless(t1 < t2) self.failUnless(t2 > t1) self.failUnless(t1 <= t2) self.failUnless(t2 >= t1) self.failUnless(t1 != t2) self.failUnless(t2 != t1) self.failUnless(not t1 == t2) self.failUnless(not t2 == t1) self.failUnless(not t1 > t2) self.failUnless(not t2 < t1) self.failUnless(not t1 >= t2) self.failUnless(not t2 <= t1) self.assertEqual(cmp(t1, t2), -1) self.assertEqual(cmp(t2, t1), 1) # 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) def test_utcfromtimestamp(self): import time ts = time.time() expected = time.gmtime(ts) got = self.theclass.utcfromtimestamp(ts) self.verify_field_equality(expected, got) 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.failUnless(abs(from_timestamp - from_now) <= tolerance) 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) 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) self.assertEqual(t.strftime("%m %d %y %S %M %H %j"), "12 31 04 33 22 06 366") 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)) 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) # too many args self.assertRaises(TypeError, combine, "date", "time") # wrong types 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) def test_astimezone(self): # Pretty boring! The TZ test is more interesting here. astimezone() # simply can't be applied to a naive object. dt = self.theclass.now() f = FixedOffset(44, "") self.assertRaises(TypeError, dt.astimezone) # not enough args self.assertRaises(TypeError, dt.astimezone, f, f) # too many args self.assertRaises(TypeError, dt.astimezone, dt) # arg wrong type self.assertRaises(ValueError, dt.astimezone, f) # naive self.assertRaises(ValueError, dt.astimezone, tz=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 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 TestTime(unittest.TestCase): theclass = time def test_basic_attributes(self): t = self.theclass(12, 0) self.assertEqual(t.hour, 12) self.assertEqual(t.minute, 0) self.assertEqual(t.second, 0) self.assertEqual(t.microsecond, 0) def test_basic_attributes_nonzero(self): # Make sure all attributes are non-zero so bugs in # bit-shifting access show up. t = self.theclass(12, 59, 59, 8000) self.assertEqual(t.hour, 12) self.assertEqual(t.minute, 59) self.assertEqual(t.second, 59) self.assertEqual(t.microsecond, 8000) def test_roundtrip(self): t = self.theclass(1, 2, 3, 4) # Verify t -> string -> time identity. s = repr(t) self.failUnless(s.startswith('datetime.')) s = s[9:] t2 = eval(s) self.assertEqual(t, t2) # Verify identity via reconstructing from pieces. t2 = self.theclass(t.hour, t.minute, t.second, t.microsecond) self.assertEqual(t, t2) def test_comparing(self): args = [1, 2, 3, 4] t1 = self.theclass(*args) t2 = self.theclass(*args) self.failUnless(t1 == t2) self.failUnless(t1 <= t2) self.failUnless(t1 >= t2) self.failUnless(not t1 != t2) self.failUnless(not t1 < t2) self.failUnless(not t1 > t2) self.assertEqual(cmp(t1, t2), 0) self.assertEqual(cmp(t2, t1), 0) for i in range(len(args)): newargs = args[:] newargs[i] = args[i] + 1 t2 = self.theclass(*newargs) # this is larger than t1 self.failUnless(t1 < t2) self.failUnless(t2 > t1) self.failUnless(t1 <= t2) self.failUnless(t2 >= t1) self.failUnless(t1 != t2) self.failUnless(t2 != t1) self.failUnless(not t1 == t2) self.failUnless(not t2 == t1) self.failUnless(not t1 > t2) self.failUnless(not t2 < t1) self.failUnless(not t1 >= t2) self.failUnless(not t2 <= t1) self.assertEqual(cmp(t1, t2), -1) self.assertEqual(cmp(t2, t1), 1) badargs = (10, 10L, 34.5, "abc", {}, [], ()) if CMP_BUG_FIXED: badargs += (date(1, 1, 1), datetime(1, 1, 1, 1, 1), timedelta(9)) for badarg in badargs: self.assertRaises(TypeError, lambda: t1 == badarg) self.assertRaises(TypeError, lambda: t1 != badarg) self.assertRaises(TypeError, lambda: t1 <= badarg) self.assertRaises(TypeError, lambda: t1 < badarg) self.assertRaises(TypeError, lambda: t1 > badarg) self.assertRaises(TypeError, lambda: t1 >= badarg) self.assertRaises(TypeError, lambda: badarg == t1) self.assertRaises(TypeError, lambda: badarg != t1) self.assertRaises(TypeError, lambda: badarg <= t1) self.assertRaises(TypeError, lambda: badarg < t1) self.assertRaises(TypeError, lambda: badarg > t1) self.assertRaises(TypeError, lambda: badarg >= t1) def test_bad_constructor_arguments(self): # bad hours self.theclass(0, 0) # no exception self.theclass(23, 0) # no exception self.assertRaises(ValueError, self.theclass, -1, 0) self.assertRaises(ValueError, self.theclass, 24, 0) # bad minutes self.theclass(23, 0) # no exception self.theclass(23, 59) # no exception self.assertRaises(ValueError, self.theclass, 23, -1) self.assertRaises(ValueError, self.theclass, 23, 60) # bad seconds self.theclass(23, 59, 0) # no exception self.theclass(23, 59, 59) # no exception self.assertRaises(ValueError, self.theclass, 23, 59, -1) self.assertRaises(ValueError, self.theclass, 23, 59, 60) # bad microseconds self.theclass(23, 59, 59, 0) # no exception self.theclass(23, 59, 59, 999999) # no exception self.assertRaises(ValueError, self.theclass, 23, 59, 59, -1) self.assertRaises(ValueError, self.theclass, 23, 59, 59, 1000000) def test_hash_equality(self): d = self.theclass(23, 30, 17) e = self.theclass(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(0, 5, 17) e = self.theclass(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_isoformat(self): t = self.theclass(4, 5, 1, 123) self.assertEqual(t.isoformat(), "04:05:01.000123") self.assertEqual(t.isoformat(), str(t)) t = self.theclass() self.assertEqual(t.isoformat(), "00:00:00") self.assertEqual(t.isoformat(), str(t)) t = self.theclass(microsecond=1) self.assertEqual(t.isoformat(), "00:00:00.000001") self.assertEqual(t.isoformat(), str(t)) t = self.theclass(microsecond=10) self.assertEqual(t.isoformat(), "00:00:00.000010") self.assertEqual(t.isoformat(), str(t)) t = self.theclass(microsecond=100) self.assertEqual(t.isoformat(), "00:00:00.000100") self.assertEqual(t.isoformat(), str(t)) t = self.theclass(microsecond=1000) self.assertEqual(t.isoformat(), "00:00:00.001000") self.assertEqual(t.isoformat(), str(t)) t = self.theclass(microsecond=10000) self.assertEqual(t.isoformat(), "00:00:00.010000") self.assertEqual(t.isoformat(), str(t)) t = self.theclass(microsecond=100000) self.assertEqual(t.isoformat(), "00:00:00.100000") self.assertEqual(t.isoformat(), str(t)) def test_strftime(self): t = self.theclass(1, 2, 3, 4) self.assertEqual(t.strftime('%H %M %S'), "01 02 03") # A naive object replaces %z and %Z with empty strings. self.assertEqual(t.strftime("'%z' '%Z'"), "'' ''") def test_str(self): self.assertEqual(str(self.theclass(1, 2, 3, 4)), "01:02:03.000004") self.assertEqual(str(self.theclass(10, 2, 3, 4000)), "10:02:03.004000") self.assertEqual(str(self.theclass(0, 2, 3, 400000)), "00:02:03.400000") self.assertEqual(str(self.theclass(12, 2, 3, 0)), "12:02:03") self.assertEqual(str(self.theclass(23, 15, 0, 0)), "23:15:00") def test_repr(self): name = 'datetime.' + self.theclass.__name__ self.assertEqual(repr(self.theclass(1, 2, 3, 4)), "%s(1, 2, 3, 4)" % name) self.assertEqual(repr(self.theclass(10, 2, 3, 4000)), "%s(10, 2, 3, 4000)" % name) self.assertEqual(repr(self.theclass(0, 2, 3, 400000)), "%s(0, 2, 3, 400000)" % name) self.assertEqual(repr(self.theclass(12, 2, 3, 0)), "%s(12, 2, 3)" % name) self.assertEqual(repr(self.theclass(23, 15, 0, 0)), "%s(23, 15)" % name) def test_resolution_info(self): self.assert_(isinstance(self.theclass.min, self.theclass)) self.assert_(isinstance(self.theclass.max, self.theclass)) self.assert_(isinstance(self.theclass.resolution, timedelta)) self.assert_(self.theclass.max > self.theclass.min) def test_pickling(self): args = 20, 59, 16, 64**2 orig = self.theclass(*args) for pickler, unpickler, proto in pickle_choices: green = pickler.dumps(orig, proto) derived = unpickler.loads(green) self.assertEqual(orig, derived) def test_bool(self): cls = self.theclass self.failUnless(cls(1)) self.failUnless(cls(0, 1)) self.failUnless(cls(0, 0, 1)) self.failUnless(cls(0, 0, 0, 1)) self.failUnless(not cls(0)) self.failUnless(not cls()) def test_replace(self): cls = self.theclass args = [1, 2, 3, 4] base = cls(*args) self.assertEqual(base, base.replace()) i = 0 for name, newval in (("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(1) self.assertRaises(ValueError, base.replace, hour=24) self.assertRaises(ValueError, base.replace, minute=-1) self.assertRaises(ValueError, base.replace, second=100) self.assertRaises(ValueError, base.replace, microsecond=1000000) # A mixin for classes with a tzinfo= argument. Subclasses must define # theclass as a class atribute, and theclass(1, 1, 1, tzinfo=whatever) # must be legit (which is true for time and datetime). class TZInfoBase(unittest.TestCase): def test_argument_passing(self): cls = self.theclass # A datetime passes itself on, a time passes None. class introspective(tzinfo): def tzname(self, dt): return dt and "real" or "none" def utcoffset(self, dt): return timedelta(minutes = dt and 42 or -42) dst = utcoffset obj = cls(1, 2, 3, tzinfo=introspective()) expected = cls is time and "none" or "real" self.assertEqual(obj.tzname(), expected) expected = timedelta(minutes=(cls is time and -42 or 42)) self.assertEqual(obj.utcoffset(), expected) self.assertEqual(obj.dst(), expected) def test_bad_tzinfo_classes(self): cls = self.theclass self.assertRaises(TypeError, cls, 1, 1, 1, tzinfo=12) class NiceTry(object): def __init__(self): pass def utcoffset(self, dt): pass self.assertRaises(TypeError, cls, 1, 1, 1, tzinfo=NiceTry) class BetterTry(tzinfo): def __init__(self): pass def utcoffset(self, dt): pass b = BetterTry() t = cls(1, 1, 1, tzinfo=b) self.failUnless(t.tzinfo is b) def test_utc_offset_out_of_bounds(self): class Edgy(tzinfo): def __init__(self, offset): self.offset = timedelta(minutes=offset) def utcoffset(self, dt): return self.offset cls = self.theclass for offset, legit in ((-1440, False), (-1439, True), (1439, True), (1440, False)): if cls is time: t = cls(1, 2, 3, tzinfo=Edgy(offset)) elif cls is datetime: t = cls(6, 6, 6, 1, 2, 3, tzinfo=Edgy(offset)) else: assert 0, "impossible" if legit: aofs = abs(offset) h, m = divmod(aofs, 60) tag = "%c%02d:%02d" % (offset < 0 and '-' or '+', h, m) if isinstance(t, datetime): t = t.timetz() self.assertEqual(str(t), "01:02:03" + tag) else: self.assertRaises(ValueError, str, t) def test_tzinfo_classes(self): cls = self.theclass class C1(tzinfo): def utcoffset(self, dt): return None def dst(self, dt): return None def tzname(self, dt): return None for t in (cls(1, 1, 1), cls(1, 1, 1, tzinfo=None), cls(1, 1, 1, tzinfo=C1())): self.failUnless(t.utcoffset() is None) self.failUnless(t.dst() is None) self.failUnless(t.tzname() is None) class C3(tzinfo): def utcoffset(self, dt): return timedelta(minutes=-1439) def dst(self, dt): return timedelta(minutes=1439) def tzname(self, dt): return "aname" t = cls(1, 1, 1, tzinfo=C3()) self.assertEqual(t.utcoffset(), timedelta(minutes=-1439)) self.assertEqual(t.dst(), timedelta(minutes=1439)) self.assertEqual(t.tzname(), "aname") # Wrong types. class C4(tzinfo): def utcoffset(self, dt): return "aname" def dst(self, dt): return 7 def tzname(self, dt): return 0 t = cls(1, 1, 1, tzinfo=C4()) self.assertRaises(TypeError, t.utcoffset) self.assertRaises(TypeError, t.dst) self.assertRaises(TypeError, t.tzname) # Offset out of range. class C6(tzinfo): def utcoffset(self, dt): return timedelta(hours=-24) def dst(self, dt): return timedelta(hours=24) t = cls(1, 1, 1, tzinfo=C6()) self.assertRaises(ValueError, t.utcoffset) self.assertRaises(ValueError, t.dst) # Not a whole number of minutes. class C7(tzinfo): def utcoffset(self, dt): return timedelta(seconds=61) def dst(self, dt): return timedelta(microseconds=-81) t = cls(1, 1, 1, tzinfo=C7()) self.assertRaises(ValueError, t.utcoffset) self.assertRaises(ValueError, t.dst) def test_aware_compare(self): cls = self.theclass # Ensure that utcoffset() gets ignored if the comparands have # the same tzinfo member. class OperandDependentOffset(tzinfo): def utcoffset(self, t): if t.minute < 10: # d0 and d1 equal after adjustment return timedelta(minutes=t.minute) else: # d2 off in the weeds return timedelta(minutes=59) base = cls(8, 9, 10, tzinfo=OperandDependentOffset()) d0 = base.replace(minute=3) d1 = base.replace(minute=9) d2 = base.replace(minute=11) for x in d0, d1, d2: for y in d0, d1, d2: got = cmp(x, y) expected = cmp(x.minute, y.minute) self.assertEqual(got, expected) # However, if they're different members, uctoffset is not ignored. # Note that a time can't actually have an operand-depedent offset, # though (and time.utcoffset() passes None to tzinfo.utcoffset()), # so skip this test for time. if cls is not time: d0 = base.replace(minute=3, tzinfo=OperandDependentOffset()) d1 = base.replace(minute=9, tzinfo=OperandDependentOffset()) d2 = base.replace(minute=11, tzinfo=OperandDependentOffset()) for x in d0, d1, d2: for y in d0, d1, d2: got = cmp(x, y) if (x is d0 or x is d1) and (y is d0 or y is d1): expected = 0 elif x is y is d2: expected = 0 elif x is d2: expected = -1 else: assert y is d2 expected = 1 self.assertEqual(got, expected) # Testing time objects with a non-None tzinfo. class TestTimeTZ(TestTime, TZInfoBase): theclass = time def test_empty(self): t = self.theclass() self.assertEqual(t.hour, 0) self.assertEqual(t.minute, 0) self.assertEqual(t.second, 0) self.assertEqual(t.microsecond, 0) self.failUnless(t.tzinfo is None) def test_zones(self): est = FixedOffset(-300, "EST", 1) utc = FixedOffset(0, "UTC", -2) met = FixedOffset(60, "MET", 3) t1 = time( 7, 47, tzinfo=est) t2 = time(12, 47, tzinfo=utc) t3 = time(13, 47, tzinfo=met) t4 = time(microsecond=40) t5 = time(microsecond=40, tzinfo=utc) self.assertEqual(t1.tzinfo, est) self.assertEqual(t2.tzinfo, utc) self.assertEqual(t3.tzinfo, met) self.failUnless(t4.tzinfo is None) self.assertEqual(t5.tzinfo, utc) self.assertEqual(t1.utcoffset(), timedelta(minutes=-300)) self.assertEqual(t2.utcoffset(), timedelta(minutes=0)) self.assertEqual(t3.utcoffset(), timedelta(minutes=60)) self.failUnless(t4.utcoffset() is None) self.assertRaises(TypeError, t1.utcoffset, "no args") self.assertEqual(t1.tzname(), "EST") self.assertEqual(t2.tzname(), "UTC") self.assertEqual(t3.tzname(), "MET") self.failUnless(t4.tzname() is None) self.assertRaises(TypeError, t1.tzname, "no args") self.assertEqual(t1.dst(), timedelta(minutes=1)) self.assertEqual(t2.dst(), timedelta(minutes=-2)) self.assertEqual(t3.dst(), timedelta(minutes=3)) self.failUnless(t4.dst() is None) self.assertRaises(TypeError, t1.dst, "no args") self.assertEqual(hash(t1), hash(t2)) self.assertEqual(hash(t1), hash(t3)) self.assertEqual(hash(t2), hash(t3)) self.assertEqual(t1, t2) self.assertEqual(t1, t3) self.assertEqual(t2, t3) self.assertRaises(TypeError, lambda: t4 == t5) # mixed tz-aware & naive self.assertRaises(TypeError, lambda: t4 < t5) # mixed tz-aware & naive self.assertRaises(TypeError, lambda: t5 < t4) # mixed tz-aware & naive self.assertEqual(str(t1), "07:47:00-05:00") self.assertEqual(str(t2), "12:47:00+00:00") self.assertEqual(str(t3), "13:47:00+01:00") self.assertEqual(str(t4), "00:00:00.000040") self.assertEqual(str(t5), "00:00:00.000040+00:00") self.assertEqual(t1.isoformat(), "07:47:00-05:00") self.assertEqual(t2.isoformat(), "12:47:00+00:00") self.assertEqual(t3.isoformat(), "13:47:00+01:00") self.assertEqual(t4.isoformat(), "00:00:00.000040") self.assertEqual(t5.isoformat(), "00:00:00.000040+00:00") d = 'datetime.time' self.assertEqual(repr(t1), d + "(7, 47, tzinfo=est)") self.assertEqual(repr(t2), d + "(12, 47, tzinfo=utc)") self.assertEqual(repr(t3), d + "(13, 47, tzinfo=met)") self.assertEqual(repr(t4), d + "(0, 0, 0, 40)") self.assertEqual(repr(t5), d + "(0, 0, 0, 40, tzinfo=utc)") self.assertEqual(t1.strftime("%H:%M:%S %%Z=%Z %%z=%z"), "07:47:00 %Z=EST %z=-0500") self.assertEqual(t2.strftime("%H:%M:%S %Z %z"), "12:47:00 UTC +0000") self.assertEqual(t3.strftime("%H:%M:%S %Z %z"), "13:47:00 MET +0100") yuck = FixedOffset(-1439, "%z %Z %%z%%Z") t1 = time(23, 59, tzinfo=yuck) self.assertEqual(t1.strftime("%H:%M %%Z='%Z' %%z='%z'"), "23:59 %Z='%z %Z %%z%%Z' %z='-2359'") # Check that an invalid tzname result raises an exception. class Badtzname(tzinfo): def tzname(self, dt): return 42 t = time(2, 3, 4, tzinfo=Badtzname()) self.assertEqual(t.strftime("%H:%M:%S"), "02:03:04") self.assertRaises(TypeError, t.strftime, "%Z") def test_hash_edge_cases(self): # Offsets that overflow a basic time. t1 = self.theclass(0, 1, 2, 3, tzinfo=FixedOffset(1439, "")) t2 = self.theclass(0, 0, 2, 3, tzinfo=FixedOffset(1438, "")) self.assertEqual(hash(t1), hash(t2)) t1 = self.theclass(23, 58, 6, 100, tzinfo=FixedOffset(-1000, "")) t2 = self.theclass(23, 48, 6, 100, tzinfo=FixedOffset(-1010, "")) self.assertEqual(hash(t1), hash(t2)) def test_pickling(self): # Try one without a tzinfo. args = 20, 59, 16, 64**2 orig = self.theclass(*args) for pickler, unpickler, proto in pickle_choices: green = pickler.dumps(orig, proto) derived = unpickler.loads(green) self.assertEqual(orig, derived) # Try one with a tzinfo. tinfo = PicklableFixedOffset(-300, 'cookie') orig = self.theclass(5, 6, 7, tzinfo=tinfo) for pickler, unpickler, proto in pickle_choices: green = pickler.dumps(orig, proto) derived = unpickler.loads(green) self.assertEqual(orig, derived) self.failUnless(isinstance(derived.tzinfo, PicklableFixedOffset)) self.assertEqual(derived.utcoffset(), timedelta(minutes=-300)) self.assertEqual(derived.tzname(), 'cookie') def test_more_bool(self): # Test cases with non-None tzinfo. cls = self.theclass t = cls(0, tzinfo=FixedOffset(-300, "")) self.failUnless(t) t = cls(5, tzinfo=FixedOffset(-300, "")) self.failUnless(t) t = cls(5, tzinfo=FixedOffset(300, "")) self.failUnless(not t) t = cls(23, 59, tzinfo=FixedOffset(23*60 + 59, "")) self.failUnless(not t) # Mostly ensuring this doesn't overflow internally. t = cls(0, tzinfo=FixedOffset(23*60 + 59, "")) self.failUnless(t) # But this should yield a value error -- the utcoffset is bogus. t = cls(0, tzinfo=FixedOffset(24*60, "")) self.assertRaises(ValueError, lambda: bool(t)) # Likewise. t = cls(0, tzinfo=FixedOffset(-24*60, "")) self.assertRaises(ValueError, lambda: bool(t)) def test_replace(self): cls = self.theclass z100 = FixedOffset(100, "+100") zm200 = FixedOffset(timedelta(minutes=-200), "-200") args = [1, 2, 3, 4, z100] base = cls(*args) self.assertEqual(base, base.replace()) i = 0 for name, newval in (("hour", 5), ("minute", 6), ("second", 7), ("microsecond", 8), ("tzinfo", zm200)): newargs = args[:] newargs[i] = newval expected = cls(*newargs) got = base.replace(**{name: newval}) self.assertEqual(expected, got) i += 1 # Ensure we can get rid of a tzinfo. self.assertEqual(base.tzname(), "+100") base2 = base.replace(tzinfo=None) self.failUnless(base2.tzinfo is None) self.failUnless(base2.tzname() is None) # Ensure we can add one. base3 = base2.replace(tzinfo=z100) self.assertEqual(base, base3) self.failUnless(base.tzinfo is base3.tzinfo) # Out of bounds. base = cls(1) self.assertRaises(ValueError, base.replace, hour=24) self.assertRaises(ValueError, base.replace, minute=-1) self.assertRaises(ValueError, base.replace, second=100) self.assertRaises(ValueError, base.replace, microsecond=1000000) def test_mixed_compare(self): t1 = time(1, 2, 3) t2 = time(1, 2, 3) self.assertEqual(t1, t2) t2 = t2.replace(tzinfo=None) self.assertEqual(t1, t2) t2 = t2.replace(tzinfo=FixedOffset(None, "")) self.assertEqual(t1, t2) if CMP_BUG_FIXED: t2 = t2.replace(tzinfo=FixedOffset(0, "")) self.assertRaises(TypeError, lambda: t1 == t2) # In time w/ identical tzinfo objects, utcoffset is ignored. class Varies(tzinfo): def __init__(self): self.offset = timedelta(minutes=22) def utcoffset(self, t): self.offset += timedelta(minutes=1) return self.offset v = Varies() t1 = t2.replace(tzinfo=v) t2 = t2.replace(tzinfo=v) self.assertEqual(t1.utcoffset(), timedelta(minutes=23)) self.assertEqual(t2.utcoffset(), timedelta(minutes=24)) self.assertEqual(t1, t2) # But if they're not identical, it isn't ignored. t2 = t2.replace(tzinfo=Varies()) self.failUnless(t1 < t2) # t1's offset counter still going up # Testing datetime objects with a non-None tzinfo. class TestDateTimeTZ(TestDateTime, TZInfoBase): theclass = datetime def test_trivial(self): dt = self.theclass(1, 2, 3, 4, 5, 6, 7) self.assertEqual(dt.year, 1) self.assertEqual(dt.month, 2) self.assertEqual(dt.day, 3) self.assertEqual(dt.hour, 4) self.assertEqual(dt.minute, 5) self.assertEqual(dt.second, 6) self.assertEqual(dt.microsecond, 7) self.assertEqual(dt.tzinfo, None) def test_even_more_compare(self): # The test_compare() and test_more_compare() inherited from TestDate # and TestDateTime covered non-tzinfo cases. # Smallest possible after UTC adjustment. t1 = self.theclass(1, 1, 1, tzinfo=FixedOffset(1439, "")) # Largest possible after UTC adjustment. t2 = self.theclass(MAXYEAR, 12, 31, 23, 59, 59, 999999, tzinfo=FixedOffset(-1439, "")) # Make sure those compare correctly, and w/o overflow. self.failUnless(t1 < t2) self.failUnless(t1 != t2) self.failUnless(t2 > t1) self.failUnless(t1 == t1) self.failUnless(t2 == t2) # Equal afer adjustment. t1 = self.theclass(1, 12, 31, 23, 59, tzinfo=FixedOffset(1, "")) t2 = self.theclass(2, 1, 1, 3, 13, tzinfo=FixedOffset(3*60+13+2, "")) self.assertEqual(t1, t2) # Change t1 not to subtract a minute, and t1 should be larger. t1 = self.theclass(1, 12, 31, 23, 59, tzinfo=FixedOffset(0, "")) self.failUnless(t1 > t2) # Change t1 to subtract 2 minutes, and t1 should be smaller. t1 = self.theclass(1, 12, 31, 23, 59, tzinfo=FixedOffset(2, "")) self.failUnless(t1 < t2) # Back to the original t1, but make seconds resolve it. t1 = self.theclass(1, 12, 31, 23, 59, tzinfo=FixedOffset(1, ""), second=1) self.failUnless(t1 > t2) # Likewise, but make microseconds resolve it. t1 = self.theclass(1, 12, 31, 23, 59, tzinfo=FixedOffset(1, ""), microsecond=1) self.failUnless(t1 > t2) # Make t2 naive and it should fail. t2 = self.theclass.min self.assertRaises(TypeError, lambda: t1 == t2) self.assertEqual(t2, t2) # It's also naive if it has tzinfo but tzinfo.utcoffset() is None. class Naive(tzinfo): def utcoffset(self, dt): return None t2 = self.theclass(5, 6, 7, tzinfo=Naive()) self.assertRaises(TypeError, lambda: t1 == t2) self.assertEqual(t2, t2) # OTOH, it's OK to compare two of these mixing the two ways of being # naive. t1 = self.theclass(5, 6, 7) self.assertEqual(t1, t2) # Try a bogus uctoffset. class Bogus(tzinfo): def utcoffset(self, dt): return timedelta(minutes=1440) # out of bounds t1 = self.theclass(2, 2, 2, tzinfo=Bogus()) t2 = self.theclass(2, 2, 2, tzinfo=FixedOffset(0, "")) self.assertRaises(ValueError, lambda: t1 == t2) def test_pickling(self): # Try one without a tzinfo. args = 6, 7, 23, 20, 59, 1, 64**2 orig = self.theclass(*args) for pickler, unpickler, proto in pickle_choices: green = pickler.dumps(orig, proto) derived = unpickler.loads(green) self.assertEqual(orig, derived) # Try one with a tzinfo. tinfo = PicklableFixedOffset(-300, 'cookie') orig = self.theclass(*args, **{'tzinfo': tinfo}) derived = self.theclass(1, 1, 1, tzinfo=FixedOffset(0, "", 0)) for pickler, unpickler, proto in pickle_choices: green = pickler.dumps(orig, proto) derived = unpickler.loads(green) self.assertEqual(orig, derived) self.failUnless(isinstance(derived.tzinfo, PicklableFixedOffset)) self.assertEqual(derived.utcoffset(), timedelta(minutes=-300)) self.assertEqual(derived.tzname(), 'cookie') def test_extreme_hashes(self): # If an attempt is made to hash these via subtracting the offset # then hashing a datetime object, OverflowError results. The # Python implementation used to blow up here. t = self.theclass(1, 1, 1, tzinfo=FixedOffset(1439, "")) hash(t) t = self.theclass(MAXYEAR, 12, 31, 23, 59, 59, 999999, tzinfo=FixedOffset(-1439, "")) hash(t) # OTOH, an OOB offset should blow up. t = self.theclass(5, 5, 5, tzinfo=FixedOffset(-1440, "")) self.assertRaises(ValueError, hash, t) def test_zones(self): est = FixedOffset(-300, "EST") utc = FixedOffset(0, "UTC") met = FixedOffset(60, "MET") t1 = datetime(2002, 3, 19, 7, 47, tzinfo=est) t2 = datetime(2002, 3, 19, 12, 47, tzinfo=utc) t3 = datetime(2002, 3, 19, 13, 47, tzinfo=met) self.assertEqual(t1.tzinfo, est) self.assertEqual(t2.tzinfo, utc) self.assertEqual(t3.tzinfo, met) self.assertEqual(t1.utcoffset(), timedelta(minutes=-300)) self.assertEqual(t2.utcoffset(), timedelta(minutes=0)) self.assertEqual(t3.utcoffset(), timedelta(minutes=60)) self.assertEqual(t1.tzname(), "EST") self.assertEqual(t2.tzname(), "UTC") self.assertEqual(t3.tzname(), "MET") self.assertEqual(hash(t1), hash(t2)) self.assertEqual(hash(t1), hash(t3)) self.assertEqual(hash(t2), hash(t3)) self.assertEqual(t1, t2) self.assertEqual(t1, t3) self.assertEqual(t2, t3) self.assertEqual(str(t1), "2002-03-19 07:47:00-05:00") self.assertEqual(str(t2), "2002-03-19 12:47:00+00:00") self.assertEqual(str(t3), "2002-03-19 13:47:00+01:00") d = 'datetime.datetime(2002, 3, 19, ' self.assertEqual(repr(t1), d + "7, 47, tzinfo=est)") self.assertEqual(repr(t2), d + "12, 47, tzinfo=utc)") self.assertEqual(repr(t3), d + "13, 47, tzinfo=met)") def test_combine(self): met = FixedOffset(60, "MET") d = date(2002, 3, 4) tz = time(18, 45, 3, 1234, tzinfo=met) dt = datetime.combine(d, tz) self.assertEqual(dt, datetime(2002, 3, 4, 18, 45, 3, 1234, tzinfo=met)) def test_extract(self): met = FixedOffset(60, "MET") dt = self.theclass(2002, 3, 4, 18, 45, 3, 1234, tzinfo=met) self.assertEqual(dt.date(), date(2002, 3, 4)) self.assertEqual(dt.time(), time(18, 45, 3, 1234)) self.assertEqual(dt.timetz(), time(18, 45, 3, 1234, tzinfo=met)) def test_tz_aware_arithmetic(self): import random now = self.theclass.now() tz55 = FixedOffset(-330, "west 5:30") timeaware = now.time().replace(tzinfo=tz55) nowaware = self.theclass.combine(now.date(), timeaware) self.failUnless(nowaware.tzinfo is tz55) self.assertEqual(nowaware.timetz(), timeaware) # Can't mix aware and non-aware. self.assertRaises(TypeError, lambda: now - nowaware) self.assertRaises(TypeError, lambda: nowaware - now) # And adding datetime's doesn't make sense, aware or not. self.assertRaises(TypeError, lambda: now + nowaware) self.assertRaises(TypeError, lambda: nowaware + now) self.assertRaises(TypeError, lambda: nowaware + nowaware) # Subtracting should yield 0. self.assertEqual(now - now, timedelta(0)) self.assertEqual(nowaware - nowaware, timedelta(0)) # Adding a delta should preserve tzinfo. delta = timedelta(weeks=1, minutes=12, microseconds=5678) nowawareplus = nowaware + delta self.failUnless(nowaware.tzinfo is tz55) nowawareplus2 = delta + nowaware self.failUnless(nowawareplus2.tzinfo is tz55) self.assertEqual(nowawareplus, nowawareplus2) # that - delta should be what we started with, and that - what we # started with should be delta. diff = nowawareplus - delta self.failUnless(diff.tzinfo is tz55) self.assertEqual(nowaware, diff) self.assertRaises(TypeError, lambda: delta - nowawareplus) self.assertEqual(nowawareplus - nowaware, delta) # Make up a random timezone. tzr = FixedOffset(random.randrange(-1439, 1440), "randomtimezone") # Attach it to nowawareplus. nowawareplus = nowawareplus.replace(tzinfo=tzr) self.failUnless(nowawareplus.tzinfo is tzr) # Make sure the difference takes the timezone adjustments into account. got = nowaware - nowawareplus # Expected: (nowaware base - nowaware offset) - # (nowawareplus base - nowawareplus offset) = # (nowaware base - nowawareplus base) + # (nowawareplus offset - nowaware offset) = # -delta + nowawareplus offset - nowaware offset expected = nowawareplus.utcoffset() - nowaware.utcoffset() - delta self.assertEqual(got, expected) # Try max possible difference. min = self.theclass(1, 1, 1, tzinfo=FixedOffset(1439, "min")) max = self.theclass(MAXYEAR, 12, 31, 23, 59, 59, 999999, tzinfo=FixedOffset(-1439, "max")) maxdiff = max - min self.assertEqual(maxdiff, self.theclass.max - self.theclass.min + timedelta(minutes=2*1439)) def test_tzinfo_now(self): meth = self.theclass.now # Ensure it doesn't require tzinfo (i.e., that this doesn't blow up). base = meth() # Try with and without naming the keyword. off42 = FixedOffset(42, "42") another = meth(off42) again = meth(tz=off42) self.failUnless(another.tzinfo is again.tzinfo) self.assertEqual(another.utcoffset(), timedelta(minutes=42)) # Bad argument with and w/o naming the keyword. self.assertRaises(TypeError, meth, 16) self.assertRaises(TypeError, meth, tzinfo=16) # Bad keyword name. self.assertRaises(TypeError, meth, tinfo=off42) # Too many args. self.assertRaises(TypeError, meth, off42, off42) # We don't know which time zone we're in, and don't have a tzinfo # class to represent it, so seeing whether a tz argument actually # does a conversion is tricky. weirdtz = FixedOffset(timedelta(hours=15, minutes=58), "weirdtz", 0) utc = FixedOffset(0, "utc", 0) for dummy in range(3): now = datetime.now(weirdtz) self.failUnless(now.tzinfo is weirdtz) utcnow = datetime.utcnow().replace(tzinfo=utc) now2 = utcnow.astimezone(weirdtz) if abs(now - now2) < timedelta(seconds=30): break # Else the code is broken, or more than 30 seconds passed between # calls; assuming the latter, just try again. else: # Three strikes and we're out. self.fail("utcnow(), now(tz), or astimezone() may be broken") def test_tzinfo_fromtimestamp(self): import time meth = self.theclass.fromtimestamp ts = time.time() # Ensure it doesn't require tzinfo (i.e., that this doesn't blow up). base = meth(ts) # Try with and without naming the keyword. off42 = FixedOffset(42, "42") another = meth(ts, off42) again = meth(ts, tz=off42) self.failUnless(another.tzinfo is again.tzinfo) self.assertEqual(another.utcoffset(), timedelta(minutes=42)) # Bad argument with and w/o naming the keyword. self.assertRaises(TypeError, meth, ts, 16) self.assertRaises(TypeError, meth, ts, tzinfo=16) # Bad keyword name. self.assertRaises(TypeError, meth, ts, tinfo=off42) # Too many args. self.assertRaises(TypeError, meth, ts, off42, off42) # Too few args. self.assertRaises(TypeError, meth) # Try to make sure tz= actually does some conversion. timestamp = 1000000000 # 2001-09-09 01:46:40 UTC, give or take utc = FixedOffset(0, "utc", 0) expected = datetime(2001, 9, 9, 1, 46, 40) got = datetime.utcfromtimestamp(timestamp) # We don't support leap seconds, but maybe the platfrom insists # on using them, so don't demand exact equality). self.failUnless(abs(got - expected) < timedelta(minutes=1)) est = FixedOffset(-5*60, "est", 0) expected -= timedelta(hours=5) got = datetime.fromtimestamp(timestamp, est).replace(tzinfo=None) self.failUnless(abs(got - expected) < timedelta(minutes=1)) def test_tzinfo_utcnow(self): meth = self.theclass.utcnow # Ensure it doesn't require tzinfo (i.e., that this doesn't blow up). base = meth() # Try with and without naming the keyword; for whatever reason, # utcnow() doesn't accept a tzinfo argument. off42 = FixedOffset(42, "42") self.assertRaises(TypeError, meth, off42) self.assertRaises(TypeError, meth, tzinfo=off42) def test_tzinfo_utcfromtimestamp(self): import time meth = self.theclass.utcfromtimestamp ts = time.time() # Ensure it doesn't require tzinfo (i.e., that this doesn't blow up). base = meth(ts) # Try with and without naming the keyword; for whatever reason, # utcfromtimestamp() doesn't accept a tzinfo argument. off42 = FixedOffset(42, "42") self.assertRaises(TypeError, meth, ts, off42) self.assertRaises(TypeError, meth, ts, tzinfo=off42) def test_tzinfo_timetuple(self): # TestDateTime tested most of this. datetime adds a twist to the # DST flag. class DST(tzinfo): def __init__(self, dstvalue): if isinstance(dstvalue, int): dstvalue = timedelta(minutes=dstvalue) self.dstvalue = dstvalue def dst(self, dt): return self.dstvalue cls = self.theclass for dstvalue, flag in (-33, 1), (33, 1), (0, 0), (None, -1): d = cls(1, 1, 1, 10, 20, 30, 40, tzinfo=DST(dstvalue)) t = d.timetuple() self.assertEqual(1, t.tm_year) self.assertEqual(1, t.tm_mon) self.assertEqual(1, t.tm_mday) self.assertEqual(10, t.tm_hour) self.assertEqual(20, t.tm_min) self.assertEqual(30, t.tm_sec) self.assertEqual(0, t.tm_wday) self.assertEqual(1, t.tm_yday) self.assertEqual(flag, t.tm_isdst) # dst() returns wrong type. self.assertRaises(TypeError, cls(1, 1, 1, tzinfo=DST("x")).timetuple) # dst() at the edge. self.assertEqual(cls(1,1,1, tzinfo=DST(1439)).timetuple().tm_isdst, 1) self.assertEqual(cls(1,1,1, tzinfo=DST(-1439)).timetuple().tm_isdst, 1) # dst() out of range. self.assertRaises(ValueError, cls(1,1,1, tzinfo=DST(1440)).timetuple) self.assertRaises(ValueError, cls(1,1,1, tzinfo=DST(-1440)).timetuple) def test_utctimetuple(self): class DST(tzinfo): def __init__(self, dstvalue): if isinstance(dstvalue, int): dstvalue = timedelta(minutes=dstvalue) self.dstvalue = dstvalue def dst(self, dt): return self.dstvalue cls = self.theclass # This can't work: DST didn't implement utcoffset. self.assertRaises(NotImplementedError, cls(1, 1, 1, tzinfo=DST(0)).utcoffset) class UOFS(DST): def __init__(self, uofs, dofs=None): DST.__init__(self, dofs) self.uofs = timedelta(minutes=uofs) def utcoffset(self, dt): return self.uofs # Ensure tm_isdst is 0 regardless of what dst() says: DST is never # in effect for a UTC time. for dstvalue in -33, 33, 0, None: d = cls(1, 2, 3, 10, 20, 30, 40, tzinfo=UOFS(-53, dstvalue)) t = d.utctimetuple() self.assertEqual(d.year, t.tm_year) self.assertEqual(d.month, t.tm_mon) self.assertEqual(d.day, t.tm_mday) self.assertEqual(11, t.tm_hour) # 20mm + 53mm = 1hn + 13mm self.assertEqual(13, t.tm_min) self.assertEqual(d.second, t.tm_sec) self.assertEqual(d.weekday(), t.tm_wday) self.assertEqual(d.toordinal() - date(1, 1, 1).toordinal() + 1, t.tm_yday) self.assertEqual(0, t.tm_isdst) # At the edges, UTC adjustment can normalize into years out-of-range # for a datetime object. Ensure that a correct timetuple is # created anyway. tiny = cls(MINYEAR, 1, 1, 0, 0, 37, tzinfo=UOFS(1439)) # That goes back 1 minute less than a full day. t = tiny.utctimetuple() self.assertEqual(t.tm_year, MINYEAR-1) self.assertEqual(t.tm_mon, 12) self.assertEqual(t.tm_mday, 31) self.assertEqual(t.tm_hour, 0) self.assertEqual(t.tm_min, 1) self.assertEqual(t.tm_sec, 37) self.assertEqual(t.tm_yday, 366) # "year 0" is a leap year self.assertEqual(t.tm_isdst, 0) huge = cls(MAXYEAR, 12, 31, 23, 59, 37, 999999, tzinfo=UOFS(-1439)) # That goes forward 1 minute less than a full day. t = huge.utctimetuple() self.assertEqual(t.tm_year, MAXYEAR+1) self.assertEqual(t.tm_mon, 1) self.assertEqual(t.tm_mday, 1) self.assertEqual(t.tm_hour, 23) self.assertEqual(t.tm_min, 58) self.assertEqual(t.tm_sec, 37) self.assertEqual(t.tm_yday, 1) self.assertEqual(t.tm_isdst, 0) def test_tzinfo_isoformat(self): zero = FixedOffset(0, "+00:00") plus = FixedOffset(220, "+03:40") minus = FixedOffset(-231, "-03:51") unknown = FixedOffset(None, "") cls = self.theclass datestr = '0001-02-03' for ofs in None, zero, plus, minus, unknown: for us in 0, 987001: d = cls(1, 2, 3, 4, 5, 59, us, tzinfo=ofs) timestr = '04:05:59' + (us and '.987001' or '') ofsstr = ofs is not None and d.tzname() or '' tailstr = timestr + ofsstr iso = d.isoformat() self.assertEqual(iso, datestr + 'T' + tailstr) self.assertEqual(iso, d.isoformat('T')) self.assertEqual(d.isoformat('k'), datestr + 'k' + tailstr) self.assertEqual(str(d), datestr + ' ' + tailstr) def test_replace(self): cls = self.theclass z100 = FixedOffset(100, "+100") zm200 = FixedOffset(timedelta(minutes=-200), "-200") args = [1, 2, 3, 4, 5, 6, 7, z100] 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), ("tzinfo", zm200)): newargs = args[:] newargs[i] = newval expected = cls(*newargs) got = base.replace(**{name: newval}) self.assertEqual(expected, got) i += 1 # Ensure we can get rid of a tzinfo. self.assertEqual(base.tzname(), "+100") base2 = base.replace(tzinfo=None) self.failUnless(base2.tzinfo is None) self.failUnless(base2.tzname() is None) # Ensure we can add one. base3 = base2.replace(tzinfo=z100) self.assertEqual(base, base3) self.failUnless(base.tzinfo is base3.tzinfo) # Out of bounds. base = cls(2000, 2, 29) self.assertRaises(ValueError, base.replace, year=2001) def test_more_astimezone(self): # The inherited test_astimezone covered some trivial and error cases. fnone = FixedOffset(None, "None") f44m = FixedOffset(44, "44") fm5h = FixedOffset(-timedelta(hours=5), "m300") dt = self.theclass.now(tz=f44m) self.failUnless(dt.tzinfo is f44m) # Replacing with degenerate tzinfo raises an exception. self.assertRaises(ValueError, dt.astimezone, fnone) # Ditto with None tz. self.assertRaises(TypeError, dt.astimezone, None) # Replacing with same tzinfo makes no change. x = dt.astimezone(dt.tzinfo) self.failUnless(x.tzinfo is f44m) self.assertEqual(x.date(), dt.date()) self.assertEqual(x.time(), dt.time()) # Replacing with different tzinfo does adjust. got = dt.astimezone(fm5h) self.failUnless(got.tzinfo is fm5h) self.assertEqual(got.utcoffset(), timedelta(hours=-5)) expected = dt - dt.utcoffset() # in effect, convert to UTC expected += fm5h.utcoffset(dt) # and from there to local time expected = expected.replace(tzinfo=fm5h) # and attach new tzinfo self.assertEqual(got.date(), expected.date()) self.assertEqual(got.time(), expected.time()) self.assertEqual(got.timetz(), expected.timetz()) self.failUnless(got.tzinfo is expected.tzinfo) self.assertEqual(got, expected) def test_aware_subtract(self): cls = self.theclass # Ensure that utcoffset() is ignored when the operands have the # same tzinfo member. class OperandDependentOffset(tzinfo): def utcoffset(self, t): if t.minute < 10: # d0 and d1 equal after adjustment return timedelta(minutes=t.minute) else: # d2 off in the weeds return timedelta(minutes=59) base = cls(8, 9, 10, 11, 12, 13, 14, tzinfo=OperandDependentOffset()) d0 = base.replace(minute=3) d1 = base.replace(minute=9) d2 = base.replace(minute=11) for x in d0, d1, d2: for y in d0, d1, d2: got = x - y expected = timedelta(minutes=x.minute - y.minute) self.assertEqual(got, expected) # OTOH, if the tzinfo members are distinct, utcoffsets aren't # ignored. base = cls(8, 9, 10, 11, 12, 13, 14) d0 = base.replace(minute=3, tzinfo=OperandDependentOffset()) d1 = base.replace(minute=9, tzinfo=OperandDependentOffset()) d2 = base.replace(minute=11, tzinfo=OperandDependentOffset()) for x in d0, d1, d2: for y in d0, d1, d2: got = x - y if (x is d0 or x is d1) and (y is d0 or y is d1): expected = timedelta(0) elif x is y is d2: expected = timedelta(0) elif x is d2: expected = timedelta(minutes=(11-59)-0) else: assert y is d2 expected = timedelta(minutes=0-(11-59)) self.assertEqual(got, expected) def test_mixed_compare(self): t1 = datetime(1, 2, 3, 4, 5, 6, 7) t2 = datetime(1, 2, 3, 4, 5, 6, 7) self.assertEqual(t1, t2) t2 = t2.replace(tzinfo=None) self.assertEqual(t1, t2) t2 = t2.replace(tzinfo=FixedOffset(None, "")) self.assertEqual(t1, t2) if CMP_BUG_FIXED: t2 = t2.replace(tzinfo=FixedOffset(0, "")) self.assertRaises(TypeError, lambda: t1 == t2) # In datetime w/ identical tzinfo objects, utcoffset is ignored. class Varies(tzinfo): def __init__(self): self.offset = timedelta(minutes=22) def utcoffset(self, t): self.offset += timedelta(minutes=1) return self.offset v = Varies() t1 = t2.replace(tzinfo=v) t2 = t2.replace(tzinfo=v) self.assertEqual(t1.utcoffset(), timedelta(minutes=23)) self.assertEqual(t2.utcoffset(), timedelta(minutes=24)) self.assertEqual(t1, t2) # But if they're not identical, it isn't ignored. t2 = t2.replace(tzinfo=Varies()) self.failUnless(t1 < t2) # t1's offset counter still going up # Pain to set up DST-aware tzinfo classes. def first_sunday_on_or_after(dt): days_to_go = 6 - dt.weekday() if days_to_go: dt += timedelta(days_to_go) return dt ZERO = timedelta(0) HOUR = timedelta(hours=1) DAY = timedelta(days=1) # In the US, DST starts at 2am (standard time) on the first Sunday in April. DSTSTART = datetime(1, 4, 1, 2) # and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct, # which is the first Sunday on or after Oct 25. Because we view 1:MM as # being standard time on that day, there is no spelling in local time of # the last hour of DST (that's 1:MM DST, but 1:MM is taken as standard time). DSTEND = datetime(1, 10, 25, 1) class USTimeZone(tzinfo): def __init__(self, hours, reprname, stdname, dstname): self.stdoffset = timedelta(hours=hours) self.reprname = reprname self.stdname = stdname self.dstname = dstname def __repr__(self): return self.reprname def tzname(self, dt): if self.dst(dt): return self.dstname else: return self.stdname def utcoffset(self, dt): return self.stdoffset + self.dst(dt) def dst(self, dt): if dt is None or dt.tzinfo is None: # An exception instead may be sensible here, in one or more of # the cases. return ZERO assert dt.tzinfo is self # Find first Sunday in April. start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year)) assert start.weekday() == 6 and start.month == 4 and start.day <= 7 # Find last Sunday in October. end = first_sunday_on_or_after(DSTEND.replace(year=dt.year)) assert end.weekday() == 6 and end.month == 10 and end.day >= 25 # Can't compare naive to aware objects, so strip the timezone from # dt first. if start <= dt.replace(tzinfo=None) < end: return HOUR else: return ZERO Eastern = USTimeZone(-5, "Eastern", "EST", "EDT") Central = USTimeZone(-6, "Central", "CST", "CDT") Mountain = USTimeZone(-7, "Mountain", "MST", "MDT") Pacific = USTimeZone(-8, "Pacific", "PST", "PDT") utc_real = FixedOffset(0, "UTC", 0) # For better test coverage, we want another flavor of UTC that's west of # the Eastern and Pacific timezones. utc_fake = FixedOffset(-12*60, "UTCfake", 0) class TestTimezoneConversions(unittest.TestCase): # The DST switch times for 2002, in std time. dston = datetime(2002, 4, 7, 2) dstoff = datetime(2002, 10, 27, 1) theclass = datetime # Check a time that's inside DST. def checkinside(self, dt, tz, utc, dston, dstoff): self.assertEqual(dt.dst(), HOUR) # Conversion to our own timezone is always an identity. self.assertEqual(dt.astimezone(tz), dt) asutc = dt.astimezone(utc) there_and_back = asutc.astimezone(tz) # Conversion to UTC and back isn't always an identity here, # because there are redundant spellings (in local time) of # UTC time when DST begins: the clock jumps from 1:59:59 # to 3:00:00, and a local time of 2:MM:SS doesn't really # make sense then. The classes above treat 2:MM:SS as # daylight time then (it's "after 2am"), really an alias # for 1:MM:SS standard time. The latter form is what # conversion back from UTC produces. if dt.date() == dston.date() and dt.hour == 2: # We're in the redundant hour, and coming back from # UTC gives the 1:MM:SS standard-time spelling. self.assertEqual(there_and_back + HOUR, dt) # Although during was considered to be in daylight # time, there_and_back is not. self.assertEqual(there_and_back.dst(), ZERO) # They're the same times in UTC. self.assertEqual(there_and_back.astimezone(utc), dt.astimezone(utc)) else: # We're not in the redundant hour. self.assertEqual(dt, there_and_back) # Because we have a redundant spelling when DST begins, there is # (unforunately) an hour when DST ends that can't be spelled at all in # local time. When DST ends, the clock jumps from 1:59 back to 1:00 # again. The hour 1:MM DST has no spelling then: 1:MM is taken to be # standard time. 1:MM DST == 0:MM EST, but 0:MM is taken to be # daylight time. The hour 1:MM daylight == 0:MM standard can't be # expressed in local time. Nevertheless, we want conversion back # from UTC to mimic the local clock's "repeat an hour" behavior. nexthour_utc = asutc + HOUR nexthour_tz = nexthour_utc.astimezone(tz) if dt.date() == dstoff.date() and dt.hour == 0: # We're in the hour before the last DST hour. The last DST hour # is ineffable. We want the conversion back to repeat 1:MM. self.assertEqual(nexthour_tz, dt.replace(hour=1)) nexthour_utc += HOUR nexthour_tz = nexthour_utc.astimezone(tz) self.assertEqual(nexthour_tz, dt.replace(hour=1)) else: self.assertEqual(nexthour_tz - dt, HOUR) # Check a time that's outside DST. def checkoutside(self, dt, tz, utc): self.assertEqual(dt.dst(), ZERO) # Conversion to our own timezone is always an identity. self.assertEqual(dt.astimezone(tz), dt) # Converting to UTC and back is an identity too. asutc = dt.astimezone(utc) there_and_back = asutc.astimezone(tz) self.assertEqual(dt, there_and_back) def convert_between_tz_and_utc(self, tz, utc): dston = self.dston.replace(tzinfo=tz) # Because 1:MM on the day DST ends is taken as being standard time, # there is no spelling in tz for the last hour of daylight time. # For purposes of the test, the last hour of DST is 0:MM, which is # taken as being daylight time (and 1:MM is taken as being standard # time). dstoff = self.dstoff.replace(tzinfo=tz) for delta in (timedelta(weeks=13), DAY, HOUR, timedelta(minutes=1), timedelta(microseconds=1)): self.checkinside(dston, tz, utc, dston, dstoff) for during in dston + delta, dstoff - delta: self.checkinside(during, tz, utc, dston, dstoff) self.checkoutside(dstoff, tz, utc) for outside in dston - delta, dstoff + delta: self.checkoutside(outside, tz, utc) def test_easy(self): # Despite the name of this test, the endcases are excruciating. self.convert_between_tz_and_utc(Eastern, utc_real) self.convert_between_tz_and_utc(Pacific, utc_real) self.convert_between_tz_and_utc(Eastern, utc_fake) self.convert_between_tz_and_utc(Pacific, utc_fake) # The next is really dancing near the edge. It works because # Pacific and Eastern are far enough apart that their "problem # hours" don't overlap. self.convert_between_tz_and_utc(Eastern, Pacific) self.convert_between_tz_and_utc(Pacific, Eastern) # OTOH, these fail! Don't enable them. The difficulty is that # the edge case tests assume that every hour is representable in # the "utc" class. This is always true for a fixed-offset tzinfo # class (lke utc_real and utc_fake), but not for Eastern or Central. # For these adjacent DST-aware time zones, the range of time offsets # tested ends up creating hours in the one that aren't representable # in the other. For the same reason, we would see failures in the # Eastern vs Pacific tests too if we added 3*HOUR to the list of # offset deltas in convert_between_tz_and_utc(). # # self.convert_between_tz_and_utc(Eastern, Central) # can't work # self.convert_between_tz_and_utc(Central, Eastern) # can't work def test_tricky(self): # 22:00 on day before daylight starts. fourback = self.dston - timedelta(hours=4) ninewest = FixedOffset(-9*60, "-0900", 0) fourback = fourback.replace(tzinfo=ninewest) # 22:00-0900 is 7:00 UTC == 2:00 EST == 3:00 DST. Since it's "after # 2", we should get the 3 spelling. # If we plug 22:00 the day before into Eastern, it "looks like std # time", so its offset is returned as -5, and -5 - -9 = 4. Adding 4 # to 22:00 lands on 2:00, which makes no sense in local time (the # local clock jumps from 1 to 3). The point here is to make sure we # get the 3 spelling. expected = self.dston.replace(hour=3) got = fourback.astimezone(Eastern).replace(tzinfo=None) self.assertEqual(expected, got) # Similar, but map to 6:00 UTC == 1:00 EST == 2:00 DST. In that # case we want the 1:00 spelling. sixutc = self.dston.replace(hour=6, tzinfo=utc_real) # Now 6:00 "looks like daylight", so the offset wrt Eastern is -4, # and adding -4-0 == -4 gives the 2:00 spelling. We want the 1:00 EST # spelling. expected = self.dston.replace(hour=1) got = sixutc.astimezone(Eastern).replace(tzinfo=None) self.assertEqual(expected, got) # Now on the day DST ends, we want "repeat an hour" behavior. # UTC 4:MM 5:MM 6:MM 7:MM checking these # EST 23:MM 0:MM 1:MM 2:MM # EDT 0:MM 1:MM 2:MM 3:MM # wall 0:MM 1:MM 1:MM 2:MM against these for utc in utc_real, utc_fake: for tz in Eastern, Pacific: first_std_hour = self.dstoff - timedelta(hours=2) # 23:MM # Convert that to UTC. first_std_hour -= tz.utcoffset(None) # Adjust for possibly fake UTC. asutc = first_std_hour + utc.utcoffset(None) # First UTC hour to convert; this is 4:00 when utc=utc_real & # tz=Eastern. asutcbase = asutc.replace(tzinfo=utc) for tzhour in (0, 1, 1, 2): expectedbase = self.dstoff.replace(hour=tzhour) for minute in 0, 30, 59: expected = expectedbase.replace(minute=minute) asutc = asutcbase.replace(minute=minute) astz = asutc.astimezone(tz) self.assertEqual(astz.replace(tzinfo=None), expected) asutcbase += HOUR def test_bogus_dst(self): class ok(tzinfo): def utcoffset(self, dt): return HOUR def dst(self, dt): return HOUR now = self.theclass.now().replace(tzinfo=utc_real) # Doesn't blow up. now.astimezone(ok()) # Does blow up. class notok(ok): def dst(self, dt): return None self.assertRaises(ValueError, now.astimezone, notok()) def test_fromutc(self): self.assertRaises(TypeError, Eastern.fromutc) # not enough args now = datetime.utcnow().replace(tzinfo=utc_real) self.assertRaises(ValueError, Eastern.fromutc, now) # wrong tzinfo now = now.replace(tzinfo=Eastern) # insert correct tzinfo enow = Eastern.fromutc(now) # doesn't blow up self.assertEqual(enow.tzinfo, Eastern) # has right tzinfo member self.assertRaises(TypeError, Eastern.fromutc, now, now) # too many args self.assertRaises(TypeError, Eastern.fromutc, date.today()) # wrong type # Always converts UTC to standard time. class FauxUSTimeZone(USTimeZone): def fromutc(self, dt): return dt + self.stdoffset FEastern = FauxUSTimeZone(-5, "FEastern", "FEST", "FEDT") # UTC 4:MM 5:MM 6:MM 7:MM 8:MM 9:MM # EST 23:MM 0:MM 1:MM 2:MM 3:MM 4:MM # EDT 0:MM 1:MM 2:MM 3:MM 4:MM 5:MM # Check around DST start. start = self.dston.replace(hour=4, tzinfo=Eastern) fstart = start.replace(tzinfo=FEastern) for wall in 23, 0, 1, 3, 4, 5: expected = start.replace(hour=wall) if wall == 23: expected -= timedelta(days=1) got = Eastern.fromutc(start) self.assertEqual(expected, got) expected = fstart + FEastern.stdoffset got = FEastern.fromutc(fstart) self.assertEqual(expected, got) # Ensure astimezone() calls fromutc() too. got = fstart.replace(tzinfo=utc_real).astimezone(FEastern) self.assertEqual(expected, got) start += HOUR fstart += HOUR # Check around DST end. start = self.dstoff.replace(hour=4, tzinfo=Eastern) fstart = start.replace(tzinfo=FEastern) for wall in 0, 1, 1, 2, 3, 4: expected = start.replace(hour=wall) got = Eastern.fromutc(start) self.assertEqual(expected, got) expected = fstart + FEastern.stdoffset got = FEastern.fromutc(fstart) self.assertEqual(expected, got) # Ensure astimezone() calls fromutc() too. got = fstart.replace(tzinfo=utc_real).astimezone(FEastern) self.assertEqual(expected, got) start += HOUR fstart += HOUR def test_suite(): allsuites = [unittest.makeSuite(klass, 'test') for klass in (TestModule, TestTZInfo, TestTimeDelta, TestDateOnly, TestDate, TestDateTime, TestTime, TestTimeTZ, TestDateTimeTZ, TestTimezoneConversions, ) ] return unittest.TestSuite(allsuites) def test_main(): import gc import sys thesuite = test_suite() lastrc = None while True: test_support.run_suite(thesuite) if 1: # change to 0, under a debug build, for some leak detection break gc.collect() if gc.garbage: raise SystemError("gc.garbage not empty after test run: %r" % gc.garbage) if hasattr(sys, 'gettotalrefcount'): thisrc = sys.gettotalrefcount() print >> sys.stderr, '*' * 10, 'total refs:', thisrc, if lastrc: print >> sys.stderr, 'delta:', thisrc - lastrc else: print >> sys.stderr lastrc = thisrc if __name__ == "__main__": test_main()