Skip to content

Commit

Permalink
feat: feature flag configurable custom backend (apache#16618)
Browse files Browse the repository at this point in the history
* feat: feature flag configurable custom backend

* fix lint

* simpler approach

* fix tests

* revert dependency updates

* Update superset/utils/feature_flag_manager.py

Co-authored-by: Ville Brofeldt <[email protected]>

* Update superset/config.py

Co-authored-by: Ville Brofeldt <[email protected]>

Co-authored-by: Ville Brofeldt <[email protected]>
  • Loading branch information
dpgaspar and villebro authored Sep 13, 2021
1 parent 1cc7263 commit f2bc139
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 5 deletions.
9 changes: 8 additions & 1 deletion superset/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,14 @@ def _try_json_readsha(filepath: str, length: int) -> Optional[str]:
# feature_flags_dict['some_feature'] = g.user and g.user.get_id() == 5
# return feature_flags_dict
GET_FEATURE_FLAGS_FUNC: Optional[Callable[[Dict[str, bool]], Dict[str, bool]]] = None

# A function that receives a feature flag name and an optional default value.
# Has a similar utility to GET_FEATURE_FLAGS_FUNC but it's useful to not force the
# evaluation of all feature flags when just evaluating a single one.
#
# Note that the default `get_feature_flags` will evaluate each feature with this
# callable when the config key is set, so don't use both GET_FEATURE_FLAGS_FUNC
# and IS_FEATURE_ENABLED_FUNC in conjunction.
IS_FEATURE_ENABLED_FUNC: Optional[Callable[[str, Optional[bool]], bool]] = None
# A function that expands/overrides the frontend `bootstrap_data.common` object.
# Can be used to implement custom frontend functionality,
# or dynamically change certain configs.
Expand Down
18 changes: 15 additions & 3 deletions superset/utils/feature_flag_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,36 @@ class FeatureFlagManager:
def __init__(self) -> None:
super().__init__()
self._get_feature_flags_func = None
self._is_feature_enabled_func = None
self._feature_flags: Dict[str, Any] = {}

def init_app(self, app: Flask) -> None:
self._get_feature_flags_func = app.config["GET_FEATURE_FLAGS_FUNC"]
self._is_feature_enabled_func = app.config["IS_FEATURE_ENABLED_FUNC"]
self._feature_flags = app.config["DEFAULT_FEATURE_FLAGS"]
self._feature_flags.update(app.config["FEATURE_FLAGS"])

def get_feature_flags(self) -> Dict[str, Any]:
if self._get_feature_flags_func:
return self._get_feature_flags_func(deepcopy(self._feature_flags))

if callable(self._is_feature_enabled_func):
return dict(
map(
lambda kv: (kv[0], self._is_feature_enabled_func(kv[0], kv[1])),
self._feature_flags.items(),
)
)
return self._feature_flags

def is_feature_enabled(self, feature: str) -> bool:
"""Utility function for checking whether a feature is turned on"""
if self._is_feature_enabled_func:
return (
self._is_feature_enabled_func(feature, self._feature_flags[feature])
if feature in self._feature_flags
else False
)
feature_flags = self.get_feature_flags()

if feature_flags and feature in feature_flags:
return feature_flags[feature]

return False
45 changes: 44 additions & 1 deletion tests/integration_tests/feature_flag_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@
# under the License.
from unittest.mock import patch

from superset import is_feature_enabled
from parameterized import parameterized

from superset import get_feature_flags, is_feature_enabled
from tests.integration_tests.base_tests import SupersetTestCase


def dummy_is_feature_enabled(feature_flag_name: str, default: bool = True) -> bool:
return True if feature_flag_name.startswith("True_") else default


class TestFeatureFlag(SupersetTestCase):
@patch.dict(
"superset.extensions.feature_flag_manager._feature_flags",
Expand All @@ -38,3 +44,40 @@ def test_nonexistent_feature_flags(self):
def test_feature_flags(self):
self.assertEqual(is_feature_enabled("foo"), "bar")
self.assertEqual(is_feature_enabled("super"), "set")


@patch.dict(
"superset.extensions.feature_flag_manager._feature_flags",
{"True_Flag1": False, "True_Flag2": True, "Flag3": False, "Flag4": True},
clear=True,
)
class TestFeatureFlagBackend(SupersetTestCase):
@parameterized.expand(
[
("True_Flag1", True),
("True_Flag2", True),
("Flag3", False),
("Flag4", True),
("True_DoesNotExist", False),
]
)
@patch(
"superset.extensions.feature_flag_manager._is_feature_enabled_func",
dummy_is_feature_enabled,
)
def test_feature_flags_override(self, feature_flag_name, expected):
self.assertEqual(is_feature_enabled(feature_flag_name), expected)

@patch(
"superset.extensions.feature_flag_manager._is_feature_enabled_func",
dummy_is_feature_enabled,
)
@patch(
"superset.extensions.feature_flag_manager._get_feature_flags_func", None,
)
def test_get_feature_flags(self):
feature_flags = get_feature_flags()
self.assertEqual(
feature_flags,
{"True_Flag1": True, "True_Flag2": True, "Flag3": False, "Flag4": True},
)

0 comments on commit f2bc139

Please sign in to comment.