Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 327098a

Browse files
committed
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.
1 parent 4440f22 commit 327098a

4 files changed

Lines changed: 68 additions & 73 deletions

File tree

Doc/lib/libdatetime.tex

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ \section{\module{datetime} ---
3030

3131
For applications requiring more, \class{datetime} and \class{time}
3232
objects have an optional time zone information member,
33-
\member{tzinfo}, that can contain an instance of a subclass of
33+
\member{tzinfo}, that can contain an instance of a subclass of
3434
the abstract \class{tzinfo} class. These \class{tzinfo} objects
3535
capture information about the offset from UTC time, the time zone
3636
name, and whether Daylight Saving Time is in effect. Note that no
@@ -1048,8 +1048,10 @@ \subsection{\class{tzinfo} Objects \label{datetime-tzinfo}}
10481048

10491049
If \method{utcoffset()} does not return \code{None},
10501050
\method{dst()} should not return \code{None} either.
1051-
\end{methoddesc}
10521051

1052+
The default implementation of \method{utcoffset()} raises
1053+
\exception{NotImplementedError}.
1054+
\end{methoddesc}
10531055

10541056
\begin{methoddesc}{dst}{self, dt}
10551057
Return the daylight saving time (DST) adjustment, in minutes east of
@@ -1060,7 +1062,7 @@ \subsection{\class{tzinfo} Objects \label{datetime-tzinfo}}
10601062
Note that DST offset, if applicable, has
10611063
already been added to the UTC offset returned by
10621064
\method{utcoffset()}, so there's no need to consult \method{dst()}
1063-
unless you're interested in displaying DST info separately. For
1065+
unless you're interested in obtaining DST info separately. For
10641066
example, \method{datetime.timetuple()} calls its \member{tzinfo}
10651067
member's \method{dst()} method to determine how the
10661068
\member{tm_isdst} flag should be set, and
@@ -1080,6 +1082,10 @@ \subsection{\class{tzinfo} Objects \label{datetime-tzinfo}}
10801082
cannot detect violations; it's the programmer's responsibility to
10811083
ensure it.
10821084

1085+
The default implementation of \method{dst()} raises
1086+
\exception{NotImplementedError}.
1087+
\end{methoddesc}
1088+
10831089
\begin{methoddesc}{tzname}{self, dt}
10841090
Return the timezone name corresponding to the \class{datetime}
10851091
object represented
@@ -1092,8 +1098,9 @@ \subsection{\class{tzinfo} Objects \label{datetime-tzinfo}}
10921098
will wish to return different names depending on the specific value
10931099
of \var{dt} passed, especially if the \class{tzinfo} class is
10941100
accounting for daylight time.
1095-
\end{methoddesc}
10961101

1102+
The default implementation of \method{tzname()} raises
1103+
\exception{NotImplementedError}.
10971104
\end{methoddesc}
10981105

10991106
These methods are called by a \class{datetime} or \class{time} object,
@@ -1106,21 +1113,23 @@ \subsection{\class{tzinfo} Objects \label{datetime-tzinfo}}
11061113
When \code{None} is passed, it's up to the class designer to decide the
11071114
best response. For example, returning \code{None} is appropriate if the
11081115
class wishes to say that time objects don't participate in the
1109-
\class{tzinfo} protocol. In other applications, it may be more useful
1110-
for \code{utcoffset(None)} to return the standard UTC offset.
1116+
\class{tzinfo} protocol. It may be more useful for \code{utcoffset(None)}
1117+
to return the standard UTC offset, as there is no other convention for
1118+
discovering the standard offset.
11111119

11121120
When a \class{datetime} object is passed in response to a
11131121
\class{datetime} method, \code{dt.tzinfo} is the same object as
11141122
\var{self}. \class{tzinfo} methods can rely on this, unless
11151123
user code calls \class{tzinfo} methods directly. The intent is that
11161124
the \class{tzinfo} methods interpret \var{dt} as being in local time,
1117-
and not need to worry about objects in other timezones.
1125+
and not need worry about objects in other timezones.
11181126

11191127
Example \class{tzinfo} classes:
11201128

11211129
\verbatiminput{tzinfo-examples.py}
11221130

1123-
Note that there are unavoidable subtleties twice per year in a tzinfo
1131+
Note that there are unavoidable subtleties twice per year in a
1132+
\class{tzinfo}
11241133
subclass accounting for both standard and daylight time, at the DST
11251134
transition points. For concreteness, consider US Eastern (UTC -0500),
11261135
where EDT begins the minute after 1:59 (EST) on the first Sunday in
@@ -1140,32 +1149,29 @@ \subsection{\class{tzinfo} Objects \label{datetime-tzinfo}}
11401149
to 3:00. A wall time of the form 2:MM doesn't really make sense on that
11411150
day, so \code{astimezone(Eastern)} won't deliver a result with
11421151
\code{hour==2} on the
1143-
day DST begins. How an Eastern instance chooses to interpret 2:MM on
1144-
that day is its business. The example Eastern implementation above
1145-
chose to
1146-
consider it as a time in EDT, simply because it "looks like it's
1147-
after 2:00", and so synonymous with the EST 1:MM times on that day.
1148-
Your Eastern class may wish, for example, to raise an exception instead
1149-
when it sees a 2:MM time on the day EDT begins.
1152+
day DST begins. In order for \method{astimezone()} to make this
1153+
guarantee, the \class{tzinfo} \method{dst()} method must consider times
1154+
in the "missing hour" (2:MM for Eastern) to be in daylight time.
11501155

11511156
When DST ends (the "end" line), there's a potentially worse problem:
1152-
there's an hour that can't be spelled unambiguously in local wall time, the
1153-
hour beginning at the moment DST ends. In this example, that's times of
1154-
the form 6:MM UTC on the day daylight time ends. The local wall clock
1157+
there's an hour that can't be spelled unambiguously in local wall time:
1158+
the last hour of daylight time. In Eastern, that's times of
1159+
the form 5:MM UTC on the day daylight time ends. The local wall clock
11551160
leaps from 1:59 (daylight time) back to 1:00 (standard time) again.
1156-
1:MM is taken as daylight time (it's "before 2:00"), so maps to 5:MM UTC.
1157-
2:MM is taken as standard time (it's "after 2:00"), so maps to 7:MM UTC.
1158-
There is no local time that maps to 6:MM UTC on this day.
1159-
1160-
Just as the wall clock does, \code{astimezone(Eastern)} maps both UTC
1161-
hours 5:MM
1162-
and 6:MM to Eastern hour 1:MM on this day. However, this result is
1163-
ambiguous (there's no way for Eastern to know which repetition of 1:MM
1164-
is intended). Applications that can't bear such ambiguity
1165-
should avoid using hybrid tzinfo classes; there are no
1166-
ambiguities when using UTC, or any other fixed-offset tzinfo subclass
1167-
(such as a class representing only EST (fixed offset -5 hours), or only
1168-
EDT (fixed offset -4 hours)).
1161+
Local times of the form 1:MM are ambiguous. \method{astimezone()} mimics
1162+
the local clock's behavior by mapping two adjacent UTC hours into the
1163+
same local hour then. In the Eastern example, UTC times of the form
1164+
5:MM and 6:MM both map to 1:MM when converted to Eastern. In order for
1165+
\method{astimezone()} to make this guarantee, the \class{tzinfo}
1166+
\method{dst()} method must consider times in the "repeated hour" to be in
1167+
standard time. This is easily arranged, as in the example, by expressing
1168+
DST switch times in the time zone's standard local time.
1169+
1170+
Applications that can't bear such ambiguities should avoid using hybrid
1171+
\class{tzinfo} subclasses; there are no ambiguities when using UTC, or
1172+
any other fixed-offset \class{tzinfo} subclass (such as a class
1173+
representing only EST (fixed offset -5 hours), or only EDT (fixed offset
1174+
-4 hours)).
11691175

11701176

11711177
\subsection{\method{strftime()} Behavior}

Doc/lib/tzinfo-examples.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def first_sunday_on_or_after(dt):
9191
DSTSTART = datetime(1, 4, 1, 2)
9292
# and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct.
9393
# which is the first Sunday on or after Oct 25.
94-
DSTEND = datetime(1, 10, 25, 2)
94+
DSTEND = datetime(1, 10, 25, 1)
9595

9696
class USTimeZone(tzinfo):
9797

Lib/test/test_datetime.py

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2561,8 +2561,10 @@ def first_sunday_on_or_after(dt):
25612561
# In the US, DST starts at 2am (standard time) on the first Sunday in April.
25622562
DSTSTART = datetime(1, 4, 1, 2)
25632563
# and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct,
2564-
# which is the first Sunday on or after Oct 25.
2565-
DSTEND = datetime(1, 10, 25, 2)
2564+
# which is the first Sunday on or after Oct 25. Because we view 1:MM as
2565+
# being standard time on that day, there is no spelling in local time of
2566+
# the last hour of DST (that's 1:MM DST, but 1:MM is taken as standard time).
2567+
DSTEND = datetime(1, 10, 25, 1)
25662568

25672569
class USTimeZone(tzinfo):
25682570

@@ -2616,9 +2618,9 @@ def dst(self, dt):
26162618
utc_fake = FixedOffset(-12*60, "UTCfake", 0)
26172619

26182620
class TestTimezoneConversions(unittest.TestCase):
2619-
# The DST switch times for 2002, in local time.
2621+
# The DST switch times for 2002, in std time.
26202622
dston = datetime(2002, 4, 7, 2)
2621-
dstoff = datetime(2002, 10, 27, 2)
2623+
dstoff = datetime(2002, 10, 27, 1)
26222624

26232625
theclass = datetime
26242626

@@ -2656,25 +2658,25 @@ def checkinside(self, dt, tz, utc, dston, dstoff):
26562658
# We're not in the redundant hour.
26572659
self.assertEqual(dt, there_and_back)
26582660

2659-
# Because we have a redundant spelling when DST begins,
2660-
# there is (unforunately) an hour when DST ends that can't
2661-
# be spelled at all in local time. When DST ends, the
2662-
# clock jumps from 1:59:59 back to 1:00:00 again. The
2663-
# hour beginning then has no spelling in local time:
2664-
# 1:MM:SS is taken to be daylight time, and 2:MM:SS as
2665-
# standard time. The hour 1:MM:SS standard time ==
2666-
# 2:MM:SS daylight time can't be expressed in local time.
2667-
# Nevertheless, we want conversion back from UTC to mimic
2668-
# the local clock's "repeat an hour" behavior.
2661+
# Because we have a redundant spelling when DST begins, there is
2662+
# (unforunately) an hour when DST ends that can't be spelled at all in
2663+
# local time. When DST ends, the clock jumps from 1:59 back to 1:00
2664+
# again. The hour 1:MM DST has no spelling then: 1:MM is taken to be
2665+
# standard time. 1:MM DST == 0:MM EST, but 0:MM is taken to be
2666+
# daylight time. The hour 1:MM daylight == 0:MM standard can't be
2667+
# expressed in local time. Nevertheless, we want conversion back
2668+
# from UTC to mimic the local clock's "repeat an hour" behavior.
26692669
nexthour_utc = asutc + HOUR
26702670
nexthour_tz = nexthour_utc.astimezone(tz)
2671-
if dt.date() == dstoff.date() and dt.hour == 1:
2672-
# We're in the hour before DST ends. The hour after
2671+
if dt.date() == dstoff.date() and dt.hour == 0:
2672+
# We're in the hour before the last DST hour. The last DST hour
26732673
# is ineffable. We want the conversion back to repeat 1:MM.
2674-
expected_diff = ZERO
2674+
self.assertEqual(nexthour_tz, dt.replace(hour=1))
2675+
nexthour_utc += HOUR
2676+
nexthour_tz = nexthour_utc.astimezone(tz)
2677+
self.assertEqual(nexthour_tz, dt.replace(hour=1))
26752678
else:
2676-
expected_diff = HOUR
2677-
self.assertEqual(nexthour_tz - dt, expected_diff)
2679+
self.assertEqual(nexthour_tz - dt, HOUR)
26782680

26792681
# Check a time that's outside DST.
26802682
def checkoutside(self, dt, tz, utc):
@@ -2687,6 +2689,11 @@ def checkoutside(self, dt, tz, utc):
26872689

26882690
def convert_between_tz_and_utc(self, tz, utc):
26892691
dston = self.dston.replace(tzinfo=tz)
2692+
# Because 1:MM on the day DST ends is taken as being standard time,
2693+
# there is no spelling in tz for the last hour of daylight time.
2694+
# For purposes of the test, the last hour of DST is 0:MM, which is
2695+
# taken as being daylight time (and 1:MM is taken as being standard
2696+
# time).
26902697
dstoff = self.dstoff.replace(tzinfo=tz)
26912698
for delta in (timedelta(weeks=13),
26922699
DAY,
@@ -2759,7 +2766,7 @@ def test_tricky(self):
27592766
# wall 0:MM 1:MM 1:MM 2:MM against these
27602767
for utc in utc_real, utc_fake:
27612768
for tz in Eastern, Pacific:
2762-
first_std_hour = self.dstoff - timedelta(hours=3) # 23:MM
2769+
first_std_hour = self.dstoff - timedelta(hours=2) # 23:MM
27632770
# Convert that to UTC.
27642771
first_std_hour -= tz.utcoffset(None)
27652772
# Adjust for possibly fake UTC.

Modules/datetimemodule.c

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4046,7 +4046,7 @@ datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
40464046

40474047
PyObject *result;
40484048
PyObject *temp;
4049-
int selfoff, resoff, dst1, dst2;
4049+
int selfoff, resoff, dst1;
40504050
int none;
40514051
int delta;
40524052

@@ -4128,26 +4128,8 @@ datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
41284128
temp = new_datetime(y, m, d, hh, mm, ss, us, tzinfo);
41294129
if (temp == NULL)
41304130
goto Fail;
4131-
4132-
dst2 = call_dst(tzinfo, temp, &none);
4133-
if (dst2 == -1 && PyErr_Occurred()) {
4134-
Py_DECREF(temp);
4135-
goto Fail;
4136-
}
4137-
if (none) {
4138-
Py_DECREF(temp);
4139-
goto Inconsistent;
4140-
}
4141-
4142-
if (dst1 == dst2) {
4143-
/* The normal case: we want temp, not result. */
4144-
Py_DECREF(result);
4145-
result = temp;
4146-
}
4147-
else {
4148-
/* The "unspellable hour" at the end of DST. */
4149-
Py_DECREF(temp);
4150-
}
4131+
Py_DECREF(result);
4132+
result = temp;
41514133
return result;
41524134

41534135
Inconsistent:

0 commit comments

Comments
 (0)