[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)