diff --git a/openedx/core/lib/features_setting_proxy.py b/openedx/core/lib/features_setting_proxy.py index eb38ad7f9100..95c61203306e 100644 --- a/openedx/core/lib/features_setting_proxy.py +++ b/openedx/core/lib/features_setting_proxy.py @@ -24,8 +24,37 @@ def __init__(self, namespace=None): """Store the namespace (as a dict)""" self.ns = namespace or {} + _MISSING = object() + + def _resolve(self, key): + """Return key's value if it's been overridden via @override_settings, else self._MISSING. + + We deliberately walk only the UserSettingsHolder chain (the layers that + @override_settings pushes onto django.conf.settings._wrapped) rather + than reading django.conf.settings.X directly. Django's bottom Settings + layer is a *snapshot* taken at init time, so it doesn't reflect runtime + mutations of the settings module's globals (which is what + proxy.ns mutations and legacy patch.dict(settings.FEATURES, ...) do). + Walking only the explicit override layers lets the new + @override_settings(X=Y) path work while leaving the legacy patch.dict + path untouched. + """ + from django.conf import settings as django_settings + wrapped = django_settings._wrapped # pylint: disable=protected-access + # UserSettingsHolder has a `default_settings` attribute and stores + # explicit overrides in its own __dict__; the bottom Settings has no + # default_settings, so the loop terminates there. + while hasattr(wrapped, 'default_settings'): + if key in wrapped.__dict__: + return wrapped.__dict__[key] + wrapped = wrapped.default_settings + return self._MISSING + def __getitem__(self, key): - """Retrieve a feature flag by key""" + """Retrieve a feature flag by key, preferring @override_settings overrides.""" + value = self._resolve(key) + if value is not self._MISSING: + return value return self.ns[key] def __setitem__(self, key, value): @@ -49,14 +78,17 @@ def __len__(self): return len(self.ns) def __contains__(self, key): - return key in self.ns + return self._resolve(key) is not self._MISSING or key in self.ns def clear(self): """Remove all feature flags from the namespace.""" self.ns.clear() def get(self, key, default=None): - """Standard dict-style get with default""" + """Standard dict-style get with default; prefers @override_settings overrides.""" + value = self._resolve(key) + if value is not self._MISSING: + return value return self.ns.get(key, default) def update(self, other=(), /, **kwds):