Skip to content

Commit

Permalink
Enable overriding environ values
Browse files Browse the repository at this point in the history
  • Loading branch information
Daniel Hubacek committed Apr 22, 2020
1 parent cac164b commit 8525d29
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 4 deletions.
32 changes: 28 additions & 4 deletions src/appsettings/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Django AppSettings package."""
import os

from django.core.exceptions import ImproperlyConfigured
from django.core.signals import setting_changed
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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 = {}
68 changes: 68 additions & 0 deletions tests/test_appsettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import tempfile
from pathlib import Path
from typing import Dict, cast
from unittest import mock

import pytest
Expand Down Expand Up @@ -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

Expand Down

0 comments on commit 8525d29

Please sign in to comment.