Commits

Christian Boos committed 4e69c68

0.12-stable: make `to_datetime` more robust (#10863)

- when given a naive datetime and a pytz timezone info,
the returned aware datetime instance had a wrong utcoffset
due to a misuse of the pytz API
- in general, make `to_datetime` always return aware and
normalized datetime instances

Comments (0)

Files changed (2)

trac/util/datefmt.py

 # -- conversion
 
 def to_datetime(t, tzinfo=None):
-    """Convert `t` into a `datetime` object, using the following rules:
-    
-     - If `t` is already a `datetime` object, it is simply returned.
-     - If `t` is None, the current time will be used.
-     - If `t` is a number, it is interpreted as a timestamp.
-     
-    If no `tzinfo` is given, the local timezone will be used.
+    """Convert ``t`` into a `datetime` object in the ``tzinfo`` timezone.
+
+    If no ``tzinfo`` is given, the local timezone `localtz` will be used.
+
+    ``t`` is converted using the following rules:
+
+     - If ``t`` is already a `datetime` object,
+       - if it is timezone-"naive", it is localized to ``tzinfo``
+       - if it is already timezone-aware, ``t`` is mapped to the given
+         timezone (`datetime.datetime.astimezone`)
+     - If ``t`` is None, the current time will be used.
+     - If ``t`` is a number, it is interpreted as a timestamp.
 
     Any other input will trigger a `TypeError`.
+
+    All returned datetime instances are timezone aware and normalized.
     """
+    tz = tzinfo or localtz
     if t is None:
-        return datetime.now(tzinfo or localtz)
+        dt = datetime.now(tz)
     elif isinstance(t, datetime):
-        return t
+        if t.tzinfo:
+            dt = t.astimezone(tz)
+        else:
+            dt = tz.localize(t)
     elif isinstance(t, date):
-        return (tzinfo or localtz).localize(datetime(t.year, t.month, t.day))
+        dt = tz.localize(datetime(t.year, t.month, t.day))
     elif isinstance(t, (int, long, float)):
         if not (_min_ts <= t <= _max_ts):
             # Handle microsecond timestamps for 0.11 compatibility
             # Work around negative fractional times bug in Python 2.4
             # http://bugs.python.org/issue1646728
             frac, integer = math.modf(t)
-            return datetime.fromtimestamp(integer - 1, tzinfo or localtz) \
-                   + timedelta(seconds=frac + 1)
-        return datetime.fromtimestamp(t, tzinfo or localtz)
+            dt = datetime.fromtimestamp(integer - 1, tz) + \
+                    timedelta(seconds=frac + 1)
+        else:
+            dt = datetime.fromtimestamp(t, tz)
+    if dt:
+        return tz.normalize(dt)
     raise TypeError('expecting datetime, int, long, float, or None; got %s' %
                     type(t))
 
     `tzinfo` will default to the local timezone if left to `None`.
     """
     tz = tzinfo or localtz
-    t = to_datetime(t, tzinfo).astimezone(tz)
+    t = to_datetime(t, tz)
     normalize_Z = False
     if format.lower().startswith('iso8601'):
         if 'date' in format:

trac/util/tests/datefmt.py

             t_utc = datetime.datetime(2009, 8, 1, 10, 0, 0, 0, datefmt.utc)
             self.assertEqual(t_utc, t)
 
+        def test_to_datetime_normalized(self):
+            tz = datefmt.get_timezone('Europe/Paris')
+            t = datetime.datetime(2012, 3, 25, 2, 15)
+            dt = datefmt.to_datetime(t, tz)
+            self.assertEqual(datetime.timedelta(0, 7200), dt.utcoffset())
+
+        def test_to_datetime_astimezone(self):
+            tz = datefmt.get_timezone('Europe/Paris')
+            t = datetime.datetime(2012, 3, 25, 2, 15, tzinfo=datefmt.utc)
+            dt = datefmt.to_datetime(t, tz)
+            self.assertEqual(datetime.timedelta(0, 7200), dt.utcoffset())
+
+        def test_to_datetime_localtz(self):
+            t = datetime.datetime(2012, 3, 25, 2, 15)
+            dt = datefmt.to_datetime(t)
+            self.assertEqual(datefmt.localtz, dt.tzinfo)
+
+        def test_to_datetime_localtz(self):
+            dt = datefmt.to_datetime(None)
+            self.assertEqual(datefmt.localtz, dt.tzinfo)
+
 
 class DateFormatTestCase(unittest.TestCase):