Commit 521fc15e authored by Tim Peters's avatar Tim Peters

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.
parent ba2f875d
This diff is collapsed.
......@@ -4751,6 +4751,11 @@ datetimetz_astimezone(PyDateTime_DateTimeTZ *self, PyObject *args,
int ss = DATE_GET_SECOND(self);
int us = DATE_GET_MICROSECOND(self);
PyObject *result;
PyObject *temp;
int myoff, otoff, newoff;
int none;
PyObject *tzinfo;
static char *keywords[] = {"tz", NULL};
......@@ -4760,30 +4765,127 @@ datetimetz_astimezone(PyDateTime_DateTimeTZ *self, PyObject *args,
if (check_tzinfo_subclass(tzinfo) < 0)
return NULL;
if (tzinfo != Py_None && self->tzinfo != Py_None) {
int none;
int selfoffset;
selfoffset = call_utcoffset(self->tzinfo,
(PyObject *)self,
&none);
if (selfoffset == -1 && PyErr_Occurred())
return NULL;
if (! none) {
int tzoffset;
tzoffset = call_utcoffset(tzinfo,
(PyObject *)self,
&none);
if (tzoffset == -1 && PyErr_Occurred())
return NULL;
if (! none) {
mm -= selfoffset - tzoffset;
if (normalize_datetime(&y, &m, &d,
&hh, &mm, &ss, &us) < 0)
return NULL;
}
}
}
return new_datetimetz(y, m, d, hh, mm, ss, us, tzinfo);
/* Don't call utcoffset unless necessary. */
result = new_datetimetz(y, m, d, hh, mm, ss, us, tzinfo);
if (result == NULL ||
tzinfo == Py_None ||
self->tzinfo == Py_None ||
self->tzinfo == tzinfo)
return result;
/* Get the offsets. If either object turns out to be naive, again
* there's no conversion of date or time fields.
*/
myoff = call_utcoffset(self->tzinfo, (PyObject *)self, &none);
if (myoff == -1 && PyErr_Occurred())
goto Fail;
if (none)
return result;
otoff = call_utcoffset(tzinfo, result, &none);
if (otoff == -1 && PyErr_Occurred())
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;
}
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 *
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment