Merge pull request #37067 from WGU-Open-edX/collapse-features-dictionary

refactor: flatten FEATURES dictionary with backward compatible proxy
This commit is contained in:
Feanil Patel
2025-09-18 10:39:48 -04:00
committed by GitHub
18 changed files with 1657 additions and 1535 deletions

View File

@@ -24,7 +24,7 @@ def plugin_settings(settings): # lint-amnesty, pylint: disable=missing-function
settings.ACE_ROUTING_KEY = ACE_ROUTING_KEY
settings.FEATURES['test_django_plugin'] = True
settings.test_django_plugin = True
settings.FCM_APP_NAME = 'fcm-edx-platform'
settings.ACE_CHANNEL_DEFAULT_PUSH = 'push_notification'

View File

@@ -71,9 +71,8 @@ class CourseDetails:
self.pre_requisite_courses = [] # pre-requisite courses
self.entrance_exam_enabled = "" # is entrance exam enabled
self.entrance_exam_id = "" # the content location for the entrance exam
self.entrance_exam_minimum_score_pct = settings.FEATURES.get(
'ENTRANCE_EXAM_MIN_SCORE_PCT',
'50'
self.entrance_exam_minimum_score_pct = str(
settings.ENTRANCE_EXAM_MIN_SCORE_PCT
) # minimum passing score for entrance exam content module/tree,
self.self_paced = None
self.learning_info = []

View File

@@ -0,0 +1,106 @@
"""
Features Proxy Implementation
"""
import warnings
from collections.abc import MutableMapping, Mapping
class FeaturesProxy(MutableMapping):
"""
A proxy for feature flags stored in the settings namespace.
Features:
- Flattens features from configuration (e.g., YAML or env) into the local settings namespace.
- Automatically updates `django.conf.settings` when a feature is modified.
- Acts like a dict (get, set, update, etc.).
Example usage:
fp = FeaturesProxy(LocalNamespace)
fp["NEW_FEATURE"] = True
val = fp.get("EXISTING_FLAG", False)
"""
def __init__(self, namespace=None):
"""Store the namespace (as a dict)"""
self.ns = namespace or {}
def __getitem__(self, key):
"""Retrieve a feature flag by key"""
return self.ns[key]
def __setitem__(self, key, value):
"""Sets a key-value pair while emitting a deprecation warning about using FEATURES as a dict."""
warnings.warn(
f"Accessing FEATURES as a dict is deprecated. "
f"Add '{key} = {value!r}' to your Django settings module instead of modifying FEATURES.",
DeprecationWarning,
stacklevel=2
)
self.ns[key] = value
def __delitem__(self, key):
"""Remove a feature flag from the namespace."""
del self.ns[key]
def __iter__(self):
return iter(self.ns)
def __len__(self):
return len(self.ns)
def __contains__(self, key):
return 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"""
return self.ns.get(key, default)
def update(self, other=(), /, **kwds):
"""
Update multiple features at once, ensuring each goes through __setitem__
to emit deprecation warnings.
Mirrors dict.update() behavior:
- If `other` is a mapping, uses its keys.
- If `other` is iterable of pairs, updates from those.
- Then applies any keyword arguments.
Examples:
proxy.update({'FEATURE_A': True})
-> other = {'FEATURE_A': True}
proxy.update([('FEATURE_A', True)])
-> other = [('FEATURE_A', True)]
proxy.update(FEATURE_B=False)
-> kwds = {'FEATURE_B': False}
proxy.update({'FEATURE_A': True}, FEATURE_B=False)
-> other={'FEATURE_A': True}; kwds = {'FEATURE_B': False}
"""
if isinstance(other, Mapping):
# Handles objects that formally conform to the Mapping interface
# Mapping-like types: defaultdict, OrderedDict, Counter
for key in other:
self[key] = other[key]
elif hasattr(other, "keys"):
# Fallback for objects that implement a .keys() method but
# may not formally subclass Mapping
for key in other.keys():
self[key] = other[key]
else:
for key, value in other:
self[key] = value
for key, value in kwds.items():
self[key] = value
def copy(self):
"""
Return a shallow copy of the underlying namespace wrapped in a new FeaturesProxy.
"""
return FeaturesProxy(self.ns.copy())

View File

@@ -886,3 +886,5 @@ API_DOCUMENTATION_URL = 'https://course-catalog-api-guide.readthedocs.io/en/late
AUTH_DOCUMENTATION_URL = 'https://course-catalog-api-guide.readthedocs.io/en/latest/authentication/index.html'
CSRF_TRUSTED_ORIGINS = []
ENTRANCE_EXAM_MIN_SCORE_PCT = 50

View File

@@ -16,6 +16,6 @@ def plugin_settings(settings):
.. toggle_creation_date: 2017-11-08
.. toggle_tickets: https://github.com/openedx/edx-platform/pull/16496
"""
settings.FEATURES['ENABLE_ANNOUNCEMENTS'] = False
settings.ENABLE_ANNOUNCEMENTS = False
# Configure number of announcements to show per page
settings.FEATURES['ANNOUNCEMENTS_PER_PAGE'] = 5
settings.ANNOUNCEMENTS_PER_PAGE = 5

View File

@@ -5,4 +5,4 @@ def plugin_settings(settings):
"""
Test settings for Announcements
"""
settings.FEATURES['ENABLE_ANNOUNCEMENTS'] = True
settings.ENABLE_ANNOUNCEMENTS = True