mirror of
https://github.com/python/cpython.git
synced 2025-10-17 12:18:23 +00:00
New rule for tzinfo subclasses handling both standard and daylight time:
When daylight time ends, an hour repeats on the local clock (for example, in US Eastern, the clock jumps from 1:59 back to 1:00 again). Times in the repeated hour are ambiguous. A tzinfo subclass that wants to play with astimezone() needs to treat times in the repeated hour as being standard time. astimezone() previously required that such times be treated as daylight time. There seems no killer argument either way, but Guido wants the standard-time version, and it does seem easier the new way to code both American (local-time based) and European (UTC-based) switch rules, and the astimezone() implementation is simpler.
This commit is contained in:
parent
4440f22e98
commit
327098a613
4 changed files with 67 additions and 72 deletions
|
@ -30,7 +30,7 @@ the cost of ignoring some aspects of reality.
|
||||||
|
|
||||||
For applications requiring more, \class{datetime} and \class{time}
|
For applications requiring more, \class{datetime} and \class{time}
|
||||||
objects have an optional time zone information member,
|
objects have an optional time zone information member,
|
||||||
\member{tzinfo}, that can contain an instance of a subclass of
|
\member{tzinfo}, that can contain an instance of a subclass of
|
||||||
the abstract \class{tzinfo} class. These \class{tzinfo} objects
|
the abstract \class{tzinfo} class. These \class{tzinfo} objects
|
||||||
capture information about the offset from UTC time, the time zone
|
capture information about the offset from UTC time, the time zone
|
||||||
name, and whether Daylight Saving Time is in effect. Note that no
|
name, and whether Daylight Saving Time is in effect. Note that no
|
||||||
|
@ -1048,8 +1048,10 @@ implement all of them.
|
||||||
|
|
||||||
If \method{utcoffset()} does not return \code{None},
|
If \method{utcoffset()} does not return \code{None},
|
||||||
\method{dst()} should not return \code{None} either.
|
\method{dst()} should not return \code{None} either.
|
||||||
\end{methoddesc}
|
|
||||||
|
|
||||||
|
The default implementation of \method{utcoffset()} raises
|
||||||
|
\exception{NotImplementedError}.
|
||||||
|
\end{methoddesc}
|
||||||
|
|
||||||
\begin{methoddesc}{dst}{self, dt}
|
\begin{methoddesc}{dst}{self, dt}
|
||||||
Return the daylight saving time (DST) adjustment, in minutes east of
|
Return the daylight saving time (DST) adjustment, in minutes east of
|
||||||
|
@ -1060,7 +1062,7 @@ implement all of them.
|
||||||
Note that DST offset, if applicable, has
|
Note that DST offset, if applicable, has
|
||||||
already been added to the UTC offset returned by
|
already been added to the UTC offset returned by
|
||||||
\method{utcoffset()}, so there's no need to consult \method{dst()}
|
\method{utcoffset()}, so there's no need to consult \method{dst()}
|
||||||
unless you're interested in displaying DST info separately. For
|
unless you're interested in obtaining DST info separately. For
|
||||||
example, \method{datetime.timetuple()} calls its \member{tzinfo}
|
example, \method{datetime.timetuple()} calls its \member{tzinfo}
|
||||||
member's \method{dst()} method to determine how the
|
member's \method{dst()} method to determine how the
|
||||||
\member{tm_isdst} flag should be set, and
|
\member{tm_isdst} flag should be set, and
|
||||||
|
@ -1080,6 +1082,10 @@ implement all of them.
|
||||||
cannot detect violations; it's the programmer's responsibility to
|
cannot detect violations; it's the programmer's responsibility to
|
||||||
ensure it.
|
ensure it.
|
||||||
|
|
||||||
|
The default implementation of \method{dst()} raises
|
||||||
|
\exception{NotImplementedError}.
|
||||||
|
\end{methoddesc}
|
||||||
|
|
||||||
\begin{methoddesc}{tzname}{self, dt}
|
\begin{methoddesc}{tzname}{self, dt}
|
||||||
Return the timezone name corresponding to the \class{datetime}
|
Return the timezone name corresponding to the \class{datetime}
|
||||||
object represented
|
object represented
|
||||||
|
@ -1092,8 +1098,9 @@ implement all of them.
|
||||||
will wish to return different names depending on the specific value
|
will wish to return different names depending on the specific value
|
||||||
of \var{dt} passed, especially if the \class{tzinfo} class is
|
of \var{dt} passed, especially if the \class{tzinfo} class is
|
||||||
accounting for daylight time.
|
accounting for daylight time.
|
||||||
\end{methoddesc}
|
|
||||||
|
|
||||||
|
The default implementation of \method{tzname()} raises
|
||||||
|
\exception{NotImplementedError}.
|
||||||
\end{methoddesc}
|
\end{methoddesc}
|
||||||
|
|
||||||
These methods are called by a \class{datetime} or \class{time} object,
|
These methods are called by a \class{datetime} or \class{time} object,
|
||||||
|
@ -1106,21 +1113,23 @@ class \class{datetime}.
|
||||||
When \code{None} is passed, it's up to the class designer to decide the
|
When \code{None} is passed, it's up to the class designer to decide the
|
||||||
best response. For example, returning \code{None} is appropriate if the
|
best response. For example, returning \code{None} is appropriate if the
|
||||||
class wishes to say that time objects don't participate in the
|
class wishes to say that time objects don't participate in the
|
||||||
\class{tzinfo} protocol. In other applications, it may be more useful
|
\class{tzinfo} protocol. It may be more useful for \code{utcoffset(None)}
|
||||||
for \code{utcoffset(None)} to return the standard UTC offset.
|
to return the standard UTC offset, as there is no other convention for
|
||||||
|
discovering the standard offset.
|
||||||
|
|
||||||
When a \class{datetime} object is passed in response to a
|
When a \class{datetime} object is passed in response to a
|
||||||
\class{datetime} method, \code{dt.tzinfo} is the same object as
|
\class{datetime} method, \code{dt.tzinfo} is the same object as
|
||||||
\var{self}. \class{tzinfo} methods can rely on this, unless
|
\var{self}. \class{tzinfo} methods can rely on this, unless
|
||||||
user code calls \class{tzinfo} methods directly. The intent is that
|
user code calls \class{tzinfo} methods directly. The intent is that
|
||||||
the \class{tzinfo} methods interpret \var{dt} as being in local time,
|
the \class{tzinfo} methods interpret \var{dt} as being in local time,
|
||||||
and not need to worry about objects in other timezones.
|
and not need worry about objects in other timezones.
|
||||||
|
|
||||||
Example \class{tzinfo} classes:
|
Example \class{tzinfo} classes:
|
||||||
|
|
||||||
\verbatiminput{tzinfo-examples.py}
|
\verbatiminput{tzinfo-examples.py}
|
||||||
|
|
||||||
Note that there are unavoidable subtleties twice per year in a tzinfo
|
Note that there are unavoidable subtleties twice per year in a
|
||||||
|
\class{tzinfo}
|
||||||
subclass accounting for both standard and daylight time, at the DST
|
subclass accounting for both standard and daylight time, at the DST
|
||||||
transition points. For concreteness, consider US Eastern (UTC -0500),
|
transition points. For concreteness, consider US Eastern (UTC -0500),
|
||||||
where EDT begins the minute after 1:59 (EST) on the first Sunday in
|
where EDT begins the minute after 1:59 (EST) on the first Sunday in
|
||||||
|
@ -1140,32 +1149,29 @@ When DST starts (the "start" line), the local wall clock leaps from 1:59
|
||||||
to 3:00. A wall time of the form 2:MM doesn't really make sense on that
|
to 3:00. A wall time of the form 2:MM doesn't really make sense on that
|
||||||
day, so \code{astimezone(Eastern)} won't deliver a result with
|
day, so \code{astimezone(Eastern)} won't deliver a result with
|
||||||
\code{hour==2} on the
|
\code{hour==2} on the
|
||||||
day DST begins. How an Eastern instance chooses to interpret 2:MM on
|
day DST begins. In order for \method{astimezone()} to make this
|
||||||
that day is its business. The example Eastern implementation above
|
guarantee, the \class{tzinfo} \method{dst()} method must consider times
|
||||||
chose to
|
in the "missing hour" (2:MM for Eastern) to be in daylight time.
|
||||||
consider it as a time in EDT, simply because it "looks like it's
|
|
||||||
after 2:00", and so synonymous with the EST 1:MM times on that day.
|
|
||||||
Your Eastern class may wish, for example, to raise an exception instead
|
|
||||||
when it sees a 2:MM time on the day EDT begins.
|
|
||||||
|
|
||||||
When DST ends (the "end" line), there's a potentially worse problem:
|
When DST ends (the "end" line), there's a potentially worse problem:
|
||||||
there's an hour that can't be spelled unambiguously in local wall time, the
|
there's an hour that can't be spelled unambiguously in local wall time:
|
||||||
hour beginning at the moment DST ends. In this example, that's times of
|
the last hour of daylight time. In Eastern, that's times of
|
||||||
the form 6:MM UTC on the day daylight time ends. The local wall clock
|
the form 5:MM UTC on the day daylight time ends. The local wall clock
|
||||||
leaps from 1:59 (daylight time) back to 1:00 (standard time) again.
|
leaps from 1:59 (daylight time) back to 1:00 (standard time) again.
|
||||||
1:MM is taken as daylight time (it's "before 2:00"), so maps to 5:MM UTC.
|
Local times of the form 1:MM are ambiguous. \method{astimezone()} mimics
|
||||||
2:MM is taken as standard time (it's "after 2:00"), so maps to 7:MM UTC.
|
the local clock's behavior by mapping two adjacent UTC hours into the
|
||||||
There is no local time that maps to 6:MM UTC on this day.
|
same local hour then. In the Eastern example, UTC times of the form
|
||||||
|
5:MM and 6:MM both map to 1:MM when converted to Eastern. In order for
|
||||||
|
\method{astimezone()} to make this guarantee, the \class{tzinfo}
|
||||||
|
\method{dst()} method must consider times in the "repeated hour" to be in
|
||||||
|
standard time. This is easily arranged, as in the example, by expressing
|
||||||
|
DST switch times in the time zone's standard local time.
|
||||||
|
|
||||||
Just as the wall clock does, \code{astimezone(Eastern)} maps both UTC
|
Applications that can't bear such ambiguities should avoid using hybrid
|
||||||
hours 5:MM
|
\class{tzinfo} subclasses; there are no ambiguities when using UTC, or
|
||||||
and 6:MM to Eastern hour 1:MM on this day. However, this result is
|
any other fixed-offset \class{tzinfo} subclass (such as a class
|
||||||
ambiguous (there's no way for Eastern to know which repetition of 1:MM
|
representing only EST (fixed offset -5 hours), or only EDT (fixed offset
|
||||||
is intended). Applications that can't bear such ambiguity
|
-4 hours)).
|
||||||
should avoid using hybrid tzinfo classes; there are no
|
|
||||||
ambiguities when using UTC, or any other fixed-offset tzinfo subclass
|
|
||||||
(such as a class representing only EST (fixed offset -5 hours), or only
|
|
||||||
EDT (fixed offset -4 hours)).
|
|
||||||
|
|
||||||
|
|
||||||
\subsection{\method{strftime()} Behavior}
|
\subsection{\method{strftime()} Behavior}
|
||||||
|
|
|
@ -91,7 +91,7 @@ def first_sunday_on_or_after(dt):
|
||||||
DSTSTART = datetime(1, 4, 1, 2)
|
DSTSTART = datetime(1, 4, 1, 2)
|
||||||
# and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct.
|
# and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct.
|
||||||
# which is the first Sunday on or after Oct 25.
|
# which is the first Sunday on or after Oct 25.
|
||||||
DSTEND = datetime(1, 10, 25, 2)
|
DSTEND = datetime(1, 10, 25, 1)
|
||||||
|
|
||||||
class USTimeZone(tzinfo):
|
class USTimeZone(tzinfo):
|
||||||
|
|
||||||
|
|
|
@ -2561,8 +2561,10 @@ DAY = timedelta(days=1)
|
||||||
# In the US, DST starts at 2am (standard time) on the first Sunday in April.
|
# In the US, DST starts at 2am (standard time) on the first Sunday in April.
|
||||||
DSTSTART = datetime(1, 4, 1, 2)
|
DSTSTART = datetime(1, 4, 1, 2)
|
||||||
# and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct,
|
# and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct,
|
||||||
# which is the first Sunday on or after Oct 25.
|
# which is the first Sunday on or after Oct 25. Because we view 1:MM as
|
||||||
DSTEND = datetime(1, 10, 25, 2)
|
# being standard time on that day, there is no spelling in local time of
|
||||||
|
# the last hour of DST (that's 1:MM DST, but 1:MM is taken as standard time).
|
||||||
|
DSTEND = datetime(1, 10, 25, 1)
|
||||||
|
|
||||||
class USTimeZone(tzinfo):
|
class USTimeZone(tzinfo):
|
||||||
|
|
||||||
|
@ -2616,9 +2618,9 @@ utc_real = FixedOffset(0, "UTC", 0)
|
||||||
utc_fake = FixedOffset(-12*60, "UTCfake", 0)
|
utc_fake = FixedOffset(-12*60, "UTCfake", 0)
|
||||||
|
|
||||||
class TestTimezoneConversions(unittest.TestCase):
|
class TestTimezoneConversions(unittest.TestCase):
|
||||||
# The DST switch times for 2002, in local time.
|
# The DST switch times for 2002, in std time.
|
||||||
dston = datetime(2002, 4, 7, 2)
|
dston = datetime(2002, 4, 7, 2)
|
||||||
dstoff = datetime(2002, 10, 27, 2)
|
dstoff = datetime(2002, 10, 27, 1)
|
||||||
|
|
||||||
theclass = datetime
|
theclass = datetime
|
||||||
|
|
||||||
|
@ -2656,25 +2658,25 @@ class TestTimezoneConversions(unittest.TestCase):
|
||||||
# We're not in the redundant hour.
|
# We're not in the redundant hour.
|
||||||
self.assertEqual(dt, there_and_back)
|
self.assertEqual(dt, there_and_back)
|
||||||
|
|
||||||
# Because we have a redundant spelling when DST begins,
|
# Because we have a redundant spelling when DST begins, there is
|
||||||
# there is (unforunately) an hour when DST ends that can't
|
# (unforunately) an hour when DST ends that can't be spelled at all in
|
||||||
# be spelled at all in local time. When DST ends, the
|
# local time. When DST ends, the clock jumps from 1:59 back to 1:00
|
||||||
# clock jumps from 1:59:59 back to 1:00:00 again. The
|
# again. The hour 1:MM DST has no spelling then: 1:MM is taken to be
|
||||||
# hour beginning then has no spelling in local time:
|
# standard time. 1:MM DST == 0:MM EST, but 0:MM is taken to be
|
||||||
# 1:MM:SS is taken to be daylight time, and 2:MM:SS as
|
# daylight time. The hour 1:MM daylight == 0:MM standard can't be
|
||||||
# standard time. The hour 1:MM:SS standard time ==
|
# expressed in local time. Nevertheless, we want conversion back
|
||||||
# 2:MM:SS daylight time can't be expressed in local time.
|
# from UTC to mimic the local clock's "repeat an hour" behavior.
|
||||||
# Nevertheless, we want conversion back from UTC to mimic
|
|
||||||
# the local clock's "repeat an hour" behavior.
|
|
||||||
nexthour_utc = asutc + HOUR
|
nexthour_utc = asutc + HOUR
|
||||||
nexthour_tz = nexthour_utc.astimezone(tz)
|
nexthour_tz = nexthour_utc.astimezone(tz)
|
||||||
if dt.date() == dstoff.date() and dt.hour == 1:
|
if dt.date() == dstoff.date() and dt.hour == 0:
|
||||||
# We're in the hour before DST ends. The hour after
|
# We're in the hour before the last DST hour. The last DST hour
|
||||||
# is ineffable. We want the conversion back to repeat 1:MM.
|
# is ineffable. We want the conversion back to repeat 1:MM.
|
||||||
expected_diff = ZERO
|
self.assertEqual(nexthour_tz, dt.replace(hour=1))
|
||||||
|
nexthour_utc += HOUR
|
||||||
|
nexthour_tz = nexthour_utc.astimezone(tz)
|
||||||
|
self.assertEqual(nexthour_tz, dt.replace(hour=1))
|
||||||
else:
|
else:
|
||||||
expected_diff = HOUR
|
self.assertEqual(nexthour_tz - dt, HOUR)
|
||||||
self.assertEqual(nexthour_tz - dt, expected_diff)
|
|
||||||
|
|
||||||
# Check a time that's outside DST.
|
# Check a time that's outside DST.
|
||||||
def checkoutside(self, dt, tz, utc):
|
def checkoutside(self, dt, tz, utc):
|
||||||
|
@ -2687,6 +2689,11 @@ class TestTimezoneConversions(unittest.TestCase):
|
||||||
|
|
||||||
def convert_between_tz_and_utc(self, tz, utc):
|
def convert_between_tz_and_utc(self, tz, utc):
|
||||||
dston = self.dston.replace(tzinfo=tz)
|
dston = self.dston.replace(tzinfo=tz)
|
||||||
|
# Because 1:MM on the day DST ends is taken as being standard time,
|
||||||
|
# there is no spelling in tz for the last hour of daylight time.
|
||||||
|
# For purposes of the test, the last hour of DST is 0:MM, which is
|
||||||
|
# taken as being daylight time (and 1:MM is taken as being standard
|
||||||
|
# time).
|
||||||
dstoff = self.dstoff.replace(tzinfo=tz)
|
dstoff = self.dstoff.replace(tzinfo=tz)
|
||||||
for delta in (timedelta(weeks=13),
|
for delta in (timedelta(weeks=13),
|
||||||
DAY,
|
DAY,
|
||||||
|
@ -2759,7 +2766,7 @@ class TestTimezoneConversions(unittest.TestCase):
|
||||||
# wall 0:MM 1:MM 1:MM 2:MM against these
|
# wall 0:MM 1:MM 1:MM 2:MM against these
|
||||||
for utc in utc_real, utc_fake:
|
for utc in utc_real, utc_fake:
|
||||||
for tz in Eastern, Pacific:
|
for tz in Eastern, Pacific:
|
||||||
first_std_hour = self.dstoff - timedelta(hours=3) # 23:MM
|
first_std_hour = self.dstoff - timedelta(hours=2) # 23:MM
|
||||||
# Convert that to UTC.
|
# Convert that to UTC.
|
||||||
first_std_hour -= tz.utcoffset(None)
|
first_std_hour -= tz.utcoffset(None)
|
||||||
# Adjust for possibly fake UTC.
|
# Adjust for possibly fake UTC.
|
||||||
|
|
|
@ -4046,7 +4046,7 @@ datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
|
||||||
|
|
||||||
PyObject *result;
|
PyObject *result;
|
||||||
PyObject *temp;
|
PyObject *temp;
|
||||||
int selfoff, resoff, dst1, dst2;
|
int selfoff, resoff, dst1;
|
||||||
int none;
|
int none;
|
||||||
int delta;
|
int delta;
|
||||||
|
|
||||||
|
@ -4128,26 +4128,8 @@ datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
|
||||||
temp = new_datetime(y, m, d, hh, mm, ss, us, tzinfo);
|
temp = new_datetime(y, m, d, hh, mm, ss, us, tzinfo);
|
||||||
if (temp == NULL)
|
if (temp == NULL)
|
||||||
goto Fail;
|
goto Fail;
|
||||||
|
Py_DECREF(result);
|
||||||
dst2 = call_dst(tzinfo, temp, &none);
|
result = temp;
|
||||||
if (dst2 == -1 && PyErr_Occurred()) {
|
|
||||||
Py_DECREF(temp);
|
|
||||||
goto Fail;
|
|
||||||
}
|
|
||||||
if (none) {
|
|
||||||
Py_DECREF(temp);
|
|
||||||
goto Inconsistent;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dst1 == dst2) {
|
|
||||||
/* The normal case: we want temp, not result. */
|
|
||||||
Py_DECREF(result);
|
|
||||||
result = temp;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
/* The "unspellable hour" at the end of DST. */
|
|
||||||
Py_DECREF(temp);
|
|
||||||
}
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
Inconsistent:
|
Inconsistent:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue