Repository: boto/boto

Issue:
Fix s3 RFC1123 date parsing error when locale != en_US
Boto couldn't parse s3 dates when the locale is not set to en_US. Used ko_KR and ran into `'Thu, 15 May 2014 09:06:03 GMT' does not match format "%a, %d %b %Y %H:%M:%S %Z"` even though the date clearly matched. 

Trying to set LC_TIME time to 'en_US.UTF-8' or 'en_US' before parsing RFC1123. Tested on ubuntu trusty.



Discussion:
Interesting bug, can you please add a unit test to cover the case of a non-US locale to make sure this fixes the issue and to make sure the issue is not reintroduced in the future? Once added, I can merge this in. Thanks!

Added tests. Though testing locale specific functionality is a bit tricky since it depends on the locale to be installed on the system that executes the tests.

I used de_DE since it seems to be a common locale among systems. ko_KR might be too exotic. 

I'll look into mocks once I find some free time


Resolving patch:
diff --git a/boto/utils.py b/boto/utils.py
--- a/boto/utils.py
+++ b/boto/utils.py
@@ -61,6 +61,11 @@
 import email.encoders
 import gzip
 import base64
+import threading
+import locale
+
+from contextlib import contextmanager
+
 try:
     from hashlib import md5
 except ImportError:
@@ -446,7 +451,20 @@ def get_instance_userdata(version='latest', sep=None,
 ISO8601 = '%Y-%m-%dT%H:%M:%SZ'
 ISO8601_MS = '%Y-%m-%dT%H:%M:%S.%fZ'
 RFC1123 = '%a, %d %b %Y %H:%M:%S %Z'
+LOCALE_LOCK = threading.Lock()
+
+@contextmanager
+def setlocale(name):
+    """
+    A context manager to set the locale in a threadsafe manner.
+    """
+    with LOCALE_LOCK:
+        saved = locale.setlocale(locale.LC_ALL)
 
+        try:
+            yield locale.setlocale(locale.LC_ALL, name)
+        finally:
+            locale.setlocale(locale.LC_ALL, saved)
 
 def get_ts(ts=None):
     if not ts:
@@ -455,17 +473,18 @@ def get_ts(ts=None):
 
 
 def parse_ts(ts):
-    ts = ts.strip()
-    try:
-        dt = datetime.datetime.strptime(ts, ISO8601)
-        return dt
-    except ValueError:
+    with setlocale('C'):
+        ts = ts.strip()
         try:
-            dt = datetime.datetime.strptime(ts, ISO8601_MS)
+            dt = datetime.datetime.strptime(ts, ISO8601)
             return dt
         except ValueError:
-            dt = datetime.datetime.strptime(ts, RFC1123)
-            return dt
+            try:
+                dt = datetime.datetime.strptime(ts, ISO8601_MS)
+                return dt
+            except ValueError:
+                dt = datetime.datetime.strptime(ts, RFC1123)
+                return dt
 
 
 def find_class(module_name, class_name=None):


Test patch:
diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py
--- a/tests/unit/utils/test_utils.py
+++ b/tests/unit/utils/test_utils.py
@@ -24,8 +24,10 @@
 except ImportError:
     import unittest
 
+import datetime
 import hashlib
 import hmac
+import locale
 import mock
 import thread
 import time
@@ -259,5 +261,32 @@ def test_user_data(self):
             'http://169.254.169.254/latest/user-data',
             retry_on_404=False)
 
+
+class TestStringToDatetimeParsing(unittest.TestCase):
+    """ Test string to datetime parsing """
+    def setUp(self):
+        self._saved = locale.setlocale(locale.LC_ALL)
+        locale.setlocale(locale.LC_ALL, 'de_DE.UTF-8')
+
+    def tearDown(self):
+        locale.setlocale(locale.LC_ALL, self._saved)
+
+    def test_nonus_locale(self):
+        test_string = 'Thu, 15 May 2014 09:06:03 GMT'
+
+        # Default strptime shoudl fail
+        with self.assertRaises(ValueError):
+            datetime.datetime.strptime(test_string, boto.utils.RFC1123)
+
+        # Our parser should succeed
+        result = boto.utils.parse_ts(test_string)
+
+        self.assertEqual(2014, result.year)
+        self.assertEqual(5, result.month)
+        self.assertEqual(15, result.day)
+        self.assertEqual(9, result.hour)
+        self.assertEqual(6, result.minute)
+
+
 if __name__ == '__main__':
     unittest.main()