Commit 4e749a11 authored by Alexander Belopolsky's avatar Alexander Belopolsky

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

implementing ``datetime.tzinfo`` interface.
parent 510b6227
...@@ -28,11 +28,14 @@ For applications requiring more, :class:`datetime` and :class:`time` objects ...@@ -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 have an optional time zone information member, :attr:`tzinfo`, that can contain
an instance of a subclass of the abstract :class:`tzinfo` class. These an instance of a subclass of the abstract :class:`tzinfo` class. These
:class:`tzinfo` objects capture information about the offset from UTC time, the :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 time zone name, and whether Daylight Saving Time is in effect. Note that only
concrete :class:`tzinfo` classes are supplied by the :mod:`datetime` module. one concrete :class:`tzinfo` class, the :class:`timezone` class, is supplied by the
Supporting timezones at whatever level of detail is required is up to the :mod:`datetime` module. The :class:`timezone` class can reprsent simple
application. The rules for time adjustment across the world are more political timezones with fixed offset from UTC such as UTC itself or North American EST and
than rational, and there is no standard suitable for every application. 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: The :mod:`datetime` module exports the following constants:
...@@ -99,6 +102,14 @@ Available Types ...@@ -99,6 +102,14 @@ Available Types
time adjustment (for example, to account for time zone and/or daylight saving time adjustment (for example, to account for time zone and/or daylight saving
time). 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 these types are immutable.
Objects of the :class:`date` type are always naive. Objects of the :class:`date` type are always naive.
...@@ -116,6 +127,7 @@ Subclass relationships:: ...@@ -116,6 +127,7 @@ Subclass relationships::
object object
timedelta timedelta
tzinfo tzinfo
timezone
time time
date date
datetime datetime
...@@ -660,8 +672,8 @@ Other constructors, all class methods: ...@@ -660,8 +672,8 @@ Other constructors, all class methods:
Return the current UTC date and time, with :attr:`tzinfo` ``None``. This is like 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 :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) .. classmethod:: datetime.fromtimestamp(timestamp, tz=None)
...@@ -1318,8 +1330,10 @@ Example: ...@@ -1318,8 +1330,10 @@ Example:
:class:`tzinfo` is an abstract base class, meaning that this class should not be :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) instantiated directly. You need to derive a concrete subclass, and (at least)
supply implementations of the standard :class:`tzinfo` methods needed by the supply implementations of the standard :class:`tzinfo` methods needed by the
:class:`datetime` methods you use. The :mod:`datetime` module does not supply :class:`datetime` methods you use. The :mod:`datetime` module supplies
any concrete subclasses of :class:`tzinfo`. 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 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 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 ...@@ -1520,9 +1534,65 @@ arranged, as in the example, by expressing DST switch times in the time zone's
standard local time. standard local time.
Applications that can't bear such ambiguities should avoid using hybrid Applications that can't bear such ambiguities should avoid using hybrid
:class:`tzinfo` subclasses; there are no ambiguities when using UTC, or any :class:`tzinfo` subclasses; there are no ambiguities when using :class:`timezone`,
other fixed-offset :class:`tzinfo` subclass (such as a class representing only or any other fixed-offset :class:`tzinfo` subclass (such as a class representing
EST (fixed offset -5 hours), or only EDT (fixed offset -4 hours)). 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: .. _strftime-strptime-behavior:
......
...@@ -15,6 +15,7 @@ from datetime import MINYEAR, MAXYEAR ...@@ -15,6 +15,7 @@ from datetime import MINYEAR, MAXYEAR
from datetime import timedelta from datetime import timedelta
from datetime import tzinfo from datetime import tzinfo
from datetime import time from datetime import time
from datetime import timezone
from datetime import date, datetime from datetime import date, datetime
pickle_choices = [(pickle, pickle, proto) for proto in range(3)] pickle_choices = [(pickle, pickle, proto) for proto in range(3)]
...@@ -49,6 +50,7 @@ class TestModule(unittest.TestCase): ...@@ -49,6 +50,7 @@ class TestModule(unittest.TestCase):
# tzinfo tests # tzinfo tests
class FixedOffset(tzinfo): class FixedOffset(tzinfo):
def __init__(self, offset, name, dstoffset=42): def __init__(self, offset, name, dstoffset=42):
if isinstance(offset, int): if isinstance(offset, int):
offset = timedelta(minutes=offset) offset = timedelta(minutes=offset)
...@@ -67,6 +69,7 @@ class FixedOffset(tzinfo): ...@@ -67,6 +69,7 @@ class FixedOffset(tzinfo):
return self.__dstoffset return self.__dstoffset
class PicklableFixedOffset(FixedOffset): class PicklableFixedOffset(FixedOffset):
def __init__(self, offset=None, name=None, dstoffset=None): def __init__(self, offset=None, name=None, dstoffset=None):
FixedOffset.__init__(self, offset, name, dstoffset) FixedOffset.__init__(self, offset, name, dstoffset)
...@@ -131,6 +134,97 @@ class TestTZInfo(unittest.TestCase): ...@@ -131,6 +134,97 @@ class TestTZInfo(unittest.TestCase):
self.assertEqual(derived.utcoffset(None), offset) self.assertEqual(derived.utcoffset(None), offset)
self.assertEqual(derived.tzname(None), 'cookie') 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 # Base clase for testing a particular aspect of timedelta, time, date and
# datetime comparisons. # datetime comparisons.
...@@ -2729,20 +2823,21 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase, unittest.TestCase): ...@@ -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 # 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 # class to represent it, so seeing whether a tz argument actually
# does a conversion is tricky. # does a conversion is tricky.
weirdtz = FixedOffset(timedelta(hours=15, minutes=58), "weirdtz", 0)
utc = FixedOffset(0, "utc", 0) utc = FixedOffset(0, "utc", 0)
for dummy in range(3): for weirdtz in [FixedOffset(timedelta(hours=15, minutes=58), "weirdtz", 0),
now = datetime.now(weirdtz) timezone(timedelta(hours=15, minutes=58), "weirdtz"),]:
self.assertTrue(now.tzinfo is weirdtz) for dummy in range(3):
utcnow = datetime.utcnow().replace(tzinfo=utc) now = datetime.now(weirdtz)
now2 = utcnow.astimezone(weirdtz) self.assertTrue(now.tzinfo is weirdtz)
if abs(now - now2) < timedelta(seconds=30): utcnow = datetime.utcnow().replace(tzinfo=utc)
break now2 = utcnow.astimezone(weirdtz)
# Else the code is broken, or more than 30 seconds passed between if abs(now - now2) < timedelta(seconds=30):
# calls; assuming the latter, just try again. break
else: # Else the code is broken, or more than 30 seconds passed between
# Three strikes and we're out. # calls; assuming the latter, just try again.
self.fail("utcnow(), now(tz), or astimezone() may be broken") else:
# Three strikes and we're out.
self.fail("utcnow(), now(tz), or astimezone() may be broken")
def test_tzinfo_fromtimestamp(self): def test_tzinfo_fromtimestamp(self):
import time import time
......
...@@ -406,6 +406,7 @@ Bob Kahn ...@@ -406,6 +406,7 @@ Bob Kahn
Kurt B. Kaiser Kurt B. Kaiser
Tamito Kajiyama Tamito Kajiyama
Peter van Kampen Peter van Kampen
Rafe Kaplan
Jacob Kaplan-Moss Jacob Kaplan-Moss
Lou Kates Lou Kates
Hiroaki Kawai Hiroaki Kawai
......
...@@ -1306,6 +1306,14 @@ Library ...@@ -1306,6 +1306,14 @@ Library
Extension Modules 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 - Issue #8973: Add __all__ to struct module; this ensures that
help(struct) includes documentation for the struct.Struct class. help(struct) includes documentation for the struct.Struct class.
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment