Repository: joke2k/django-environ
Pull request #134 (2017-06-29 11:04:37)

Fix parsing option values None, True and False
As a fix for #133 I propose using `ast.literal_eval()` to parse query parameters that may contain strings, intergers, `None` or booleans.


Patch:
diff --git a/environ/environ.py b/environ/environ.py
index 24368b3..b818c91 100644
--- a/environ/environ.py
+++ b/environ/environ.py
@@ -2,6 +2,7 @@
 Django-environ allows you to utilize 12factor inspired environment
 variables to configure your Django application.
 """
+import ast
 import json
 import logging
 import os
@@ -30,9 +31,21 @@ __version__ = tuple(VERSION.split('.'))
 def _cast_int(v):
     return int(v) if hasattr(v, 'isdigit') and v.isdigit() else v
 
+
 def _cast_urlstr(v):
     return urllib.parse.unquote_plus(v) if isinstance(v, str) else v
 
+
+def _cast(value):
+    # Safely evaluate an expression node or a string containing a Python
+    # literal or container display.
+    # https://docs.python.org/3/library/ast.html#ast.literal_eval
+    try:
+        return ast.literal_eval(value)
+    except ValueError:
+        return value
+
+
 # back compatibility with redis_cache package
 DJANGO_REDIS_DRIVER = 'django_redis.cache.RedisCache'
 DJANGO_REDIS_CACHE_DRIVER = 'redis_cache.RedisCache'
@@ -402,7 +415,7 @@ class Env(object):
             config_options = {}
             for k, v in urllib.parse.parse_qs(url.query).items():
                 if k.upper() in cls._DB_BASE_OPTIONS:
-                    config.update({k.upper(): _cast_int(v[0])})
+                    config.update({k.upper(): _cast(v[0])})
                 else:
                     config_options.update({k: _cast_int(v[0])})
             config['OPTIONS'] = config_options
@@ -458,7 +471,7 @@ class Env(object):
         if url.query:
             config_options = {}
             for k, v in urllib.parse.parse_qs(url.query).items():
-                opt = {k.upper(): _cast_int(v[0])}
+                opt = {k.upper(): _cast(v[0])}
                 if k.upper() in cls._CACHE_BASE_OPTIONS:
                     config.update(opt)
                 else:


Test patch:
diff --git a/environ/test.py b/environ/test.py
index b7f79ba..0544f98 100644
--- a/environ/test.py
+++ b/environ/test.py
@@ -358,6 +358,12 @@ class DatabaseTestSuite(unittest.TestCase):
         url = Env.db_url_config(url)
         self.assertEqual(url['CONN_MAX_AGE'], 600)
 
+        url = 'postgres://user:pass@host:1234/dbname?conn_max_age=None&autocommit=True&atomic_requests=False'
+        url = Env.db_url_config(url)
+        self.assertEqual(url['CONN_MAX_AGE'], None)
+        self.assertEqual(url['AUTOCOMMIT'], True)
+        self.assertEqual(url['ATOMIC_REQUESTS'], False)
+
         url = 'mysql://user:pass@host:1234/dbname?init_command=SET storage_engine=INNODB'
         url = Env.db_url_config(url)
         self.assertEqual(url['OPTIONS'], {
@@ -378,6 +384,20 @@ class DatabaseTestSuite(unittest.TestCase):
 
 class CacheTestSuite(unittest.TestCase):
 
+    def test_base_options_parsing(self):
+        url = 'memcache://127.0.0.1:11211/?timeout=0&key_prefix=cache_&key_function=foo.get_key&version=1'
+        url = Env.cache_url_config(url)
+
+        self.assertEqual(url['KEY_PREFIX'], 'cache_')
+        self.assertEqual(url['KEY_FUNCTION'], 'foo.get_key')
+        self.assertEqual(url['TIMEOUT'], 0)
+        self.assertEqual(url['VERSION'], 1)
+
+        url = 'redis://127.0.0.1:6379/?timeout=None'
+        url = Env.cache_url_config(url)
+
+        self.assertEqual(url['TIMEOUT'], None)
+
     def test_memcache_parsing(self):
         url = 'memcache://127.0.0.1:11211'
         url = Env.cache_url_config(url)