[Zope3-checkins] SVN: Zope3/trunk/src/pytz/ Updated pytz -- merge
-r 37924:37926
svn+ssh://svn.zope.org/repos/main/Zope3/branch/stub-pytzpickle
Stuart Bishop
stuart at stuartbishop.net
Tue Aug 16 07:38:59 EDT 2005
Log message for revision 37957:
Updated pytz -- merge -r 37924:37926 svn+ssh://svn.zope.org/repos/main/Zope3/branch/stub-pytzpickle
Changed:
U Zope3/trunk/src/pytz/README.txt
U Zope3/trunk/src/pytz/__init__.py
U Zope3/trunk/src/pytz/reference.py
U Zope3/trunk/src/pytz/tests/test_docs.py
U Zope3/trunk/src/pytz/tests/test_tzinfo.py
U Zope3/trunk/src/pytz/tzinfo.py
-=-
Modified: Zope3/trunk/src/pytz/README.txt
===================================================================
--- Zope3/trunk/src/pytz/README.txt 2005-08-16 11:09:08 UTC (rev 37956)
+++ Zope3/trunk/src/pytz/README.txt 2005-08-16 11:38:59 UTC (rev 37957)
@@ -38,7 +38,11 @@
>>> from datetime import datetime, timedelta
>>> from pytz import timezone
>>> utc = timezone('UTC')
+>>> utc.zone
+'UTC'
>>> eastern = timezone('US/Eastern')
+>>> eastern.zone
+'US/Eastern'
>>> fmt = '%Y-%m-%d %H:%M:%S %Z%z'
The preferred way of dealing with times is to always work in UTC,
Modified: Zope3/trunk/src/pytz/__init__.py
===================================================================
--- Zope3/trunk/src/pytz/__init__.py 2005-08-16 11:09:08 UTC (rev 37956)
+++ Zope3/trunk/src/pytz/__init__.py 2005-08-16 11:38:59 UTC (rev 37957)
@@ -1,7 +1,4 @@
-#!/usr/bin/env python
'''
-$Id: __init__.py,v 1.12 2005/02/15 20:21:41 zenzen Exp $
-
datetime.tzinfo timezone definitions generated from the
Olson timezone database:
@@ -12,22 +9,71 @@
'''
# The Olson database has historically been updated about 4 times a year
-OLSON_VERSION = '2005i'
+OLSON_VERSION = '2005k'
VERSION = OLSON_VERSION
#VERSION = OLSON_VERSION + '.2'
+__version__ = OLSON_VERSION
OLSEN_VERSION = OLSON_VERSION # Old releases had this misspelling
-__all__ = ['timezone', 'all_timezones', 'common_timezones', 'utc']
+__all__ = [
+ 'timezone', 'all_timezones', 'common_timezones', 'utc',
+ 'AmbiguousTimeError',
+ ]
import sys, datetime
+from tzinfo import AmbiguousTimeError, unpickler
-from tzinfo import AmbiguousTimeError
+def timezone(zone):
+ ''' Return a datetime.tzinfo implementation for the given timezone
+
+ >>> from datetime import datetime, timedelta
+ >>> utc = timezone('UTC')
+ >>> eastern = timezone('US/Eastern')
+ >>> eastern.zone
+ 'US/Eastern'
+ >>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc)
+ >>> loc_dt = utc_dt.astimezone(eastern)
+ >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
+ >>> loc_dt.strftime(fmt)
+ '2002-10-27 01:00:00 EST (-0500)'
+ >>> (loc_dt - timedelta(minutes=10)).strftime(fmt)
+ '2002-10-27 00:50:00 EST (-0500)'
+ >>> eastern.normalize(loc_dt - timedelta(minutes=10)).strftime(fmt)
+ '2002-10-27 01:50:00 EDT (-0400)'
+ >>> (loc_dt + timedelta(minutes=10)).strftime(fmt)
+ '2002-10-27 01:10:00 EST (-0500)'
+ '''
+ zone = _munge_zone(zone)
+ if zone.upper() == 'UTC':
+ return utc
+ zone_bits = ['zoneinfo'] + zone.split('/')
+ # Load zone's module
+ module_name = '.'.join(zone_bits)
+ try:
+ module = __import__(module_name, globals(), locals())
+ except ImportError:
+ raise KeyError, zone
+ rv = module
+ for bit in zone_bits[1:]:
+ rv = getattr(rv, bit)
+
+ # Return instance from that module
+ rv = getattr(rv, zone_bits[-1])
+ assert type(rv) != type(sys)
+ return rv
+
+
+def _munge_zone(zone):
+ ''' Convert a zone into a string suitable for use as a Python identifier
+ '''
+ return zone.replace('+', '_plus_').replace('-', '_minus_')
+
+
ZERO = datetime.timedelta(0)
HOUR = datetime.timedelta(hours=1)
-# A UTC class.
class UTC(datetime.tzinfo):
"""UTC
@@ -35,7 +81,11 @@
Identical to the reference UTC implementation given in Python docs except
that it unpickles using the single module global instance defined beneath
this class declaration.
+
+ Also contains extra attributes and methods to match other pytz tzinfo
+ instances.
"""
+ zone = "UTC"
def utcoffset(self, dt):
return ZERO
@@ -62,11 +112,15 @@
return dt.replace(tzinfo=self)
def __repr__(self):
- return '<UTC>'
+ return "<UTC>"
+ def __str__(self):
+ return "UTC"
-UTC = utc = UTC()
+UTC = utc = UTC() # UTC is a singleton
+
+
def _UTC():
"""Factory function for utc unpickling.
@@ -99,50 +153,16 @@
return utc
_UTC.__safe_for_unpickling__ = True
-def timezone(zone):
- ''' Return a datetime.tzinfo implementation for the given timezone
-
- >>> from datetime import datetime, timedelta
- >>> utc = timezone('UTC')
- >>> eastern = timezone('US/Eastern')
- >>> eastern.zone
- 'US/Eastern'
- >>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc)
- >>> loc_dt = utc_dt.astimezone(eastern)
- >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
- >>> loc_dt.strftime(fmt)
- '2002-10-27 01:00:00 EST (-0500)'
- >>> (loc_dt - timedelta(minutes=10)).strftime(fmt)
- '2002-10-27 00:50:00 EST (-0500)'
- >>> eastern.normalize(loc_dt - timedelta(minutes=10)).strftime(fmt)
- '2002-10-27 01:50:00 EDT (-0400)'
- >>> (loc_dt + timedelta(minutes=10)).strftime(fmt)
- '2002-10-27 01:10:00 EST (-0500)'
- '''
- zone = _munge_zone(zone)
- if zone.upper() == 'UTC':
- return utc
- zone_bits = ['zoneinfo'] + zone.split('/')
- # Load zone's module
- module_name = '.'.join(zone_bits)
- try:
- module = __import__(module_name, globals(), locals())
- except ImportError:
- raise KeyError, zone
- rv = module
- for bit in zone_bits[1:]:
- rv = getattr(rv, bit)
+def _p(*args):
+ """Factory function for unpickling pytz tzinfo instances.
- # Return instance from that module
- rv = getattr(rv, zone_bits[-1])
- assert type(rv) != type(sys)
- return rv
+ Just a wrapper around tzinfo.unpickler to save a few bytes in each pickle
+ by shortening the path.
+ """
+ return unpickler(*args)
+_p.__safe_for_unpickling__ = True
-def _munge_zone(zone):
- ''' Convert a zone into a string suitable for use as a Python identifier
- '''
- return zone.replace('+', '_plus_').replace('-', '_minus_')
def _test():
import doctest, os, sys
Modified: Zope3/trunk/src/pytz/reference.py
===================================================================
--- Zope3/trunk/src/pytz/reference.py 2005-08-16 11:09:08 UTC (rev 37956)
+++ Zope3/trunk/src/pytz/reference.py 2005-08-16 11:38:59 UTC (rev 37957)
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
'''
$Id: reference.py,v 1.2 2004/10/25 04:14:00 zenzen Exp $
Modified: Zope3/trunk/src/pytz/tests/test_docs.py
===================================================================
--- Zope3/trunk/src/pytz/tests/test_docs.py 2005-08-16 11:09:08 UTC (rev 37956)
+++ Zope3/trunk/src/pytz/tests/test_docs.py 2005-08-16 11:38:59 UTC (rev 37957)
@@ -1,15 +1,35 @@
-#!/usr/bin/env python
# -*- coding: ascii -*-
import unittest, os, os.path, sys
-from zope.testing.doctest import DocFileSuite
-sys.path.insert(0, os.path.join(os.pardir, os.pardir))
+from doctest import DocTestSuite
-README = DocFileSuite('../README.txt')
+# We test the documentation this way instead of using DocFileSuite so
+# we can run the tests under Python 2.3
+def test_README():
+ pass
+this_dir = os.path.dirname(__file__)
+locs = [
+ os.path.join(this_dir, os.pardir, 'README.txt'),
+ os.path.join(this_dir, os.pardir, os.pardir, 'README.txt'),
+ ]
+for loc in locs:
+ if os.path.exists(loc):
+ test_README.__doc__ = open(loc).read()
+ break
+if test_README.__doc__ is None:
+ raise RuntimeError('README.txt not found')
+
+README = DocTestSuite()
+
def test_suite():
+ "For the Z3 test runner"
return README
if __name__ == '__main__':
+ sys.path.insert(0, os.path.normpath(os.path.join(
+ this_dir, os.pardir, os.pardir
+ )))
unittest.main(defaultTest='README')
+
Modified: Zope3/trunk/src/pytz/tests/test_tzinfo.py
===================================================================
--- Zope3/trunk/src/pytz/tests/test_tzinfo.py 2005-08-16 11:09:08 UTC (rev 37956)
+++ Zope3/trunk/src/pytz/tests/test_tzinfo.py 2005-08-16 11:38:59 UTC (rev 37957)
@@ -1,17 +1,15 @@
-#!/usr/bin/env python
# -*- coding: ascii -*-
-'''
-$Id: test_tzinfo.py,v 1.9 2004/10/25 04:14:00 zenzen Exp $
-'''
-__rcs_id__ = '$Id: test_tzinfo.py,v 1.9 2004/10/25 04:14:00 zenzen Exp $'
-__version__ = '$Revision: 1.9 $'[11:-2]
-
import sys, os, os.path
-sys.path.insert(0, os.path.join(os.pardir, os.pardir))
-
import unittest, doctest
+import cPickle as pickle
from datetime import datetime, tzinfo, timedelta
+
+if __name__ == '__main__':
+ # Only munge path if invoked as a script. Testrunners should have setup
+ # the paths already
+ sys.path.insert(0, os.path.join(os.pardir, os.pardir))
+
import pytz
from pytz import reference
@@ -26,6 +24,12 @@
class BasicTest(unittest.TestCase):
+ def testVersion(self):
+ # Ensuring the correct version of pytz has been loaded
+ self.failUnlessEqual('2005k', pytz.__version__,
+ 'Incorrect pytz version loaded. Import path is stuffed.'
+ )
+
def testGMT(self):
now = datetime.now(tz=GMT)
self.failUnless(now.utcoffset() == NOTIME)
@@ -40,6 +44,60 @@
self.failUnless(now.timetuple() == now.utctimetuple())
+class PicklingTest(unittest.TestCase):
+
+ def _roundtrip_tzinfo(self, tz):
+ p = pickle.dumps(tz)
+ unpickled_tz = pickle.loads(p)
+ self.failUnless(tz is unpickled_tz, '%s did not roundtrip' % tz.zone)
+
+ def _roundtrip_datetime(self, dt):
+ # Ensure that the tzinfo attached to a datetime instance
+ # is identical to the one returned. This is important for
+ # DST timezones, as some state is stored in the tzinfo.
+ tz = dt.tzinfo
+ p = pickle.dumps(dt)
+ unpickled_dt = pickle.loads(p)
+ unpickled_tz = unpickled_dt.tzinfo
+ self.failUnless(tz is unpickled_tz, '%s did not roundtrip' % tz.zone)
+
+ def testDst(self):
+ tz = pytz.timezone('Europe/Amsterdam')
+ dt = datetime(2004, 2, 1, 0, 0, 0)
+
+ for localized_tz in tz._tzinfos.values():
+ self._roundtrip_tzinfo(localized_tz)
+ self._roundtrip_datetime(dt.replace(tzinfo=localized_tz))
+
+ def testRoundtrip(self):
+ dt = datetime(2004, 2, 1, 0, 0, 0)
+ for zone in pytz.all_timezones:
+ tz = pytz.timezone(zone)
+ self._roundtrip_tzinfo(tz)
+
+ def testDatabaseFixes(self):
+ # Hack the pickle to make it refer to a timezone abbreviation
+ # that does not match anything. The unpickler should be able
+ # to repair this case
+ tz = pytz.timezone('Australia/Melbourne')
+ p = pickle.dumps(tz)
+ tzname = tz._tzname
+ hacked_p = p.replace(tzname, '???')
+ self.failIfEqual(p, hacked_p)
+ unpickled_tz = pickle.loads(hacked_p)
+ self.failUnless(tz is unpickled_tz)
+
+ # Simulate a database correction. In this case, the incorrect
+ # data will continue to be used.
+ p = pickle.dumps(tz)
+ new_utcoffset = tz._utcoffset.seconds + 42
+ hacked_p = p.replace(str(tz._utcoffset.seconds), str(new_utcoffset))
+ self.failIfEqual(p, hacked_p)
+ unpickled_tz = pickle.loads(hacked_p)
+ self.failUnlessEqual(unpickled_tz._utcoffset.seconds, new_utcoffset)
+ self.failUnless(tz is not unpickled_tz)
+
+
class USEasternDSTStartTestCase(unittest.TestCase):
tzinfo = pytz.timezone('US/Eastern')
@@ -65,10 +123,11 @@
}
def _test_tzname(self, utc_dt, wanted):
+ tzname = wanted['tzname']
dt = utc_dt.astimezone(self.tzinfo)
- self.failUnlessEqual(dt.tzname(),wanted['tzname'],
+ self.failUnlessEqual(dt.tzname(), tzname,
'Expected %s as tzname for %s. Got %s' % (
- wanted['tzname'],str(utc_dt),dt.tzname()
+ tzname, str(utc_dt), dt.tzname()
)
)
@@ -76,26 +135,18 @@
utcoffset = wanted['utcoffset']
dt = utc_dt.astimezone(self.tzinfo)
self.failUnlessEqual(
- dt.utcoffset(),utcoffset,
+ dt.utcoffset(), wanted['utcoffset'],
'Expected %s as utcoffset for %s. Got %s' % (
- utcoffset,utc_dt,dt.utcoffset()
+ utcoffset, utc_dt, dt.utcoffset()
)
)
- return
- dt_wanted = utc_dt.replace(tzinfo=None) + utcoffset
- dt_got = dt.replace(tzinfo=None)
- self.failUnlessEqual(
- dt_wanted,
- dt_got,
- 'Got %s. Wanted %s' % (str(dt_got),str(dt_wanted))
- )
def _test_dst(self, utc_dt, wanted):
dst = wanted['dst']
dt = utc_dt.astimezone(self.tzinfo)
self.failUnlessEqual(dt.dst(),dst,
'Expected %s as dst for %s. Got %s' % (
- dst,utc_dt,dt.dst()
+ dst, utc_dt, dt.dst()
)
)
@@ -391,18 +442,12 @@
suite = unittest.TestSuite()
suite.addTest(doctest.DocTestSuite('pytz'))
suite.addTest(doctest.DocTestSuite('pytz.tzinfo'))
- suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(
- __import__('__main__')
- ))
+ import test_tzinfo
+ suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(test_tzinfo))
return suite
+DEFAULT = test_suite()
+
if __name__ == '__main__':
- suite = test_suite()
- if '-v' in sys.argv:
- runner = unittest.TextTestRunner(verbosity=2)
- else:
- runner = unittest.TextTestRunner()
- runner.run(suite)
+ unittest.main(defaultTest='DEFAULT')
-# vim: set filetype=python ts=4 sw=4 et
-
Modified: Zope3/trunk/src/pytz/tzinfo.py
===================================================================
--- Zope3/trunk/src/pytz/tzinfo.py 2005-08-16 11:09:08 UTC (rev 37956)
+++ Zope3/trunk/src/pytz/tzinfo.py 2005-08-16 11:38:59 UTC (rev 37957)
@@ -1,10 +1,13 @@
-#!/usr/bin/env python
-'''$Id: tzinfo.py,v 1.7 2005/02/15 20:21:52 zenzen Exp $'''
+'''Base classes and helpers for building zone specific tzinfo classes'''
from datetime import datetime, timedelta, tzinfo
from bisect import bisect_right
from sets import Set
+import pytz
+
+__all__ = []
+
_timedelta_cache = {}
def memorized_timedelta(seconds):
'''Create only one instance of each distinct timedelta'''
@@ -41,6 +44,11 @@
_notime = memorized_timedelta(0)
+def _to_seconds(td):
+ '''Convert a timedelta to seconds'''
+ return td.seconds + td.days * 24 * 60 * 60
+
+
class BaseTzInfo(tzinfo):
# Overridden in subclass
_utcoffset = None
@@ -49,14 +57,13 @@
def __str__(self):
return self.zone
-
+
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
-
'''
def fromutc(self, dt):
'''See datetime.tzinfo.fromutc'''
@@ -89,7 +96,12 @@
def __repr__(self):
return '<StaticTzInfo %r>' % (self.zone,)
+ def __reduce__(self):
+ # Special pickle to zone remains a singleton and to cope with
+ # database changes.
+ return pytz._p, (self.zone,)
+
class DstTzInfo(BaseTzInfo):
'''A timezone that has a variable offset from UTC
@@ -293,6 +305,17 @@
self.zone, self._tzname, self._utcoffset, dst
)
+ def __reduce__(self):
+ # Special pickle to zone remains a singleton and to cope with
+ # database changes.
+ return pytz._p, (
+ self.zone,
+ _to_seconds(self._utcoffset),
+ _to_seconds(self._dst),
+ self._tzname
+ )
+
+
class AmbiguousTimeError(Exception):
'''Exception raised when attempting to create an ambiguous wallclock time.
@@ -301,7 +324,56 @@
possibilities may be correct, unless further information is supplied.
See DstTzInfo.normalize() for more info
-
'''
+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
+ upgrade.
+ """
+ # Raises a KeyError if zone no longer exists, which should never happen
+ # and would be a bug.
+ tz = pytz.timezone(zone)
+
+ # A StaticTzInfo - just return it
+ if utcoffset is None:
+ return tz
+
+ # This pickle was created from a DstTzInfo. We need to
+ # determine which of the list of tzinfo instances for this zone
+ # to use in order to restore the state of any datetime instances using
+ # it correctly.
+ utcoffset = memorized_timedelta(utcoffset)
+ dstoffset = memorized_timedelta(dstoffset)
+ try:
+ return tz._tzinfos[(utcoffset, dstoffset, tzname)]
+ except KeyError:
+ # The particular state requested in this timezone no longer exists.
+ # This indicates a corrupt pickle, or the timezone database has been
+ # corrected violently enough to make this particular
+ # (utcoffset,dstoffset) no longer exist in the zone, or the
+ # abbreviation has been changed.
+ pass
+
+ # See if we can find an entry differing only by tzname. Abbreviations
+ # get changed from the initial guess by the database maintainers to
+ # match reality when this information is discovered.
+ for localized_tz in tz._tzinfos.values():
+ if (localized_tz._utcoffset == utcoffset
+ and localized_tz._dst == dstoffset):
+ return localized_tz
+
+ # This (utcoffset, dstoffset) information has been removed from the
+ # zone. Add it back. This might occur when the database maintainers have
+ # corrected incorrect information. datetime instances using this
+ # incorrect information will continue to do so, exactly as they were
+ # before being pickled. This is purely an overly paranoid safety net - I
+ # doubt this will ever been needed in real life.
+ inf = (utcoffset, dstoffset, tzname)
+ tz._tzinfos[inf] = tz.__class__(inf, tz._tzinfos)
+ return tz._tzinfos[inf]
+
More information about the Zope3-Checkins
mailing list