Repository: dateutil/dateutil (Python)

Issue:
RFC822 timestamp using GMT is parsed into BST if local time is UK time
The following code snipped prints `True` unless the machine is using UK time.

```python
from datetime import datetime
from dateutil.parser import parse
from dateutil.tz import tzutc


d1 = datetime(2002, 10, 2, 13, 0, tzinfo=tzutc())
d2 = parse('Wed, 02 Oct 2002 13:00:00 GMT')
print(d1 == d2)
```

It appears that at [this point](https://github.com/dateutil/dateutil/blob/master/dateutil/parser.py#L605) in the parse function, the parser sees that local time is one of `('GMT', 'BST')` because in the Summer the UK uses BST (GMT+1).

Source issue: boto/botocore#1070


Hints:
This is a tricky situation, because the API is pretty clear about the fact that it's going to check your local time zones for valid zone names to parse. The real issue is that this is really fuzzy and context-dependent behavior. I'd love to deprecate it at some point in the future and make it a default-off option.

That said, I don't think this should be removed as is, because it would actually remove existing functionality with no way to replicate the old behavior, whereas currently you can get your desired behavior this way:

``` python
from datetime import datetime
from dateutil.parser import parse
from dateutil.tz import tzutc


d1 = datetime(2002, 10, 2, 13, 0, tzinfo=tzutc())
d2 = parse('Wed, 02 Oct 2002 13:00:00 GMT', tzinfos={'GMT': tzutc()})
print(d1 == d2)
```

This is generally advisable, because if you notice, RFC822 also specifies 'EST', 'EDT', and a number of other zones, none of which can be parsed _at all_ by `dateutil.parser` unless they are the local zone, (and never will be) for ambiguity reasons. The real lesson is that you need to provide the context yourself if you know it.

That said, there is _one_ possible tweak that I think I would be comfortable with, which is that we can improve the time zone parser logic such that when it decides to assign `tzlocal()` it will check if the time zone that actually gets assigned is the time zone we were expecting, e.g.: `dt.replace(tzinfo=res.tzname).tzname() == res.tzname`, and if not, see if there's another way to parse it that _does_ create a time that exists. This same logic should also be used to assign whether or not the parsed date has the `fold` attribute, but I will make a separate issue for that.


Patch:
diff --git a/dateutil/parser/_parser.py b/dateutil/parser/_parser.py
--- a/dateutil/parser/_parser.py
+++ b/dateutil/parser/_parser.py
@@ -628,8 +628,18 @@ def parse(self, timestr, default=None,
                     tzinfos and res.tzname in tzinfos):
                 tzinfo = self._build_tzinfo(tzinfos, res.tzname, res.tzoffset)
                 ret = ret.replace(tzinfo=tzinfo)
+                ret = self._assign_tzname(ret, res.tzname)
             elif res.tzname and res.tzname in time.tzname:
                 ret = ret.replace(tzinfo=tz.tzlocal())
+
+                # Handle ambiguous local datetime
+                ret = self._assign_tzname(ret, res.tzname)
+
+                # This is mostly relevant for winter GMT zones parsed in the UK
+                if (ret.tzname() != res.tzname and
+                        res.tzname in self.info.UTCZONE):
+                    ret = ret.replace(tzinfo=tz.tzutc())
+
             elif res.tzoffset == 0:
                 ret = ret.replace(tzinfo=tz.tzutc())
             elif res.tzoffset:
@@ -1142,6 +1152,14 @@ def _build_tzinfo(self, tzinfos, tzname, tzoffset):
                              "tz string, or int offset.")
         return tzinfo
 
+    def _assign_tzname(self, dt, tzname):
+        if dt.tzname() != tzname:
+            new_dt = tz.enfold(dt, fold=1)
+            if new_dt.tzname() == tzname:
+                return new_dt
+
+        return dt
+
 
 DEFAULTPARSER = parser()
 


Test patch:
diff --git a/dateutil/test/test_parser.py b/dateutil/test/test_parser.py
--- a/dateutil/test/test_parser.py
+++ b/dateutil/test/test_parser.py
@@ -4,21 +4,28 @@
 import itertools
 from datetime import datetime, timedelta
 import unittest
-import pytest
+import sys
 
+from dateutil import tz
 from dateutil.tz import tzoffset
 from dateutil.parser import parse, parserinfo
 
+from ._common import TZEnvContext
+
 from six import assertRaisesRegex, PY3
 from six.moves import StringIO
 
+import pytest
+
+# Platform info
+IS_WIN = sys.platform.startswith('win')
+
 try:
     datetime.now().strftime('%-d')
     PLATFORM_HAS_DASH_D = True
 except ValueError:
     PLATFORM_HAS_DASH_D = False
 
-import pytest
 
 class TestFormat(unittest.TestCase):
 
@@ -1000,3 +1007,57 @@ def test_extraneous_year3(self):
         res = parse(dstr, fuzzy_with_tokens=True)
         expected = datetime(1994, 12, 1)
         assert res == expected
+
+
+@pytest.mark.skipif(IS_WIN, reason='Windows does not use TZ var')
+def test_parse_unambiguous_nonexistent_local():
+    # When dates are specified "EST" even when they should be "EDT" in the
+    # local time zone, we should still assign the local time zone
+    with TZEnvContext('EST+5EDT,M3.2.0/2,M11.1.0/2'):
+        dt_exp = datetime(2011, 8, 1, 12, 30, tzinfo=tz.tzlocal())
+        dt = parse('2011-08-01T12:30 EST')
+
+        assert dt.tzname() == 'EDT'
+        assert dt == dt_exp
+
+
+@pytest.mark.skipif(IS_WIN, reason='Windows does not use TZ var')
+def test_tzlocal_in_gmt():
+    # GH #318
+    with TZEnvContext('GMT0BST,M3.5.0,M10.5.0'):
+        # This is an imaginary datetime in tz.tzlocal() but should still
+        # parse using the GMT-as-alias-for-UTC rule
+        dt = parse('2004-05-01T12:00 GMT')
+        dt_exp = datetime(2004, 5, 1, 12, tzinfo=tz.tzutc())
+
+        assert dt == dt_exp
+
+
+@pytest.mark.skipif(IS_WIN, reason='Windows does not use TZ var')
+def test_tzlocal_parse_fold():
+    # One manifestion of GH #318
+    with TZEnvContext('EST+5EDT,M3.2.0/2,M11.1.0/2'):
+        dt_exp = datetime(2011, 11, 6, 1, 30, tzinfo=tz.tzlocal())
+        dt_exp = tz.enfold(dt_exp, fold=1)
+        dt = parse('2011-11-06T01:30 EST')
+
+        # Because this is ambiguous, kuntil `tz.tzlocal() is tz.tzlocal()`
+        # we'll just check the attributes we care about rather than
+        # dt == dt_exp
+        assert dt.tzname() == dt_exp.tzname()
+        assert dt.replace(tzinfo=None) == dt_exp.replace(tzinfo=None)
+        assert getattr(dt, 'fold') == getattr(dt_exp, 'fold')
+        assert dt.astimezone(tz.tzutc()) == dt_exp.astimezone(tz.tzutc())
+
+
+def test_parse_tzinfos_fold():
+    NYC = tz.gettz('America/New_York')
+    tzinfos = {'EST': NYC, 'EDT': NYC}
+
+    dt_exp = tz.enfold(datetime(2011, 11, 6, 1, 30, tzinfo=NYC), fold=1)
+    dt = parse('2011-11-06T01:30 EST', tzinfos=tzinfos)
+
+    assert dt == dt_exp
+    assert dt.tzinfo is dt_exp.tzinfo
+    assert getattr(dt, 'fold') == getattr(dt_exp, 'fold')
+    assert dt.astimezone(tz.tzutc()) == dt_exp.astimezone(tz.tzutc())