[Zope3-checkins] SVN: Zope3/branches/Zope-3.1/src/pytz/ Updated pytz -- merge -r 37924:37926 svn+ssh://svn.zope.org/repos/main/Zope3/branches/stub-pytzpickle

Stuart Bishop stuart at stuartbishop.net
Tue Aug 16 07:55:11 EDT 2005


Log message for revision 37960:
  Updated pytz -- merge -r 37924:37926 svn+ssh://svn.zope.org/repos/main/Zope3/branches/stub-pytzpickle

Changed:
  U   Zope3/branches/Zope-3.1/src/pytz/README.txt
  U   Zope3/branches/Zope-3.1/src/pytz/__init__.py
  U   Zope3/branches/Zope-3.1/src/pytz/reference.py
  U   Zope3/branches/Zope-3.1/src/pytz/tests/test_docs.py
  U   Zope3/branches/Zope-3.1/src/pytz/tests/test_tzinfo.py
  U   Zope3/branches/Zope-3.1/src/pytz/tzinfo.py

-=-
Modified: Zope3/branches/Zope-3.1/src/pytz/README.txt
===================================================================
--- Zope3/branches/Zope-3.1/src/pytz/README.txt	2005-08-16 11:52:21 UTC (rev 37959)
+++ Zope3/branches/Zope-3.1/src/pytz/README.txt	2005-08-16 11:55:10 UTC (rev 37960)
@@ -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/branches/Zope-3.1/src/pytz/__init__.py
===================================================================
--- Zope3/branches/Zope-3.1/src/pytz/__init__.py	2005-08-16 11:52:21 UTC (rev 37959)
+++ Zope3/branches/Zope-3.1/src/pytz/__init__.py	2005-08-16 11:55:10 UTC (rev 37960)
@@ -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/branches/Zope-3.1/src/pytz/reference.py
===================================================================
--- Zope3/branches/Zope-3.1/src/pytz/reference.py	2005-08-16 11:52:21 UTC (rev 37959)
+++ Zope3/branches/Zope-3.1/src/pytz/reference.py	2005-08-16 11:55:10 UTC (rev 37960)
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
 '''
 $Id: reference.py,v 1.2 2004/10/25 04:14:00 zenzen Exp $
 

Modified: Zope3/branches/Zope-3.1/src/pytz/tests/test_docs.py
===================================================================
--- Zope3/branches/Zope-3.1/src/pytz/tests/test_docs.py	2005-08-16 11:52:21 UTC (rev 37959)
+++ Zope3/branches/Zope-3.1/src/pytz/tests/test_docs.py	2005-08-16 11:55:10 UTC (rev 37960)
@@ -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/branches/Zope-3.1/src/pytz/tests/test_tzinfo.py
===================================================================
--- Zope3/branches/Zope-3.1/src/pytz/tests/test_tzinfo.py	2005-08-16 11:52:21 UTC (rev 37959)
+++ Zope3/branches/Zope-3.1/src/pytz/tests/test_tzinfo.py	2005-08-16 11:55:10 UTC (rev 37960)
@@ -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/branches/Zope-3.1/src/pytz/tzinfo.py
===================================================================
--- Zope3/branches/Zope-3.1/src/pytz/tzinfo.py	2005-08-16 11:52:21 UTC (rev 37959)
+++ Zope3/branches/Zope-3.1/src/pytz/tzinfo.py	2005-08-16 11:55:10 UTC (rev 37960)
@@ -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