django-appsettings icon indicating copy to clipboard operation
django-appsettings copied to clipboard

Multiple override_settings with environ variable

Open rastytheamateur opened this issue 4 years ago • 6 comments

The following code is not working properly. Exiting the second override loads the original environ variable value.

ATTR=terminal DJANGO_SETTINGS_MODULE=settings python test.py

class Settings(AppSettings):
    attr = StringSetting(default='Hello')

S = Settings()

if __name__ == '__main__':
    Settings.check()

    print(S.attr)  # output: terminal [correct]
    with override_settings(ATTR='Override1'):
        print(S.attr)  # output: Override1 [correct]
        with override_settings(ATTR='Override2'):
            print(S.attr)  # output: Override2 [correct]
        print(S.attr)  # output: terminal [incorrect]       <----
    print(S.attr)  # output: terminal [correct]

rastytheamateur avatar Sep 14 '20 14:09 rastytheamateur

Ouch, I don't think we had nested with statements in mind when implementing settings overrides and cache invalidation. Maybe there's a way to "unstack" the values when invalidating cache.

pawamoy avatar Sep 14 '20 15:09 pawamoy

I think this is not a serious bug (I believe nobody really uses environ variables with nested overrides). I will fix it later.

rastytheamateur avatar Sep 14 '20 16:09 rastytheamateur

It might not be possible, unless the override_settings manager does something special on exit that we could hook on to restore the previously stacked cache.

pawamoy avatar Sep 14 '20 16:09 pawamoy

Ah it's totally possible in fact, Django sends the signal twice with a boolean value in the enter argument :slightly_smiling_face:

    def invalidate_cache(self, enter, **kwargs):
        """Invalidate cache. Run when receive ``setting_changed`` signal."""
        if enter:
            self._caches.append(copy.deepcopy(self._cache))  # maybe the copy is not necessary?
            self._cache = {}
        elif self._caches:
            self._cache = self._caches.pop()
        else:  # shouldn't happen, we always enter before exiting
            self._cache = {}

...or something similar :slightly_smiling_face:

https://docs.djangoproject.com/en/3.1/ref/signals/#setting-changed

pawamoy avatar Sep 14 '20 16:09 pawamoy

The problem is not is cache, but in environment variables.

# ATTR=terminal
with override_settings(ATTR='Override1'):
    # Override shifts the environment: __DAP_ATTR=terminal, ATTR not defined
    print(S.attr)  # output: Override1 [correct]
    with override_settings(ATTR='Override2'):
        # There is no ATTR environment variable, so nothing to shift
        print(S.attr)  # output: Override2 [correct]
    # On exit, the override_settings finds __DAP_ATTR in environment and unshifts it: ATTR=terminal, __DAP_ATTR not defined
    # Since the cache is empty and the environment has priority, it's used and returned.
    print(S.attr)  # output: terminal [incorrect]       <----
print(S.attr)  # output: terminal [correct]

ziima avatar Sep 15 '20 07:09 ziima

I do see one realistic example of nested overrides. If you decorate a test class and then want to change the value in one of its methods, that creates a nested override right there. It's probably not a top priority, but it would be nice to fix it eventually.

stinovlas avatar Sep 15 '20 08:09 stinovlas