Index: Modules/datetimemodule.c =================================================================== --- Modules/datetimemodule.c (revision 81758) +++ Modules/datetimemodule.c (working copy) @@ -102,6 +102,7 @@ static PyTypeObject PyDateTime_DeltaType; static PyTypeObject PyDateTime_TimeType; static PyTypeObject PyDateTime_TZInfoType; +static PyTypeObject PyDateTime_TimeZoneType; /* --------------------------------------------------------------------------- * Math utilities. @@ -771,6 +772,56 @@ #define new_delta(d, s, us, normalize) \ new_delta_ex(d, s, us, normalize, &PyDateTime_DeltaType) + +typedef struct +{ + PyObject_HEAD + PyObject *offset; + PyObject *name; +} PyDateTime_TimeZone; + +static PyObject * +new_timezone_ex(PyTypeObject *type, PyObject *offset, PyObject *name) +{ + PyDateTime_TimeZone *self; + + if (offset == NULL) { + offset = new_delta(0, 0, 0, 0); + if (offset == NULL) + return NULL; + } + else if (PyDelta_Check(offset)) + Py_INCREF(offset); + else { + PyErr_Format(PyExc_ValueError, "offset must be a timedelta"); + return NULL; + } + if (GET_TD_MICROSECONDS(offset) != 0 || GET_TD_SECONDS(offset) % 60 != 0) { + Py_DECREF(offset); + PyErr_Format(PyExc_ValueError, "offset must be a timedelta" + " representing a whole number of minutes"); + return NULL; + } + if ((GET_TD_DAYS(offset) == -1 && GET_TD_SECONDS(offset) == 0) || + GET_TD_DAYS(offset) < -1 || GET_TD_DAYS(offset) >= 1) { + Py_DECREF(offset); + PyErr_Format(PyExc_ValueError, "offset must be a timedelta" + " between timedelta(1) and -timedelta(1)."); + return NULL; + } + + self = (PyDateTime_TimeZone *)type->tp_alloc(type, 0); + if (self == NULL) + return NULL; + self->offset = offset; + Py_XINCREF(name); + self->name = name; + return (PyObject *)self; +} + +#define new_timezone(offset, name) \ + new_timezone_ex(&PyDateTime_TimeZoneType, offset, name) + /* --------------------------------------------------------------------------- * tzinfo helpers. */ @@ -3283,6 +3334,149 @@ 0, /* tp_free */ }; +static char *timezone_kws[] = {"offset", "name", NULL}; + +static PyObject * +timezone_new(PyTypeObject *type, PyObject *args, PyObject *kw) +{ + PyObject *offset; + PyObject *name = NULL; + if (PyArg_ParseTupleAndKeywords(args, kw, "O|O!:timezone", timezone_kws, + &offset, &PyUnicode_Type, &name)) + return new_timezone(offset, name); + + return NULL; +} + + +static void +timezone_dealloc(PyDateTime_TimeZone *self) +{ + Py_DECREF(self->offset); + Py_XDECREF(self->name); + Py_TYPE(self)->tp_free((PyObject *)self); +} + + +static PyObject * +timezone_richcompare(PyDateTime_TimeZone *self, PyDateTime_TimeZone *other, int op) +{ + if (op != Py_EQ) { + Py_INCREF(Py_NotImplemented); + return Py_NotImplemented; + } + return delta_richcompare(self->offset, other->offset, op); +} + +static long +timezone_hash(PyDateTime_TimeZone *self) +{ + return delta_hash((PyDateTime_Delta *)self->offset); +} + +static PyObject * +timezone_tzname(PyDateTime_TimeZone *self, PyObject *dt) +{ + char buf[10]; + int hours, minutes, seconds; + PyObject *offset; + char sign; + + if (self->name != NULL) { + Py_INCREF(self->name); + return self->name; + } + if (delta_bool((PyDateTime_Delta *)self->offset) == 0) + return PyUnicode_FromString("UTC"); + if (GET_TD_DAYS(self->offset) < 0) { + sign = '-'; + offset = delta_negative((PyDateTime_Delta *)self->offset); + if (offset == NULL) + return NULL; + } + else { + sign = '+'; + offset = self->offset; + Py_INCREF(offset); + } + seconds = GET_TD_SECONDS(offset); + Py_DECREF(offset); + minutes = divmod(seconds, 60, &seconds); + hours = divmod(minutes, 60, &minutes); + /* XXX ignore sub-minute data */ + PyOS_snprintf(buf, sizeof(buf), "UTC%c%02d%02d", sign, hours, minutes); + + return PyUnicode_FromString(buf); +} + +static PyObject * +timezone_utcoffset(PyDateTime_TimeZone *self, PyObject *dt) +{ + Py_INCREF(self->offset); + return self->offset; +} + +static PyObject * +timezone_dst(PyObject *self, PyObject *dt) +{ + Py_RETURN_NONE; +} + +static PyMethodDef timezone_methods[] = { + {"tzname", (PyCFunction)timezone_tzname, METH_O, + PyDoc_STR("If name is specified when timezone is created, returns the name." + " Otherwise returns offset as 'UTC(+|-)HHMM'.")}, + {"utcoffset", (PyCFunction)timezone_utcoffset, METH_O, + PyDoc_STR("Returns fixed offset. Ignores its argument.")}, + {"dst", (PyCFunction)timezone_dst, METH_O, + PyDoc_STR("Returns None. Ignores its argumnt.")}, + {NULL, NULL} +}; + +static char timezone_doc[] = +PyDoc_STR("Fixed offet from UTC implementation of tzinfo."); + +static PyTypeObject PyDateTime_TimeZoneType = { + PyVarObject_HEAD_INIT(NULL, 0) + "datetime.timezone", /* tp_name */ + sizeof(PyDateTime_TimeZone), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)timezone_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_reserved */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + (hashfunc)timezone_hash, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + timezone_doc, /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + (richcmpfunc)timezone_richcompare,/* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + timezone_methods, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + &PyDateTime_TZInfoType, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0, /* tp_alloc */ + timezone_new, /* tp_new */ +}; + /* * PyDateTime_Time implementation. */ @@ -4971,6 +5165,7 @@ PyObject *m; /* a module object */ PyObject *d; /* its dict */ PyObject *x; + PyObject *delta; m = PyModule_Create(&datetimemodule); if (m == NULL) @@ -4986,6 +5181,8 @@ return NULL; if (PyType_Ready(&PyDateTime_TZInfoType) < 0) return NULL; + if (PyType_Ready(&PyDateTime_TimeZoneType) < 0) + return NULL; /* timedelta values */ d = PyDateTime_DeltaType.tp_dict; @@ -5059,6 +5256,37 @@ return NULL; Py_DECREF(x); + /* timezone values */ + d = PyDateTime_TimeZoneType.tp_dict; + + delta = new_delta(0, 0, 0, 0); + if (delta == NULL) + return NULL; + x = new_timezone(delta, NULL); + Py_DECREF(delta); + if (x == NULL || PyDict_SetItemString(d, "utc", x) < 0) + return NULL; + Py_DECREF(x); + + delta = new_delta(-1, 60, 0, 1); /* -23:59 */ + if (delta == NULL) + return NULL; + x = new_timezone(delta, NULL); + Py_DECREF(delta); + if (x == NULL || PyDict_SetItemString(d, "min", x) < 0) + return NULL; + Py_DECREF(x); + + delta = new_delta(0, (23 * 60 + 59) * 60, 0, 0); /* +23:59 */ + if (delta == NULL) + return NULL; + x = new_timezone(delta, NULL); + Py_DECREF(delta); + if (x == NULL || PyDict_SetItemString(d, "max", x) < 0) + return NULL; + Py_DECREF(x); + + /* module initialization */ PyModule_AddIntConstant(m, "MINYEAR", MINYEAR); PyModule_AddIntConstant(m, "MAXYEAR", MAXYEAR); @@ -5079,6 +5307,9 @@ Py_INCREF(&PyDateTime_TZInfoType); PyModule_AddObject(m, "tzinfo", (PyObject *) &PyDateTime_TZInfoType); + Py_INCREF(&PyDateTime_TimeZoneType); + PyModule_AddObject(m, "timezone", (PyObject *) &PyDateTime_TimeZoneType); + x = PyCapsule_New(&CAPI, PyDateTime_CAPSULE_NAME, NULL); if (x == NULL) return NULL; Index: Lib/test/test_datetime.py =================================================================== --- Lib/test/test_datetime.py (revision 81758) +++ Lib/test/test_datetime.py (working copy) @@ -15,6 +15,7 @@ from datetime import timedelta from datetime import tzinfo from datetime import time +from datetime import timezone from datetime import date, datetime pickle_choices = [(pickle, pickle, proto) for proto in range(3)] @@ -49,6 +50,7 @@ # tzinfo tests class FixedOffset(tzinfo): + def __init__(self, offset, name, dstoffset=42): if isinstance(offset, int): offset = timedelta(minutes=offset) @@ -67,6 +69,7 @@ return self.__dstoffset class PicklableFixedOffset(FixedOffset): + def __init__(self, offset=None, name=None, dstoffset=None): FixedOffset.__init__(self, offset, name, dstoffset) @@ -131,6 +134,63 @@ self.assertEqual(derived.utcoffset(None), offset) self.assertEqual(derived.tzname(None), 'cookie') +class TestTimeZone(unittest.TestCase): + + def test_class_members(self): + zero = timedelta(0) + limit = timedelta(hours=23, minutes=59) + self.assertEquals(timezone.utc.utcoffset(None), zero) + self.assertEquals(timezone.min.utcoffset(None), -limit) + self.assertEquals(timezone.max.utcoffset(None), limit) + + + def test_constructor(self): + ACST = timezone(timedelta(hours=9.5), 'ACDT') + EST = timezone(-timedelta(hours=5), 'EST') + self.assertEquals(timezone.utc, timezone(timedelta(0))) + # invalid offsets + for invalid in [timedelta(microseconds=1), timedelta(1, 1), + timedelta(seconds=1)]: + self.assertRaises(ValueError, timezone, invalid) + self.assertRaises(ValueError, timezone, -invalid) + + def test_inheritance(self): + self.assertTrue(isinstance(timezone.utc, tzinfo)) + + def test_utcoffset(self): + dummy = datetime(1, 1, 1) + for h in [0, 1.5, 12]: + offset = h * HOUR + self.assertEquals(offset, timezone(offset).utcoffset(dummy)) + self.assertEquals(-offset, timezone(-offset).utcoffset(dummy)) + + def test_dst(self): + self.assertEquals(None, timezone.utc.dst(datetime(2009, 1, 1))) + + def test_tzname(self): + self.assertEquals('UTC', timezone(ZERO).tzname(None)) + self.assertEquals('UTC-0500', timezone(-5 * HOUR).tzname(None)) + self.assertEquals('UTC+0930', timezone(9.5 * HOUR).tzname(None)) + self.assertEquals('XYZ', timezone(-5 * HOUR, 'XYZ').tzname(None)) + + def test_comparison(self): + self.assertNotEqual(timezone(ZERO), timezone(HOUR)) + self.assertEqual(timezone(HOUR), timezone(HOUR)) + self.assertEqual(timezone(-5 * HOUR), timezone(-5 * HOUR, 'EST')) + self.assertRaises(TypeError, lambda: timezone(ZERO) < timezone(ZERO)) + self.assertIn(timezone(ZERO), {timezone(ZERO)}) + + def test_aware_datetime(self): + # test that timezone instances can be used by datetime + t = datetime(1, 1, 1) + for tz in [timezone.min, timezone.max, timezone.utc]: + self.assertEquals(tz.tzname(t), + t.replace(tzinfo=tz).tzname()) + self.assertEquals(tz.utcoffset(t), + t.replace(tzinfo=tz).utcoffset()) + self.assertEquals(tz.dst(t), + t.replace(tzinfo=tz).dst()) + ############################################################################# # Base clase for testing a particular aspect of timedelta, time, date and # datetime comparisons.