summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStuart Bishop <stuart@stuartbishop.net>2009-09-29 03:01:22 +0700
committerStuart Bishop <stuart@stuartbishop.net>2009-09-29 03:01:22 +0700
commitb27e3755e8d2773bc78e36cc0efcb315501f7466 (patch)
treeccacece83f973db7838399bf3a29437b6b4812df
parent71efe45ecca5ff51869dddfae1db895e620dd4f0 (diff)
parent973fb5fb04143fd42986402f20433be69fca5aa6 (diff)
downloadpytz-b27e3755e8d2773bc78e36cc0efcb315501f7466.tar.gz
Fix test suite failures and edge cases not being tested for
-rw-r--r--gen_tests.py6
-rw-r--r--gen_tzinfo.py4
-rw-r--r--src/pytz/tests/test_tzinfo.py138
-rw-r--r--src/pytz/tzfile.py15
-rw-r--r--src/pytz/tzinfo.py36
-rw-r--r--test_zdump.py96
6 files changed, 227 insertions, 68 deletions
diff --git a/gen_tests.py b/gen_tests.py
index 14d8512..a195c1c 100644
--- a/gen_tests.py
+++ b/gen_tests.py
@@ -23,7 +23,11 @@ def main():
print 'Collecting zdump(1) output for %s in zdump.out' % (zone,)
tname = zone.replace(
'+', '_plus_').replace('-', '_minus_').replace('/','_')
- zd_out, zd_in = popen2.popen2('%s -v -c 1800,2038 %s' % (zdump, zone))
+ # We don't yet support v2 format tzfile(5) files, so limit
+ # the daterange we test against - zdump understands v2 format
+ # files and will output historical records we can't cope with
+ # otherwise.
+ zd_out, zd_in = popen2.popen2('%s -v -c 1902,2038 %s' % (zdump, zone))
zd_in.close()
# Skip bogus output on 64bit architectures, per Bug #213816
lines = [
diff --git a/gen_tzinfo.py b/gen_tzinfo.py
index ddeb7e5..5db2792 100644
--- a/gen_tzinfo.py
+++ b/gen_tzinfo.py
@@ -27,7 +27,7 @@ def allzones():
])
stripnum = len(os.path.commonprefix(zones))
zones = [z[stripnum:] for z in zones]
-
+
if target:
wanted = target + ['US/Eastern', 'UTC']
zones = [z for z in zones if z in wanted]
@@ -128,7 +128,7 @@ def add_allzones(filename):
def main(destdir):
_destdir = os.path.join(os.path.abspath(destdir), 'dist')
-
+
dupe_src(_destdir)
add_allzones(os.path.join(_destdir, 'pytz', '__init__.py'))
diff --git a/src/pytz/tests/test_tzinfo.py b/src/pytz/tests/test_tzinfo.py
index d79b26f..e9d2b7b 100644
--- a/src/pytz/tests/test_tzinfo.py
+++ b/src/pytz/tests/test_tzinfo.py
@@ -26,6 +26,22 @@ NOTIME = timedelta(0)
UTC = pytz.timezone('UTC')
GMT = pytz.timezone('GMT')
+
+def prettydt(dt):
+ """datetime as a string using a known format.
+
+ We don't use strftime as it doesn't handle years earlier than 1900
+ per http://bugs.python.org/issue1777412
+ """
+ if dt.utcoffset() >= timedelta(0):
+ offset = '+%s' % (dt.utcoffset(),)
+ else:
+ offset = '-%s' % (-1 * dt.utcoffset(),)
+ return '%04d-%02d-%02d %02d:%02d:%02d %s %s' % (
+ dt.year, dt.month, dt.day,
+ dt.hour, dt.minute, dt.second,
+ dt.tzname(), offset)
+
class BasicTest(unittest.TestCase):
def testVersion(self):
@@ -194,13 +210,15 @@ class USEasternDSTStartTestCase(unittest.TestCase):
# Make sure arithmetic crossing DST boundaries ends
# up in the correct timezone after normalization
+ utc_plus_delta = (utc_dt + delta).astimezone(self.tzinfo)
+ local_plus_delta = self.tzinfo.normalize(dt + delta)
self.failUnlessEqual(
- (utc_dt + delta).astimezone(self.tzinfo).strftime(fmt),
- self.tzinfo.normalize(dt + delta).strftime(fmt),
+ prettydt(utc_plus_delta),
+ prettydt(local_plus_delta),
'Incorrect result for delta==%d days. Wanted %r. Got %r'%(
days,
- (utc_dt + delta).astimezone(self.tzinfo).strftime(fmt),
- self.tzinfo.normalize(dt + delta).strftime(fmt),
+ prettydt(utc_plus_delta),
+ prettydt(local_plus_delta),
)
)
@@ -340,7 +358,6 @@ class VilniusCESTStartTestCase(USEasternDSTStartTestCase):
# causing the clocks to go backwards for this summer time
# switchover.
tzinfo = pytz.timezone('Europe/Vilnius')
- instant = timedelta(seconds=31)
transition_time = datetime(1941, 6, 23, 21, 00, 00, tzinfo=UTC)
before = {
'tzname': 'MSK',
@@ -354,6 +371,117 @@ class VilniusCESTStartTestCase(USEasternDSTStartTestCase):
}
+class LondonHistoryStartTestCase(USEasternDSTStartTestCase):
+ # The first known timezone transition in London was in 1847 when
+ # clocks where synchronized to GMT. However, we currently only
+ # understand v1 format tzfile(5) files which does handle years
+ # this far in the past, so our earliest known transition is in
+ # 1916.
+ tzinfo = pytz.timezone('Europe/London')
+ # transition_time = datetime(1847, 12, 1, 1, 15, 00, tzinfo=UTC)
+ # before = {
+ # 'tzname': 'LMT',
+ # 'utcoffset': timedelta(minutes=-75),
+ # 'dst': timedelta(0),
+ # }
+ # after = {
+ # 'tzname': 'GMT',
+ # 'utcoffset': timedelta(0),
+ # 'dst': timedelta(0),
+ # }
+ transition_time = datetime(1916, 5, 21, 2, 00, 00, tzinfo=UTC)
+ before = {
+ 'tzname': 'GMT',
+ 'utcoffset': timedelta(0),
+ 'dst': timedelta(0),
+ }
+ after = {
+ 'tzname': 'BST',
+ 'utcoffset': timedelta(hours=1),
+ 'dst': timedelta(hours=1),
+ }
+
+
+class LondonHistoryEndTestCase(USEasternDSTStartTestCase):
+ # Timezone switchovers are projected into the future, even
+ # though no official statements exist or could be believed even
+ # if they did exist. We currently only check the last known
+ # transition in 2037, as we are still using v1 format tzfile(5)
+ # files.
+ tzinfo = pytz.timezone('Europe/London')
+ # transition_time = datetime(2499, 10, 25, 1, 0, 0, tzinfo=UTC)
+ transition_time = datetime(2037, 10, 25, 1, 0, 0, tzinfo=UTC)
+ before = {
+ 'tzname': 'BST',
+ 'utcoffset': timedelta(hours=1),
+ 'dst': timedelta(hours=1),
+ }
+ after = {
+ 'tzname': 'GMT',
+ 'utcoffset': timedelta(0),
+ 'dst': timedelta(0),
+ }
+
+
+class NoumeaHistoryStartTestCase(USEasternDSTStartTestCase):
+ # Noumea adopted a whole hour offset in 1912. Previously
+ # it was 11 hours, 5 minutes and 48 seconds off UTC. However,
+ # due to limitations of the Python datetime library, we need
+ # to round that to 11 hours 6 minutes.
+ tzinfo = pytz.timezone('Pacific/Noumea')
+ transition_time = datetime(1912, 1, 12, 12, 54, 12, tzinfo=UTC)
+ before = {
+ 'tzname': 'LMT',
+ 'utcoffset': timedelta(hours=11, minutes=6),
+ 'dst': timedelta(0),
+ }
+ after = {
+ 'tzname': 'NCT',
+ 'utcoffset': timedelta(hours=11),
+ 'dst': timedelta(0),
+ }
+
+
+class NoumeaDSTEndTestCase(USEasternDSTStartTestCase):
+ # Noumea dropped DST in 1997.
+ tzinfo = pytz.timezone('Pacific/Noumea')
+ transition_time = datetime(1997, 3, 1, 15, 00, 00, tzinfo=UTC)
+ before = {
+ 'tzname': 'NCST',
+ 'utcoffset': timedelta(hours=12),
+ 'dst': timedelta(hours=1),
+ }
+ after = {
+ 'tzname': 'NCT',
+ 'utcoffset': timedelta(hours=11),
+ 'dst': timedelta(0),
+ }
+
+
+class NoumeaNoMoreDSTTestCase(NoumeaDSTEndTestCase):
+ # Noumea dropped DST in 1997. Here we test that it stops occuring.
+ transition_time = (
+ NoumeaDSTEndTestCase.transition_time + timedelta(days=365*10))
+ before = NoumeaDSTEndTestCase.after
+ after = NoumeaDSTEndTestCase.after
+
+
+class TahitiTestCase(USEasternDSTStartTestCase):
+ # Tahiti has had a single transition in its history.
+ tzinfo = pytz.timezone('Pacific/Tahiti')
+ transition_time = datetime(1912, 10, 1, 9, 58, 16, tzinfo=UTC)
+ before = {
+ 'tzname': 'LMT',
+ 'utcoffset': timedelta(hours=-9, minutes=-58),
+ 'dst': timedelta(0),
+ }
+ after = {
+ 'tzname': 'TAHT',
+ 'utcoffset': timedelta(hours=-10),
+ 'dst': timedelta(0),
+ }
+
+
class ReferenceUSEasternDSTStartTestCase(USEasternDSTStartTestCase):
tzinfo = reference.Eastern
def test_arithmetic(self):
diff --git a/src/pytz/tzfile.py b/src/pytz/tzfile.py
index b5f818f..7ea00c1 100644
--- a/src/pytz/tzfile.py
+++ b/src/pytz/tzfile.py
@@ -12,12 +12,12 @@ from pytz.tzinfo import memorized_datetime, memorized_timedelta
def build_tzinfo(zone, fp):
- head_fmt = '>4s 16x 6l'
+ head_fmt = '>4s c 15x 6l'
head_size = calcsize(head_fmt)
- (magic,ttisgmtcnt,ttisstdcnt,leapcnt,
- timecnt,typecnt,charcnt) = unpack(head_fmt, fp.read(head_size))
+ (magic, format, ttisgmtcnt, ttisstdcnt,leapcnt, timecnt,
+ typecnt, charcnt) = unpack(head_fmt, fp.read(head_size))
- # Make sure it is a tzinfo(5) file
+ # Make sure it is a tzfile(5) file
assert magic == 'TZif'
# Read out the transition times, localtime indices and ttinfo structures.
@@ -84,12 +84,13 @@ def build_tzinfo(zone, fp):
break
dst = inf[0] - prev_inf[0] # dst offset
- if dst < 0: # Negative dst? Look further.
+ if dst <= 0: # Bad dst? Look further.
for j in range(i+1, len(transitions)):
stdinf = ttinfo[lindexes[j]]
if not stdinf[1]:
- break # Found std time.
- dst = inf[0] - stdinf[0]
+ dst = inf[0] - stdinf[0]
+ if dst > 0:
+ break # Found a useful std time.
tzname = inf[2]
diff --git a/src/pytz/tzinfo.py b/src/pytz/tzinfo.py
index 55afcb2..f31dc90 100644
--- a/src/pytz/tzinfo.py
+++ b/src/pytz/tzinfo.py
@@ -68,13 +68,13 @@ class BaseTzInfo(tzinfo):
class StaticTzInfo(BaseTzInfo):
'''A timezone that has a constant offset from UTC
- These timezones are rare, as most regions have changed their
- offset from UTC at some point in their history
+ These timezones are rare, as most locations have changed their
+ offset at some point in their history
'''
def fromutc(self, dt):
'''See datetime.tzinfo.fromutc'''
return (dt + self._utcoffset).replace(tzinfo=self)
-
+
def utcoffset(self,dt):
'''See datetime.tzinfo.utcoffset'''
return self._utcoffset
@@ -110,11 +110,10 @@ class StaticTzInfo(BaseTzInfo):
class DstTzInfo(BaseTzInfo):
'''A timezone that has a variable offset from UTC
-
- The offset might change if daylight savings time comes into effect,
- or at a point in history when the region decides to change their
- timezone definition.
+ The offset might change if daylight savings time comes into effect,
+ or at a point in history when the region decides to change their
+ timezone definition.
'''
# Overridden in subclass
_utc_transition_times = None # Sorted list of DST transition times in UTC
@@ -180,7 +179,6 @@ class DstTzInfo(BaseTzInfo):
>>> before = eastern.normalize(before)
>>> before.strftime(fmt)
'2002-10-27 01:50:00 EDT (-0400)'
-
'''
if dt.tzinfo is None:
raise ValueError, 'Naive time - no tzinfo set'
@@ -194,13 +192,13 @@ class DstTzInfo(BaseTzInfo):
def localize(self, dt, is_dst=False):
'''Convert naive time to local time.
-
+
This method should be used to construct localtimes, rather
than passing a tzinfo argument to a datetime constructor.
is_dst is used to determine the correct timezone in the ambigous
period at the end of daylight savings time.
-
+
>>> from pytz import timezone
>>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
>>> amdam = timezone('Europe/Amsterdam')
@@ -223,7 +221,7 @@ class DstTzInfo(BaseTzInfo):
AmbiguousTimeError: 2004-10-31 02:00:00
is_dst defaults to False
-
+
>>> amdam.localize(dt) == amdam.localize(dt, False)
True
@@ -252,12 +250,14 @@ class DstTzInfo(BaseTzInfo):
if dt.tzinfo is not None:
raise ValueError, 'Not naive datetime (tzinfo is already set)'
- # Find the possibly correct timezones. We probably just have one,
- # but we might end up with two if we are in the end-of-DST
- # transition period. Or possibly more in some particularly confused
- # location...
+ # Find the two best possibilities.
possible_loc_dt = set()
- for tzinfo in self._tzinfos.values():
+ for delta in [timedelta(days=-1), timedelta(days=1)]:
+ loc_dt = dt + delta
+ idx = max(0, bisect_right(
+ self._utc_transition_times, loc_dt) - 1)
+ inf = self._transition_info[idx]
+ tzinfo = self._tzinfos[inf]
loc_dt = tzinfo.normalize(dt.replace(tzinfo=tzinfo))
if loc_dt.replace(tzinfo=None) == dt:
possible_loc_dt.add(loc_dt)
@@ -324,7 +324,7 @@ class DstTzInfo(BaseTzInfo):
)
filtered_possible_loc_dt.sort(mycmp)
return filtered_possible_loc_dt[0]
-
+
def utcoffset(self, dt):
'''See datetime.tzinfo.utcoffset'''
return self._utcoffset
@@ -388,7 +388,7 @@ class NonExistentTimeError(InvalidTimeError):
def unpickler(zone, utcoffset=None, dstoffset=None, tzname=None):
"""Factory function for unpickling pytz tzinfo instances.
-
+
This is shared for both StaticTzInfo and DstTzInfo instances, because
database changes could cause a zones implementation to switch between
these two base classes and we can't break pickles on a pytz version
diff --git a/test_zdump.py b/test_zdump.py
index 0cb91ba..c815a28 100644
--- a/test_zdump.py
+++ b/test_zdump.py
@@ -19,7 +19,6 @@ class ZdumpTestCase(unittest.TestCase):
loc_dt.replace(tzinfo=None))
def local_to_utc_check(self, zone, utc_dt, loc_dt, loc_tzname, is_dst):
- loc_tz = pytz.timezone(zone)
self.failUnlessEqual(
loc_dt.astimezone(pytz.utc).replace(tzinfo=None),
utc_dt.replace(tzinfo=None))
@@ -29,19 +28,18 @@ def test_suite():
testcases = []
raw_data = open(
os.path.join(os.path.dirname(__file__), 'zdump.out'), 'r').readlines()
- raw_data.reverse() # Keep tests running in alphabetical order
last_zone = None
test_class = None
- for line in raw_data:
- m = re.match(
- r'^([^\s]+) \s+ (.+) \s UTC \s+ = \s+ (.+) \s ([^\s]+) \s+ '
- r'isdst=(0|1)$',
- line, re.X
- )
- if m:
- zone, utc_string, loc_string, tzname, is_dst = m.groups()
- else:
+ zdump_line_re = re.compile(r'''(?x)
+ ^([^\s]+) \s+ (.+) \s UTC \s+ = \s+ (.+) \s ([^\s]+) \s+ isdst=(0|1)$
+ ''')
+ for i in range(0, len(raw_data)):
+ line = raw_data[i]
+ m = zdump_line_re.search(line)
+ if m is None:
raise RuntimeError, 'Dud line %r' % (line,)
+ zone, utc_string, loc_string, tzname, is_dst = m.groups()
+ is_dst = bool(int(is_dst))
if zone != last_zone:
classname = zone.replace(
@@ -49,31 +47,49 @@ def test_suite():
test_class = type(classname, (ZdumpTestCase,), {})
testcases.append(test_class)
last_zone = zone
- prev_loc_dt = None
- prev_is_dst = False
+ skip_next_local = False
utc_dt = datetime(
*strptime(utc_string, '%a %b %d %H:%M:%S %Y')[:6])
loc_dt = datetime(
*strptime(loc_string, '%a %b %d %H:%M:%S %Y')[:6])
- # Urgh - utcoffset() and dst() have to be rounded to the nearest
- # minute, so we need to break our tests to match this limitation
- real_offset = loc_dt - utc_dt
- secs = real_offset.seconds + real_offset.days*86400
- fake_offset = timedelta(seconds=int((secs+30)/60)*60)
- loc_dt = utc_dt + fake_offset
+ def round_dt(loc_dt, utc_dt):
+ # Urgh - utcoffset() and dst() have to be rounded to the nearest
+ # minute, so we need to break our tests to match this limitation
+ real_offset = loc_dt - utc_dt
+ secs = real_offset.seconds + real_offset.days*86400
+ fake_offset = timedelta(seconds=int((secs+30)/60)*60)
+ return utc_dt + fake_offset
+
+ loc_dt = round_dt(loc_dt, utc_dt)
- # If the naive time on the previous line is greater than on this
+ # If the naive time on the next line is less than on this
# line, and we arn't seeing an end-of-dst transition, then
- # we can't do our local->utc test since we are in an ambiguous
- # time period (ie. we have wound back the clock but don't have
- # differing is_dst flags to resolve the ambiguity)
- skip_local = (
- prev_loc_dt is not None and prev_loc_dt > loc_dt and
- bool(prev_is_dst) == bool(is_dst))
- prev_loc_dt = loc_dt
- prev_is_dst = is_dst
+ # we can't do our local->utc tests for either this nor the
+ # next line since we are in an ambiguous time period (ie.
+ # we have wound back the clock but don't have differing
+ # is_dst flags to resolve the ambiguity)
+ skip_local = skip_next_local
+ skip_next_local = False
+ try:
+ m = zdump_line_re.match(raw_data[i+1])
+ except IndexError:
+ m = None
+ if m is not None:
+ (next_zone, next_utc_string, next_loc_string,
+ next_tzname, next_is_dst) = m.groups()
+ next_is_dst = bool(int(next_is_dst))
+ if next_zone == zone and next_is_dst == is_dst:
+ next_utc_dt = datetime(
+ *strptime(next_utc_string, '%a %b %d %H:%M:%S %Y')[:6])
+ next_loc_dt = round_dt(
+ datetime(*strptime(
+ next_loc_string, '%a %b %d %H:%M:%S %Y')[:6]),
+ next_utc_dt)
+ if next_loc_dt <= loc_dt:
+ skip_local = True
+ skip_next_local = True
loc_tz = pytz.timezone(zone)
loc_dt = loc_tz.localize(loc_dt, is_dst)
@@ -83,22 +99,32 @@ def test_suite():
test_name = 'test_utc_to_local_%04d_%02d_%02d_%02d_%02d_%02d' % (
utc_dt.year, utc_dt.month, utc_dt.day,
utc_dt.hour, utc_dt.minute, utc_dt.second)
- def test_utc_to_local(self):
+ def test_utc_to_local(
+ self, zone=zone, utc_dt=utc_dt, loc_dt=loc_dt, tzname=tzname,
+ is_dst=is_dst):
self.utc_to_local_check(zone, utc_dt, loc_dt, tzname, is_dst)
test_utc_to_local.__name__ = test_name
- #test_utc_to_local.__doc__ = line
setattr(test_class, test_name, test_utc_to_local)
if not skip_local:
test_name = 'test_local_to_utc_%04d_%02d_%02d_%02d_%02d_%02d' % (
- utc_dt.year, utc_dt.month, utc_dt.day,
- utc_dt.hour, utc_dt.minute, utc_dt.second)
- def test_local_to_utc(self):
- self.utc_to_local_check(zone, utc_dt, loc_dt, tzname, is_dst)
+ loc_dt.year, loc_dt.month, loc_dt.day,
+ loc_dt.hour, loc_dt.minute, loc_dt.second)
+ if is_dst:
+ test_name += '_dst'
+ else:
+ test_name += '_nodst'
+ def test_local_to_utc(
+ self, zone=zone, utc_dt=utc_dt, loc_dt=loc_dt, tzname=tzname,
+ is_dst=is_dst):
+ self.local_to_utc_check(zone, utc_dt, loc_dt, tzname, is_dst)
test_local_to_utc.__name__ = test_name
- #test_local_to_utc.__doc__ = line
setattr(test_class, test_name, test_local_to_utc)
+ classname = zone.replace(
+ '+', '_plus_').replace('-', '_minus_').replace('/','_')
+ test_class = type(classname, (ZdumpTestCase,), {})
+ testcases.append(test_class)
suite = unittest.TestSuite()
while testcases: