From bc11925cdd65859bd9e806a8cd5b7bcabf0c7659 Mon Sep 17 00:00:00 2001 From: P1roks Date: Wed, 26 Jul 2023 16:05:31 +0200 Subject: [PATCH] make DOTENV setting more robust Add additional config options to DOTENV while retaining compatibility with the old way of setting it --- configurations/base.py | 22 ++++++++++++++++++++-- docs/cookbook.rst | 14 ++++++++++++++ test_project/.env | 3 ++- tests/settings/dot_env_dict.py | 12 ++++++++++++ tests/settings/dot_env_not_required.py | 12 ++++++++++++ tests/test_env.py | 15 +++++++++++++++ 6 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 tests/settings/dot_env_dict.py create mode 100644 tests/settings/dot_env_not_required.py diff --git a/configurations/base.py b/configurations/base.py index 121e80d..61a4b90 100644 --- a/configurations/base.py +++ b/configurations/base.py @@ -95,6 +95,7 @@ def OTHER(self): """ DOTENV_LOADED = None + DOTENV_RELOAD = False @classmethod def load_dotenv(cls): @@ -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 @@ -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 @@ -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 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 8066eea..f68e71f 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -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')