Index: Include/datetime.h =================================================================== --- Include/datetime.h (revision 81646) +++ Include/datetime.h (working copy) @@ -45,7 +45,14 @@ PyObject_HEAD /* a pure abstract base clase */ } PyDateTime_TZInfo; +typedef struct +{ + PyObject_HEAD + PyObject *offset; + PyObject *name; +} PyDateTime_TimeZone; + /* The datetime and time types have hashcodes, and an optional tzinfo member, * present if and only if hastzinfo is true. */ @@ -144,6 +151,7 @@ PyTypeObject *TimeType; PyTypeObject *DeltaType; PyTypeObject *TZInfoType; + PyTypeObject *UTCType; /* constructors */ PyObject *(*Date_FromDate)(int, int, int, PyTypeObject*); Index: Doc/library/datetime.rst =================================================================== --- Doc/library/datetime.rst (revision 81646) +++ Doc/library/datetime.rst (working copy) @@ -28,11 +28,12 @@ have an optional time zone information member, :attr:`tzinfo`, that can contain an instance of a subclass of the abstract :class:`tzinfo` class. These :class:`tzinfo` objects capture information about the offset from UTC time, the -time zone name, and whether Daylight Saving Time is in effect. Note that no -concrete :class:`tzinfo` classes are supplied by the :mod:`datetime` module. -Supporting timezones at whatever level of detail is required is up to the -application. The rules for time adjustment across the world are more political -than rational, and there is no standard suitable for every application. +time zone name, and whether Daylight Saving Time is in effect. Note that only +one concrete :class:`tzinfo` class, the :class:`timezone` class, is supplied by the +:mod:`datetime` module. Supporting timezones at whatever level of detail is +required is up to the application. The rules for time adjustment across the +world are more political than rational, change frequently, and there is no +standard suitable for every application aside from UTC [#]_. The :mod:`datetime` module exports the following constants: @@ -99,6 +100,14 @@ time adjustment (for example, to account for time zone and/or daylight saving time). +.. class:: timezone + + A class that implements the :class:`tzinfo` abstract base class as a + fixed offset from the UTC. + + .. versionadded:: 3.2 + + Objects of these types are immutable. Objects of the :class:`date` type are always naive. @@ -116,6 +125,7 @@ object timedelta tzinfo + timezone time date datetime @@ -654,12 +664,16 @@ See also :meth:`today`, :meth:`utcnow`. -.. classmethod:: datetime.utcnow() +.. classmethod:: datetime.utcnow([tz_aware]) - Return the current UTC date and time, with :attr:`tzinfo` ``None``. This is like - :meth:`now`, but returns the current UTC date and time, as a naive + Return the current UTC date and time. If *tz_aware* is true, :attr:`tzinfo` is + set to an instance of :class:`UTC`, otherwise :attr:`tzinfo` is set to ``None``. + The default value for *tz_aware* is ``False``. This is like :meth:`now`, but + returns the current UTC date and time, as a :class:`datetime` object. See also :meth:`now`. + .. versionchanged:: 2.7 + Added *tz_aware* parameter. .. classmethod:: datetime.fromtimestamp(timestamp, tz=None) @@ -1517,11 +1531,37 @@ standard local time. Applications that can't bear such ambiguities should avoid using hybrid -:class:`tzinfo` subclasses; there are no ambiguities when using UTC, or any -other fixed-offset :class:`tzinfo` subclass (such as a class representing only -EST (fixed offset -5 hours), or only EDT (fixed offset -4 hours)). +:class:`tzinfo` subclasses; there are no ambiguities when using :class:`timezone`, +or any other fixed-offset :class:`tzinfo` subclass (such as a class representing +only EST (fixed offset -5 hours), or only EDT (fixed offset -4 hours)). +.. _datetime-timezone: + +:class:`timezone` Objects +-------------------------- + +A :class:`timezone` object represents a timezone that is defined by a +fixed offset from UTC. Note that objects of this class cannot be used +to represent timezone information in the locations where different +offsets are used in different days of the year or where historical +changes have been made to civil time. + + +.. class:: timezone(offset[, name]) + + The ``offset`` argument may be specified as a :class:`timedelta` + object or as a number (:class:`int` or :class:`float` object) + representing the number of hours west of UTC. The ``offset`` must be + between -12 and 12 hours, otherwise :exc:`ValueError` is raised. + +Class attributes: + +.. attribute:: timezone.utc + + The UTC timezone, ``timezone(0, 'UTC')``. + + .. _strftime-strptime-behavior: :meth:`strftime` and :meth:`strptime` Behavior @@ -1687,3 +1727,8 @@ (5) For example, if :meth:`utcoffset` returns ``timedelta(hours=-3, minutes=-30)``, ``%z`` is replaced with the string ``'-0330'``. + +.. rubric:: Footnotes + +.. [#] For an extended timezone implementation, see + `Pytz `_. Index: Doc/includes/tzinfo-examples.py =================================================================== --- Doc/includes/tzinfo-examples.py (revision 81646) +++ Doc/includes/tzinfo-examples.py (working copy) @@ -3,22 +3,6 @@ ZERO = timedelta(0) HOUR = timedelta(hours=1) -# A UTC class. - -class UTC(tzinfo): - """UTC""" - - def utcoffset(self, dt): - return ZERO - - def tzname(self, dt): - return "UTC" - - def dst(self, dt): - return ZERO - -utc = UTC() - # A class building tzinfo objects for fixed-offset time zones. # Note that FixedOffset(0, "UTC") is a different way to build a # UTC tzinfo object. Index: Lib/test/test_datetime.py =================================================================== --- Lib/test/test_datetime.py (revision 81646) +++ 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,11 +134,64 @@ self.assertEqual(derived.utcoffset(None), offset) self.assertEqual(derived.tzname(None), 'cookie') +class TestTimeZone(unittest.TestCase): + + def test_constructor(self): + ACST = timezone(timedelta(hours=9.5), 'ACDT') + EST = timezone(-timedelta(hours=5), 'EST') + self.assertEquals(ACST, timezone(9.5)) + self.assertEquals(EST, timezone(-5)) + self.assertEquals(EST, timezone(-5.0)) + self.assertEquals(timezone.utc, timezone(0)) + # invalid offsets + for invalid in [13, 13.0, timedelta(1)]: + self.assertRaises(ValueError, timezone, invalid) + self.assertRaises(ValueError, timezone, -invalid) + self.assertRaises(OverflowError, timezone, 2**100) + self.assertRaises(OverflowError, timezone, 2e100) + + def test_inheritance(self): + self.assertTrue(isinstance(timezone(0), tzinfo)) + + def test_utcoffset(self): + self.assertRaises(TypeError, timezone(0).utcoffset, None) + self.assertRaises(TypeError, timezone(0).utcoffset, 10) + + dummy = datetime(1, 1, 1) + for offset in [0, 1.5, 12]: + self.assertEquals(timedelta(hours=offset), timezone(offset).utcoffset(dummy)) + self.assertEquals(-timedelta(hours=offset), timezone(-offset).utcoffset(dummy)) + for offset in [timedelta(*args) for args in + [(0, 1), (0, 0, 1), (0, 0, 0, 1)]]: + self.assertEquals(offset, timezone(offset).utcoffset(dummy)) + self.assertEquals(-offset, timezone(-offset).utcoffset(dummy)) + + def test_dst(self): + self.assertRaises(TypeError, timezone(0).dst, None) + self.assertRaises(TypeError, timezone(0).dst, 10) + self.assertEquals(timedelta(0), timezone(0).dst(datetime(2009, 1, 1))) + + def test_tzname(self): + self.assertRaises(TypeError, timezone(0).tzname, None) + self.assertRaises(TypeError, timezone(0).tzname, 10) + self.assertEquals('UTC', timezone(0).tzname(datetime(2009, 1, 1))) + self.assertEquals('UTC-0500', timezone(-5).tzname(datetime(2009, 1, 1))) + self.assertEquals('UTC+0930', timezone(9.5).tzname(datetime(2009, 1, 1))) + self.assertEquals('XYZ', timezone(-5, 'XYZ').tzname(datetime(2009, 1, 1))) + + def test_comparison(self): + self.assertNotEqual(timezone(0), timezone(1)) + self.assertEqual(timezone(1), timezone(1)) + self.assertEqual(timezone(-5), timezone(-5, 'EST')) + self.assertRaises(TypeError, lambda: timezone(0) < timezone(0)) + self.assertIn(timezone(0), {timezone(0)}) + ############################################################################# # Base clase for testing a particular aspect of timedelta, time, date and # datetime comparisons. class HarmlessMixedComparison: + # Test that __eq__ and __ne__ don't complain for mixed-type comparisons. # Subclasses must define 'theclass', and theclass(1, 1, 1) must be a @@ -1632,6 +1688,10 @@ # Else try again a few times. self.assertTrue(abs(from_timestamp - from_now) <= tolerance) + def test_utcnow_not_tz_aware(self): + now = datetime.utcnow(tz_aware=True) + self.assertTrue(isinstance(now.tzinfo, timezone)) + def test_strptime(self): import _strptime @@ -2783,12 +2843,15 @@ meth = self.theclass.utcnow # Ensure it doesn't require tzinfo (i.e., that this doesn't blow up). base = meth() - # Try with and without naming the keyword; for whatever reason, - # utcnow() doesn't accept a tzinfo argument. - off42 = FixedOffset(42, "42") - self.assertRaises(TypeError, meth, off42) - self.assertRaises(TypeError, meth, tzinfo=off42) + # Test tzinfo parameter. + self.assertTrue(isinstance(meth(True).tzinfo, timezone)) + self.assertTrue(isinstance(meth(1).tzinfo, timezone)) + self.assertTrue(isinstance(meth(object()).tzinfo, timezone)) + + self.assertEquals(None, meth(False).tzinfo, timezone) + self.assertEquals(None, meth(0).tzinfo, timezone) + def test_tzinfo_utcfromtimestamp(self): import time meth = self.theclass.utcfromtimestamp @@ -3404,7 +3467,6 @@ start += HOUR fstart += HOUR - ############################################################################# # oddballs Index: Modules/datetimemodule.c =================================================================== --- Modules/datetimemodule.c (revision 81646) +++ 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,61 @@ #define new_delta(d, s, us, normalize) \ new_delta_ex(d, s, us, normalize, &PyDateTime_DeltaType) +static PyObject * +multiply_float_timedelta(PyObject *floatobj, PyDateTime_Delta *delta); + +static PyObject * +new_timezone_ex(PyTypeObject *type, PyObject *offset, PyObject *name) +{ + PyDateTime_TimeZone *self; + + if (offset == NULL) + offset = new_delta(0, 0, 0, 0); + else if (PyLong_Check(offset)) { + long hours; + hours = PyLong_AsLong(offset); + if (hours == -1 && PyErr_Occurred()) + return NULL; + if (hours < -12 || hours > 12) + goto range_error; + + offset = new_delta(0, hours * 3600, 0, 1); + } + else if (PyFloat_Check(offset)) { + PyObject *onehour; + onehour = new_delta(0, 3600, 0, 0); + if (onehour == NULL) + return NULL; + + offset = multiply_float_timedelta(offset, (PyDateTime_Delta *)onehour); + Py_DECREF(onehour); + } + else if (PyDelta_Check(offset)) + Py_INCREF(offset); + if (offset == NULL) + return NULL; + if ((GET_TD_DAYS(offset) == 0 && GET_TD_SECONDS(offset) > 12*3600) || + (GET_TD_DAYS(offset) == -1 && GET_TD_SECONDS(offset) < 12*3600) || + GET_TD_DAYS(offset) > 0 || + GET_TD_DAYS(offset) < -1) { + Py_DECREF(offset); + range_error: + PyErr_Format(PyExc_ValueError, "offset must be between -12 and 12 hours."); + 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 +3339,164 @@ 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 (! PyObject_TypeCheck(dt, &PyDateTime_DateTimeType)) { + PyErr_SetString(PyExc_TypeError, "argument must be datetime"); + return NULL; + } + 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) +{ + if (! PyObject_TypeCheck(dt, &PyDateTime_DateTimeType)) { + PyErr_SetString(PyExc_TypeError, "argument must be datetime"); + return NULL; + } + Py_INCREF(self->offset); + return self->offset; +} + +static PyObject * +timezone_dst(PyObject *self, PyObject *dt) +{ + if (! PyObject_TypeCheck(dt, &PyDateTime_DateTimeType)) { + PyErr_SetString(PyExc_TypeError, "argument must be datetime"); + return NULL; + } + + return new_delta(0, 0, 0, 0); +} + +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 'UTC(+|-)HHMM'.")}, + + {"utcoffset", (PyCFunction)timezone_utcoffset, METH_O, PyDoc_STR("Returns fixed offset.")}, + + {"dst", (PyCFunction)timezone_dst, METH_O, PyDoc_STR("Returns timedelta(0).")}, + + {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. */ @@ -4069,9 +4283,35 @@ * precision of a timestamp. */ static PyObject * -datetime_utcnow(PyObject *cls, PyObject *dummy) +datetime_utcnow(PyObject *cls, PyObject *args, PyObject *kw) { - return datetime_best_possible(cls, gmtime, Py_None); + PyObject *result; + PyObject *utc; + PyObject *tz_aware = Py_False; + int aware; + + static char *keywords[] = {"tz_aware", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kw, "|O:utcnow", + keywords, &tz_aware)) + return NULL; + aware = PyObject_IsTrue(tz_aware); + if (aware == -1) + return NULL; + if (aware) { + utc = new_timezone(NULL, NULL); + if (utc == NULL) { + return NULL; + } + } + else { + utc = Py_None; + Py_INCREF(utc); + } + result = datetime_best_possible(cls, gmtime, utc); + Py_DECREF(utc); + + return result; } /* Return new local datetime from timestamp (Python timestamp -- a double). */ @@ -4797,8 +5037,9 @@ PyDoc_STR("[tz] -> new datetime with tz's local day and time.")}, {"utcnow", (PyCFunction)datetime_utcnow, - METH_NOARGS | METH_CLASS, - PyDoc_STR("Return a new datetime representing UTC day and time.")}, + METH_VARARGS | METH_KEYWORDS | METH_CLASS, + PyDoc_STR("[tz_aware] -> Return a new datetime "\ + "representing UTC day and time.")}, {"fromtimestamp", (PyCFunction)datetime_fromtimestamp, METH_VARARGS | METH_KEYWORDS | METH_CLASS, @@ -4943,6 +5184,7 @@ &PyDateTime_TimeType, &PyDateTime_DeltaType, &PyDateTime_TZInfoType, + &PyDateTime_TimeZoneType, new_date_ex, new_datetime_ex, new_time_ex, @@ -4971,6 +5213,7 @@ PyObject *m; /* a module object */ PyObject *d; /* its dict */ PyObject *x; + PyObject *utc; /* UTC timezone */ m = PyModule_Create(&datetimemodule); if (m == NULL) @@ -4986,6 +5229,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 +5304,15 @@ return NULL; Py_DECREF(x); + /* timezone values */ + d = PyDateTime_TimeZoneType.tp_dict; + + x = new_timezone(NULL, NULL); + if (x == NULL || PyDict_SetItemString(d, "utc", x) < 0) + return NULL; + Py_DECREF(x); + + /* module initialization */ PyModule_AddIntConstant(m, "MINYEAR", MINYEAR); PyModule_AddIntConstant(m, "MAXYEAR", MAXYEAR); @@ -5079,6 +5333,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;