A step on the way to making tzinfo classes writable by mortals: get rid

of the timetz case.  A tzinfo method will always see a datetimetz arg,
or None, now.  In the former case, it's still possible that it will get
a datetimetz argument belonging to a different timezone.  That will get
fixed next.
This commit is contained in:
Tim Peters 2002-12-30 20:52:32 +00:00
parent 567332abc4
commit bad8ff089a
3 changed files with 116 additions and 65 deletions

View File

@ -1,5 +1,5 @@
% XXX what order should the types be discussed in?
\section{\module{datetime} ---
Basic date and time types}
@ -785,13 +785,13 @@ Instance attributes (read-only):
\begin{memberdesc}{hour}
In \code{range(24)}.
\end{memberdesc}
\begin{memberdesc}{minute}
\begin{memberdesc}{minute}
In \code{range(60)}.
\end{memberdesc}
\begin{memberdesc}{second}
\begin{memberdesc}{second}
In \code{range(60)}.
\end{memberdesc}
\begin{memberdesc}{microsecond}
\begin{memberdesc}{microsecond}
In \code{range(1000000)}.
\end{memberdesc}
@ -844,7 +844,7 @@ Instance methods:
should not be instantiated directly. You need to derive a concrete
subclass, and (at least) supply implementations of the standard
\class{tzinfo} methods needed by the \class{datetime} methods you
use. The \module{datetime} module does not supply any concrete
use. The \module{datetime} module does not supply any concrete
subclasses of \class{tzinfo}.
An instance of (a concrete subclass of) \class{tzinfo} can be passed
@ -854,21 +854,17 @@ The latter objects view their fields as being in local time, and the
from UTC, the name of the time zone, and DST offset, all relative to a
date or time object passed to them.
Special requirement for pickling: A tzinfo subclass must have an
Special requirement for pickling: A \class{tzinfo} subclass must have an
\method{__init__} method that can be called with no arguments, else it
can be pickled but possibly not unpickled again. This is a technical
requirement that may be relaxed in the future.
A concrete subclass of \class{tzinfo} may need to implement the
following methods. Exactly which methods are needed depends on the
uses made of aware \module{datetime} objects; if in doubt, simply
implement all of them. The methods are called by a \class{datetimetz}
or \class{timetz} object, passing itself as the argument. A
\class{tzinfo} subclass's methods should be prepared to accept a dt
argument of \code{None} or of type \class{timetz} or
\class{datetimetz}.
uses made of aware \module{datetime} objects. If in doubt, simply
implement all of them.
\begin{methoddesc}{utcoffset}{dt}
\begin{methoddesc}{utcoffset}{self, dt}
Return offset of local time from UTC, in minutes east of UTC. If
local time is west of UTC, this should be negative. Note that this
is intended to be the total offset from UTC; for example, if a
@ -878,10 +874,14 @@ argument of \code{None} or of type \class{timetz} or
an integer, in the range -1439 to 1439 inclusive (1440 = 24*60;
the magnitude of the offset must be less than one day), or a
\class{timedelta} object representing a whole number of minutes
in the same range.
in the same range. Most implementations of \method{utcoffset()}
will probably look like:
\begin{verbatim}
return CONSTANT # fixed-offset class
return CONSTANT + self.dst(dt) # daylight-aware class
\end{methoddesc}
\begin{methoddesc}{tzname}{dt}
\begin{methoddesc}{tzname}{self, dt}
Return the timezone name corresponding to the \class{datetime} represented
by dt, as a string. Nothing about string names is defined by the
\module{datetime} module, and there's no requirement that it mean anything
@ -893,7 +893,7 @@ argument of \code{None} or of type \class{timetz} or
of dt passed, especially if the \class{tzinfo} class is accounting for DST.
\end{methoddesc}
\begin{methoddesc}{dst}{dt}
\begin{methoddesc}{dst}{self, dt}
Return the DST offset, in minutes east of UTC, or \code{None} if
DST information isn't known. Return 0 if DST is not in effect.
If DST is in effect, return the offset as an integer or
@ -907,6 +907,26 @@ argument of \code{None} or of type \class{timetz} or
\member{tm_isdst} flag should be set.
\end{methoddesc}
These methods are called by a \class{datetimetz} or \class{timetz} object,
in response to their methods of the same names. A \class{datetimetz}
object passes itself as the argument, and a \class{timetz} object passes
\code{None} as the argument. A \class{tzinfo} subclass's methods should
therefore be prepared to accept a \var{dt} argument of \code{None}, or of
class \class{datetimetz}.
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
class wishes to say that timetz objects don't participate in the
\class{tzinfo} protocol. In other applications, it may be more useful
for \code{utcoffset(None}} to return the standard UTC offset.
When a \class{datetimetz} object is passed in response to a
\class{datetimetz} method, \code{dt.tzinfo} is the same object as
\var{self}. \class{tzinfo} methods can rely on this, unless
user code calls \class{tzinfo} methods directly. The intent is that
the \class{tzinfo} methods interpret \var{dt} as being in local time,
and not need to worry about objects in other timezones.
Example \class{tzinfo} classes:
\verbatiminput{tzinfo-examples.py}

View File

@ -1557,6 +1557,23 @@ class TestTime(unittest.TestCase):
# must be legit (which is true for timetz and datetimetz).
class TZInfoBase(unittest.TestCase):
def test_argument_passing(self):
cls = self.theclass
# A datetimetz passes itself on, a timetz passes None.
class introspective(tzinfo):
def tzname(self, dt): return dt and "real" or "none"
def utcoffset(self, dt): return dt and 42 or -42
dst = utcoffset
obj = cls(1, 2, 3, tzinfo=introspective())
expected = cls is timetz and "none" or "real"
self.assertEqual(obj.tzname(), expected)
expected = timedelta(minutes=(cls is timetz and -42 or 42))
self.assertEqual(obj.utcoffset(), expected)
self.assertEqual(obj.dst(), expected)
def test_bad_tzinfo_classes(self):
cls = self.theclass
self.assertRaises(TypeError, cls, 1, 1, 1, tzinfo=12)
@ -1677,22 +1694,26 @@ class TZInfoBase(unittest.TestCase):
self.assertEqual(got, expected)
# However, if they're different members, uctoffset is not ignored.
d0 = base.replace(minute=3, tzinfo=OperandDependentOffset())
d1 = base.replace(minute=9, tzinfo=OperandDependentOffset())
d2 = base.replace(minute=11, tzinfo=OperandDependentOffset())
for x in d0, d1, d2:
for y in d0, d1, d2:
got = cmp(x, y)
if (x is d0 or x is d1) and (y is d0 or y is d1):
expected = 0
elif x is y is d2:
expected = 0
elif x is d2:
expected = -1
else:
assert y is d2
expected = 1
self.assertEqual(got, expected)
# Note that a timetz can't actually have an operand-depedent offset,
# though (and timetz.utcoffset() passes None to tzinfo.utcoffset()),
# so skip this test for timetz.
if cls is not timetz:
d0 = base.replace(minute=3, tzinfo=OperandDependentOffset())
d1 = base.replace(minute=9, tzinfo=OperandDependentOffset())
d2 = base.replace(minute=11, tzinfo=OperandDependentOffset())
for x in d0, d1, d2:
for y in d0, d1, d2:
got = cmp(x, y)
if (x is d0 or x is d1) and (y is d0 or y is d1):
expected = 0
elif x is y is d2:
expected = 0
elif x is d2:
expected = -1
else:
assert y is d2
expected = 1
self.assertEqual(got, expected)
class TestTimeTZ(TestTime, TZInfoBase):
@ -2535,7 +2556,7 @@ class USTimeZone(tzinfo):
return self.stdoffset + self.dst(dt)
def dst(self, dt):
if dt is None or isinstance(dt, time) or dt.tzinfo is None:
if dt is None or dt.tzinfo is None:
# An exception instead may be sensible here, in one or more of
# the cases.
return ZERO

View File

@ -564,22 +564,22 @@ check_tzinfo_subclass(PyObject *p)
return -1;
}
/* Return tzinfo.methname(self), without any checking of results.
/* Return tzinfo.methname(tzinfoarg), without any checking of results.
* If tzinfo is None, returns None.
*/
static PyObject *
call_tzinfo_method(PyObject *self, PyObject *tzinfo, char *methname)
call_tzinfo_method(PyObject *tzinfo, char *methname, PyObject *tzinfoarg)
{
PyObject *result;
assert(self && tzinfo && methname);
assert(tzinfo && methname && tzinfoarg);
assert(check_tzinfo_subclass(tzinfo) >= 0);
if (tzinfo == Py_None) {
result = Py_None;
Py_INCREF(result);
}
else
result = PyObject_CallMethod(tzinfo, methname, "O", self);
result = PyObject_CallMethod(tzinfo, methname, "O", tzinfoarg);
return result;
}
@ -612,8 +612,8 @@ replace_tzinfo(PyObject *self, PyObject *newtzinfo)
((PyDateTime_DateTimeTZ *)self)->tzinfo = newtzinfo;
}
/* Internal helper.
* Call getattr(tzinfo, name)(tzinfoarg), and extract an int from the
/* Call getattr(tzinfo, name)(tzinfoarg), and extract an int from the
* result. tzinfo must be an instance of the tzinfo class. If the method
* returns None, this returns 0 and sets *none to 1. If the method doesn't
* return a Python int or long or timedelta, TypeError is raised and this
@ -635,7 +635,7 @@ call_utc_tzinfo_method(PyObject *tzinfo, char *name, PyObject *tzinfoarg,
assert(tzinfoarg != NULL);
*none = 0;
u = call_tzinfo_method(tzinfoarg, tzinfo, name);
u = call_tzinfo_method(tzinfo, name, tzinfoarg);
if (u == NULL)
return -1;
@ -702,18 +702,21 @@ call_utcoffset(PyObject *tzinfo, PyObject *tzinfoarg, int *none)
static PyObject *new_delta(int d, int sec, int usec, int normalize);
/* Call tzinfo.name(self) and return the offset as a timedelta or None. */
/* Call tzinfo.name(tzinfoarg), and return the offset as a timedelta or None.
*/
static PyObject *
offset_as_timedelta(PyObject *self, PyObject *tzinfo, char *name) {
offset_as_timedelta(PyObject *tzinfo, char *name, PyObject *tzinfoarg) {
PyObject *result;
assert(tzinfo && name && tzinfoarg);
if (tzinfo == Py_None) {
result = Py_None;
Py_INCREF(result);
}
else {
int none;
int offset = call_utc_tzinfo_method(tzinfo, name, self, &none);
int offset = call_utc_tzinfo_method(tzinfo, name, tzinfoarg,
&none);
if (offset < 0 && PyErr_Occurred())
return NULL;
if (none) {
@ -740,26 +743,26 @@ call_dst(PyObject *tzinfo, PyObject *tzinfoarg, int *none)
return call_utc_tzinfo_method(tzinfo, "dst", tzinfoarg, none);
}
/* Call tzinfo.tzname(self), and return the result. tzinfo must be
/* Call tzinfo.tzname(tzinfoarg), and return the result. tzinfo must be
* an instance of the tzinfo class or None. If tzinfo isn't None, and
* tzname() doesn't return None ora string, TypeError is raised and this
* tzname() doesn't return None or a string, TypeError is raised and this
* returns NULL.
*/
static PyObject *
call_tzname(PyObject *self, PyObject *tzinfo)
call_tzname(PyObject *tzinfo, PyObject *tzinfoarg)
{
PyObject *result;
assert(self != NULL);
assert(tzinfo != NULL);
assert(check_tzinfo_subclass(tzinfo) >= 0);
assert(tzinfoarg != NULL);
if (tzinfo == Py_None) {
result = Py_None;
Py_INCREF(result);
}
else
result = PyObject_CallMethod(tzinfo, "tzname", "O", self);
result = PyObject_CallMethod(tzinfo, "tzname", "O", tzinfoarg);
if (result != NULL && result != Py_None && ! PyString_Check(result)) {
PyErr_Format(PyExc_TypeError, "tzinfo.tzname() must "
@ -816,7 +819,9 @@ classify_utcoffset(PyObject *op, int *offset)
return (PyTime_Check(op) || PyDate_Check(op)) ?
OFFSET_NAIVE : OFFSET_UNKNOWN;
}
*offset = call_utcoffset(tzinfo, op, &none);
*offset = call_utcoffset(tzinfo,
PyTimeTZ_Check(op) ? Py_None : op,
&none);
if (*offset == -1 && PyErr_Occurred())
return OFFSET_ERROR;
return none ? OFFSET_NAIVE : OFFSET_AWARE;
@ -951,9 +956,12 @@ format_utcoffset(char *buf, size_t buflen, const char *sep,
* so this imports the module and calls it. All the hair is due to
* giving special meanings to the %z and %Z format codes via a preprocessing
* step on the format string.
* tzinfoarg is the argument to pass to the object's tzinfo method, if
* needed.
*/
static PyObject *
wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple)
wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
PyObject *tzinfoarg)
{
PyObject *result = NULL; /* guilty until proved innocent */
@ -1031,11 +1039,12 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple)
zreplacement = PyString_FromString("");
if (zreplacement == NULL) goto Done;
if (tzinfo != Py_None && tzinfo != NULL) {
assert(tzinfoarg != NULL);
if (format_utcoffset(buf,
sizeof(buf),
"",
tzinfo,
object) < 0)
tzinfoarg) < 0)
goto Done;
Py_DECREF(zreplacement);
zreplacement = PyString_FromString(buf);
@ -1053,8 +1062,9 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple)
Zreplacement = PyString_FromString("");
if (Zreplacement == NULL) goto Done;
if (tzinfo != Py_None && tzinfo != NULL) {
PyObject *temp = call_tzname(object,
tzinfo);
PyObject *temp;
assert(tzinfoarg != NULL);
temp = call_tzname(tzinfo, tzinfoarg);
if (temp == NULL) goto Done;
if (temp != Py_None) {
assert(PyString_Check(temp));
@ -2424,7 +2434,8 @@ date_strftime(PyDateTime_Date *self, PyObject *args, PyObject *kw)
tuple = PyObject_CallMethod((PyObject *)self, "timetuple", "()");
if (tuple == NULL)
return NULL;
result = wrap_strftime((PyObject *)self, format, tuple);
result = wrap_strftime((PyObject *)self, format, tuple,
(PyObject *)self);
Py_DECREF(tuple);
return result;
}
@ -3652,7 +3663,7 @@ time_strftime(PyDateTime_Time *self, PyObject *args, PyObject *kw)
if (tuple == NULL)
return NULL;
assert(PyTuple_Size(tuple) == 9);
result = wrap_strftime((PyObject *)self, format, tuple);
result = wrap_strftime((PyObject *)self, format, tuple, Py_None);
Py_DECREF(tuple);
return result;
}
@ -4140,18 +4151,17 @@ timetz_dealloc(PyDateTime_TimeTZ *self)
/* These are all METH_NOARGS, so don't need to check the arglist. */
static PyObject *
timetz_utcoffset(PyDateTime_TimeTZ *self, PyObject *unused) {
return offset_as_timedelta((PyObject *)self, self->tzinfo,
"utcoffset");
return offset_as_timedelta(self->tzinfo, "utcoffset", Py_None);
}
static PyObject *
timetz_dst(PyDateTime_TimeTZ *self, PyObject *unused) {
return offset_as_timedelta((PyObject *)self, self->tzinfo, "dst");
return offset_as_timedelta(self->tzinfo, "dst", Py_None);
}
static PyObject *
timetz_tzname(PyDateTime_TimeTZ *self, PyObject *unused) {
return call_tzname((PyObject *)self, self->tzinfo);
return call_tzname(self->tzinfo, Py_None);
}
/*
@ -4181,7 +4191,7 @@ timetz_isoformat(PyDateTime_TimeTZ *self)
/* We need to append the UTC offset. */
if (format_utcoffset(buf, sizeof(buf), ":", self->tzinfo,
(PyObject *)self) < 0) {
Py_None) < 0) {
Py_DECREF(result);
return NULL;
}
@ -4234,7 +4244,7 @@ timetz_nonzero(PyDateTime_TimeTZ *self)
}
offset = 0;
if (self->tzinfo != Py_None) {
offset = call_utcoffset(self->tzinfo, (PyObject *)self, &none);
offset = call_utcoffset(self->tzinfo, Py_None, &none);
if (offset == -1 && PyErr_Occurred())
return -1;
}
@ -4543,18 +4553,18 @@ datetimetz_dealloc(PyDateTime_DateTimeTZ *self)
/* These are all METH_NOARGS, so don't need to check the arglist. */
static PyObject *
datetimetz_utcoffset(PyDateTime_DateTimeTZ *self, PyObject *unused) {
return offset_as_timedelta((PyObject *)self, self->tzinfo,
"utcoffset");
return offset_as_timedelta(self->tzinfo, "utcoffset",
(PyObject *)self);
}
static PyObject *
datetimetz_dst(PyDateTime_DateTimeTZ *self, PyObject *unused) {
return offset_as_timedelta((PyObject *)self, self->tzinfo, "dst");
return offset_as_timedelta(self->tzinfo, "dst", (PyObject *)self);
}
static PyObject *
datetimetz_tzname(PyDateTime_DateTimeTZ *self, PyObject *unused) {
return call_tzname((PyObject *)self, self->tzinfo);
return call_tzname(self->tzinfo, (PyObject *)self);
}
/*