Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make DOTENV setting more robust #364

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions configurations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def OTHER(self):

"""
DOTENV_LOADED = None
DOTENV_RELOAD = False

@classmethod
def load_dotenv(cls):
Expand All @@ -109,6 +110,18 @@ def load_dotenv(cls):
# check if the class has DOTENV set whether with a path or None
dotenv = getattr(cls, 'DOTENV', None)

required = True
override_env = False
# check if the DOTENV is dict, and check all options of it
if isinstance(dotenv, dict):
# whether we want to override previously set envs
override_env = dotenv.get("override", False)
# whether we want to error if the file is not found
required = dotenv.get("required", True)
# whether we want to reload on dotenv, useful if we want to frequently change it
cls.DOTENV_RELOAD = dotenv.get("reload", False)
dotenv = dotenv.get("path", None)

# if DOTENV is falsy we want to disable it
if not dotenv:
return
Expand All @@ -118,6 +131,8 @@ def load_dotenv(cls):
with open(dotenv, 'r') as f:
content = f.read()
except OSError as e:
if not required:
return
raise ImproperlyConfigured("Couldn't read .env file "
"with the path {}. Error: "
"{}".format(dotenv, e)) from e
Expand All @@ -133,13 +148,16 @@ def load_dotenv(cls):
m3 = re.match(r'\A"(.*)"\Z', val)
if m3:
val = re.sub(r'\\(.)', r'\1', m3.group(1))
os.environ.setdefault(key, val)
if override_env:
os.environ[key] = val
else:
os.environ.setdefault(key, val)

cls.DOTENV_LOADED = dotenv

@classmethod
def pre_setup(cls):
if cls.DOTENV_LOADED is None:
if cls.DOTENV_LOADED is None or cls.DOTENV_RELOAD:
cls.load_dotenv()

@classmethod
Expand Down
14 changes: 14 additions & 0 deletions docs/cookbook.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ A ``.env`` file is a ``.ini``-style file. It must contain a list of
API_KEY1=1234
API_KEY2=5678

``DOTENV`` can also be a dictionary, and then its behavior can be configured more:
.. code-block:: python
BASE_DIR = os.path.dirname(os.path.dirname(__file__))

class Dev(Configuration):
DOTENV = {
"path": str(os.path.join(BASE_DIR, '.env')),
# if True, overriddes previously set environmental variables, if False only sets them if they haven't been set before
"override": True,
# if True errors if the DOTENV is not found at path, if False return
"required": False,
# if True, reloads DOTENV dynamically for example on hot reload
"reload": True,

Envdir
------

Expand Down
3 changes: 2 additions & 1 deletion test_project/.env
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
DJANGO_DOTENV_VALUE='is set'
DJANGO_DOTENV_VALUE='is set'
DJANGO_DOTENV_OVERRIDE='overridden'
12 changes: 12 additions & 0 deletions tests/settings/dot_env_dict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from configurations import Configuration, values


class DotEnvConfiguration(Configuration):

DOTENV = {
'path': 'test_project/.env',
'override': True,
}

DOTENV_VALUE = values.Value()
DOTENV_OVERRIDE = values.Value("Not overridden")
12 changes: 12 additions & 0 deletions tests/settings/dot_env_not_required.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from configurations import Configuration, values


class DotEnvConfiguration(Configuration):

DOTENV = {
'path': 'some_nonexistant_path',
'override': True,
'required': False,
}

DOTENV_OVERRIDE = values.Value("Not overridden")
15 changes: 15 additions & 0 deletions tests/test_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,18 @@ def test_env_loaded(self):
from tests.settings import dot_env
self.assertEqual(dot_env.DOTENV_VALUE, 'is set')
self.assertEqual(dot_env.DOTENV_LOADED, dot_env.DOTENV)

@patch.dict(os.environ, clear=True,
DJANGO_CONFIGURATION='DotEnvConfiguration',
DJANGO_SETTINGS_MODULE='tests.settings.dot_env_dict')
def test_env_dict(self):
from tests.settings import dot_env_dict
self.assertEqual(dot_env_dict.DOTENV_VALUE, 'is set')
self.assertEqual(dot_env_dict.DOTENV_OVERRIDE, 'overridden')

@patch.dict(os.environ, clear=True,
DJANGO_CONFIGURATION='DotEnvConfiguration',
DJANGO_SETTINGS_MODULE='tests.settings.dot_env_not_required')
def test_env_not_required(self):
from tests.settings import dot_env_not_required
self.assertEqual(dot_env_not_required.DOTENV_OVERRIDE, 'Not overridden')
Loading