[Python-checkins] python/nondist/sandbox/datetime EU.py,1.5,1.6 US.py,1.18,1.19 datetime.py,1.146,1.147 test_datetime.py,1.99,1.100

tim_one@users.sourceforge.net tim_one@users.sourceforge.net
Tue, 21 Jan 2003 13:19:56 -0800


Update of /cvsroot/python/python/nondist/sandbox/datetime
In directory sc8-pr-cvs1:/tmp/cvs-serv13847

Modified Files:
	EU.py US.py datetime.py test_datetime.py 
Log Message:
Refactoring of, and new rules for, dt.astimezone(tz).

dt must be aware now, and tz.utcoffset() and tz.dst() must not return None.
The old dt.astimezone(None) no longer works to change an aware datetime
into a naive datetime; use dt.replace(tzinfo=None) instead.

The tzinfo base class now supplies a new fromutc(self, dt) method, and
datetime.astimezone(tz) invokes tz.fromutc().  The default implementation
of fromutc() reproduces the same results as the old astimezone()
implementation, but tzinfo subclasses can override fromutc() if the
default implementation isn't strong enough to get the correct results
in all cases (for example, this may be necessary if a tzinfo subclass
models a time zone whose "standard offset" (wrt UTC) changed in some
year(s), or in some variations of double-daylight time -- the creativity
of time zone politics can't be captured in a single default implementation).


Index: EU.py
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/datetime/EU.py,v
retrieving revision 1.5
retrieving revision 1.6
diff -C2 -d -r1.5 -r1.6
*** EU.py	20 Jan 2003 22:06:37 -0000	1.5
--- EU.py	21 Jan 2003 21:19:44 -0000	1.6
***************
*** 70,74 ****
          # in order to compare to the naive dston and dstoff).
          dt -= self.offset
!         if dston <= dt.astimezone(None) < dstoff:
              return HOUR
          else:
--- 70,74 ----
          # in order to compare to the naive dston and dstoff).
          dt -= self.offset
!         if dston <= dt.replace(tzinfo=None) < dstoff:
              return HOUR
          else:

Index: US.py
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/datetime/US.py,v
retrieving revision 1.18
retrieving revision 1.19
diff -C2 -d -r1.18 -r1.19
*** US.py	20 Jan 2003 22:06:38 -0000	1.18
--- US.py	21 Jan 2003 21:19:45 -0000	1.19
***************
*** 84,88 ****
          # Can't compare naive to aware objects, so strip the timezone from
          # dt first.
!         if start <= dt.astimezone(None) < end:
              return self.dstoff
          else:
--- 84,88 ----
          # Can't compare naive to aware objects, so strip the timezone from
          # dt first.
!         if start <= dt.replace(tzinfo=None) < end:
              return self.dstoff
          else:

Index: datetime.py
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/datetime/datetime.py,v
retrieving revision 1.146
retrieving revision 1.147
diff -C2 -d -r1.146 -r1.147
*** datetime.py	20 Jan 2003 22:06:39 -0000	1.146
--- datetime.py	21 Jan 2003 21:19:45 -0000	1.147
***************
*** 878,881 ****
--- 878,913 ----
          raise NotImplementedError("tzinfo subclass must override dst()")
  
+     def _inconsistent_dst(self):
+         raise ValueError("astimezone():  tz.dst() gave "
+                          "inconsistent results; cannot convert")
+ 
+     def fromutc(self, dt):
+         "datetime in UTC -> datetime in local time."
+ 
+         if dt.tzinfo is not self:
+             raise ValueError("dt.tzinfo is not self")
+ 
+         dtoff = dt.utcoffset()
+         if dtoff is None:
+             raise ValueError("fromutc() requires a non-None utcoffset() "
+                              "result")
+ 
+         # See the long comment block at the end of this file for an
+         # explanation of this algorithm.
+         dtdst = dt.dst()
+         if dtdst is None:
+             raise ValueError("fromutc() requires a non_none dst() result")
+         delta = dtoff - dtdst
+         if delta:
+             dt += delta
+             dtdst = dt.dst()
+             if dtdst is None:
+                 raise ValueError("fromutc(): dt.dst gave inconsistent "
+                                  "results; cannot convert")
+         if dtdst:
+             return dt + dtdst
+         else:
+             return dt
+ 
      # pickle support
  
***************
*** 1274,1335 ****
                            microsecond, tzinfo)
  
-     def _inconsistent_dst(self):
-         raise ValueError("astimezone():  tz.dst() gave "
-                          "inconsistent results; cannot convert")
- 
      def astimezone(self, tz):
!         _check_tzinfo_arg(tz)
!         other = self.replace(tzinfo=tz)
! 
!         # Don't call utcoffset unless necessary.  First check trivial cases.
!         if tz is None or self._tzinfo is None or self._tzinfo is tz:
!             return other
! 
!         # Get the offsets.  If either object turns out to be naive, again
!         # there's no conversion of date or time fields.
!         myoff = self.utcoffset()
!         if myoff is None:
!             return other
!         otoff = other.utcoffset()
!         if otoff is None:
!             return other
  
!         # See the long comment block at the end of this file for an
!         # explanation of this algorithm.  That it always works requires a
!         # pretty intricate proof.  There are many equivalent ways to code
!         # up the proof as an algorithm.  This way favors calling dst() over
!         # calling utcoffset(), because "the usual" utcoffset() calls dst()
!         # itself, and calling the latter instead saves a Python-level
!         # function call.  This way of coding it also follow the proof
!         # closely, w/ x=self, y=other, z=other, and z'=another.
!         otdst = other.dst()
!         if otdst is None:
!             raise ValueError("astimezone(): utcoffset() returned a duration "
!                              "but dst() returned None")
!         delta = otoff - otdst - myoff
!         if delta:
!             other += delta
!             otdst = other.dst()
!             if otdst is None:
!                 self._inconsistent_dst()
!         if not otdst:
!             return other
  
!         another = other + otdst
!         return another
  
!         # XXX Leaving this unreachable code here for a while.  It may be
!         # XXX needed again real soon <wink>.
!         anotherdst = another.dst()
!         if anotherdst is None:
!             self._inconsistent_dst()
  
!         if otdst == anotherdst:
!             other = another
!         else:
!             # This is the "unspellable hour" case, and we *don't* want
!             # the DST spelling here.
!             pass
!         return other
  
      # Ways to produce a string.
--- 1306,1328 ----
                            microsecond, tzinfo)
  
      def astimezone(self, tz):
!         if not isinstance(tz, tzinfo):
!             raise TypeError("tz argument must be an instance of tzinfo")
  
!         mytz = self.tzinfo
!         if mytz is None:
!             raise ValueError("astimezone() requires an aware datetime")
  
!         if tz is mytz:
!             return self
  
!         # Convert self to UTC, and attach the new time zone object.
!         myoffset = self.utcoffset()
!         if myoffset is None:
!             raise ValuError("astimezone() requires an aware datetime")
!         utc = (self - myoffset).replace(tzinfo=tz)
  
!         # Convert from UTC to tz's local time.
!         return tz.fromutc(utc)
  
      # Ways to produce a string.

Index: test_datetime.py
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/datetime/test_datetime.py,v
retrieving revision 1.99
retrieving revision 1.100
diff -C2 -d -r1.99 -r1.100
*** test_datetime.py	20 Jan 2003 22:06:41 -0000	1.99
--- test_datetime.py	21 Jan 2003 21:19:46 -0000	1.100
***************
*** 1312,1329 ****
  
      def test_astimezone(self):
!         # Pretty boring!  The TZ test is more interesting here.
          dt = self.theclass.now()
          f = FixedOffset(44, "")
-         for dtz in dt.astimezone(f), dt.astimezone(tz=f):
-             self.failUnless(isinstance(dtz, datetime))
-             self.assertEqual(dt.date(), dtz.date())
-             self.assertEqual(dt.time(), dtz.time())
-             self.failUnless(dtz.tzinfo is f)
-             self.assertEqual(dtz.utcoffset(), timedelta(minutes=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
  
  
  class TestTime(unittest.TestCase):
--- 1312,1336 ----
  
      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):
***************
*** 2442,2456 ****
          dt = self.theclass.now(tzinfo=f44m)
          self.failUnless(dt.tzinfo is f44m)
!         # Replacing with degenerate tzinfo doesn't do any adjustment.
!         for x in dt.astimezone(fnone), dt.astimezone(tz=fnone):
!             self.failUnless(x.tzinfo is fnone)
!             self.assertEqual(x.date(), dt.date())
!             self.assertEqual(x.time(), dt.time())
!         # Ditt with None tz.
!         x = dt.astimezone(tz=None)
!         self.failUnless(x.tzinfo is None)
!         self.assertEqual(x.date(), dt.date())
!         self.assertEqual(x.time(), dt.time())
!         # Ditto replacing with same tzinfo.
          x = dt.astimezone(dt.tzinfo)
          self.failUnless(x.tzinfo is f44m)
--- 2449,2457 ----
          dt = self.theclass.now(tzinfo=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)
***************
*** 2602,2606 ****
          # Can't compare naive to aware objects, so strip the timezone from
          # dt first.
!         if start <= dt.astimezone(None) < end:
              return HOUR
          else:
--- 2603,2607 ----
          # Can't compare naive to aware objects, so strip the timezone from
          # dt first.
!         if start <= dt.replace(tzinfo=None) < end:
              return HOUR
          else:
***************
*** 2629,2634 ****
          # Conversion to our own timezone is always an identity.
          self.assertEqual(dt.astimezone(tz), dt)
-         # Conversion to None is always the same as stripping tzinfo.
-         self.assertEqual(dt.astimezone(None), dt.replace(tzinfo=None))
  
          asutc = dt.astimezone(utc)
--- 2630,2633 ----
***************
*** 2683,2688 ****
          # Conversion to our own timezone is always an identity.
          self.assertEqual(dt.astimezone(tz), dt)
!         # Conversion to None is always the same as stripping tzinfo.
!         self.assertEqual(dt.astimezone(None), dt.replace(tzinfo=None))
  
      def convert_between_tz_and_utc(self, tz, utc):
--- 2682,2690 ----
          # 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):
***************
*** 2736,2740 ****
          fourback = self.dston - timedelta(hours=4)
          ninewest = FixedOffset(-9*60, "-0900", 0)
!         fourback = fourback.astimezone(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.
--- 2738,2742 ----
          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.
***************
*** 2745,2759 ****
          # get the 3 spelling.
          expected = self.dston.replace(hour=3)
!         got = fourback.astimezone(Eastern).astimezone(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).astimezone(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).astimezone(None)
          self.assertEqual(expected, got)
  
--- 2747,2761 ----
          # 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)