Issue #5094: The ``datetime`` module now has a simple concrete class

implementing ``datetime.tzinfo`` interface.
This commit is contained in:
Alexander Belopolsky 2010-06-14 14:15:50 +00:00
parent 510b6227a7
commit 4e749a1113
5 changed files with 483 additions and 26 deletions

View File

@ -28,11 +28,14 @@ For applications requiring more, :class:`datetime` and :class:`time` objects
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. The :class:`timezone` class can reprsent simple
timezones with fixed offset from UTC such as UTC itself or North American EST and
EDT timezones. 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 +102,14 @@ Available Types
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 +127,7 @@ Subclass relationships::
object
timedelta
tzinfo
timezone
time
date
datetime
@ -660,8 +672,8 @@ Other constructors, all class methods:
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
:class:`datetime` object. See also :meth:`now`.
:class:`datetime` object. An aware current UTC datetime can be obtained by
calling ``datetime.now(timezone.utc)``. See also :meth:`now`.
.. classmethod:: datetime.fromtimestamp(timestamp, tz=None)
@ -1318,8 +1330,10 @@ Example:
:class:`tzinfo` is an abstract base class, meaning that this class 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 :mod:`datetime` module does not supply
any concrete subclasses of :class:`tzinfo`.
:class:`datetime` methods you use. The :mod:`datetime` module supplies
a simple concrete subclass of :class:`tzinfo` :class:`timezone` which can reprsent
timezones with fixed offset from UTC such as UTC itself or North American EST and
EDT.
An instance of (a concrete subclass of) :class:`tzinfo` can be passed to the
constructors for :class:`datetime` and :class:`time` objects. The latter objects
@ -1520,9 +1534,65 @@ arranged, as in the example, by expressing DST switch times in the time zone's
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 must be specified as a :class:`timedelta`
object representing the difference between the local time and UTC. It must
be within the range [``-timedelta(hours=23, minutes=59),
``timedelta(hours=23, minutes=59)``] and represent whole number of minutes,
otherwise :exc:`ValueError` is raised.
The ``name`` argument is optional. If specified it must be a string that
used as the value returned by the ``tzname(dt)`` method. Otherwise,
``tzname(dt)`` returns a string 'UTCsHH:MM', where s is the sign of
``offset``, HH and MM are two digits of ``offset.hours`` and
``offset.minutes`` respectively.
.. method:: timezone.utcoffset(self, dt)
Returns the fixed value specified when the :class:`timezone` instance is
constructed. The ``dt`` argument is ignored. The return value is a
:class:`timedelta` instance equal to the difference between the
local time and UTC.
.. method:: timezone.tzname(self, dt)
Returns the fixed value specified when the :class:`timezone` instance is
constructed or a string 'UTCsHH:MM', where s is the sign of
``offset``, HH and MM are two digits of ``offset.hours`` and
``offset.minutes`` respectively. The ``dt`` argument is ignored.
.. method:: timezone.dst(self, dt)
Always returns ``None``.
.. method:: timezone.fromutc(self, dt)
Returns ``dt + offset``. The ``dt`` argument must be aware with ``tzinfo``
set to ``self``.
Class attributes:
.. attribute:: timezone.utc
The UTC timezone, ``timezone(0, 'UTC')``.
.. _strftime-strptime-behavior:

View File

@ -15,6 +15,7 @@ from datetime import MINYEAR, MAXYEAR
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 @@ class TestModule(unittest.TestCase):
# tzinfo tests
class FixedOffset(tzinfo):
def __init__(self, offset, name, dstoffset=42):
if isinstance(offset, int):
offset = timedelta(minutes=offset)
@ -67,6 +69,7 @@ class FixedOffset(tzinfo):
return self.__dstoffset
class PicklableFixedOffset(FixedOffset):
def __init__(self, offset=None, name=None, dstoffset=None):
FixedOffset.__init__(self, offset, name, dstoffset)
@ -131,6 +134,97 @@ class TestTZInfo(unittest.TestCase):
self.assertEqual(derived.utcoffset(None), offset)
self.assertEqual(derived.tzname(None), 'cookie')
class TestTimeZone(unittest.TestCase):
def setUp(self):
self.ACDT = timezone(timedelta(hours=9.5), 'ACDT')
self.EST = timezone(-timedelta(hours=5), 'EST')
self.DT = datetime(2010, 1, 1)
def test_str(self):
for tz in [self.ACDT, self.EST, timezone.utc,
timezone.min, timezone.max]:
self.assertEqual(str(tz), tz.tzname(None))
def test_class_members(self):
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):
self.assertEquals(timezone.utc, timezone(timedelta(0)))
# invalid offsets
for invalid in [timedelta(microseconds=1), timedelta(1, 1),
timedelta(seconds=1), timedelta(1), -timedelta(1)]:
self.assertRaises(ValueError, timezone, invalid)
self.assertRaises(ValueError, timezone, -invalid)
with self.assertRaises(TypeError): timezone(None)
with self.assertRaises(TypeError): timezone(42)
with self.assertRaises(TypeError): timezone(ZERO, None)
with self.assertRaises(TypeError): timezone(ZERO, 42)
def test_inheritance(self):
self.assertTrue(isinstance(timezone.utc, tzinfo))
self.assertTrue(isinstance(self.EST, tzinfo))
def test_utcoffset(self):
dummy = self.DT
for h in [0, 1.5, 12]:
offset = h * HOUR
self.assertEquals(offset, timezone(offset).utcoffset(dummy))
self.assertEquals(-offset, timezone(-offset).utcoffset(dummy))
with self.assertRaises(TypeError): self.EST.utcoffset('')
with self.assertRaises(TypeError): self.EST.utcoffset(5)
def test_dst(self):
self.assertEquals(None, timezone.utc.dst(self.DT))
with self.assertRaises(TypeError): self.EST.dst('')
with self.assertRaises(TypeError): self.EST.dst(5)
def test_tzname(self):
self.assertEquals('UTC+00:00', timezone(ZERO).tzname(None))
self.assertEquals('UTC-05:00', timezone(-5 * HOUR).tzname(None))
self.assertEquals('UTC+09:30', timezone(9.5 * HOUR).tzname(None))
self.assertEquals('UTC-00:01', timezone(timedelta(minutes=-1)).tzname(None))
self.assertEquals('XYZ', timezone(-5 * HOUR, 'XYZ').tzname(None))
with self.assertRaises(TypeError): self.EST.tzname('')
with self.assertRaises(TypeError): self.EST.tzname(5)
def test_fromutc(self):
with self.assertRaises(ValueError):
timezone.utc.fromutc(self.DT)
for tz in [self.EST, self.ACDT, Eastern]:
utctime = self.DT.replace(tzinfo=tz)
local = tz.fromutc(utctime)
self.assertEquals(local - utctime, tz.utcoffset(local))
self.assertEquals(local,
self.DT.replace(tzinfo=timezone.utc))
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'))
with self.assertRaises(TypeError): 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.
@ -2729,20 +2823,21 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase, unittest.TestCase):
# We don't know which time zone we're in, and don't have a tzinfo
# class to represent it, so seeing whether a tz argument actually
# does a conversion is tricky.
weirdtz = FixedOffset(timedelta(hours=15, minutes=58), "weirdtz", 0)
utc = FixedOffset(0, "utc", 0)
for dummy in range(3):
now = datetime.now(weirdtz)
self.assertTrue(now.tzinfo is weirdtz)
utcnow = datetime.utcnow().replace(tzinfo=utc)
now2 = utcnow.astimezone(weirdtz)
if abs(now - now2) < timedelta(seconds=30):
break
# Else the code is broken, or more than 30 seconds passed between
# calls; assuming the latter, just try again.
else:
# Three strikes and we're out.
self.fail("utcnow(), now(tz), or astimezone() may be broken")
for weirdtz in [FixedOffset(timedelta(hours=15, minutes=58), "weirdtz", 0),
timezone(timedelta(hours=15, minutes=58), "weirdtz"),]:
for dummy in range(3):
now = datetime.now(weirdtz)
self.assertTrue(now.tzinfo is weirdtz)
utcnow = datetime.utcnow().replace(tzinfo=utc)
now2 = utcnow.astimezone(weirdtz)
if abs(now - now2) < timedelta(seconds=30):
break
# Else the code is broken, or more than 30 seconds passed between
# calls; assuming the latter, just try again.
else:
# Three strikes and we're out.
self.fail("utcnow(), now(tz), or astimezone() may be broken")
def test_tzinfo_fromtimestamp(self):
import time

View File

@ -406,6 +406,7 @@ Bob Kahn
Kurt B. Kaiser
Tamito Kajiyama
Peter van Kampen
Rafe Kaplan
Jacob Kaplan-Moss
Lou Kates
Hiroaki Kawai

View File

@ -1306,6 +1306,14 @@ Library
Extension Modules
-----------------
- Issue #5094: The ``datetime`` module now has a simple concrete class
implementing ``datetime.tzinfo`` interface. Instances of the new
class, ``datetime.timezone``, return fixed name and UTC offset from
their ``tzname(dt)`` and ``utcoffset(dt)`` methods. The ``dst(dt)``
method always returns ``None``. A class attribute, ``utc`` contains
an instance representing the UTC timezone. Original patch by Rafe
Kaplan.
- Issue #8973: Add __all__ to struct module; this ensures that
help(struct) includes documentation for the struct.Struct class.

View File

@ -102,6 +102,7 @@ static PyTypeObject PyDateTime_DateTimeType;
static PyTypeObject PyDateTime_DeltaType;
static PyTypeObject PyDateTime_TimeType;
static PyTypeObject PyDateTime_TZInfoType;
static PyTypeObject PyDateTime_TimeZoneType;
/* ---------------------------------------------------------------------------
* Math utilities.
@ -771,6 +772,52 @@ new_delta_ex(int days, int seconds, int microseconds, int normalize,
#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;
/* Create new timezone instance checking offset range. This
function does not check the name argument. Caller must assure
that offset is a timedelta instance and name is either NULL
or a unicode object. */
static PyObject *
new_timezone(PyObject *offset, PyObject *name)
{
PyDateTime_TimeZone *self;
PyTypeObject *type = &PyDateTime_TimeZoneType;
assert(offset != NULL);
assert(PyDelta_Check(offset));
assert(name == NULL || PyUnicode_Check(name));
if (GET_TD_MICROSECONDS(offset) != 0 || GET_TD_SECONDS(offset) % 60 != 0) {
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) {
PyErr_Format(PyExc_ValueError, "offset must be a timedelta"
" strictly between -timedelta(hours=24) and"
" timedelta(hours=24).");
return NULL;
}
self = (PyDateTime_TimeZone *)(type->tp_alloc(type, 0));
if (self == NULL) {
return NULL;
}
Py_INCREF(offset);
self->offset = offset;
Py_XINCREF(name);
self->name = name;
return (PyObject *)self;
}
/* ---------------------------------------------------------------------------
* tzinfo helpers.
*/
@ -3261,7 +3308,7 @@ static PyTypeObject PyDateTime_TZInfoType = {
PyObject_GenericGetAttr, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
tzinfo_doc, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
@ -3283,6 +3330,206 @@ static PyTypeObject PyDateTime_TZInfoType = {
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,
&PyDateTime_DeltaType, &offset,
&PyUnicode_Type, &name))
return new_timezone(offset, name);
return NULL;
}
static void
timezone_dealloc(PyDateTime_TimeZone *self)
{
Py_CLEAR(self->offset);
Py_CLEAR(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 && op != Py_NE) {
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);
}
/* Check argument type passed to tzname, utcoffset, or dst methods.
Returns 0 for good argument. Returns -1 and sets exception info
otherwise.
*/
static int
_timezone_check_argument(PyObject *dt, const char *meth)
{
if (dt == Py_None || PyDateTime_Check(dt))
return 0;
PyErr_Format(PyExc_TypeError, "%s(dt) argument must be a datetime instance"
" or None, not %.200s", meth, Py_TYPE(dt)->tp_name);
return -1;
}
static PyObject *
timezone_str(PyDateTime_TimeZone *self)
{
char buf[10];
int hours, minutes, seconds;
PyObject *offset;
char sign;
if (self->name != NULL) {
Py_INCREF(self->name);
return self->name;
}
/* Offset is normalized, so it is negative if days < 0 */
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);
}
/* Offset is not negative here. */
seconds = GET_TD_SECONDS(offset);
Py_DECREF(offset);
minutes = divmod(seconds, 60, &seconds);
hours = divmod(minutes, 60, &minutes);
assert(seconds == 0);
/* XXX ignore sub-minute data, curently not allowed. */
PyOS_snprintf(buf, sizeof(buf), "UTC%c%02d:%02d", sign, hours, minutes);
return PyUnicode_FromString(buf);
}
static PyObject *
timezone_tzname(PyDateTime_TimeZone *self, PyObject *dt)
{
if (_timezone_check_argument(dt, "tzname") == -1)
return NULL;
return timezone_str(self);
}
static PyObject *
timezone_utcoffset(PyDateTime_TimeZone *self, PyObject *dt)
{
if (_timezone_check_argument(dt, "utcoffset") == -1)
return NULL;
Py_INCREF(self->offset);
return self->offset;
}
static PyObject *
timezone_dst(PyObject *self, PyObject *dt)
{
if (_timezone_check_argument(dt, "dst") == -1)
return NULL;
Py_RETURN_NONE;
}
static PyObject *
add_datetime_timedelta(PyDateTime_DateTime *date, PyDateTime_Delta *delta,
int factor);
static PyObject *
timezone_fromutc(PyDateTime_TimeZone *self, PyDateTime_DateTime *dt)
{
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;
}
return add_datetime_timedelta(dt, (PyDateTime_Delta *)self->offset, 1);
}
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 argument.")},
{"fromutc", (PyCFunction)timezone_fromutc, METH_O,
PyDoc_STR("datetime in UTC -> datetime in local time.")},
{NULL, NULL}
};
static char timezone_doc[] =
PyDoc_STR("Fixed offset 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 */
(reprfunc)timezone_str, /* 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 +5218,7 @@ PyInit_datetime(void)
PyObject *m; /* a module object */
PyObject *d; /* its dict */
PyObject *x;
PyObject *delta;
m = PyModule_Create(&datetimemodule);
if (m == NULL)
@ -4986,6 +5234,8 @@ PyInit_datetime(void)
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 +5309,36 @@ PyInit_datetime(void)
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 +5359,9 @@ PyInit_datetime(void)
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;