[Python-checkins] python/nondist/sandbox/datetime EU.py,1.2,1.3 US.py,1.13,1.14 datetime.py,1.129,1.130 test_datetime.py,1.88,1.89

tim_one@users.sourceforge.net tim_one@users.sourceforge.net
Mon, 30 Dec 2002 21:58:05 -0800


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

Modified Files:
	EU.py US.py datetime.py test_datetime.py 
Log Message:
A new, and much hairier, implementation of astimezone(), building on
an idea from Guido.  This restores that the datetime implementation
never passes a datetime d to a tzinfo method unless d.tzinfo is the
tzinfo instance whose method is being called.  That in turn allows
enormous simplifications in user-written tzinfo classes (see the Python
sandbox US.py and EU.py for fully fleshed-out examples).

d.astimezone(tz) also raises ValueError now if d lands in the one hour
of the year that can't be expressed in tz (this can happen iff tz models
both standard and daylight time).  That it used to return a nonsense
result always ate at me, and it turned out that it seemed impossible to
force a consistent nonsense result under the new implementation (which
doesn't know anything about how tzinfo classes implement their methods --
it can only infer properties indirectly).

Doc changes will have to wait for tomorrow.  Ditto getting the C
implementation back in synch.


Index: EU.py
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/datetime/EU.py,v
retrieving revision 1.2
retrieving revision 1.3
diff -C2 -d -r1.2 -r1.3
*** EU.py	28 Dec 2002 18:16:46 -0000	1.2
--- EU.py	31 Dec 2002 05:58:02 -0000	1.3
***************
*** 20,27 ****
  from dateutil import MARCH, OCTOBER, SUNDAY, weekday_of_month
  
- DAY = timedelta(days=1)
  HOUR = timedelta(hours=1)
  ZERO = timedelta()
! ONE_AM = time(1)
  
  class Fixed(tzinfo):
--- 20,30 ----
  from dateutil import MARCH, OCTOBER, SUNDAY, weekday_of_month
  
  HOUR = timedelta(hours=1)
  ZERO = timedelta()
! 
! # The switches are at 1AM UTC on the last Sundays in March and
! # October.
! _dston = datetime(1, MARCH, 1, 1)
! _dstoff = datetime(1, OCTOBER, 1, 1)
  
  class Fixed(tzinfo):
***************
*** 57,78 ****
  
      def dst(self, dt):
!         dston = weekday_of_month(SUNDAY, date(dt.year, MARCH, 1), -1)
!         dstoff = weekday_of_month(SUNDAY, date(dt.year, OCTOBER, 1), -1)
!         d = dt.date()
!         # Get a quick result on dates not near a DST switch:
!         if dston < d < dstoff - DAY:
!             return HOUR
!         if d < dston - DAY or dstoff < d:
              return ZERO
!         # Need to be more careful in edge cases:
!         dston = datetime.combine(dston, ONE_AM)
!         dstoff = datetime.combine(dstoff, ONE_AM)
!         if dt.tzinfo is self:
!             # Can use self.offset
!             d = datetime.combine(dt.date(), dt.time()) - self.offset
!         else:
!             # Can call dt.tzoffset() without risking recursion
!             d = datetime.combine(dt.date(), dt.time()) - dt.utcoffset()
!         if dston <= d < dstoff:
              return HOUR
          else:
--- 60,74 ----
  
      def dst(self, dt):
!         if dt is None or dt.tzinfo is None:
              return ZERO
!         assert dt.tzinfo is self
!         dston = _dston.replace(year=dt.year)
!         dstoff = _dstoff.replace(year=dt.year)
!         dston = weekday_of_month(SUNDAY, dston,  -1)
!         dstoff = weekday_of_month(SUNDAY, dstoff, -1)
!         # Convert dt to a naive UTC too (we have to strip the tzinfo member
!         # in order to compare to the naive dston and dstoff).
!         dt -= self.offset
!         if dston <= dt.astimezone(None) < dstoff:
              return HOUR
          else:

Index: US.py
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/datetime/US.py,v
retrieving revision 1.13
retrieving revision 1.14
diff -C2 -d -r1.13 -r1.14
*** US.py	30 Dec 2002 19:43:21 -0000	1.13
--- US.py	31 Dec 2002 05:58:02 -0000	1.14
***************
*** 73,85 ****
              return self.zero
  
!         convert_endpoints_to_utc = False
!         if dt.tzinfo is not self:
!             # Convert dt to UTC.
!             offset = dt.utcoffset()
!             if offset is None:
!                 # Again, an exception instead may be sensible.
!                 return self.zero
!             convert_endpoints_to_utc = True
!             dt -= offset
  
          # Find first Sunday in April.
--- 73,77 ----
              return self.zero
  
!         assert dt.tzinfo is self
  
          # Find first Sunday in April.
***************
*** 91,98 ****
          assert end.weekday() == 6 and end.month == 10 and end.day >= 25
  
-         if convert_endpoints_to_utc:
-             start -= self.stdoff    # start is in std time
-             end -= self.stdoff + self.dstoff # end is in DST time
- 
          # Can't compare naive to aware objects, so strip the timezone from
          # dt first.
--- 83,86 ----
***************
*** 219,243 ****
  Sun Oct 27 06:00:00 2002
  
! What happens when we convert that to Eastern?
  
  >>> paradox = phantom.astimezone(Eastern)
! >>> printstuff(paradox)
! 2002-10-27 01:00:00-04:00
! EDT
! (2002, 10, 27, 1, 0, 0, 6, 300, 1)
! Sun Oct 27 01:00:00 2002
! 
! We get *something*, of course, but converting it back to UTC isn't an
! identity (not because of a bug in Eastern, but because this particular
! UTC time simply has no spelling in Eastern:  6:MM:SS UTC would be
! 1:MM:SS in EST or 2:MM:SS in EDT, but Eastern takes 1:MM:SS as being
! daylight and 2:MM:SS as being standard on this day):
! 
! >>> printstuff(paradox.astimezone(utc))
! 2002-10-27 05:00:00+00:00
! utc
! (2002, 10, 27, 5, 0, 0, 6, 300, 0)
! Sun Oct 27 05:00:00 2002
! """
  
  __test__ = {'brainbuster': brainbuster_test}
--- 207,218 ----
  Sun Oct 27 06:00:00 2002
  
! What happens when we convert that to Eastern?  astimezone detects the
! impossibilty of the task, and raises an exception.
  
  >>> paradox = phantom.astimezone(Eastern)
! Traceback (most recent call last):
! ...
! ValueError: astimezone():  the source datetimetz can't be expressed in the target timezone's local time
!  """
  
  __test__ = {'brainbuster': brainbuster_test}

Index: datetime.py
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/datetime/datetime.py,v
retrieving revision 1.129
retrieving revision 1.130
diff -C2 -d -r1.129 -r1.130
*** datetime.py	30 Dec 2002 19:43:21 -0000	1.129
--- datetime.py	31 Dec 2002 05:58:02 -0000	1.130
***************
*** 1523,1526 ****
--- 1523,1527 ----
  datetime.resolution = timedelta(microseconds=1)
  
+ _HOUR = timedelta(hours=1)
  
  class datetimetz(datetime):
***************
*** 1613,1629 ****
                            microsecond, tzinfo)
  
      def astimezone(self, tz):
          _check_tzinfo_arg(tz)
!         # Don't call utcoffset unless it's necessary.
!         if tz is not None:
!             offset = self.utcoffset()
!             if offset is not None:
!                 newoffset = tz.utcoffset(self)
!                 if newoffset is not None:
!                     if not isinstance(newoffset, timedelta):
!                         newoffset = timedelta(minutes=newoffset)
!                     diff = offset - newoffset
!                     self -= diff # this can overflow; can't be helped
!         return self.replace(tzinfo=tz)
  
      def isoformat(self, sep='T'):
--- 1614,1683 ----
                            microsecond, tzinfo)
  
+     def _inconsistent_utcoffset_error(self):
+         raise ValueError("astimezone():  tz.utcoffset() gave "
+                          "inconsistent results; cannot convert")
+ 
+     def _finish_astimezone(self, other, otoff):
+         # If this is the first hour of DST, it may be a local time that
+         # doesn't make sense on the local clock, in which case the naive
+         # hour before it (in standard time) is equivalent and does make
+         # sense on the local clock.  So force that.
+         alt = other - _HOUR
+         altoff = alt.utcoffset()
+         if altoff is None:
+             self._inconsistent_utcoffset_error()
+         # Are alt and other really the same time?  alt == other iff
+         # alt - altoff == other - otoff, iff
+         # (other - _HOUR) - altoff = other - otoff, iff
+         # otoff - altoff == _HOUR
+         diff = otoff - altoff
+         if diff == _HOUR:
+             return alt      # use the local time that makes sense
+ 
+         # There's still a problem with the unspellable (in local time)
+         # hour after DST ends.
+         if self == other:
+             return other
+         # Else there's no way to spell self in zone other.tz.
+         raise ValueError("astimezone():  the source datetimetz can't be "
+                          "expressed in the target timezone's local time")
+ 
      def astimezone(self, tz):
          _check_tzinfo_arg(tz)
!         # This is somewhat convoluted because we can only call
!         # tzinfo.utcoffset(dt) when dt.tzinfo is tzinfo.  It's more
!         # convoluted due to DST headaches (redundant spellings and
!         # "missing" hours in local time -- see the tests for details).
!         other = self.replace(tzinfo=tz) # this does no conversion
! 
!         # 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
! 
!         other += otoff - myoff
!         # If tz is a fixed-offset class, we're done, but we can't know
!         # whether it is.  If it's a DST-aware class, and we're not near a
!         # DST boundary, we're also done.  If we crossed a DST boundary,
!         # the offset will be different now, and that's our only clue.
!         # Unfortunately, we can be in trouble even if we didn't cross a
!         # DST boundary, if we landed on one of the DST "problem hours".
!         newoff = other.utcoffset()
!         if newoff is None:
!             self._inconsistent_utcoffset_error()
!         if newoff != otoff:
!             other += newoff - otoff
!             otoff = other.utcoffset()
!             if otoff is None:
!                 self._inconsistent_utcoffset_error()
!         return self._finish_astimezone(other, otoff)
  
      def isoformat(self, sep='T'):

Index: test_datetime.py
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/datetime/test_datetime.py,v
retrieving revision 1.88
retrieving revision 1.89
diff -C2 -d -r1.88 -r1.89
*** test_datetime.py	30 Dec 2002 19:43:21 -0000	1.88
--- test_datetime.py	31 Dec 2002 05:58:02 -0000	1.89
***************
*** 2575,2588 ****
              # the cases.
              return ZERO
! 
!         convert_endpoints_to_utc = False
!         if dt.tzinfo is not self:
!             # Convert dt to UTC.
!             offset = dt.utcoffset()
!             if offset is None:
!                 # Again, an exception instead may be sensible.
!                 return ZERO
!             convert_endpoints_to_utc = True
!             dt -= offset
  
          # Find first Sunday in April.
--- 2575,2579 ----
              # the cases.
              return ZERO
!         assert dt.tzinfo is self
  
          # Find first Sunday in April.
***************
*** 2594,2601 ****
          assert end.weekday() == 6 and end.month == 10 and end.day >= 25
  
-         if convert_endpoints_to_utc:
-             start -= self.stdoffset    # start is in std time
-             end -= self.stdoffset + HOUR # end is in DST time
- 
          # Can't compare naive to aware objects, so strip the timezone from
          # dt first.
--- 2585,2588 ----
***************
*** 2669,2673 ****
                  # 2:MM:SS daylight time can't be expressed in local time.
                  nexthour_utc = asutc + HOUR
-                 nexthour_tz = nexthour_utc.astimezone(tz)
                  if during.date() == dstoff.date() and during.hour == 1:
                      # We're in the hour before DST ends.  The hour after
--- 2656,2659 ----
***************
*** 2683,2688 ****
                      # being standard time.  But it's not -- on this day
                      # it's taken as daylight time.
!                     self.assertEqual(during, nexthour_tz)
                  else:
                      self.assertEqual(nexthour_tz - during, HOUR)
  
--- 2669,2676 ----
                      # being standard time.  But it's not -- on this day
                      # it's taken as daylight time.
!                     self.assertRaises(ValueError,
!                                       nexthour_utc.astimezone, tz)
                  else:
+                     nexthour_tz = nexthour_utc.astimezone(utc)
                      self.assertEqual(nexthour_tz - during, HOUR)