Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: A Better API for Derived Settings
Browse files Browse the repository at this point in the history
kdmccormick committed Jan 30, 2025
1 parent 4f13ee0 commit 712fabf
Showing 8 changed files with 137 additions and 149 deletions.
55 changes: 14 additions & 41 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
@@ -144,7 +144,7 @@
get_theme_base_dirs_from_settings
)
from openedx.core.lib.license import LicenseMixin
from openedx.core.lib.derived import derived, derived_collection_entry
from openedx.core.lib.derived import Derived
from openedx.core.release import doc_version

# pylint: enable=useless-suppression
@@ -740,7 +740,7 @@
# Don't look for template source files inside installed applications.
'APP_DIRS': False,
# Instead, look for template source files in these dirs.
'DIRS': _make_mako_template_dirs,
'DIRS': Derived(_make_mako_template_dirs),
# Options specific to this backend.
'OPTIONS': {
'loaders': (
@@ -759,7 +759,7 @@
'NAME': 'mako',
'BACKEND': 'common.djangoapps.edxmako.backend.Mako',
'APP_DIRS': False,
'DIRS': _make_mako_template_dirs,
'DIRS': Derived(_make_mako_template_dirs),
'OPTIONS': {
'context_processors': CONTEXT_PROCESSORS,
'debug': False,
@@ -778,8 +778,6 @@
}
},
]
derived_collection_entry('TEMPLATES', 0, 'DIRS')
derived_collection_entry('TEMPLATES', 1, 'DIRS')
DEFAULT_TEMPLATE_ENGINE = TEMPLATES[0]

#################################### AWS #######################################
@@ -825,8 +823,7 @@
# Warning: Must have trailing slash to activate correct logout view
# (auth_backends, not LMS user_authn)
FRONTEND_LOGOUT_URL = '/logout/'
FRONTEND_REGISTER_URL = lambda settings: settings.LMS_ROOT_URL + '/register'
derived('FRONTEND_REGISTER_URL')
FRONTEND_REGISTER_URL = Derived(lambda settings: settings.LMS_ROOT_URL + '/register')

LMS_ENROLLMENT_API_PATH = "/api/enrollment/v1/"
ENTERPRISE_API_URL = LMS_INTERNAL_ROOT_URL + '/enterprise/api/v1/'
@@ -1316,8 +1313,7 @@
STATICI18N_FILENAME_FUNCTION = 'statici18n.utils.legacy_filename'
STATICI18N_ROOT = PROJECT_ROOT / "static"

LOCALE_PATHS = _make_locale_paths
derived('LOCALE_PATHS')
LOCALE_PATHS = Derived(_make_locale_paths)

# Messages
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
@@ -2087,10 +2083,9 @@
# See annotations in lms/envs/common.py for details.
RETIRED_EMAIL_DOMAIN = 'retired.invalid'
# See annotations in lms/envs/common.py for details.
RETIRED_USERNAME_FMT = lambda settings: settings.RETIRED_USERNAME_PREFIX + '{}'
RETIRED_USERNAME_FMT = Derived(lambda settings: settings.RETIRED_USERNAME_PREFIX + '{}')
# See annotations in lms/envs/common.py for details.
RETIRED_EMAIL_FMT = lambda settings: settings.RETIRED_EMAIL_PREFIX + '{}@' + settings.RETIRED_EMAIL_DOMAIN
derived('RETIRED_USERNAME_FMT', 'RETIRED_EMAIL_FMT')
RETIRED_EMAIL_FMT = Derived(lambda settings: settings.RETIRED_EMAIL_PREFIX + '{}@' + settings.RETIRED_EMAIL_DOMAIN)
# See annotations in lms/envs/common.py for details.
RETIRED_USER_SALTS = ['abc', '123']
# See annotations in lms/envs/common.py for details.
@@ -2367,13 +2362,12 @@
############## Settings for Studio Context Sensitive Help ##############

HELP_TOKENS_INI_FILE = REPO_ROOT / "cms" / "envs" / "help_tokens.ini"
HELP_TOKENS_LANGUAGE_CODE = lambda settings: settings.LANGUAGE_CODE
HELP_TOKENS_VERSION = lambda settings: doc_version()
HELP_TOKENS_LANGUAGE_CODE = Derived(lambda settings: settings.LANGUAGE_CODE)
HELP_TOKENS_VERSION = Derived(lambda settings: doc_version())
HELP_TOKENS_BOOKS = {
'learner': 'https://edx.readthedocs.io/projects/open-edx-learner-guide',
'course_author': 'https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course',
}
derived('HELP_TOKENS_LANGUAGE_CODE', 'HELP_TOKENS_VERSION')

# Used with Email sending
RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS = 5
@@ -2876,15 +2870,15 @@ def _should_send_learning_badge_events(settings):
},
'org.openedx.content_authoring.xblock.published.v1': {
'course-authoring-xblock-lifecycle':
{'event_key_field': 'xblock_info.usage_key', 'enabled': _should_send_xblock_events},
{'event_key_field': 'xblock_info.usage_key', 'enabled': Derived(_should_send_xblock_events)},
},
'org.openedx.content_authoring.xblock.deleted.v1': {
'course-authoring-xblock-lifecycle':
{'event_key_field': 'xblock_info.usage_key', 'enabled': _should_send_xblock_events},
{'event_key_field': 'xblock_info.usage_key', 'enabled': Derived(_should_send_xblock_events)},
},
'org.openedx.content_authoring.xblock.duplicated.v1': {
'course-authoring-xblock-lifecycle':
{'event_key_field': 'xblock_info.usage_key', 'enabled': _should_send_xblock_events},
{'event_key_field': 'xblock_info.usage_key', 'enabled': Derived(_should_send_xblock_events)},
},
# LMS events. These have to be copied over here because lms.common adds some derived entries as well,
# and the derivation fails if the keys are missing. If we ever remove the import of lms.common, we can remove these.
@@ -2899,38 +2893,17 @@ def _should_send_learning_badge_events(settings):
"org.openedx.learning.course.passing.status.updated.v1": {
"learning-badges-lifecycle": {
"event_key_field": "course_passing_status.course.course_key",
"enabled": _should_send_learning_badge_events,
"enabled": Derived(_should_send_learning_badge_events),
},
},
"org.openedx.learning.ccx.course.passing.status.updated.v1": {
"learning-badges-lifecycle": {
"event_key_field": "course_passing_status.course.ccx_course_key",
"enabled": _should_send_learning_badge_events,
"enabled": Derived(_should_send_learning_badge_events),
},
},
}


derived_collection_entry('EVENT_BUS_PRODUCER_CONFIG', 'org.openedx.content_authoring.xblock.published.v1',
'course-authoring-xblock-lifecycle', 'enabled')
derived_collection_entry('EVENT_BUS_PRODUCER_CONFIG', 'org.openedx.content_authoring.xblock.duplicated.v1',
'course-authoring-xblock-lifecycle', 'enabled')
derived_collection_entry('EVENT_BUS_PRODUCER_CONFIG', 'org.openedx.content_authoring.xblock.deleted.v1',
'course-authoring-xblock-lifecycle', 'enabled')

derived_collection_entry(
"EVENT_BUS_PRODUCER_CONFIG",
"org.openedx.learning.course.passing.status.updated.v1",
"learning-badges-lifecycle",
"enabled",
)
derived_collection_entry(
"EVENT_BUS_PRODUCER_CONFIG",
"org.openedx.learning.ccx.course.passing.status.updated.v1",
"learning-badges-lifecycle",
"enabled",
)

################### Authoring API ######################

# This affects the Authoring API swagger docs but not the legacy swagger docs under /api-docs/.
47 changes: 13 additions & 34 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
@@ -69,7 +69,7 @@
get_themes_unchecked,
get_theme_base_dirs_from_settings
)
from openedx.core.lib.derived import derived, derived_collection_entry
from openedx.core.lib.derived import Derived
from openedx.core.release import doc_version
from lms.djangoapps.lms_xblock.mixin import LmsBlockMixin

@@ -1395,7 +1395,7 @@ def _make_mako_template_dirs(settings):
# Don't look for template source files inside installed applications.
'APP_DIRS': False,
# Instead, look for template source files in these dirs.
'DIRS': _make_mako_template_dirs,
'DIRS': Derived(_make_mako_template_dirs),
# Options specific to this backend.
'OPTIONS': {
'context_processors': CONTEXT_PROCESSORS,
@@ -1404,7 +1404,6 @@ def _make_mako_template_dirs(settings):
}
},
]
derived_collection_entry('TEMPLATES', 1, 'DIRS')
DEFAULT_TEMPLATE_ENGINE = TEMPLATES[0]
DEFAULT_TEMPLATE_ENGINE_DIRS = DEFAULT_TEMPLATE_ENGINE['DIRS'][:]

@@ -1734,7 +1733,7 @@ def _make_mako_template_dirs(settings):
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'OPTIONS': {
'default_class': 'xmodule.hidden_block.HiddenBlock',
'fs_root': lambda settings: settings.DATA_DIR,
'fs_root': Derived(lambda settings: settings.DATA_DIR),
'render_template': 'common.djangoapps.edxmako.shortcuts.render_to_string',
}
},
@@ -1744,7 +1743,7 @@ def _make_mako_template_dirs(settings):
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'OPTIONS': {
'default_class': 'xmodule.hidden_block.HiddenBlock',
'fs_root': lambda settings: settings.DATA_DIR,
'fs_root': Derived(lambda settings: settings.DATA_DIR),
'render_template': 'common.djangoapps.edxmako.shortcuts.render_to_string',
}
}
@@ -2054,8 +2053,7 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
for locale_path in settings.COMPREHENSIVE_THEME_LOCALE_PATHS:
locale_paths += (path(locale_path), )
return locale_paths
LOCALE_PATHS = _make_locale_paths
derived('LOCALE_PATHS')
LOCALE_PATHS = Derived(_make_locale_paths)

# Messages
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
@@ -4661,13 +4659,12 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
############## Settings for LMS Context Sensitive Help ##############

HELP_TOKENS_INI_FILE = REPO_ROOT / "lms" / "envs" / "help_tokens.ini"
HELP_TOKENS_LANGUAGE_CODE = lambda settings: settings.LANGUAGE_CODE
HELP_TOKENS_VERSION = lambda settings: doc_version()
HELP_TOKENS_LANGUAGE_CODE = Derived(lambda settings: settings.LANGUAGE_CODE)
HELP_TOKENS_VERSION = Derived(lambda settings: doc_version())
HELP_TOKENS_BOOKS = {
'learner': 'https://edx.readthedocs.io/projects/open-edx-learner-guide',
'course_author': 'https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course',
}
derived('HELP_TOKENS_LANGUAGE_CODE', 'HELP_TOKENS_VERSION')

############## OPEN EDX ENTERPRISE SERVICE CONFIGURATION ######################
# The Open edX Enterprise service is currently hosted via the LMS container/process.
@@ -4955,14 +4952,13 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
# .. setting_description: Set the format a retired user username field gets transformed into, where {}
# is replaced with the hash of the original username. This is a derived setting that depends on
# RETIRED_USERNAME_PREFIX value.
RETIRED_USERNAME_FMT = lambda settings: settings.RETIRED_USERNAME_PREFIX + '{}'
RETIRED_USERNAME_FMT = Derived(lambda settings: settings.RETIRED_USERNAME_PREFIX + '{}'),
# .. setting_name: RETIRED_EMAIL_FMT
# .. setting_default: retired__user_{}@retired.invalid
# .. setting_description: Set the format a retired user email field gets transformed into, where {} is
# replaced with the hash of the original email. This is a derived setting that depends on
# RETIRED_EMAIL_PREFIX and RETIRED_EMAIL_DOMAIN values.
RETIRED_EMAIL_FMT = lambda settings: settings.RETIRED_EMAIL_PREFIX + '{}@' + settings.RETIRED_EMAIL_DOMAIN
derived('RETIRED_USERNAME_FMT', 'RETIRED_EMAIL_FMT')
RETIRED_EMAIL_FMT = Derived(lambda settings: settings.RETIRED_EMAIL_PREFIX + '{}@' + settings.RETIRED_EMAIL_DOMAIN),
# .. setting_name: RETIRED_USER_SALTS
# .. setting_default: ['abc', '123']
# .. setting_description: Set a list of salts used for hashing usernames and emails on users retirement.
@@ -5450,11 +5446,11 @@ def _should_send_learning_badge_events(settings):
EVENT_BUS_PRODUCER_CONFIG = {
'org.openedx.learning.certificate.created.v1': {
'learning-certificate-lifecycle':
{'event_key_field': 'certificate.course.course_key', 'enabled': _should_send_certificate_events},
{'event_key_field': 'certificate.course.course_key', 'enabled': Derived(_should_send_certificate_events)},
},
'org.openedx.learning.certificate.revoked.v1': {
'learning-certificate-lifecycle':
{'event_key_field': 'certificate.course.course_key', 'enabled': _should_send_certificate_events},
{'event_key_field': 'certificate.course.course_key', 'enabled': Derived(_should_send_certificate_events)},
},
'org.openedx.learning.course.unenrollment.completed.v1': {
'course-unenrollment-lifecycle':
@@ -5516,33 +5512,16 @@ def _should_send_learning_badge_events(settings):
"org.openedx.learning.course.passing.status.updated.v1": {
"learning-badges-lifecycle": {
"event_key_field": "course_passing_status.course.course_key",
"enabled": _should_send_learning_badge_events,
"enabled": Derived(_should_send_learning_badge_events),
},
},
"org.openedx.learning.ccx.course.passing.status.updated.v1": {
"learning-badges-lifecycle": {
"event_key_field": "course_passing_status.course.ccx_course_key",
"enabled": _should_send_learning_badge_events,
"enabled": Derived(_should_send_learning_badge_events),
},
},
}
derived_collection_entry('EVENT_BUS_PRODUCER_CONFIG', 'org.openedx.learning.certificate.created.v1',
'learning-certificate-lifecycle', 'enabled')
derived_collection_entry('EVENT_BUS_PRODUCER_CONFIG', 'org.openedx.learning.certificate.revoked.v1',
'learning-certificate-lifecycle', 'enabled')

derived_collection_entry(
"EVENT_BUS_PRODUCER_CONFIG",
"org.openedx.learning.course.passing.status.updated.v1",
"learning-badges-lifecycle",
"enabled",
)
derived_collection_entry(
"EVENT_BUS_PRODUCER_CONFIG",
"org.openedx.learning.ccx.course.passing.status.updated.v1",
"learning-badges-lifecycle",
"enabled",
)

BEAMER_PRODUCT_ID = ""

6 changes: 2 additions & 4 deletions lms/envs/docs/README.rst
Original file line number Diff line number Diff line change
@@ -56,8 +56,7 @@ For example:
for locale_path in settings.COMPREHENSIVE_THEME_LOCALE_PATHS:
locale_paths += (path(locale_path), )
return locale_paths
LOCALE_PATHS = _make_locale_paths
derived('LOCALE_PATHS')
LOCALE_PATHS = Derived(_make_locale_paths)
In this case, ``LOCALE_PATHS`` will be defined correctly at the end of the
settings module parsing no matter what ``REPO_ROOT``,
@@ -92,7 +91,6 @@ when nested within each other:
'NAME': 'mako',
'BACKEND': 'common.djangoapps.edxmako.backend.Mako',
'APP_DIRS': False,
'DIRS': _make_mako_template_dirs,
'DIRS': Derived(_make_mako_template_dirs),
},
]
derived_collection_entry('TEMPLATES', 1, 'DIRS')
11 changes: 0 additions & 11 deletions lms/envs/production.py
Original file line number Diff line number Diff line change
@@ -349,17 +349,6 @@ def get_env_setting(setting):
# use the one from common.py
MODULESTORE = convert_module_store_setting_if_needed(_YAML_TOKENS.get('MODULESTORE', MODULESTORE))

# After conversion above, the modulestore will have a "stores" list with all defined stores, for all stores, add the
# fs_root entry to derived collection so that if it's a callable it can be resolved. We need to do this because the
# `derived_collection_entry` takes an exact index value but the config file might have overridden the number of stores
# and so we can't be sure that the 2 we define in common.py will be there when we try to derive settings. This could
# lead to exceptions being thrown when the `derive_settings` call later in this file tries to update settings. We call
# the derived_collection_entry function here to ensure that we update the fs_root for any callables that remain after
# we've updated the MODULESTORE setting from our config file.
for idx, store in enumerate(MODULESTORE['default']['OPTIONS']['stores']):
if 'OPTIONS' in store and 'fs_root' in store["OPTIONS"]:
derived_collection_entry('MODULESTORE', 'default', 'OPTIONS', 'stores', idx, 'OPTIONS', 'fs_root')

BROKER_URL = "{}://{}:{}@{}/{}".format(CELERY_BROKER_TRANSPORT,
CELERY_BROKER_USER,
CELERY_BROKER_PASSWORD,
1 change: 1 addition & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ files =
openedx/core/djangoapps/content_staging,
openedx/core/djangoapps/content_libraries,
openedx/core/djangoapps/xblock,
openedx/core/lib/derived.py,
openedx/core/types,
openedx/core/djangoapps/content_tagging,
xmodule/util/keys.py,
150 changes: 100 additions & 50 deletions openedx/core/lib/derived.py
Original file line number Diff line number Diff line change
@@ -4,71 +4,121 @@
other settings have been set. The derived setting can also be overridden by setting the
derived setting to an actual value.
"""
from __future__ import annotations

import re
import sys
import types
import typing as t

# Global list holding all settings which will be derived.
__DERIVED = []

Settings: t.TypeAlias = types.ModuleType

def derived(*settings):

T = t.TypeVar('T')


class Derived(t.Generic[T]):
"""
Registers settings which are derived from other settings.
Can be called multiple times to add more derived settings.
A temporary Django setting value, defined with a function which generates the setting's eventual value.
Args:
settings (str): Setting names to register.
Said function (`calculate_value`) should accept a Django settings module, and return a calculated value.
To ensure that application code does not encounter an instance of this class in your settings, be sure to call
`derive_settings` somewhere in your terminal settings file.
"""
__DERIVED.extend(settings)
def __init__(self, calculate_value: t.Callable[[Settings], T]):
self.calculate_value = calculate_value


def derived_collection_entry(collection_name, *accessors):
def derive_settings(module_name: str) -> None:
"""
Registers a setting which is a dictionary or list and needs a derived value for a particular entry.
Can be called multiple times to add more derived settings.
In the Django settings module at `module_name`, replace `Derived` values with their cacluated values.
Args:
collection_name (str): Name of setting which contains a dictionary or list.
accessors (int|str): Sequence of dictionary keys and list indices in the collection (and
collections within it) leading to the value which will be derived.
For example: 0, 'DIRS'.
The replacement happens recursively for any values or containers defined by a Django setting name (which is: an
uppercase top-level variable name which is not prefixed by an underscore). Within containers,
"""
__DERIVED.append((collection_name, accessors))
module = sys.modules[module_name]
_derive_dict(module, vars(module), key_filter=_key_is_a_setting_name)


_SETTING_NAME_REGEX = re.compile(r'^[A-Z][A-Z0-9_]*$')


def _key_is_a_setting_name(key: str) -> bool:
return bool(_SETTING_NAME_REGEX.match(key))


def derive_settings(module_name):
def _match_every_key(_key: str) -> bool:
return True


def _derive_recursively(settings: Settings, value: t.Any) -> t.Any:
"""
Derives all registered settings and sets them onto a particular module.
Skips deriving settings that are set to a value.
Recursively evaluate `Derive` objects` in `value` and any child containers. Return the evaluated version of `value`.
Args:
module_name (str): Name of module to which the derived settings will be added.
* If `value` is a `Derive` object, then use `settings` to calculate and return its value.
* If `value` is a mutable container, then recursively evaluate it in-place.
* If `value` is an immutable container, then recursively evalute a shallow copy of it.
Keep in mind that immutable containers (particularly: tuples) can contain mutable containers. In such a case, the
original and shallow-copied mutable containers will both reference the same child mutable container object.
"""
module = sys.modules[module_name]
for derived in __DERIVED: # lint-amnesty, pylint: disable=redefined-outer-name
if isinstance(derived, str):
setting = getattr(module, derived)
if callable(setting):
setting_val = setting(module)
setattr(module, derived, setting_val)
elif isinstance(derived, tuple):
# If a tuple, two elements are expected - else ignore.
if len(derived) == 2:
# The first element is the name of the attribute which is expected to be a dictionary or list.
# The second element is a list of string keys in that dictionary leading to a derived setting.
collection = getattr(module, derived[0])
accessors = derived[1]
for accessor in accessors[:-1]:
collection = collection[accessor]
setting = collection[accessors[-1]]
if callable(setting):
setting_val = setting(module)
collection[accessors[-1]] = setting_val


def clear_for_tests():
"""
Clears all settings to be derived. For tests only.
"""
global __DERIVED
__DERIVED = []
if isinstance(value, Derived):
return value.calculate_value(settings)
elif isinstance(value, dict):
return _derive_dict(settings, value)
elif isinstance(value, list):
return _derive_list(settings, value)
elif isinstance(value, tuple):
return _derive_tuple(settings, value)
elif isinstance(value, frozenset):
return _derive_frozenset(settings, value)
else:
return value


def _derive_dict(settings: Settings, the_dict: dict, key_filter: t.Callable[[str], bool] = _match_every_key) -> dict:
"""
Recursively evaluate `Derive` objects in `the_dict` and any child containers. Modifies `the_dict` in place.
Optionally takes a `key_filter`. Items that do not match the provided `key_filter` will be left alone.
"""
for key, value in the_dict.items():
if key_filter(key):
the_dict[key] = _derive_recursively(settings, value)
return the_dict


def _derive_list(settings: Settings, the_list: list) -> list:
"""
Recursively evaluate `Derive` objects in `the_list` and any child containers. Modifies `the_list` in place.
"""
for ix in range(len(the_list)):
the_list[ix] = _derive_recursively(settings, the_list[ix])
return the_list


def _derive_tuple(settings: Settings, tup: tuple) -> tuple:
"""
Recursively evaluate `Derive` objects in `tup` and any child containers. Returns a shallow copy of `tup`.
"""
return tuple(_derive_recursively(settings, item) for item in tup)


def _derive_set(settings: Settings, the_set: set) -> set:
"""
Recursively evaluate `Derive` objects in `the_set` and any child containers. Modifies `the_set` in-place.
"""
for original in the_set:
derived = _derive_recursively(settings, original)
if derived != original:
the_set.remove(original)
the_set.add(derived)
return the_set


def _derive_frozenset(settings: Settings, the_set: frozenset) -> frozenset:
"""
Recursively evaluate `Derive` objects in `the_set` and any child containers. Returns a shallow copy of `the_set`.
"""
return frozenset(_derive_recursively(settings, item) for item in the_set)
14 changes: 5 additions & 9 deletions openedx/core/lib/tests/test_derived.py
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@

import sys
from unittest import TestCase
from openedx.core.lib.derived import derived, derived_collection_entry, derive_settings, clear_for_tests
from openedx.core.lib.derived import Derived, derive_settings


class TestDerivedSettings(TestCase):
@@ -14,18 +14,14 @@ class TestDerivedSettings(TestCase):
"""
def setUp(self):
super().setUp()
clear_for_tests()
self.module = sys.modules[__name__]
self.module.SIMPLE_VALUE = 'paneer'
self.module.DERIVED_VALUE = lambda settings: 'mutter ' + settings.SIMPLE_VALUE
self.module.ANOTHER_DERIVED_VALUE = lambda settings: settings.DERIVED_VALUE + ' with naan'
self.module.DERIVED_VALUE = Derived(lambda settings: 'mutter ' + settings.SIMPLE_VALUE)
self.module.ANOTHER_DERIVED_VALUE = Derived(lambda settings: settings.DERIVED_VALUE + ' with naan')
self.module.UNREGISTERED_DERIVED_VALUE = lambda settings: settings.SIMPLE_VALUE + ' is cheese'
derived('DERIVED_VALUE', 'ANOTHER_DERIVED_VALUE')
self.module.DICT_VALUE = {}
self.module.DICT_VALUE['test_key'] = lambda settings: settings.DERIVED_VALUE * 3
derived_collection_entry('DICT_VALUE', 'test_key')
self.module.DICT_VALUE['list_key'] = ['not derived', lambda settings: settings.DERIVED_VALUE]
derived_collection_entry('DICT_VALUE', 'list_key', 1)
self.module.DICT_VALUE['test_key'] = Derived(lambda settings: settings.DERIVED_VALUE * 3)
self.module.DICT_VALUE['list_key'] = ['not derived', Derived(lambda settings: settings.DERIVED_VALUE)]

def test_derived_settings_are_derived(self):
derive_settings(__name__)
2 changes: 2 additions & 0 deletions xmodule/modulestore/modulestore_settings.py
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@
import copy
import warnings

from openedx.core.lib.derived import Derived


def convert_module_store_setting_if_needed(module_store_setting):
"""

0 comments on commit 712fabf

Please sign in to comment.