From 855fe88b241a512d21b7c716fcae88331ae50a98 Mon Sep 17 00:00:00 2001 From: Tim Peters Date: Sun, 22 Dec 2002 03:43:39 +0000 Subject: Implemented a Wiki suggestion: {timetz,datetimetz}.{utcoffset,dst}() now return a timedelta (or None) instead of an int (or None). tzinfo.{utcoffset,dst)() can now return a timedelta (or an int, or None). Curiously, this was much easier to do in the C implementation than in the Python implementation (which lives in the Zope3 code tree) -- the C code already had lots of hair to extract C ints from offset objects, and used C ints internally. --- Lib/test/test_datetime.py | 189 ++++++++++++++++++++++++++----------------- Modules/datetimemodule.c | 199 ++++++++++++++++++++++++++++------------------ 2 files changed, 241 insertions(+), 147 deletions(-) diff --git a/Lib/test/test_datetime.py b/Lib/test/test_datetime.py index 9fd38f1..ca26872 100644 --- a/Lib/test/test_datetime.py +++ b/Lib/test/test_datetime.py @@ -1458,35 +1458,120 @@ class TestTime(unittest.TestCase): self.failUnless(not cls(0)) self.failUnless(not cls()) - -class TestTimeTZ(TestTime): - - theclass = timetz - - def test_empty(self): - t = self.theclass() - self.assertEqual(t.hour, 0) - self.assertEqual(t.minute, 0) - self.assertEqual(t.second, 0) - self.assertEqual(t.microsecond, 0) - self.failUnless(t.tzinfo is None) +# A mixin for classes with a tzinfo= argument. Subclasses must define +# theclass as a class atribute, and theclass(1, 1, 1, tzinfo=whatever) +# must be legit (which is true for timetz and datetimetz). +class TZInfoBase(unittest.TestCase): def test_bad_tzinfo_classes(self): - tz = self.theclass - self.assertRaises(TypeError, tz, tzinfo=12) + cls = self.theclass + self.assertRaises(TypeError, cls, 1, 1, 1, tzinfo=12) class NiceTry(object): def __init__(self): pass def utcoffset(self, dt): pass - self.assertRaises(TypeError, tz, tzinfo=NiceTry) + self.assertRaises(TypeError, cls, 1, 1, 1, tzinfo=NiceTry) class BetterTry(tzinfo): def __init__(self): pass def utcoffset(self, dt): pass b = BetterTry() - t = tz(tzinfo=b) + t = cls(1, 1, 1, tzinfo=b) self.failUnless(t.tzinfo is b) + def test_utc_offset_out_of_bounds(self): + class Edgy(tzinfo): + def __init__(self, offset): + self.offset = offset + def utcoffset(self, dt): + return self.offset + + cls = self.theclass + for offset, legit in ((-1440, False), + (-1439, True), + (1439, True), + (1440, False)): + if cls is timetz: + t = cls(1, 2, 3, tzinfo=Edgy(offset)) + elif cls is datetimetz: + t = cls(6, 6, 6, 1, 2, 3, tzinfo=Edgy(offset)) + if legit: + aofs = abs(offset) + h, m = divmod(aofs, 60) + tag = "%c%02d:%02d" % (offset < 0 and '-' or '+', h, m) + if isinstance(t, datetimetz): + t = t.timetz() + self.assertEqual(str(t), "01:02:03" + tag) + else: + self.assertRaises(ValueError, str, t) + + def test_tzinfo_classes(self): + cls = self.theclass + class C1(tzinfo): + def utcoffset(self, dt): return None + def dst(self, dt): return None + def tzname(self, dt): return None + for t in (cls(1, 1, 1), + cls(1, 1, 1, tzinfo=None), + cls(1, 1, 1, tzinfo=C1())): + self.failUnless(t.utcoffset() is None) + self.failUnless(t.dst() is None) + self.failUnless(t.tzname() is None) + + class C2(tzinfo): + def utcoffset(self, dt): return -1439 + def dst(self, dt): return 1439 + def tzname(self, dt): return "aname" + class C3(tzinfo): + def utcoffset(self, dt): return timedelta(minutes=-1439) + def dst(self, dt): return timedelta(minutes=1439) + def tzname(self, dt): return "aname" + for t in cls(1, 1, 1, tzinfo=C2()), cls(1, 1, 1, tzinfo=C3()): + self.assertEqual(t.utcoffset(), timedelta(minutes=-1439)) + self.assertEqual(t.dst(), timedelta(minutes=1439)) + self.assertEqual(t.tzname(), "aname") + + # Wrong types. + class C4(tzinfo): + def utcoffset(self, dt): return "aname" + def dst(self, dt): return () + def tzname(self, dt): return 0 + t = cls(1, 1, 1, tzinfo=C4()) + self.assertRaises(TypeError, t.utcoffset) + self.assertRaises(TypeError, t.dst) + self.assertRaises(TypeError, t.tzname) + + # Offset out of range. + class C5(tzinfo): + def utcoffset(self, dt): return -1440 + def dst(self, dt): return 1440 + class C6(tzinfo): + def utcoffset(self, dt): return timedelta(hours=-24) + def dst(self, dt): return timedelta(hours=24) + for t in cls(1, 1, 1, tzinfo=C5()), cls(1, 1, 1, tzinfo=C6()): + self.assertRaises(ValueError, t.utcoffset) + self.assertRaises(ValueError, t.dst) + + # Not a whole number of minutes. + class C7(tzinfo): + def utcoffset(self, dt): return timedelta(seconds=61) + def dst(self, dt): return timedelta(microseconds=-81) + t = cls(1, 1, 1, tzinfo=C7()) + self.assertRaises(ValueError, t.utcoffset) + self.assertRaises(ValueError, t.dst) + + +class TestTimeTZ(TestTime, TZInfoBase): + theclass = timetz + + def test_empty(self): + t = self.theclass() + self.assertEqual(t.hour, 0) + self.assertEqual(t.minute, 0) + self.assertEqual(t.second, 0) + self.assertEqual(t.microsecond, 0) + self.failUnless(t.tzinfo is None) + def test_zones(self): est = FixedOffset(-300, "EST", 1) utc = FixedOffset(0, "UTC", -2) @@ -1503,9 +1588,9 @@ class TestTimeTZ(TestTime): self.failUnless(t4.tzinfo is None) self.assertEqual(t5.tzinfo, utc) - self.assertEqual(t1.utcoffset(), -300) - self.assertEqual(t2.utcoffset(), 0) - self.assertEqual(t3.utcoffset(), 60) + self.assertEqual(t1.utcoffset(), timedelta(minutes=-300)) + self.assertEqual(t2.utcoffset(), timedelta(minutes=0)) + self.assertEqual(t3.utcoffset(), timedelta(minutes=60)) self.failUnless(t4.utcoffset() is None) self.assertRaises(TypeError, t1.utcoffset, "no args") @@ -1515,9 +1600,9 @@ class TestTimeTZ(TestTime): self.failUnless(t4.tzname() is None) self.assertRaises(TypeError, t1.tzname, "no args") - self.assertEqual(t1.dst(), 1) - self.assertEqual(t2.dst(), -2) - self.assertEqual(t3.dst(), 3) + self.assertEqual(t1.dst(), timedelta(minutes=1)) + self.assertEqual(t2.dst(), timedelta(minutes=-2)) + self.assertEqual(t3.dst(), timedelta(minutes=3)) self.failUnless(t4.dst() is None) self.assertRaises(TypeError, t1.dst, "no args") @@ -1578,26 +1663,6 @@ class TestTimeTZ(TestTime): t2 = self.theclass(23, 48, 6, 100, tzinfo=FixedOffset(-1010, "")) self.assertEqual(hash(t1), hash(t2)) - def test_utc_offset_out_of_bounds(self): - class Edgy(tzinfo): - def __init__(self, offset): - self.offset = offset - def utcoffset(self, dt): - return self.offset - - for offset, legit in ((-1440, False), - (-1439, True), - (1439, True), - (1440, False)): - t = timetz(1, 2, 3, tzinfo=Edgy(offset)) - if legit: - aofs = abs(offset) - h, m = divmod(aofs, 60) - tag = "%c%02d:%02d" % (offset < 0 and '-' or '+', h, m) - self.assertEqual(str(t), "01:02:03" + tag) - else: - self.assertRaises(ValueError, str, t) - def test_pickling(self): import pickle, cPickle @@ -1623,7 +1688,7 @@ class TestTimeTZ(TestTime): derived.__setstate__(state) self.assertEqual(orig, derived) self.failUnless(isinstance(derived.tzinfo, PicklableFixedOffset)) - self.assertEqual(derived.utcoffset(), -300) + self.assertEqual(derived.utcoffset(), timedelta(minutes=-300)) self.assertEqual(derived.tzname(), 'cookie') for pickler in pickle, cPickle: @@ -1633,7 +1698,7 @@ class TestTimeTZ(TestTime): self.assertEqual(orig, derived) self.failUnless(isinstance(derived.tzinfo, PicklableFixedOffset)) - self.assertEqual(derived.utcoffset(), -300) + self.assertEqual(derived.utcoffset(), timedelta(minutes=-300)) self.assertEqual(derived.tzname(), 'cookie') def test_more_bool(self): @@ -1664,8 +1729,7 @@ class TestTimeTZ(TestTime): t = cls(0, tzinfo=FixedOffset(-24*60, "")) self.assertRaises(ValueError, lambda: bool(t)) -class TestDateTimeTZ(TestDateTime): - +class TestDateTimeTZ(TestDateTime, TZInfoBase): theclass = datetimetz def test_trivial(self): @@ -1744,22 +1808,6 @@ class TestDateTimeTZ(TestDateTime): t2 = self.theclass(2, 2, 2, tzinfo=FixedOffset(0, "")) self.assertRaises(ValueError, lambda: t1 == t1) - def test_bad_tzinfo_classes(self): - tz = self.theclass - self.assertRaises(TypeError, tz, 1, 2, 3, tzinfo=12) - - class NiceTry(object): - def __init__(self): pass - def utcoffset(self, dt): pass - self.assertRaises(TypeError, tz, 1, 2, 3, tzinfo=NiceTry) - - class BetterTry(tzinfo): - def __init__(self): pass - def utcoffset(self, dt): pass - b = BetterTry() - t = tz(1, 2, 3, tzinfo=b) - self.failUnless(t.tzinfo is b) - def test_pickling(self): import pickle, cPickle @@ -1785,7 +1833,7 @@ class TestDateTimeTZ(TestDateTime): derived.__setstate__(state) self.assertEqual(orig, derived) self.failUnless(isinstance(derived.tzinfo, PicklableFixedOffset)) - self.assertEqual(derived.utcoffset(), -300) + self.assertEqual(derived.utcoffset(), timedelta(minutes=-300)) self.assertEqual(derived.tzname(), 'cookie') for pickler in pickle, cPickle: @@ -1795,7 +1843,7 @@ class TestDateTimeTZ(TestDateTime): self.assertEqual(orig, derived) self.failUnless(isinstance(derived.tzinfo, PicklableFixedOffset)) - self.assertEqual(derived.utcoffset(), -300) + self.assertEqual(derived.utcoffset(), timedelta(minutes=-300)) self.assertEqual(derived.tzname(), 'cookie') def test_extreme_hashes(self): @@ -1822,9 +1870,9 @@ class TestDateTimeTZ(TestDateTime): self.assertEqual(t1.tzinfo, est) self.assertEqual(t2.tzinfo, utc) self.assertEqual(t3.tzinfo, met) - self.assertEqual(t1.utcoffset(), -300) - self.assertEqual(t2.utcoffset(), 0) - self.assertEqual(t3.utcoffset(), 60) + self.assertEqual(t1.utcoffset(), timedelta(minutes=-300)) + self.assertEqual(t2.utcoffset(), timedelta(minutes=0)) + self.assertEqual(t3.utcoffset(), timedelta(minutes=60)) self.assertEqual(t1.tzname(), "EST") self.assertEqual(t2.tzname(), "UTC") self.assertEqual(t3.tzname(), "MET") @@ -1914,8 +1962,7 @@ class TestDateTimeTZ(TestDateTime): # (nowaware base - nowawareplus base) + # (nowawareplus offset - nowaware offset) = # -delta + nowawareplus offset - nowaware offset - expected = timedelta(minutes=nowawareplus.utcoffset() - - nowaware.utcoffset()) - delta + expected = nowawareplus.utcoffset() - nowaware.utcoffset() - delta self.assertEqual(got, expected) # Try max possible difference. @@ -1935,7 +1982,7 @@ class TestDateTimeTZ(TestDateTime): another = meth(off42) again = meth(tzinfo=off42) self.failUnless(another.tzinfo is again.tzinfo) - self.assertEqual(another.utcoffset(), 42) + self.assertEqual(another.utcoffset(), timedelta(minutes=42)) # Bad argument with and w/o naming the keyword. self.assertRaises(TypeError, meth, 16) self.assertRaises(TypeError, meth, tzinfo=16) @@ -1955,7 +2002,7 @@ class TestDateTimeTZ(TestDateTime): another = meth(ts, off42) again = meth(ts, tzinfo=off42) self.failUnless(another.tzinfo is again.tzinfo) - self.assertEqual(another.utcoffset(), 42) + self.assertEqual(another.utcoffset(), timedelta(minutes=42)) # Bad argument with and w/o naming the keyword. self.assertRaises(TypeError, meth, ts, 16) self.assertRaises(TypeError, meth, ts, tzinfo=16) diff --git a/Modules/datetimemodule.c b/Modules/datetimemodule.c index 6dff659..9a9ab7e 100644 --- a/Modules/datetimemodule.c +++ b/Modules/datetimemodule.c @@ -549,6 +549,40 @@ normalize_datetime(int *year, int *month, int *day, * tzinfo helpers. */ +/* Ensure that p is None or of a tzinfo subclass. Return 0 if OK; if not + * raise TypeError and return -1. + */ +static int +check_tzinfo_subclass(PyObject *p) +{ + if (p == Py_None || PyTZInfo_Check(p)) + return 0; + PyErr_Format(PyExc_TypeError, + "tzinfo argument must be None or of a tzinfo subclass, " + "not type '%s'", + p->ob_type->tp_name); + return -1; +} + +/* Return tzinfo.methname(self), without any checking of results. + * If tzinfo is None, returns None. + */ +static PyObject * +call_tzinfo_method(PyObject *self, PyObject *tzinfo, char *methname) +{ + PyObject *result; + + assert(self && tzinfo && methname); + assert(check_tzinfo_subclass(tzinfo) >= 0); + if (tzinfo == Py_None) { + result = Py_None; + Py_INCREF(result); + } + else + result = PyObject_CallMethod(tzinfo, methname, "O", self); + return result; +} + /* If self has a tzinfo member, return a BORROWED reference to it. Else * return NULL, which is NOT AN ERROR. There are no error returns here, * and the caller must not decref the result. @@ -566,28 +600,15 @@ get_tzinfo_member(PyObject *self) return tzinfo; } -/* Ensure that p is None or of a tzinfo subclass. Return 0 if OK; if not - * raise TypeError and return -1. - */ -static int -check_tzinfo_subclass(PyObject *p) -{ - if (p == Py_None || PyTZInfo_Check(p)) - return 0; - PyErr_Format(PyExc_TypeError, - "tzinfo argument must be None or of a tzinfo subclass, " - "not type '%s'", - p->ob_type->tp_name); - return -1; -} - /* Internal helper. * 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, TypeError is raised and this returns -1. - * If it does return an int or long, but is outside the valid range for - * a UTC minute offset, ValueError is raised and this returns -1. + * return a Python int or long or timedelta, TypeError is raised and this + * returns -1. If it returns an int or long, but is outside the valid + * range for a UTC minute offset, or it returns a timedelta and the value is + * out of range or isn't a whole number of minutes, ValueError is raised and + * this returns -1. * Else *none is set to 0 and the integer method result is returned. */ static int @@ -602,7 +623,7 @@ call_utc_tzinfo_method(PyObject *tzinfo, char *name, PyObject *tzinfoarg, assert(tzinfoarg != NULL); *none = 0; - u = PyObject_CallMethod(tzinfo, name, "O", tzinfoarg); + u = call_tzinfo_method(tzinfoarg, tzinfo, name); if (u == NULL) return -1; @@ -614,12 +635,35 @@ call_utc_tzinfo_method(PyObject *tzinfo, char *name, PyObject *tzinfoarg, if (PyInt_Check(u)) result = PyInt_AS_LONG(u); + else if (PyLong_Check(u)) result = PyLong_AsLong(u); + + else if (PyDelta_Check(u)) { + const int days = GET_TD_DAYS(u); + if (days < -1 || days > 0) + result = 24*60; /* trigger ValueError below */ + else { + /* next line can't overflow because we know days + * is -1 or 0 now + */ + int ss = days * 24 * 3600 + GET_TD_SECONDS(u); + result = divmod(ss, 60, &ss); + if (ss || GET_TD_MICROSECONDS(u)) { + PyErr_Format(PyExc_ValueError, + "tzinfo.%s() must return a " + "whole number of minutes", + name); + result = -1; + goto Done; + } + } + } else { PyErr_Format(PyExc_TypeError, - "tzinfo.%s() must return None or int or long", - name); + "tzinfo.%s() must return None, integer or " + "timedelta, not '%s'", + name, u->ob_type->tp_name); goto Done; } @@ -649,6 +693,32 @@ call_utcoffset(PyObject *tzinfo, PyObject *tzinfoarg, int *none) return call_utc_tzinfo_method(tzinfo, "utcoffset", tzinfoarg, 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. */ +static PyObject * +offset_as_timedelta(PyObject *self, PyObject *tzinfo, char *name) { + PyObject *result; + + if (tzinfo == Py_None) { + result = Py_None; + Py_INCREF(result); + } + else { + int none; + int offset = call_utc_tzinfo_method(tzinfo, name, self, &none); + if (offset < 0 && PyErr_Occurred()) + return NULL; + if (none) { + result = Py_None; + Py_INCREF(result); + } + else + result = new_delta(0, offset * 60, 0, 1); + } + return result; +} + /* Call tzinfo.dst(tzinfoarg), and extract an integer from the * result. tzinfo must be an instance of the tzinfo class. If dst() * returns None, call_dst returns 0 and sets *none to 1. If dst() @@ -663,22 +733,29 @@ call_dst(PyObject *tzinfo, PyObject *tzinfoarg, int *none) return call_utc_tzinfo_method(tzinfo, "dst", tzinfoarg, none); } -/* Call tzinfo.tzname(tzinfoarg), and return the result. tzinfo must be - * an instance of the tzinfo class. If tzname() doesn't return None or - * a string, TypeError is raised and this returns NULL. +/* Call tzinfo.tzname(self), 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 + * returns NULL. */ static PyObject * -call_tzname(PyObject *tzinfo, PyObject *tzinfoarg) +call_tzname(PyObject *self, PyObject *tzinfo) { PyObject *result; + assert(self != NULL); assert(tzinfo != NULL); - assert(PyTZInfo_Check(tzinfo)); - assert(tzinfoarg != NULL); + assert(check_tzinfo_subclass(tzinfo) >= 0); - result = PyObject_CallMethod(tzinfo, "tzname", "O", tzinfoarg); - if (result != NULL && result != Py_None && !PyString_Check(result)) { - PyErr_Format(PyExc_TypeError, ".tzinfo.tzname() must " + if (tzinfo == Py_None) { + result = Py_None; + Py_INCREF(result); + } + else + result = PyObject_CallMethod(tzinfo, "tzname", "O", self); + + if (result != NULL && result != Py_None && ! PyString_Check(result)) { + PyErr_Format(PyExc_TypeError, "tzinfo.tzname() must " "return None or a string, not '%s'", result->ob_type->tp_name); Py_DECREF(result); @@ -699,7 +776,7 @@ typedef enum { /* date, * datetime, * datetimetz with None tzinfo, - * datetimetz where utcoffset() return None + * datetimetz where utcoffset() returns None * time, * timetz with None tzinfo, * timetz where utcoffset() returns None @@ -919,8 +996,8 @@ 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(tzinfo, - object); + PyObject *temp = call_tzname(object, + tzinfo); if (temp == NULL) goto Done; if (temp != Py_None) { assert(PyString_Check(temp)); @@ -3917,38 +3994,24 @@ timetz_dealloc(PyDateTime_TimeTZ *self) } /* - * Indirect access to tzinfo methods. One more "convenience function" and - * it won't be possible to find the useful methods anymore <0.5 wink>. + * Indirect access to tzinfo methods. */ -static PyObject * -timetz_convienience(PyDateTime_TimeTZ *self, char *name) -{ - PyObject *result; - - if (self->tzinfo == Py_None) { - result = Py_None; - Py_INCREF(result); - } - else - result = PyObject_CallMethod(self->tzinfo, name, "O", self); - return result; -} - /* These are all METH_NOARGS, so don't need to check the arglist. */ static PyObject * timetz_utcoffset(PyDateTime_TimeTZ *self, PyObject *unused) { - return timetz_convienience(self, "utcoffset"); + return offset_as_timedelta((PyObject *)self, self->tzinfo, + "utcoffset"); } static PyObject * -timetz_tzname(PyDateTime_TimeTZ *self, PyObject *unused) { - return timetz_convienience(self, "tzname"); +timetz_dst(PyDateTime_TimeTZ *self, PyObject *unused) { + return offset_as_timedelta((PyObject *)self, self->tzinfo, "dst"); } static PyObject * -timetz_dst(PyDateTime_TimeTZ *self, PyObject *unused) { - return timetz_convienience(self, "dst"); +timetz_tzname(PyDateTime_TimeTZ *self, PyObject *unused) { + return call_tzname((PyObject *)self, self->tzinfo); } /* @@ -4325,37 +4388,21 @@ datetimetz_dealloc(PyDateTime_DateTimeTZ *self) * Indirect access to tzinfo methods. */ -/* Internal helper. - * Call a tzinfo object's method, or return None if tzinfo is None. - */ -static PyObject * -datetimetz_convienience(PyDateTime_DateTimeTZ *self, char *name) -{ - PyObject *result; - - if (self->tzinfo == Py_None) { - result = Py_None; - Py_INCREF(result); - } - else - result = PyObject_CallMethod(self->tzinfo, name, "O", self); - return result; -} - /* These are all METH_NOARGS, so don't need to check the arglist. */ static PyObject * datetimetz_utcoffset(PyDateTime_DateTimeTZ *self, PyObject *unused) { - return datetimetz_convienience(self, "utcoffset"); + return offset_as_timedelta((PyObject *)self, self->tzinfo, + "utcoffset"); } static PyObject * -datetimetz_tzname(PyDateTime_DateTimeTZ *self, PyObject *unused) { - return datetimetz_convienience(self, "tzname"); +datetimetz_dst(PyDateTime_DateTimeTZ *self, PyObject *unused) { + return offset_as_timedelta((PyObject *)self, self->tzinfo, "dst"); } static PyObject * -datetimetz_dst(PyDateTime_DateTimeTZ *self, PyObject *unused) { - return datetimetz_convienience(self, "dst"); +datetimetz_tzname(PyDateTime_DateTimeTZ *self, PyObject *unused) { + return call_tzname((PyObject *)self, self->tzinfo); } /* -- cgit v0.12