diff --git a/configurations/base.py b/configurations/base.py index 6065ed0..4564bbb 100644 --- a/configurations/base.py +++ b/configurations/base.py @@ -99,6 +99,7 @@ class Configuration(metaclass=ConfigurationBase): """ DOTENV_LOADED = None + DOTENV_RELOAD = False @classmethod def load_dotenv(cls): @@ -113,6 +114,18 @@ class Configuration(metaclass=ConfigurationBase): # 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 @@ -122,6 +135,8 @@ class Configuration(metaclass=ConfigurationBase): with open(dotenv) 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 @@ -137,13 +152,16 @@ class Configuration(metaclass=ConfigurationBase): 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 diff --git a/docs/cookbook.rst b/docs/cookbook.rst index 0a3d76f..dd20f2f 100644 --- a/docs/cookbook.rst +++ b/docs/cookbook.rst @@ -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 ------ diff --git a/test_project/.env b/test_project/.env index 507755e..b4bc567 100644 --- a/test_project/.env +++ b/test_project/.env @@ -1 +1,2 @@ -DJANGO_DOTENV_VALUE='is set' \ No newline at end of file +DJANGO_DOTENV_VALUE='is set' +DJANGO_DOTENV_OVERRIDE='overridden' diff --git a/tests/settings/dot_env_dict.py b/tests/settings/dot_env_dict.py new file mode 100644 index 0000000..fcc28fe --- /dev/null +++ b/tests/settings/dot_env_dict.py @@ -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") diff --git a/tests/settings/dot_env_not_required.py b/tests/settings/dot_env_not_required.py new file mode 100644 index 0000000..7263170 --- /dev/null +++ b/tests/settings/dot_env_not_required.py @@ -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") diff --git a/tests/test_env.py b/tests/test_env.py index 50b7f66..9530d45 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -13,3 +13,18 @@ class DotEnvLoadingTests(TestCase): self.assertEqual(dot_env.DOTENV_VALUE, 'is set') self.assertEqual(dot_env.DOTENV_VALUE_METHOD, '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')