From bad8ff089a04372465c3143a3567b9712673c155 Mon Sep 17 00:00:00 2001 From: Tim Peters Date: Mon, 30 Dec 2002 20:52:32 +0000 Subject: 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. --- Doc/lib/libdatetime.tex | 54 +++++++++++++++++++++++----------- Lib/test/test_datetime.py | 55 ++++++++++++++++++++++++----------- Modules/datetimemodule.c | 74 +++++++++++++++++++++++++++-------------------- 3 files changed, 117 insertions(+), 66 deletions(-) diff --git a/Doc/lib/libdatetime.tex b/Doc/lib/libdatetime.tex index e8ac0dd..f149e46 100644 --- a/Doc/lib/libdatetime.tex +++ b/Doc/lib/libdatetime.tex @@ -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}. - -\begin{methoddesc}{utcoffset}{dt} +uses made of aware \module{datetime} objects. If in doubt, simply +implement all of them. + +\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} diff --git a/Lib/test/test_datetime.py b/Lib/test/test_datetime.py index 264d75c..41ceae7 100644 --- a/Lib/test/test_datetime.py +++ b/Lib/test/test_datetime.py @@ -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 diff --git a/Modules/datetimemodule.c b/Modules/datetimemodule.c index 4381d03..3fe1185 100644 --- a/Modules/datetimemodule.c +++ b/Modules/datetimemodule.c @@ -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); } /* -- cgit v0.12