mirror of
https://github.com/python/cpython.git
synced 2025-10-09 16:34:44 +00:00
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). Guido doesn't like this -- expect it to change. New tests of conversion between adjacent DST-aware timezones don't pass yet, and are commented out. Running the datetime tests in a loop under a debug build leaks 9 references per test run, but I don't believe the datetime code is the cause (it didn't leak the last time I changed the C code, and the leak is the same if I disable all the tests that invoke the only function that changed here). I'll pursue that next.
This commit is contained in:
parent
ba2f875d90
commit
521fc15e62
2 changed files with 211 additions and 109 deletions
|
@ -2560,16 +2560,7 @@ class USTimeZone(tzinfo):
|
||||||
# An exception instead may be sensible here, in one or more of
|
# An exception instead may be sensible here, in one or more of
|
||||||
# the cases.
|
# the cases.
|
||||||
return ZERO
|
return ZERO
|
||||||
|
assert dt.tzinfo is self
|
||||||
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.
|
# Find first Sunday in April.
|
||||||
start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year))
|
start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year))
|
||||||
|
@ -2579,10 +2570,6 @@ class USTimeZone(tzinfo):
|
||||||
end = first_sunday_on_or_after(DSTEND.replace(year=dt.year))
|
end = first_sunday_on_or_after(DSTEND.replace(year=dt.year))
|
||||||
assert end.weekday() == 6 and end.month == 10 and end.day >= 25
|
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
|
# Can't compare naive to aware objects, so strip the timezone from
|
||||||
# dt first.
|
# dt first.
|
||||||
if start <= dt.astimezone(None) < end:
|
if start <= dt.astimezone(None) < end:
|
||||||
|
@ -2590,8 +2577,10 @@ class USTimeZone(tzinfo):
|
||||||
else:
|
else:
|
||||||
return ZERO
|
return ZERO
|
||||||
|
|
||||||
Eastern = USTimeZone(-5, "Eastern", "EST", "EDT")
|
Eastern = USTimeZone(-5, "Eastern", "EST", "EDT")
|
||||||
Pacific = USTimeZone(-8, "Pacific", "PST", "PDT")
|
Central = USTimeZone(-6, "Central", "CST", "CDT")
|
||||||
|
Mountain = USTimeZone(-7, "Mountain", "MST", "MDT")
|
||||||
|
Pacific = USTimeZone(-8, "Pacific", "PST", "PDT")
|
||||||
utc_real = FixedOffset(0, "UTC", 0)
|
utc_real = FixedOffset(0, "UTC", 0)
|
||||||
# For better test coverage, we want another flavor of UTC that's west of
|
# For better test coverage, we want another flavor of UTC that's west of
|
||||||
# the Eastern and Pacific timezones.
|
# the Eastern and Pacific timezones.
|
||||||
|
@ -2602,6 +2591,78 @@ class TestTimezoneConversions(unittest.TestCase):
|
||||||
dston = datetimetz(2002, 4, 7, 2)
|
dston = datetimetz(2002, 4, 7, 2)
|
||||||
dstoff = datetimetz(2002, 10, 27, 2)
|
dstoff = datetimetz(2002, 10, 27, 2)
|
||||||
|
|
||||||
|
|
||||||
|
# Check a time that's inside DST.
|
||||||
|
def checkinside(self, dt, tz, utc, dston, dstoff):
|
||||||
|
self.assertEqual(dt.dst(), HOUR)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
there_and_back = asutc.astimezone(tz)
|
||||||
|
|
||||||
|
# Conversion to UTC and back isn't always an identity here,
|
||||||
|
# because there are redundant spellings (in local time) of
|
||||||
|
# UTC time when DST begins: the clock jumps from 1:59:59
|
||||||
|
# to 3:00:00, and a local time of 2:MM:SS doesn't really
|
||||||
|
# make sense then. The classes above treat 2:MM:SS as
|
||||||
|
# daylight time then (it's "after 2am"), really an alias
|
||||||
|
# for 1:MM:SS standard time. The latter form is what
|
||||||
|
# conversion back from UTC produces.
|
||||||
|
if dt.date() == dston.date() and dt.hour == 2:
|
||||||
|
# We're in the redundant hour, and coming back from
|
||||||
|
# UTC gives the 1:MM:SS standard-time spelling.
|
||||||
|
self.assertEqual(there_and_back + HOUR, dt)
|
||||||
|
# Although during was considered to be in daylight
|
||||||
|
# time, there_and_back is not.
|
||||||
|
self.assertEqual(there_and_back.dst(), ZERO)
|
||||||
|
# They're the same times in UTC.
|
||||||
|
self.assertEqual(there_and_back.astimezone(utc),
|
||||||
|
dt.astimezone(utc))
|
||||||
|
else:
|
||||||
|
# We're not in the redundant hour.
|
||||||
|
self.assertEqual(dt, there_and_back)
|
||||||
|
|
||||||
|
# Because we have a redundant spelling when DST begins,
|
||||||
|
# there is (unforunately) an hour when DST ends that can't
|
||||||
|
# be spelled at all in local time. When DST ends, the
|
||||||
|
# clock jumps from 1:59:59 back to 1:00:00 again. The
|
||||||
|
# hour beginning then has no spelling in local time:
|
||||||
|
# 1:MM:SS is taken to be daylight time, and 2:MM:SS as
|
||||||
|
# standard time. The hour 1:MM:SS standard time ==
|
||||||
|
# 2:MM:SS daylight time can't be expressed in local time.
|
||||||
|
nexthour_utc = asutc + HOUR
|
||||||
|
if dt.date() == dstoff.date() and dt.hour == 1:
|
||||||
|
# We're in the hour before DST ends. The hour after
|
||||||
|
# is ineffable.
|
||||||
|
# For concreteness, picture Eastern. during is of
|
||||||
|
# the form 1:MM:SS, it's daylight time, so that's
|
||||||
|
# 5:MM:SS UTC. Adding an hour gives 6:MM:SS UTC.
|
||||||
|
# Daylight time ended at 2+4 == 6:00:00 UTC, so
|
||||||
|
# 6:MM:SS is (correctly) taken to be standard time.
|
||||||
|
# But standard time is at offset -5, and that maps
|
||||||
|
# right back to the 1:MM:SS Eastern we started with.
|
||||||
|
# That's correct, too, *if* 1:MM:SS were taken as
|
||||||
|
# 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 - dt, HOUR)
|
||||||
|
|
||||||
|
# Check a time that's outside DST.
|
||||||
|
def checkoutside(self, dt, tz, utc):
|
||||||
|
self.assertEqual(dt.dst(), ZERO)
|
||||||
|
|
||||||
|
# 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):
|
def convert_between_tz_and_utc(self, tz, utc):
|
||||||
dston = self.dston.replace(tzinfo=tz)
|
dston = self.dston.replace(tzinfo=tz)
|
||||||
dstoff = self.dstoff.replace(tzinfo=tz)
|
dstoff = self.dstoff.replace(tzinfo=tz)
|
||||||
|
@ -2611,77 +2672,13 @@ class TestTimezoneConversions(unittest.TestCase):
|
||||||
timedelta(minutes=1),
|
timedelta(minutes=1),
|
||||||
timedelta(microseconds=1)):
|
timedelta(microseconds=1)):
|
||||||
|
|
||||||
for during in dston, dston + delta, dstoff - delta:
|
self.checkinside(dston, tz, utc, dston, dstoff)
|
||||||
self.assertEqual(during.dst(), HOUR)
|
for during in dston + delta, dstoff - delta:
|
||||||
|
self.checkinside(during, tz, utc, dston, dstoff)
|
||||||
|
|
||||||
# Conversion to our own timezone is always an identity.
|
self.checkoutside(dstoff, tz, utc)
|
||||||
self.assertEqual(during.astimezone(tz), during)
|
for outside in dston - delta, dstoff + delta:
|
||||||
# Conversion to None is always the same as stripping tzinfo.
|
self.checkoutside(outside, tz, utc)
|
||||||
self.assertEqual(during.astimezone(None),
|
|
||||||
during.replace(tzinfo=None))
|
|
||||||
|
|
||||||
asutc = during.astimezone(utc)
|
|
||||||
there_and_back = asutc.astimezone(tz)
|
|
||||||
|
|
||||||
# Conversion to UTC and back isn't always an identity here,
|
|
||||||
# because there are redundant spellings (in local time) of
|
|
||||||
# UTC time when DST begins: the clock jumps from 1:59:59
|
|
||||||
# to 3:00:00, and a local time of 2:MM:SS doesn't really
|
|
||||||
# make sense then. The classes above treat 2:MM:SS as
|
|
||||||
# daylight time then (it's "after 2am"), really an alias
|
|
||||||
# for 1:MM:SS standard time. The latter form is what
|
|
||||||
# conversion back from UTC produces.
|
|
||||||
if during.date() == dston.date() and during.hour == 2:
|
|
||||||
# We're in the redundant hour, and coming back from
|
|
||||||
# UTC gives the 1:MM:SS standard-time spelling.
|
|
||||||
self.assertEqual(there_and_back + HOUR, during)
|
|
||||||
# Although during was considered to be in daylight
|
|
||||||
# time, there_and_back is not.
|
|
||||||
self.assertEqual(there_and_back.dst(), ZERO)
|
|
||||||
# They're the same times in UTC.
|
|
||||||
self.assertEqual(there_and_back.astimezone(utc),
|
|
||||||
during.astimezone(utc))
|
|
||||||
else:
|
|
||||||
# We're not in the redundant hour.
|
|
||||||
self.assertEqual(during, there_and_back)
|
|
||||||
|
|
||||||
# Because we have a redundant spelling when DST begins,
|
|
||||||
# there is (unforunately) an hour when DST ends that can't
|
|
||||||
# be spelled at all in local time. When DST ends, the
|
|
||||||
# clock jumps from 1:59:59 back to 1:00:00 again. The
|
|
||||||
# hour beginning then has no spelling in local time:
|
|
||||||
# 1:MM:SS is taken to be daylight time, and 2:MM:SS as
|
|
||||||
# standard time. The hour 1:MM:SS standard time ==
|
|
||||||
# 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
|
|
||||||
# is ineffable.
|
|
||||||
# For concreteness, picture Eastern. during is of
|
|
||||||
# the form 1:MM:SS, it's daylight time, so that's
|
|
||||||
# 5:MM:SS UTC. Adding an hour gives 6:MM:SS UTC.
|
|
||||||
# Daylight time ended at 2+4 == 6:00:00 UTC, so
|
|
||||||
# 6:MM:SS is (correctly) taken to be standard time.
|
|
||||||
# But standard time is at offset -5, and that maps
|
|
||||||
# right back to the 1:MM:SS Eastern we started with.
|
|
||||||
# That's correct, too, *if* 1:MM:SS were taken as
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
for outside in dston - delta, dstoff, dstoff + delta:
|
|
||||||
self.assertEqual(outside.dst(), ZERO)
|
|
||||||
there_and_back = outside.astimezone(utc).astimezone(tz)
|
|
||||||
self.assertEqual(outside, there_and_back)
|
|
||||||
|
|
||||||
# Conversion to our own timezone is always an identity.
|
|
||||||
self.assertEqual(outside.astimezone(tz), outside)
|
|
||||||
# Conversion to None is always the same as stripping tzinfo.
|
|
||||||
self.assertEqual(outside.astimezone(None),
|
|
||||||
outside.replace(tzinfo=None))
|
|
||||||
|
|
||||||
def test_easy(self):
|
def test_easy(self):
|
||||||
# Despite the name of this test, the endcases are excruciating.
|
# Despite the name of this test, the endcases are excruciating.
|
||||||
|
@ -2694,6 +2691,9 @@ class TestTimezoneConversions(unittest.TestCase):
|
||||||
# hours" don't overlap.
|
# hours" don't overlap.
|
||||||
self.convert_between_tz_and_utc(Eastern, Pacific)
|
self.convert_between_tz_and_utc(Eastern, Pacific)
|
||||||
self.convert_between_tz_and_utc(Pacific, Eastern)
|
self.convert_between_tz_and_utc(Pacific, Eastern)
|
||||||
|
# XXX These fail!
|
||||||
|
#self.convert_between_tz_and_utc(Eastern, Central)
|
||||||
|
#self.convert_between_tz_and_utc(Central, Eastern)
|
||||||
|
|
||||||
|
|
||||||
def test_suite():
|
def test_suite():
|
||||||
|
|
|
@ -4751,6 +4751,11 @@ datetimetz_astimezone(PyDateTime_DateTimeTZ *self, PyObject *args,
|
||||||
int ss = DATE_GET_SECOND(self);
|
int ss = DATE_GET_SECOND(self);
|
||||||
int us = DATE_GET_MICROSECOND(self);
|
int us = DATE_GET_MICROSECOND(self);
|
||||||
|
|
||||||
|
PyObject *result;
|
||||||
|
PyObject *temp;
|
||||||
|
int myoff, otoff, newoff;
|
||||||
|
int none;
|
||||||
|
|
||||||
PyObject *tzinfo;
|
PyObject *tzinfo;
|
||||||
static char *keywords[] = {"tz", NULL};
|
static char *keywords[] = {"tz", NULL};
|
||||||
|
|
||||||
|
@ -4760,30 +4765,127 @@ datetimetz_astimezone(PyDateTime_DateTimeTZ *self, PyObject *args,
|
||||||
if (check_tzinfo_subclass(tzinfo) < 0)
|
if (check_tzinfo_subclass(tzinfo) < 0)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
if (tzinfo != Py_None && self->tzinfo != Py_None) {
|
/* Don't call utcoffset unless necessary. */
|
||||||
int none;
|
result = new_datetimetz(y, m, d, hh, mm, ss, us, tzinfo);
|
||||||
int selfoffset;
|
if (result == NULL ||
|
||||||
selfoffset = call_utcoffset(self->tzinfo,
|
tzinfo == Py_None ||
|
||||||
(PyObject *)self,
|
self->tzinfo == Py_None ||
|
||||||
&none);
|
self->tzinfo == tzinfo)
|
||||||
if (selfoffset == -1 && PyErr_Occurred())
|
return result;
|
||||||
return NULL;
|
|
||||||
if (! none) {
|
/* Get the offsets. If either object turns out to be naive, again
|
||||||
int tzoffset;
|
* there's no conversion of date or time fields.
|
||||||
tzoffset = call_utcoffset(tzinfo,
|
*/
|
||||||
(PyObject *)self,
|
myoff = call_utcoffset(self->tzinfo, (PyObject *)self, &none);
|
||||||
&none);
|
if (myoff == -1 && PyErr_Occurred())
|
||||||
if (tzoffset == -1 && PyErr_Occurred())
|
goto Fail;
|
||||||
return NULL;
|
if (none)
|
||||||
if (! none) {
|
return result;
|
||||||
mm -= selfoffset - tzoffset;
|
|
||||||
if (normalize_datetime(&y, &m, &d,
|
otoff = call_utcoffset(tzinfo, result, &none);
|
||||||
&hh, &mm, &ss, &us) < 0)
|
if (otoff == -1 && PyErr_Occurred())
|
||||||
return NULL;
|
goto Fail;
|
||||||
}
|
if (none)
|
||||||
}
|
return result;
|
||||||
|
|
||||||
|
/* Add otoff-myoff to result. */
|
||||||
|
mm += otoff - myoff;
|
||||||
|
if (normalize_datetime(&y, &m, &d, &hh, &mm, &ss, &us) < 0)
|
||||||
|
goto Fail;
|
||||||
|
temp = new_datetimetz(y, m, d, hh, mm, ss, us, tzinfo);
|
||||||
|
if (temp == NULL)
|
||||||
|
goto Fail;
|
||||||
|
Py_DECREF(result);
|
||||||
|
result = temp;
|
||||||
|
|
||||||
|
/* 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 = call_utcoffset(tzinfo, result, &none);
|
||||||
|
if (newoff == -1 && PyErr_Occurred())
|
||||||
|
goto Fail;
|
||||||
|
if (none)
|
||||||
|
goto Inconsistent;
|
||||||
|
|
||||||
|
if (newoff != otoff) {
|
||||||
|
/* We did cross a boundary. Try to correct. */
|
||||||
|
mm += newoff - otoff;
|
||||||
|
if (normalize_datetime(&y, &m, &d, &hh, &mm, &ss, &us) < 0)
|
||||||
|
goto Fail;
|
||||||
|
temp = new_datetimetz(y, m, d, hh, mm, ss, us, tzinfo);
|
||||||
|
if (temp == NULL)
|
||||||
|
goto Fail;
|
||||||
|
Py_DECREF(result);
|
||||||
|
result = temp;
|
||||||
|
|
||||||
|
otoff = call_utcoffset(tzinfo, result, &none);
|
||||||
|
if (otoff == -1 && PyErr_Occurred())
|
||||||
|
goto Fail;
|
||||||
|
if (none)
|
||||||
|
goto Inconsistent;
|
||||||
|
}
|
||||||
|
/* 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.
|
||||||
|
*/
|
||||||
|
hh -= 1;
|
||||||
|
if (normalize_datetime(&y, &m, &d, &hh, &mm, &ss, &us) < 0)
|
||||||
|
goto Fail;
|
||||||
|
temp = new_datetimetz(y, m, d, hh, mm, ss, us, tzinfo);
|
||||||
|
if (temp == NULL)
|
||||||
|
goto Fail;
|
||||||
|
newoff = call_utcoffset(tzinfo, temp, &none);
|
||||||
|
if (newoff == -1 && PyErr_Occurred()) {
|
||||||
|
Py_DECREF(temp);
|
||||||
|
goto Fail;
|
||||||
}
|
}
|
||||||
return new_datetimetz(y, m, d, hh, mm, ss, us, tzinfo);
|
if (none) {
|
||||||
|
Py_DECREF(temp);
|
||||||
|
goto Inconsistent;
|
||||||
|
}
|
||||||
|
/* Are temp and result really the same time? temp == result iff
|
||||||
|
* temp - newoff == result - otoff, iff
|
||||||
|
* (result - HOUR) - newoff = result - otoff, iff
|
||||||
|
* otoff - newoff == HOUR
|
||||||
|
*/
|
||||||
|
if (otoff - newoff == 60) {
|
||||||
|
/* use the local time that makes sense */
|
||||||
|
Py_DECREF(result);
|
||||||
|
return temp;
|
||||||
|
}
|
||||||
|
Py_DECREF(temp);
|
||||||
|
|
||||||
|
/* There's still a problem with the unspellable (in local time)
|
||||||
|
* hour after DST ends.
|
||||||
|
*/
|
||||||
|
temp = datetime_richcompare((PyDateTime_DateTime *)self,
|
||||||
|
result, Py_EQ);
|
||||||
|
if (temp == NULL)
|
||||||
|
goto Fail;
|
||||||
|
if (temp == Py_True) {
|
||||||
|
Py_DECREF(temp);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Py_DECREF(temp);
|
||||||
|
/* Else there's no way to spell self in zone other.tz. */
|
||||||
|
PyErr_SetString(PyExc_ValueError, "astimezone(): the source "
|
||||||
|
"datetimetz can't be expressed in the target "
|
||||||
|
"timezone's local time");
|
||||||
|
goto Fail;
|
||||||
|
|
||||||
|
Inconsistent:
|
||||||
|
PyErr_SetString(PyExc_ValueError, "astimezone(): tz.utcoffset() "
|
||||||
|
"gave inconsistent results; cannot convert");
|
||||||
|
|
||||||
|
/* fall thru to failure */
|
||||||
|
Fail:
|
||||||
|
Py_DECREF(result);
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue