Repository: joke2k/django-environ
Pull request #45 (2015-09-23 16:00:51)

Merge all new features into master for version 0.4


Patch:
diff --git a/.travis.yml b/.travis.yml
index a604ab7..78c3377 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,4 +1,5 @@
 language: python
+sudo: false
 python:
  - "2.6"
  - "2.7"
diff --git a/LICENSE.txt b/LICENSE.txt
index 80d813f..5901005 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,4 +1,4 @@
-Copyright (c) 2013 Daniele Faraglia
+Copyright (c) 2013-2015, Daniele Faraglia
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.rst b/README.rst
index 5e4be10..151f8d7 100644
--- a/README.rst
+++ b/README.rst
@@ -125,7 +125,7 @@ How to install
 How to use
 ==========
 
-There are only classes, Env and Path
+There are only two classes, `environ.Env` and `environ.Path`
 
 ::
 
@@ -166,6 +166,7 @@ Supported Types
 - float
 - json
 - list (FOO=a,b,c)
+- tuple (FOO=(a,b,c))
 - dict (BAR=key=val,foo=bar)
 - url
 - path (environ.Path)
@@ -189,6 +190,7 @@ Supported Types
     - ElasticSearch: elasticsearch://
     - Solr: solr://
     - Whoosh: whoosh://
+    - Xapian: xapian://
     - Simple cache: simple://
 - email_url
     - SMTP: smtp://
@@ -212,35 +214,39 @@ Tests
 License
 =======
 
-Django-environ is licensed under the MIT License - see the LICENSE.rst file for details
+Django-environ is licensed under the MIT License - see the `LICENSE`_ file for details
 
 Changelog
 =========
 
-**0.4.0 (2015-09-19)**
+`0.4.0 - 23-September-2015 <http://github.com/joke2k/django-environ/compare/v0.3...v0.4>`__
+-------------------------------------------------------------------------------------------
   - Fix non-ascii values (broken in Python 2.x)
   - New email schemes - smtp+ssl and smtp+tls (smtps would be deprecated)
   - redis_cache replaced by django_redis
-
-**0.3.1 (2014-09-03)**
+  - Add tuple support. Thanks to @anonymouzz
   - Add LDAP url support for database (django-ldapdb)
   - Fix psql/pgsql url
 
-**0.3 (2014-06-03)**
+`0.3 - 03-June-2014 <http://github.com/joke2k/django-environ/compare/v0.2.1...v0.3>`__
+--------------------------------------------------------------------------------------
   - Add cache url support
   - Add email url support
   - Add search url support
   - Rewriting README.rst
 
-**0.2.1 (2013-04-19)**
+0.2.1 19-April-2013
+-------------------
   - environ/environ.py: Env.__call__ now uses Env.get_value instance method
 
-**0.2 (2013-04-16)**
+0.2 16-April-2013
+-----------------
   - environ/environ.py, environ/test.py, environ/test_env.txt: add advanced
     float parsing (comma and dot symbols to separate thousands and decimals)
   - README.rst, docs/index.rst: fix TYPO in documentation
 
-**0.1 (2013-04-02)**
+0.1 02-April-2013
+-----------------
   - initial release
 
 Credits
@@ -311,3 +317,5 @@ Credits
 .. |license| image:: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square
     :target: https://raw.githubusercontent.com/joke2k/django-environ/master/LICENSE.txt
     :alt: Package license
+
+.. _LICENSE: https://github.com/joke2k/django-environ/blob/master/LICENSE.txt
diff --git a/environ/environ.py b/environ/environ.py
index e847cf9..ca07f93 100644
--- a/environ/environ.py
+++ b/environ/environ.py
@@ -154,6 +154,12 @@ class Env(object):
         """
         return self.get_value(var, cast=list if not cast else [cast], default=default)
 
+    def tuple(self, var, cast=None, default=NOTSET):
+        """
+        :rtype: tuple
+        """
+        return self.get_value(var, cast=tuple if not cast else (cast,), default=default)
+
     def dict(self, var, cast=dict, default=NOTSET):
         """
         :rtype: dict
@@ -214,7 +220,9 @@ class Env(object):
         :returns: Value from environment or default (if set)
         """
 
-        logger.debug("get '{0}' casted as '{1}' with default '{2}'".format(var, cast, default))
+        logger.debug("get '{0}' casted as '{1}' with default '{2}'".format(
+            var, cast, default
+        ))
 
         if var in self.scheme:
             var_info = self.scheme[var]
@@ -251,7 +259,7 @@ class Env(object):
             value = value.lstrip('$')
             value = self.get_value(value, cast=cast, default=default)
 
-        if value != default or parse_default:
+        if value != default or (parse_default and value):
             value = self.parse_value(value, cast)
 
         return value
@@ -276,18 +284,27 @@ class Env(object):
                 value = value.lower() in cls.BOOLEAN_TRUE_STRINGS
         elif isinstance(cast, list):
             value = list(map(cast[0], [x for x in value.split(',') if x]))
+        elif isinstance(cast, tuple):
+            val = value.strip('(').strip(')').split(',')
+            value = tuple(map(cast[0], [x for x in val if x]))
         elif isinstance(cast, dict):
             key_cast = cast.get('key', str)
             value_cast = cast.get('value', str)
             value_cast_by_key = cast.get('cast', dict())
             value = dict(map(
-                lambda kv: (key_cast(kv[0]), cls.parse_value(kv[1], value_cast_by_key.get(kv[0], value_cast))),
+                lambda kv: (
+                    key_cast(kv[0]),
+                    cls.parse_value(kv[1], value_cast_by_key.get(kv[0], value_cast))
+                ),
                 [val.split('=') for val in value.split(';') if val]
             ))
         elif cast is dict:
             value = dict([val.split('=') for val in value.split(',') if val])
         elif cast is list:
             value = [x for x in value.split(',') if x]
+        elif cast is tuple:
+            val = value.strip('(').strip(')').split(',')
+            value = tuple([x for x in val if x])
         elif cast is float:
             # clean string
             float_str = re.sub(r'[^\d,\.]', '', value)
@@ -379,7 +396,7 @@ class Env(object):
         """Pulled from DJ-Cache-URL, parse an arbitrary Cache URL.
 
         :param url:
-        :param overrides:
+        :param backend:
         :return:
         """
         url = urlparse.urlparse(url) if not isinstance(url, cls.URL_CLASS) else url
@@ -663,7 +680,8 @@ class Path(object):
             return self.path('../' * other)
         elif isinstance(other, string_types):
             return Path(self.__root__.rstrip(other))
-        raise TypeError("unsupported operand type(s) for -: '{0}' and '{1}'".format(self, type(other)))
+        raise TypeError(
+            "unsupported operand type(s) for -: '{0}' and '{1}'".format(self, type(other)))
 
     def __invert__(self):
         return self.path('..')
@@ -683,11 +701,21 @@ class Path(object):
     def __unicode__(self):
         return self.__str__()
 
+    def __getitem__(self, *args, **kwargs):
+        return self.__str__().__getitem__(*args, **kwargs)
+
+    def rfind(self, *args, **kwargs):
+        return self.__str__().rfind(*args, **kwargs)
+
+    def find(self, *args, **kwargs):
+        return self.__str__().find(*args, **kwargs)
+
     @staticmethod
     def _absolute_join(base, *paths, **kwargs):
         absolute_path = os.path.abspath(os.path.join(base, *paths))
         if kwargs.get('required', False) and not os.path.exists(absolute_path):
-            raise ImproperlyConfigured("Create required path: {0}".format(absolute_path))
+            raise ImproperlyConfigured(
+                "Create required path: {0}".format(absolute_path))
         return absolute_path
 
 


Test patch:
diff --git a/environ/test.py b/environ/test.py
index 80470ae..7f66c34 100644
--- a/environ/test.py
+++ b/environ/test.py
@@ -37,6 +37,7 @@ class BaseTests(unittest.TestCase):
                     BOOL_FALSE_VAR2='False',
                     PROXIED_VAR='$STR_VAR',
                     INT_LIST='42,33',
+                    INT_TUPLE='(42,33)',
                     STR_LIST_WITH_SPACES=' foo,  bar',
                     EMPTY_LIST='',
                     DICT_VAR='foo=bar,test=on',
@@ -109,6 +110,11 @@ class EnvTests(BaseTests):
         self.assertTypeAndValue(list, [42, 33], self.env('INT_LIST', cast=[int]))
         self.assertTypeAndValue(list, [42, 33], self.env.list('INT_LIST', int))
 
+    def test_int_tuple(self):
+        self.assertTypeAndValue(tuple, (42, 33), self.env('INT_LIST', cast=(int,)))
+        self.assertTypeAndValue(tuple, (42, 33), self.env.tuple('INT_LIST', int))
+        self.assertTypeAndValue(tuple, ('42', '33'), self.env.tuple('INT_LIST'))
+
     def test_str_list_with_spaces(self):
         self.assertTypeAndValue(list, [' foo', '  bar'],
                                 self.env('STR_LIST_WITH_SPACES', cast=[str]))
@@ -134,6 +140,7 @@ class EnvTests(BaseTests):
         url = self.env.url('URL_VAR')
         self.assertEqual(url.__class__, self.env.URL_CLASS)
         self.assertEqual(url.geturl(), self.URL)
+        self.assertEqual(None, self.env.url('OTHER_URL', default=None))
 
     def test_db_url_value(self):
         pg_config = self.env.db()
@@ -562,6 +569,10 @@ class PathTests(unittest.TestCase):
         self.assertTrue(Path('/home') == Path('/home'))
         self.assertTrue(Path('/home') != Path('/home/dev'))
 
+        self.assertEqual(Path('/home/foo/').rfind('/'), str(Path('/home/foo')).rfind('/'))
+        self.assertEqual(Path('/home/foo/').find('/home'), str(Path('/home/foo/')).find('/home'))
+        self.assertEqual(Path('/home/foo/')[1], str(Path('/home/foo/'))[1])
+
         self.assertEqual(~Path('/home'), Path('/'))
         self.assertEqual(Path('/') + 'home', Path('/home'))
         self.assertEqual(Path('/') + '/home/public', Path('/home/public'))
diff --git a/environ/test_env.txt b/environ/test_env.txt
index 875d633..112ca3e 100644
--- a/environ/test_env.txt
+++ b/environ/test_env.txt
@@ -24,3 +24,4 @@ STR_LIST_WITH_SPACES= foo,  bar
 STR_VAR=bar
 INT_LIST=42,33
 CYRILLIC_VAR=фуубар
+INT_TUPLE=(42,33)