Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .annotation_safe_list.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ auth.Group:
".. no_pii:": "This model has no PII"
auth.Permission:
".. no_pii:": "This model has no PII"
waffle.Flag:
".. no_pii:": "This model has no PII"
waffle.Sample:
".. no_pii:": "This model has no PII"
waffle.Switch:
".. no_pii:": "This model has no PII"
auth.User:
".. pii:": "Contains username, password, and email address, must be retired in the consumer of this app"
".. pii_types:" : username, email_address, password
Expand Down
7 changes: 5 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ Change Log

.. There should always be an "Unreleased" section for changes pending release.

Unreleased
~~~~~~~~~~
[1.0.0] - 2020-10-13
~~~~~~~~~~~~~~~~~~~~

* Fix missing ``module_name`` argument in ``SettingDictToggle`` constructor.
* Extract waffle classes from edx-platform and move them here following ADR `#2 <docs/decisions/0002-application-toggle-state.rst>`__, `#3 <docs/decisions/0003-django-setting-toggles.rst>`__, `#4 <docs/decisions/0004-toggle-api.rst>`__. This does not introduce backward-incompatible changes, *yet*.
* Fix pinned requirements and incorrect root url that overrode edx-platform's.

[0.3.0] - 2020-09-23
~~~~~~~~~~~~~~~~~~~~
Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/0004-toggle-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Decision

All implementation code should be moved to an ``internal`` module. The Public API will be exposed as follows in ``edx_toggles/toggles/__init__.py``::

from .internal import ...
from .internal.togglemodule import ...

The benefits of this setup include:

Expand Down
2 changes: 1 addition & 1 deletion edx_toggles/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
Library and utilities for feature toggles.
"""

__version__ = '0.3.0'
__version__ = '1.0.0'

default_app_config = 'edx_toggles.apps.TogglesConfig' # pylint: disable=invalid-name
22 changes: 0 additions & 22 deletions edx_toggles/templates/edx_toggles/base.html

This file was deleted.

96 changes: 96 additions & 0 deletions edx_toggles/tests/test_testutils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""
Tests for waffle utils test utilities.
"""


import crum
from django.test import TestCase
from django.test.client import RequestFactory
from edx_django_utils.cache import RequestCache

from edx_toggles.toggles import WaffleFlag, WaffleFlagNamespace
from edx_toggles.toggles.testutils import override_waffle_flag


class OverrideWaffleFlagTests(TestCase):
"""
Tests for the override_waffle_flag decorator/context manager.
"""

def setUp(self):
super(OverrideWaffleFlagTests, self).setUp()
namespace_name = "test_namespace"
flag_name = "test_flag"
self.namespaced_flag_name = namespace_name + "." + flag_name
self.namespace = WaffleFlagNamespace(namespace_name)
self.waffle_flag = WaffleFlag(self.namespace, flag_name)

request = RequestFactory().request()
crum.set_current_request(request)
RequestCache.clear_all_namespaces()

self.addCleanup(crum.set_current_request, None)
self.addCleanup(RequestCache.clear_all_namespaces)

def temporarily_enable_flag(self):
"""
Temporarily override flag.
"""

@override_waffle_flag(self.waffle_flag, True)
def test_func():
"""
Decorated test function.
"""
self.assertTrue(self.waffle_flag.is_enabled())

test_func()

def test_override_waffle_flag_pre_cached(self):
# checks and caches the is_enabled value
self.assertFalse(self.waffle_flag.is_enabled())
# pylint: disable=protected-access
flag_cache = self.namespace._cached_flags
self.assertIn(self.namespaced_flag_name, flag_cache)

self.temporarily_enable_flag()

# test cached flag is restored
self.assertIn(self.namespaced_flag_name, flag_cache)
self.assertFalse(self.waffle_flag.is_enabled())

def test_override_waffle_flag_not_pre_cached(self):
# check that the flag is not yet cached
# pylint: disable=protected-access
flag_cache = self.namespace._cached_flags
self.assertNotIn(self.namespaced_flag_name, flag_cache)

self.temporarily_enable_flag()

# test cache is removed when no longer using decorator/context manager
self.assertNotIn(self.namespaced_flag_name, flag_cache)

def test_override_waffle_flag_as_context_manager(self):
self.assertFalse(self.waffle_flag.is_enabled())

with override_waffle_flag(self.waffle_flag, True):
self.assertTrue(self.waffle_flag.is_enabled())

self.assertFalse(self.waffle_flag.is_enabled())

def test_interlocked_overrides(self):
waffle_flag1 = self.waffle_flag
waffle_flag2 = WaffleFlag(self.namespace, waffle_flag1.flag_name + "2")
# pylint: disable=protected-access
self.namespace._cached_flags[waffle_flag2.namespaced_flag_name] = True

self.assertFalse(waffle_flag1.is_enabled())
self.assertTrue(waffle_flag2.is_enabled())

with override_waffle_flag(waffle_flag1, True):
with override_waffle_flag(waffle_flag2, False):
self.assertTrue(waffle_flag1.is_enabled())
self.assertFalse(waffle_flag2.is_enabled())

self.assertFalse(waffle_flag1.is_enabled())
self.assertTrue(waffle_flag2.is_enabled())
30 changes: 28 additions & 2 deletions edx_toggles/tests/test_toggles.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class SettingToggleTests(TestCase):
"""
SettingToggle tests
"""

def test_toggle_for_absent_setting(self):
toggle1 = toggles.SettingToggle("NAME1", True)
toggle2 = toggles.SettingToggle("NAME1", False)
Expand All @@ -36,9 +37,14 @@ class SettingDictToggleTests(TestCase):
"""
SettingDictToggle tests
"""

def test_toggle_for_absent_setting(self):
toggle1 = toggles.SettingDictToggle("NAME1", "key1", True)
toggle2 = toggles.SettingDictToggle("NAME2", "key2", False)
toggle1 = toggles.SettingDictToggle(
"NAME1", "key1", True, module_name="module1"
)
toggle2 = toggles.SettingDictToggle(
"NAME2", "key2", False, module_name="module1"
)
self.assertTrue(toggle1.is_enabled())
self.assertFalse(toggle2.is_enabled())

Expand All @@ -59,6 +65,7 @@ class ToggleInstancesTests(TestCase):
"""
Class instance-tracking tests
"""

def test_created_instances(self):
toggle1 = toggles.SettingToggle("NAME1", default=False, module_name="module1")
toggle2 = toggles.SettingToggle("NAME2", default=False, module_name="module2")
Expand All @@ -71,3 +78,22 @@ def test_deleted_instances_are_not_listed(self):
toggles.SettingToggle("NAME1", default=False, module_name="module1")
instances = toggles.SettingToggle.get_instances()
self.assertEqual([], instances)


class TestWaffleSwitch(TestCase):
"""
Tests the WaffleSwitch.
"""

NAMESPACE_NAME = "test_namespace"
WAFFLE_SWITCH_NAME = "test_switch_name"
TEST_NAMESPACE = toggles.WaffleSwitchNamespace(NAMESPACE_NAME)
WAFFLE_SWITCH = toggles.WaffleSwitch(TEST_NAMESPACE, WAFFLE_SWITCH_NAME, __name__)

def test_namespaced_switch_name(self):
"""
Verify namespaced_switch_name returns the correct namespace switch name
"""
expected = self.NAMESPACE_NAME + "." + self.WAFFLE_SWITCH_NAME
actual = self.WAFFLE_SWITCH.namespaced_switch_name
self.assertEqual(actual, expected)
3 changes: 2 additions & 1 deletion edx_toggles/toggles/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""
Expose public feature toggle API.
"""
from .internal import SettingDictToggle, SettingToggle
from .internal.setting_toggle import SettingDictToggle, SettingToggle
from .internal.waffle import WaffleFlag, WaffleFlagNamespace, WaffleSwitch, WaffleSwitchNamespace
59 changes: 0 additions & 59 deletions edx_toggles/toggles/internal.py

This file was deleted.

4 changes: 4 additions & 0 deletions edx_toggles/toggles/internal/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""
This module includes all code related to feature toggles. Remember to import publicly available classes and functions
in toggles/__init__.py.
Comment thread
robrap marked this conversation as resolved.
"""
68 changes: 68 additions & 0 deletions edx_toggles/toggles/internal/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""
Feature toggle base classes
"""

from abc import ABC


class BaseToggle(ABC):
"""
This abstract base class exposes the basic API required by toggle classes. Toggle instances are tracked in the
``_class_instances`` class method, which is exposed via the ``get_instances`` class method.
"""

# Each child class should implement its own cache of class instances, for instance via WeakSet objects.
_class_instances = None

def __init__(self, name, default=False, module_name=""):
self.name = name
self.default = default
self.module_name = module_name
self._class_instances.add(self)

def is_enabled(self):
raise NotImplementedError

@classmethod
def get_instances(cls):
"""
Return the list of class instances sorted by name.
"""
return sorted(cls._class_instances, key=lambda instance: instance.name)


class BaseNamespace(ABC):
Comment thread
robrap marked this conversation as resolved.
"""
A base class for a request cached namespace for waffle flags/switches.

An instance of this class represents a single namespace
(e.g. "course_experience"), and can be used to work with a set of
flags or switches that will all share this namespace.
"""

def __init__(self, name, log_prefix=None):
"""
Initializes the waffle namespace instance.

Arguments:
name (String): Namespace string appended to start of all waffle
flags and switches (e.g. "grades")
log_prefix (String): Optional string to be appended to log messages
(e.g. "Grades: "). Defaults to ''.

"""
assert name, "The name is required."
self.name = name
self.log_prefix = log_prefix if log_prefix else ""
Comment thread
robrap marked this conversation as resolved.

def _namespaced_name(self, setting_name):
"""
Returns the namespaced name of the waffle switch/flag.

For example, the namespaced name of a waffle switch/flag would be:
my_namespace.my_setting_name

Arguments:
setting_name (String): The name of the flag or switch.
"""
return "{}.{}".format(self.name, setting_name)
Loading