diff options
-rw-r--r-- | Lib/test/test_datetime.py | 118 | ||||
-rw-r--r-- | Modules/datetimemodule.c | 219 |
2 files changed, 217 insertions, 120 deletions
diff --git a/Lib/test/test_datetime.py b/Lib/test/test_datetime.py index 8a8d315..8878386 100644 --- a/Lib/test/test_datetime.py +++ b/Lib/test/test_datetime.py @@ -1313,20 +1313,27 @@ class TestDateTime(TestDate): self.assertRaises(ValueError, base.replace, year=2001) def test_astimezone(self): - # Pretty boring! The TZ test is more interesting here. + # Pretty boring! The TZ test is more interesting here. astimezone() + # simply can't be applied to a naive object. dt = self.theclass.now() f = FixedOffset(44, "") - for dtz in dt.astimezone(f), dt.astimezone(tz=f): - self.failUnless(isinstance(dtz, datetime)) - self.assertEqual(dt.date(), dtz.date()) - self.assertEqual(dt.time(), dtz.time()) - self.failUnless(dtz.tzinfo is f) - self.assertEqual(dtz.utcoffset(), timedelta(minutes=44)) - self.assertRaises(TypeError, dt.astimezone) # not enough args self.assertRaises(TypeError, dt.astimezone, f, f) # too many args self.assertRaises(TypeError, dt.astimezone, dt) # arg wrong type + self.assertRaises(ValueError, dt.astimezone, f) # naive + self.assertRaises(ValueError, dt.astimezone, tz=f) # naive + + class Bogus(tzinfo): + def utcoffset(self, dt): return None + def dst(self, dt): return timedelta(0) + bog = Bogus() + self.assertRaises(ValueError, dt.astimezone, bog) # naive + class AlsoBogus(tzinfo): + def utcoffset(self, dt): return timedelta(0) + def dst(self, dt): return None + alsobog = AlsoBogus() + self.assertRaises(ValueError, dt.astimezone, alsobog) # also naive class TestTime(unittest.TestCase): @@ -2443,17 +2450,11 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase): dt = self.theclass.now(tzinfo=f44m) self.failUnless(dt.tzinfo is f44m) - # Replacing with degenerate tzinfo doesn't do any adjustment. - for x in dt.astimezone(fnone), dt.astimezone(tz=fnone): - self.failUnless(x.tzinfo is fnone) - self.assertEqual(x.date(), dt.date()) - self.assertEqual(x.time(), dt.time()) - # Ditt with None tz. - x = dt.astimezone(tz=None) - self.failUnless(x.tzinfo is None) - self.assertEqual(x.date(), dt.date()) - self.assertEqual(x.time(), dt.time()) - # Ditto replacing with same tzinfo. + # Replacing with degenerate tzinfo raises an exception. + self.assertRaises(ValueError, dt.astimezone, fnone) + # Ditto with None tz. + self.assertRaises(TypeError, dt.astimezone, None) + # Replacing with same tzinfo makes no change. x = dt.astimezone(dt.tzinfo) self.failUnless(x.tzinfo is f44m) self.assertEqual(x.date(), dt.date()) @@ -2603,7 +2604,7 @@ class USTimeZone(tzinfo): # Can't compare naive to aware objects, so strip the timezone from # dt first. - if start <= dt.astimezone(None) < end: + if start <= dt.replace(tzinfo=None) < end: return HOUR else: return ZERO @@ -2630,8 +2631,6 @@ class TestTimezoneConversions(unittest.TestCase): # 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) @@ -2684,8 +2683,11 @@ class TestTimezoneConversions(unittest.TestCase): # 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)) + + # Converting to UTC and back is an identity too. + asutc = dt.astimezone(utc) + there_and_back = asutc.astimezone(tz) + self.assertEqual(dt, there_and_back) def convert_between_tz_and_utc(self, tz, utc): dston = self.dston.replace(tzinfo=tz) @@ -2737,7 +2739,7 @@ class TestTimezoneConversions(unittest.TestCase): # 22:00 on day before daylight starts. fourback = self.dston - timedelta(hours=4) ninewest = FixedOffset(-9*60, "-0900", 0) - fourback = fourback.astimezone(ninewest) + fourback = fourback.replace(tzinfo=ninewest) # 22:00-0900 is 7:00 UTC == 2:00 EST == 3:00 DST. Since it's "after # 2", we should get the 3 spelling. # If we plug 22:00 the day before into Eastern, it "looks like std @@ -2746,17 +2748,17 @@ class TestTimezoneConversions(unittest.TestCase): # local clock jumps from 1 to 3). The point here is to make sure we # get the 3 spelling. expected = self.dston.replace(hour=3) - got = fourback.astimezone(Eastern).astimezone(None) + got = fourback.astimezone(Eastern).replace(tzinfo=None) self.assertEqual(expected, got) # Similar, but map to 6:00 UTC == 1:00 EST == 2:00 DST. In that # case we want the 1:00 spelling. - sixutc = self.dston.replace(hour=6).astimezone(utc_real) + sixutc = self.dston.replace(hour=6, tzinfo=utc_real) # Now 6:00 "looks like daylight", so the offset wrt Eastern is -4, # and adding -4-0 == -4 gives the 2:00 spelling. We want the 1:00 EST # spelling. expected = self.dston.replace(hour=1) - got = sixutc.astimezone(Eastern).astimezone(None) + got = sixutc.astimezone(Eastern).replace(tzinfo=None) self.assertEqual(expected, got) # Now on the day DST ends, we want "repeat an hour" behavior. @@ -2798,6 +2800,66 @@ class TestTimezoneConversions(unittest.TestCase): def dst(self, dt): return None self.assertRaises(ValueError, now.astimezone, notok()) + def test_fromutc(self): + self.assertRaises(TypeError, Eastern.fromutc) # not enough args + now = datetime.utcnow().replace(tzinfo=utc_real) + self.assertRaises(ValueError, Eastern.fromutc, now) # wrong tzinfo + now = now.replace(tzinfo=Eastern) # insert correct tzinfo + enow = Eastern.fromutc(now) # doesn't blow up + self.assertEqual(enow.tzinfo, Eastern) # has right tzinfo member + self.assertRaises(TypeError, Eastern.fromutc, now, now) # too many args + self.assertRaises(TypeError, Eastern.fromutc, date.today()) # wrong type + + # Always converts UTC to standard time. + class FauxUSTimeZone(USTimeZone): + def fromutc(self, dt): + return dt + self.stdoffset + FEastern = FauxUSTimeZone(-5, "FEastern", "FEST", "FEDT") + + # UTC 4:MM 5:MM 6:MM 7:MM 8:MM 9:MM + # EST 23:MM 0:MM 1:MM 2:MM 3:MM 4:MM + # EDT 0:MM 1:MM 2:MM 3:MM 4:MM 5:MM + + # Check around DST start. + start = self.dston.replace(hour=4, tzinfo=Eastern) + fstart = start.replace(tzinfo=FEastern) + for wall in 23, 0, 1, 3, 4, 5: + expected = start.replace(hour=wall) + if wall == 23: + expected -= timedelta(days=1) + got = Eastern.fromutc(start) + self.assertEqual(expected, got) + + expected = fstart + FEastern.stdoffset + got = FEastern.fromutc(fstart) + self.assertEqual(expected, got) + + # Ensure astimezone() calls fromutc() too. + got = fstart.replace(tzinfo=utc_real).astimezone(FEastern) + self.assertEqual(expected, got) + + start += HOUR + fstart += HOUR + + # Check around DST end. + start = self.dstoff.replace(hour=4, tzinfo=Eastern) + fstart = start.replace(tzinfo=FEastern) + for wall in 0, 1, 1, 2, 3, 4: + expected = start.replace(hour=wall) + got = Eastern.fromutc(start) + self.assertEqual(expected, got) + + expected = fstart + FEastern.stdoffset + got = FEastern.fromutc(fstart) + self.assertEqual(expected, got) + + # Ensure astimezone() calls fromutc() too. + got = fstart.replace(tzinfo=utc_real).astimezone(FEastern) + self.assertEqual(expected, got) + + start += HOUR + fstart += HOUR + def test_suite(): allsuites = [unittest.makeSuite(klass, 'test') diff --git a/Modules/datetimemodule.c b/Modules/datetimemodule.c index d88fc9e..6f72929 100644 --- a/Modules/datetimemodule.c +++ b/Modules/datetimemodule.c @@ -2725,24 +2725,106 @@ tzinfo_nogo(const char* methodname) /* Methods. A subclass must implement these. */ -static PyObject* +static PyObject * tzinfo_tzname(PyDateTime_TZInfo *self, PyObject *dt) { return tzinfo_nogo("tzname"); } -static PyObject* +static PyObject * tzinfo_utcoffset(PyDateTime_TZInfo *self, PyObject *dt) { return tzinfo_nogo("utcoffset"); } -static PyObject* +static PyObject * tzinfo_dst(PyDateTime_TZInfo *self, PyObject *dt) { return tzinfo_nogo("dst"); } +static PyObject * +tzinfo_fromutc(PyDateTime_TZInfo *self, PyDateTime_DateTime *dt) +{ + int y, m, d, hh, mm, ss, us; + + PyObject *result; + int off, dst; + int none; + int delta; + + if (! PyDateTime_Check(dt)) { + PyErr_SetString(PyExc_TypeError, + "fromutc: argument must be a datetime"); + return NULL; + } + if (! HASTZINFO(dt) || dt->tzinfo != (PyObject *)self) { + PyErr_SetString(PyExc_ValueError, "fromutc: dt.tzinfo " + "is not self"); + return NULL; + } + + off = call_utcoffset(dt->tzinfo, (PyObject *)dt, &none); + if (off == -1 && PyErr_Occurred()) + return NULL; + if (none) { + PyErr_SetString(PyExc_ValueError, "fromutc: non-None " + "utcoffset() result required"); + return NULL; + } + + dst = call_dst(dt->tzinfo, (PyObject *)dt, &none); + if (dst == -1 && PyErr_Occurred()) + return NULL; + if (none) { + PyErr_SetString(PyExc_ValueError, "fromutc: non-None " + "dst() result required"); + return NULL; + } + + y = GET_YEAR(dt); + m = GET_MONTH(dt); + d = GET_DAY(dt); + hh = DATE_GET_HOUR(dt); + mm = DATE_GET_MINUTE(dt); + ss = DATE_GET_SECOND(dt); + us = DATE_GET_MICROSECOND(dt); + + delta = off - dst; + mm += delta; + if ((mm < 0 || mm >= 60) && + normalize_datetime(&y, &m, &d, &hh, &mm, &ss, &us) < 0) + goto Fail; + result = new_datetime(y, m, d, hh, mm, ss, us, dt->tzinfo); + if (result == NULL) + return result; + + dst = call_dst(dt->tzinfo, result, &none); + if (dst == -1 && PyErr_Occurred()) + goto Fail; + if (none) + goto Inconsistent; + if (dst == 0) + return result; + + mm += dst; + if ((mm < 0 || mm >= 60) && + normalize_datetime(&y, &m, &d, &hh, &mm, &ss, &us) < 0) + goto Fail; + Py_DECREF(result); + result = new_datetime(y, m, d, hh, mm, ss, us, dt->tzinfo); + return result; + +Inconsistent: + PyErr_SetString(PyExc_ValueError, "fromutc: tz.dst() gave" + "inconsistent results; cannot convert"); + + /* fall thru to failure */ +Fail: + Py_DECREF(result); + return NULL; +} + /* * Pickle support. This is solely so that tzinfo subclasses can use * pickling -- tzinfo itself is supposed to be uninstantiable. The @@ -2772,6 +2854,9 @@ static PyMethodDef tzinfo_methods[] = { {"dst", (PyCFunction)tzinfo_dst, METH_O, PyDoc_STR("datetime -> DST offset in minutes east of UTC.")}, + {"fromutc", (PyCFunction)tzinfo_fromutc, METH_O, + PyDoc_STR("datetime in UTC -> datetime in local time.")}, + {NULL, NULL} }; @@ -4036,109 +4121,59 @@ datetime_replace(PyDateTime_DateTime *self, PyObject *args, PyObject *kw) static PyObject * datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw) { - int y = GET_YEAR(self); - int m = GET_MONTH(self); - int d = GET_DAY(self); - int hh = DATE_GET_HOUR(self); - int mm = DATE_GET_MINUTE(self); - int ss = DATE_GET_SECOND(self); - int us = DATE_GET_MICROSECOND(self); - + int y, m, d, hh, mm, ss, us; PyObject *result; - PyObject *temp; - int selfoff, resoff, dst1; - int none; - int delta; + int offset, none; PyObject *tzinfo; static char *keywords[] = {"tz", NULL}; - if (! PyArg_ParseTupleAndKeywords(args, kw, "O:astimezone", keywords, - &tzinfo)) + if (! PyArg_ParseTupleAndKeywords(args, kw, "O!:astimezone", keywords, + &PyDateTime_TZInfoType, &tzinfo)) return NULL; - if (check_tzinfo_subclass(tzinfo) < 0) - return NULL; - - /* Don't call utcoffset unless necessary. */ - result = new_datetime(y, m, d, hh, mm, ss, us, tzinfo); - if (result == NULL || - tzinfo == Py_None || - ! HASTZINFO(self) || - 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. - */ - selfoff = call_utcoffset(self->tzinfo, (PyObject *)self, &none); - if (selfoff == -1 && PyErr_Occurred()) - goto Fail; - if (none) - return result; - - resoff = call_utcoffset(tzinfo, result, &none); - if (resoff == -1 && PyErr_Occurred()) - goto Fail; - if (none) - return result; - - /* See the long comment block at the end of this file for an - * explanation of this algorithm. That it always works requires a - * pretty intricate proof. There are many equivalent ways to code - * up the proof as an algorithm. This way favors calling dst() over - * calling utcoffset(), because "the usual" utcoffset() calls dst() - * itself, and calling the latter instead saves a Python-level - * function call. This way of coding it also follows the proof - * closely, w/ x=self, y=result, z=result, and z'=temp. - */ - dst1 = call_dst(tzinfo, result, &none); - if (dst1 == -1 && PyErr_Occurred()) - goto Fail; - if (none) { - PyErr_SetString(PyExc_ValueError, "astimezone(): utcoffset() " - "returned a duration but dst() returned None"); - goto Fail; - } - delta = resoff - dst1 - selfoff; - if (delta) { - mm += delta; - if ((mm < 0 || mm >= 60) && - normalize_datetime(&y, &m, &d, &hh, &mm, &ss, &us) < 0) - goto Fail; - temp = new_datetime(y, m, d, hh, mm, ss, us, tzinfo); - if (temp == NULL) - goto Fail; - Py_DECREF(result); - result = temp; + if (!HASTZINFO(self) || self->tzinfo == Py_None) + goto NeedAware; - dst1 = call_dst(tzinfo, result, &none); - if (dst1 == -1 && PyErr_Occurred()) - goto Fail; - if (none) - goto Inconsistent; + /* Conversion to self's own time zone is a NOP. */ + if (self->tzinfo == tzinfo) { + Py_INCREF(self); + return (PyObject *)self; } - if (dst1 == 0) - return result; - mm += dst1; + /* Convert self to UTC. */ + offset = call_utcoffset(self->tzinfo, (PyObject *)self, &none); + if (offset == -1 && PyErr_Occurred()) + return NULL; + if (none) + goto NeedAware; + + y = GET_YEAR(self); + m = GET_MONTH(self); + d = GET_DAY(self); + hh = DATE_GET_HOUR(self); + mm = DATE_GET_MINUTE(self); + ss = DATE_GET_SECOND(self); + us = DATE_GET_MICROSECOND(self); + + mm -= offset; if ((mm < 0 || mm >= 60) && normalize_datetime(&y, &m, &d, &hh, &mm, &ss, &us) < 0) - goto Fail; - temp = new_datetime(y, m, d, hh, mm, ss, us, tzinfo); - if (temp == NULL) - goto Fail; - Py_DECREF(result); - result = temp; - return result; + return NULL; -Inconsistent: - PyErr_SetString(PyExc_ValueError, "astimezone(): tz.dst() gave" - "inconsistent results; cannot convert"); + /* Attach new tzinfo and let fromutc() do the rest. */ + result = new_datetime(y, m, d, hh, mm, ss, us, tzinfo); + if (result != NULL) { + PyObject *temp = result; - /* fall thru to failure */ -Fail: - Py_DECREF(result); + result = PyObject_CallMethod(tzinfo, "fromutc", "O", temp); + Py_DECREF(temp); + } + return result; + +NeedAware: + PyErr_SetString(PyExc_ValueError, "astimezone() cannot be applied to " + "a naive datetime"); return NULL; } |