Repository: joke2k/django-environ

Issue:
Won't update environ key if it already set
imagine a case when we need to update some of the production settings in `production.py` base on `base.py`.

```py
from .base import *

environ.Env.read_env(env_file="/etc/<app>/.env")

DATABASES = {
    # read os.environ['DATABASE_URL'] and raises ImproperlyConfigured exception if not found
    "default": env.db(),
}

CELERY_BROKER_URL = env("REDIS_URL")
CELERY_RESULT_BACKEND = env("REDIS_URL")
CACHEOPS_REDIS = env("CACHE_URL")

...
``` 

since code used `setdefault` it will ignore to update that in `django-environ/environ/environ.py`
```py
# line 654
cls.ENVIRON.setdefault(key, str(val))
```


Comments:

Patch:
diff --git a/docs/tips.rst b/docs/tips.rst
index ab3161d..cff0d86 100644
--- a/docs/tips.rst
+++ b/docs/tips.rst
@@ -195,10 +195,13 @@ Values that being with a ``$`` may be interpolated. Pass ``interpolate=True`` to
    FOO
 
 
+Reading env files
+=================
+
 .. _multiple-env-files-label:
 
 Multiple env files
-==================
+------------------
 
 There is an ability point to the .env file location using an environment
 variable. This feature may be convenient in a production systems with a
@@ -227,7 +230,7 @@ while ``./manage.py runserver`` uses ``.env``.
 
 
 Using Path objects when reading env
-===================================
+-----------------------------------
 
 It is possible to use of ``pathlib.Path`` objects when reading environment file from the filesystem:
 
@@ -249,3 +252,16 @@ It is possible to use of ``pathlib.Path`` objects when reading environment file
    env.read_env(os.path.join(BASE_DIR, '.env'))
    env.read_env(pathlib.Path(str(BASE_DIR)).joinpath('.env'))
    env.read_env(pathlib.Path(str(BASE_DIR)) / '.env')
+
+
+Overwriting existing environment values from env files
+------------------------------------------------------
+
+If you want variables set within your env files to take higher precidence than
+an existing set environment variable, use the ``overwrite=True`` argument of
+``read_env``. For example:
+
+.. code-block:: python
+
+   env = environ.Env()
+   env.read_env(BASE_DIR('.env'), overwrite=True)
diff --git a/environ/environ.py b/environ/environ.py
index 83642ff..a38324f 100644
--- a/environ/environ.py
+++ b/environ/environ.py
@@ -31,10 +31,10 @@ from .fileaware_mapping import FileAwareMapping
 
 try:
     from os import PathLike
+except ImportError:  # Python 3.5 support
+    from pathlib import PurePath as PathLike
 
-    Openable = (str, PathLike)
-except ImportError:
-    Openable = (str,)
+Openable = (str, PathLike)
 
 logger = logging.getLogger(__name__)
 
@@ -732,13 +732,16 @@ class Env:
         return config
 
     @classmethod
-    def read_env(cls, env_file=None, **overrides):
+    def read_env(cls, env_file=None, overwrite=False, **overrides):
         """Read a .env file into os.environ.
 
         If not given a path to a dotenv path, does filthy magic stack
         backtracking to find the dotenv in the same directory as the file that
         called read_env.
 
+        By default, won't overwrite any existing environment variables. You can
+        enable this behaviour by setting ``overwrite=True``.
+
         Refs:
         - https://wellfire.co/learn/easier-12-factor-django
         - https://gist.github.com/bennylope/2999704
@@ -757,7 +760,8 @@ class Env:
 
         try:
             if isinstance(env_file, Openable):
-                with open(env_file) as f:
+                # Python 3.5 support (wrap path with str).
+                with open(str(env_file)) as f:
                     content = f.read()
             else:
                 with env_file as f:
@@ -788,13 +792,18 @@ class Env:
                 if m3:
                     val = re.sub(r'\\(.)', _keep_escaped_format_characters,
                                  m3.group(1))
-                cls.ENVIRON.setdefault(key, str(val))
+                overrides[key] = str(val)
             else:
                 logger.warn('Invalid line: %s', line)
 
-        # set defaults
+        if overwrite:
+            def set_environ(key, value):
+                cls.ENVIRON[key] = value
+        else:
+            set_environ = cls.ENVIRON.setdefault
+
         for key, value in overrides.items():
-            cls.ENVIRON.setdefault(key, value)
+            set_environ(key, value)
 
 
 class FileAwareEnv(Env):


Test patch:
diff --git a/tests/test_env.py b/tests/test_env.py
index afa6470..acd8dc4 100644
--- a/tests/test_env.py
+++ b/tests/test_env.py
@@ -152,6 +152,7 @@ class TestEnv:
         [
             ('a=1', dict, {'a': '1'}),
             ('a=1', dict(value=int), {'a': 1}),
+            ('a=1', dict(value=float), {'a': 1.0}),
             ('a=1,2,3', dict(value=[str]), {'a': ['1', '2', '3']}),
             ('a=1,2,3', dict(value=[int]), {'a': [1, 2, 3]}),
             ('a=1;b=1.1,2.2;c=3', dict(value=int, cast=dict(b=[float])),
@@ -163,6 +164,7 @@ class TestEnv:
         ids=[
             'dict',
             'dict_int',
+            'dict_float',
             'dict_str_list',
             'dict_int_list',
             'dict_int_cast',
@@ -307,34 +309,43 @@ class TestFileEnv(TestEnv):
             PATH_VAR=Path(__file__, is_file=True).__root__
         )
 
-    def test_read_env_path_like(self):
+    def create_temp_env_file(self, name):
         import pathlib
         import tempfile
 
-        path_like = (pathlib.Path(tempfile.gettempdir()) / 'test_pathlib.env')
+        env_file_path = (pathlib.Path(tempfile.gettempdir()) / name)
         try:
-            path_like.unlink()
+            env_file_path.unlink()
         except FileNotFoundError:
             pass
 
-        assert not path_like.exists()
+        assert not env_file_path.exists()
+        return env_file_path
+
+    def test_read_env_path_like(self):
+        env_file_path = self.create_temp_env_file('test_pathlib.env')
 
         env_key = 'SECRET'
         env_val = 'enigma'
         env_str = env_key + '=' + env_val
 
         # open() doesn't take path-like on Python < 3.6
-        try:
-            with open(path_like, 'w', encoding='utf-8') as f:
-                f.write(env_str + '\n')
-        except TypeError:
-            return
+        with open(str(env_file_path), 'w', encoding='utf-8') as f:
+            f.write(env_str + '\n')
 
-        assert path_like.exists()
-        self.env.read_env(path_like)
+        self.env.read_env(env_file_path)
         assert env_key in self.env.ENVIRON
         assert self.env.ENVIRON[env_key] == env_val
 
+    @pytest.mark.parametrize("overwrite", [True, False])
+    def test_existing_overwrite(self, overwrite):
+        env_file_path = self.create_temp_env_file('test_existing.env')
+        with open(str(env_file_path), 'w') as f:
+            f.write("EXISTING=b")
+        self.env.ENVIRON['EXISTING'] = "a"
+        self.env.read_env(env_file_path, overwrite=overwrite)
+        assert self.env.ENVIRON["EXISTING"] == ("b" if overwrite else "a")
+
 
 class TestSubClass(TestEnv):
     def setup_method(self, method):