From 8525d29c578b74bd6b514e6dbceeba1b0b35c6ae Mon Sep 17 00:00:00 2001 From: Daniel Hubacek Date: Wed, 22 Apr 2020 11:50:07 +0200 Subject: [PATCH] Enable overriding environ values --- src/appsettings/__init__.py | 32 ++++++++++++++--- tests/test_appsettings.py | 68 +++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/src/appsettings/__init__.py b/src/appsettings/__init__.py index 5de5609..eecbbd9 100644 --- a/src/appsettings/__init__.py +++ b/src/appsettings/__init__.py @@ -1,4 +1,5 @@ """Django AppSettings package.""" +import os from django.core.exceptions import ImproperlyConfigured from django.core.signals import setting_changed @@ -139,17 +140,20 @@ class AppSettings(metaclass=_Metaclass): """ + OS_ENVIRON_OVERRIDE_PREFIX = "__DAP_" # type: str + def __init__(self): """ Initialization method. - The ``invalidate_cache`` method will be connected to the Django - ``setting_changed`` signal in this method, with the dispatch UID - being the id of this very object (``id(self)``). + The ``invalidate_cache`` and ``manage_environ_invalidation`` methods will be connected to the Django + ``setting_changed`` signal in this method, with the dispatch UIDs being method initials and the id of this very + object (``id(self)``). """ if self.__class__ == AppSettings: raise RuntimeError("Do not use AppSettings class as itself, " "use it as a base for subclasses") - setting_changed.connect(self.invalidate_cache, dispatch_uid=id(self)) + setting_changed.connect(self.invalidate_cache, dispatch_uid="ic" + str(id(self))) + setting_changed.connect(self.manage_environ_invalidation, dispatch_uid="mei" + str(id(self))) self._cache = {} def __getattr__(self, item): @@ -197,6 +201,26 @@ def check(cls): if exceptions: raise ImproperlyConfigured("\n".join(exceptions)) + def manage_environ_invalidation(self, *, setting, enter, **kwargs): + """ + Manage keys and values in ``os.environ`` on setting change. + + Environ values take precedence over ``django.conf`` values by default. But when we override a setting, we want + to take the ``django.conf`` value and therefore we need to remove corresponding key from the ``os.environ``. + To be able to restore the original value later, we add a prefix (OS_ENVIRON_OVERRIDE_PREFIX) to the key and then + just remove the prefix. + """ + for item in self.settings.keys(): + if self.settings[item].full_name == setting: + setting_override_key = self.OS_ENVIRON_OVERRIDE_PREFIX + setting + if enter and setting in os.environ: + os.environ[setting_override_key] = os.environ[setting] + del os.environ[setting] + elif not enter and setting_override_key in os.environ: + os.environ[setting] = os.environ[setting_override_key] + del os.environ[setting_override_key] + break + def invalidate_cache(self, **kwargs): """Invalidate cache. Run when receive ``setting_changed`` signal.""" self._cache = {} diff --git a/tests/test_appsettings.py b/tests/test_appsettings.py index 7ecf817..474147d 100644 --- a/tests/test_appsettings.py +++ b/tests/test_appsettings.py @@ -2,6 +2,7 @@ import os import tempfile from pathlib import Path +from typing import Dict, cast from unittest import mock import pytest @@ -773,6 +774,73 @@ class AppConf(appsettings.AppSettings): assert "my_int" not in appconf._cache assert appconf.my_int == 0 + @mock.patch.dict(os.environ, {"ONE": "Env_1", "TWO": "Env_2", "THREE": "Env_3"}) + def test_environ_values_invalidation(self): + class AppConf(appsettings.AppSettings): + one = appsettings.StringSetting() + two = appsettings.StringSetting() + three = appsettings.StringSetting() + four = appsettings.StringSetting(default="Def_4") + + appconf = AppConf() + with override_settings(ONE="One", TWO="Two"): + assert "ONE" not in os.environ + assert "__DAP_ONE" in os.environ + assert appconf.one == "One" + + assert "TWO" not in os.environ + assert "__DAP_TWO" in os.environ + assert appconf.two == "Two" + + assert "THREE" in os.environ + assert "__DAP_THREE" not in os.environ + assert appconf.three == "Env_3" + + assert "FOUR" not in os.environ + assert "__DAP_FOUR" not in os.environ + assert appconf.four == "Def_4" + + assert "ONE" in os.environ + assert "__DAP_ONE" not in os.environ + assert appconf.one == "Env_1" + + assert "TWO" in os.environ + assert "__DAP_TWO" not in os.environ + assert appconf.two == "Env_2" + + assert "THREE" in os.environ + assert "__DAP_THREE" not in os.environ + assert appconf.three == "Env_3" + + assert "FOUR" not in os.environ + assert "__DAP_FOUR" not in os.environ + assert appconf.four == "Def_4" + + @mock.patch.dict(os.environ, {"SETTING": "ONE=Env_1 TWO=Env_2"}) + def test_environ_nested_setting_invalidation(self): + class AppConf(appsettings.AppSettings): + setting = cast( + Dict[str, str], + appsettings.NestedDictSetting( + settings=dict( + one=appsettings.StringSetting(required=True), two=appsettings.StringSetting(default="Def_2"), + ), + required=True, + ), + ) + + appconf = AppConf() + assert appconf.setting["one"] == "Env_1" + assert appconf.setting["two"] == "Env_2" + with override_settings(SETTING={"ONE": "One"}): + assert "SETTING" not in os.environ + assert "__DAP_SETTING" in os.environ + assert appconf.setting["one"] == "One" + assert appconf.setting["two"] == "Def_2" + + assert appconf.setting["one"] == "Env_1" + assert appconf.setting["two"] == "Env_2" + def test_check(self): assert appsettings.AppSettings.check() is None