Repository: dateutil/dateutil (Python)

Issue:
Issue with London time during ambiguous time.
Evidently the `fromutc()` algorithm for `tzfile` is not working quite right for some zones, as first reported by @jreback on pandas-dev/pandas#14631

```python
>>> from dateutil import tz
>>> from datetime import datetime
>>> LON = tz.gettz('Europe/London')
>>> dt = tz.enfold(datetime(2013, 10, 27, 1, 30, tzinfo=LON))
>>> str(dt)                               # Proper behavior
'2013-10-27 01:30:00+00:00'
>>> str(tz.enfold(dt, fold=0))    # Proper behavior
'2013-10-27 01:30:00+01:00'
>>> str(dt.astimezone(tz.tzutc()).astimezone(LON))  # Moves us forward one hour!
'2013-10-27 02:30:00+00:00'
```

This will probably be solved when we implement our own `fromutc()` for `tzfile` (which is on the to-do list anyway). Compare to the behavior of a zone in PST:

```python
>>> from dateutil import tz
>>> from datetime import datetime
>>> PT = tz.gettz('US/Pacific')
>>> dt = tz.enfold(datetime(2011, 11, 6, 1, 30, tzinfo=PT))
>>> str(dt) 
'2011-11-06 01:30:00-08:00'
>>> str(tz.enfold(dt, fold=0))
'2011-11-06 01:30:00-07:00'
>>> str(dt.astimezone(tz.tzutc()).astimezone(PT))
'2011-11-06 01:30:00-08:00'
```


Hints:

Patch:
diff --git a/dateutil/tz/_common.py b/dateutil/tz/_common.py
--- a/dateutil/tz/_common.py
+++ b/dateutil/tz/_common.py
@@ -162,7 +162,7 @@ def _fromutc(self, dt):
         occurence, chronologically, of the ambiguous datetime).
 
         :param dt:
-            A timezone-aware :class:`datetime.dateime` object.
+            A timezone-aware :class:`datetime.datetime` object.
         """
 
         # Re-implement the algorithm from Python's datetime.py
@@ -183,14 +183,14 @@ def _fromutc(self, dt):
         if dtdst is None:
             raise ValueError("fromutc() requires a non-None dst() result")
         delta = dtoff - dtdst
-        if delta:
-            dt += delta
-            # Set fold=1 so we can default to being in the fold for
-            # ambiguous dates.
-            dtdst = enfold(dt, fold=1).dst()
-            if dtdst is None:
-                raise ValueError("fromutc(): dt.dst gave inconsistent "
-                                 "results; cannot convert")
+
+        dt += delta
+        # Set fold=1 so we can default to being in the fold for
+        # ambiguous dates.
+        dtdst = enfold(dt, fold=1).dst()
+        if dtdst is None:
+            raise ValueError("fromutc(): dt.dst gave inconsistent "
+                             "results; cannot convert")
         return dt + dtdst
 
     def fromutc(self, dt):
@@ -204,7 +204,7 @@ def fromutc(self, dt):
         occurance, chronologically, of the ambiguous datetime).
 
         :param dt:
-            A timezone-aware :class:`datetime.dateime` object.
+            A timezone-aware :class:`datetime.datetime` object.
         """
         dt_wall = self._fromutc(dt)
 
diff --git a/dateutil/tz/tz.py b/dateutil/tz/tz.py
--- a/dateutil/tz/tz.py
+++ b/dateutil/tz/tz.py
@@ -310,7 +310,7 @@ class _tzfile(object):
     Lightweight class for holding the relevant transition and time zone
     information read from binary tzfiles.
     """
-    attrs = ['trans_list', 'trans_idx', 'ttinfo_list',
+    attrs = ['trans_list', 'trans_list_utc', 'trans_idx', 'ttinfo_list',
              'ttinfo_std', 'ttinfo_dst', 'ttinfo_before', 'ttinfo_first']
 
     def __init__(self, **kwargs):
@@ -320,7 +320,7 @@ def __init__(self, **kwargs):
 
 class tzfile(_tzinfo):
     """
-    This is a ``tzinfo`` subclass that allows one to use the ``tzfile(5)``
+    This is a ``tzinfo`` subclass thant allows one to use the ``tzfile(5)``
     format timezone files to extract current and historical zone information.
 
     :param fileobj:
@@ -420,10 +420,10 @@ def _read_tzfile(self, fileobj):
         # change.
 
         if timecnt:
-            out.trans_list = list(struct.unpack(">%dl" % timecnt,
-                                                  fileobj.read(timecnt*4)))
+            out.trans_list_utc = list(struct.unpack(">%dl" % timecnt,
+                                                    fileobj.read(timecnt*4)))
         else:
-            out.trans_list = []
+            out.trans_list_utc = []
 
         # Next come tzh_timecnt one-byte values of type unsigned
         # char; each one tells which of the different types of
@@ -522,7 +522,7 @@ def _read_tzfile(self, fileobj):
         out.ttinfo_dst = None
         out.ttinfo_before = None
         if out.ttinfo_list:
-            if not out.trans_list:
+            if not out.trans_list_utc:
                 out.ttinfo_std = out.ttinfo_first = out.ttinfo_list[0]
             else:
                 for i in range(timecnt-1, -1, -1):
@@ -553,6 +553,7 @@ def _read_tzfile(self, fileobj):
         # always in gmt time. Let me know if you have comments
         # about this.
         laststdoffset = None
+        out.trans_list = []
         for i, tti in enumerate(out.trans_idx):
             if not tti.isdst:
                 offset = tti.offset
@@ -565,7 +566,7 @@ def _read_tzfile(self, fileobj):
 
                 offset = laststdoffset or 0
 
-            out.trans_list[i] += offset
+            out.trans_list.append(out.trans_list_utc[i] + offset)
 
         # In case we missed any DST offsets on the way in for some reason, make
         # a second pass over the list, looking for the /next/ DST offset.
@@ -585,10 +586,11 @@ def _read_tzfile(self, fileobj):
 
         out.trans_idx = tuple(out.trans_idx)
         out.trans_list = tuple(out.trans_list)
+        out.trans_list_utc = tuple(out.trans_list_utc)
 
         return out
 
-    def _find_last_transition(self, dt):
+    def _find_last_transition(self, dt, in_utc=False):
         # If there's no list, there are no transitions to find
         if not self._trans_list:
             return None
@@ -597,14 +599,15 @@ def _find_last_transition(self, dt):
 
         # Find where the timestamp fits in the transition list - if the
         # timestamp is a transition time, it's part of the "after" period.
-        idx = bisect.bisect_right(self._trans_list, timestamp)
+        trans_list = self._trans_list_utc if in_utc else self._trans_list
+        idx = bisect.bisect_right(trans_list, timestamp)
 
         # We want to know when the previous transition was, so subtract off 1
-        return idx - 1
+        return idx - 1        
 
     def _get_ttinfo(self, idx):
         # For no list or after the last transition, default to _ttinfo_std
-        if idx is None or (idx + 1) == len(self._trans_list):
+        if idx is None or (idx + 1) >= len(self._trans_list):
             return self._ttinfo_std
 
         # If there is a list and the time is before it, return _ttinfo_before
@@ -618,6 +621,42 @@ def _find_ttinfo(self, dt):
 
         return self._get_ttinfo(idx)
 
+    def fromutc(self, dt):
+        """
+        The ``tzfile`` implementation of :py:func:`datetime.tzinfo.fromutc`.
+
+        :param dt:
+            A :py:class:`datetime.datetime` object.
+
+        :raises TypeError:
+            Raised if ``dt`` is not a :py:class:`datetime.datetime` object.
+
+        :raises ValueError:
+            Raised if this is called with a ``dt`` which does not have this
+            ``tzinfo`` attached.
+
+        :return:
+            Returns a :py:class:`datetime.datetime` object representing the
+            wall time in ``self``'s time zone.
+        """
+        # These isinstance checks are in datetime.tzinfo, so we'll preserve
+        # them, even if we don't care about duck typing.
+        if not isinstance(dt, datetime.datetime):
+            raise TypeError("fromutc() requires a datetime argument")
+
+        if dt.tzinfo is not self:
+            raise ValueError("dt.tzinfo is not self")
+
+        # First treat UTC as wall time and get the transition we're in.
+        idx = self._find_last_transition(dt, in_utc=True)
+        tti = self._get_ttinfo(idx)
+
+        dt_out = dt + datetime.timedelta(seconds=tti.offset)
+
+        fold = self.is_ambiguous(dt_out, idx=idx)
+
+        return enfold(dt_out, fold=int(fold))
+
     def is_ambiguous(self, dt, idx=None):
         """
         Whether or not the "wall time" of a given datetime is ambiguous in this
@@ -655,7 +694,7 @@ def _resolve_ambiguous_time(self, dt):
         if idx is None or idx == 0:
             return idx
 
-        # Get the current datetime as a timestamp
+        # If it's ambiguous and we're in a fold, shift to a different index.
         idx_offset = int(not _fold and self.is_ambiguous(dt, idx))
 
         return idx - idx_offset


Test patch:
diff --git a/dateutil/test/test_tz.py b/dateutil/test/test_tz.py
--- a/dateutil/test/test_tz.py
+++ b/dateutil/test/test_tz.py
@@ -274,6 +274,28 @@ def testGapNegativeUTCOffset(self):
             self.assertEqual(t0.utcoffset(), timedelta(hours=-5.0))
             self.assertEqual(t1.utcoffset(), timedelta(hours=-4.0))
 
+    def testFoldLondon(self):
+        tzname = self._get_tzname('Europe/London')
+
+        with self._gettz_context(tzname):
+            LON = self.gettz(tzname)
+            UTC = tz.tzutc()
+
+            t0_u = datetime(2013, 10, 27, 0, 30, tzinfo=UTC)   # BST
+            t1_u = datetime(2013, 10, 27, 1, 30, tzinfo=UTC)   # GMT
+
+            t0 = t0_u.astimezone(LON)
+            t1 = t1_u.astimezone(LON)
+
+            self.assertEqual(t0.replace(tzinfo=None),
+                             datetime(2013, 10, 27, 1, 30))
+            
+            self.assertEqual(t1.replace(tzinfo=None),
+                             datetime(2013, 10, 27, 1, 30))
+
+            self.assertEqual(t0.utcoffset(), timedelta(hours=1))
+            self.assertEqual(t1.utcoffset(), timedelta(hours=0))
+
     def testFoldIndependence(self):
         tzname = self._get_tzname('America/New_York')
 
@@ -740,6 +762,9 @@ class TzLocalNixTest(unittest.TestCase, TzFoldMixin):
     # POSIX string for AEST/AEDT (valid >= 2008)
     TZ_AEST = 'AEST-10AEDT,M10.1.0/2,M4.1.0/3'
 
+    # POSIX string for BST/GMT
+    TZ_LON = 'GMT0BST,M3.5.0,M10.5.0'
+
     # POSIX string for UTC
     UTC = 'UTC'
 
@@ -750,7 +775,8 @@ def gettz(self, tzname):
     def _gettz_context(self, tzname):
         tzname_map = {'Australia/Sydney': self.TZ_AEST,
                       'America/Toronto': self.TZ_EST,
-                      'America/New_York': self.TZ_EST}
+                      'America/New_York': self.TZ_EST,
+                      'Europe/London': self.TZ_LON}
 
         return TZEnvContext(tzname_map.get(tzname, tzname))
 
@@ -955,13 +981,21 @@ class TZRangeTest(unittest.TestCase, TzFoldMixin):
                                              weekday=SU(+1)),
                          end=relativedelta(month=4, day=1, hour=2,
                                            weekday=SU(+1)))
+
+    TZ_LON = tz.tzrange('GMT', timedelta(hours=0),
+                        'BST', timedelta(hours=1),
+                        start=relativedelta(month=3, day=31, weekday=SU(-1),
+                                            hours=2),
+                        end=relativedelta(month=10, day=31, weekday=SU(-1),
+                                          hours=1))
     # POSIX string for UTC
     UTC = 'UTC'
 
     def gettz(self, tzname):
         tzname_map = {'Australia/Sydney': self.TZ_AEST,
                       'America/Toronto': self.TZ_EST,
-                      'America/New_York': self.TZ_EST}
+                      'America/New_York': self.TZ_EST,
+                      'Europe/London': self.TZ_LON}
 
         return tzname_map[tzname]
 
@@ -1083,11 +1117,15 @@ class TZStrTest(unittest.TestCase, TzFoldMixin):
     # POSIX string for AEST/AEDT (valid >= 2008)
     TZ_AEST = 'AEST-10AEDT,M10.1.0/2,M4.1.0/3'
 
+    # POSIX string for GMT/BST
+    TZ_LON = 'GMT0BST,M3.5.0,M10.5.0'
+
     def gettz(self, tzname):
         # Actual time zone changes are handled by the _gettz_context function
         tzname_map = {'Australia/Sydney': self.TZ_AEST,
                       'America/Toronto': self.TZ_EST,
-                      'America/New_York': self.TZ_EST}
+                      'America/New_York': self.TZ_EST,
+                      'Europe/London': self.TZ_LON}
 
         return tz.tzstr(tzname_map[tzname])
 
@@ -1301,9 +1339,30 @@ def gettz(self, tzname):
             'END:VTIMEZONE'
             )
 
+        TZ_LON = (
+            'BEGIN:VTIMEZONE',
+            'TZID:Europe-London',
+            'BEGIN:STANDARD',
+            'DTSTART:19810301T030000',
+            'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10;BYHOUR=02',
+            'TZOFFSETFROM:+0100',
+            'TZOFFSETTO:+0000',
+            'TZNAME:GMT',
+            'END:STANDARD',
+            'BEGIN:DAYLIGHT',
+            'DTSTART:19961001T030000',
+            'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=03;BYHOUR=01',
+            'TZOFFSETFROM:+0000',
+            'TZOFFSETTO:+0100',
+            'TZNAME:BST',
+            'END:DAYLIGHT',
+            'END:VTIMEZONE'
+            )
+
         tzname_map = {'Australia/Sydney': TZ_AEST,
                       'America/Toronto': TZ_EST,
-                      'America/New_York': TZ_EST}
+                      'America/New_York': TZ_EST,
+                      'Europe/London': TZ_LON}
 
         tzc = tz.tzical(StringIO('\n'.join(tzname_map[tzname]))).get()