superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From maximebeauche...@apache.org
Subject [incubator-superset] branch master updated: Allow for dynamic feature flags (#6808)
Date Wed, 27 Feb 2019 23:11:47 GMT
This is an automated email from the ASF dual-hosted git repository.

maximebeauchemin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 3ae02d1  Allow for dynamic feature flags (#6808)
3ae02d1 is described below

commit 3ae02d1a542ed66522f6a0a51433490e7da9ea30
Author: Maxime Beauchemin <maximebeauchemin@gmail.com>
AuthorDate: Wed Feb 27 15:11:38 2019 -0800

    Allow for dynamic feature flags (#6808)
    
    * Allow for dynamic feature flags
    
    Giving more control over feature flags, allowing administrator to define
    custom logic around whether features are enabled for particular users /
    roles.
    
    The exposed function can be used for things like:
    * progressive rollout of features (1%, 5%, 50%, 100%)
    * experimentation
    * role-based feature affectation (only admins see a particular feature)
    
    * fix build
    
    * Addressing comments
    
    * Addressing @hughhh's comments
---
 superset/__init__.py          | 18 ++++++++++++------
 superset/config.py            | 16 ++++++++++++++++
 superset/views/base.py        |  4 ++--
 tests/base_tests.py           |  8 ++++++--
 tests/superset_test_config.py |  9 +++++++++
 5 files changed, 45 insertions(+), 10 deletions(-)

diff --git a/superset/__init__.py b/superset/__init__.py
index 3f72f18..5e77328 100644
--- a/superset/__init__.py
+++ b/superset/__init__.py
@@ -16,6 +16,7 @@
 # under the License.
 # pylint: disable=C,R,W
 """Package's main module!"""
+from copy import deepcopy
 import json
 import logging
 from logging.handlers import TimedRotatingFileHandler
@@ -212,15 +213,20 @@ security_manager = appbuilder.sm
 results_backend = app.config.get('RESULTS_BACKEND')
 
 # Merge user defined feature flags with default feature flags
-feature_flags = app.config.get('DEFAULT_FEATURE_FLAGS')
-feature_flags.update(app.config.get('FEATURE_FLAGS') or {})
+_feature_flags = app.config.get('DEFAULT_FEATURE_FLAGS') or {}
+_feature_flags.update(app.config.get('FEATURE_FLAGS') or {})
+
+
+def get_feature_flags():
+    GET_FEATURE_FLAGS_FUNC = app.config.get('GET_FEATURE_FLAGS_FUNC')
+    if GET_FEATURE_FLAGS_FUNC:
+        return GET_FEATURE_FLAGS_FUNC(deepcopy(_feature_flags))
+    return _feature_flags
 
 
 def is_feature_enabled(feature):
-    """
-    Utility function for checking whether a feature is turned on
-    """
-    return feature_flags.get(feature)
+    """Utility function for checking whether a feature is turned on"""
+    return get_feature_flags().get(feature)
 
 
 # Registering sources
diff --git a/superset/config.py b/superset/config.py
index e5bfd77..c44328e 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -204,6 +204,22 @@ LANGUAGES = {
 # will result in combined feature flags of { 'FOO': True, 'BAR': True, 'BAZ': True }
 DEFAULT_FEATURE_FLAGS = {}
 
+# A function that receives a dict of all feature flags
+# (DEFAULT_FEATURE_FLAGS merged with FEATURE_FLAGS)
+# can alter it, and returns a similar dict. Note the dict of feature
+# flags passed to the function is a deepcopy of the dict in the config,
+# and can therefore be mutated without side-effect
+#
+# GET_FEATURE_FLAGS_FUNC can be used to implement progressive rollouts,
+# role-based features, or a full on A/B testing framework.
+#
+# from flask import g, request
+# def GET_FEATURE_FLAGS_FUNC(feature_flags_dict):
+#     feature_flags_dict['some_feature'] = g.user and g.user.id == 5
+#     return feature_flags_dict
+GET_FEATURE_FLAGS_FUNC = None
+
+
 # ---------------------------------------------------
 # Image and file configuration
 # ---------------------------------------------------
diff --git a/superset/views/base.py b/superset/views/base.py
index f0f5fbf..4f3c8e8 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -31,7 +31,7 @@ from flask_babel import lazy_gettext as _
 import simplejson as json
 import yaml
 
-from superset import conf, db, feature_flags, security_manager
+from superset import conf, db, get_feature_flags, security_manager
 from superset.exceptions import SupersetException, SupersetSecurityException
 from superset.translations.utils import get_language_pack
 from superset.utils import core as utils
@@ -157,7 +157,7 @@ class BaseSupersetView(BaseView):
             'conf': {k: conf.get(k) for k in FRONTEND_CONF_KEYS},
             'locale': locale,
             'language_pack': get_language_pack(locale),
-            'feature_flags': feature_flags,
+            'feature_flags': get_feature_flags(),
         }
 
 
diff --git a/tests/base_tests.py b/tests/base_tests.py
index e50e8e0..8884bbb 100644
--- a/tests/base_tests.py
+++ b/tests/base_tests.py
@@ -190,10 +190,14 @@ class SupersetTestCase(unittest.TestCase):
             raise Exception('run_sql failed')
         return resp
 
-    @patch.dict('superset.feature_flags', {'FOO': True}, clear=True)
+    @patch.dict('superset._feature_flags', {'FOO': True}, clear=True)
     def test_existing_feature_flags(self):
         self.assertTrue(is_feature_enabled('FOO'))
 
-    @patch.dict('superset.feature_flags', {}, clear=True)
+    @patch.dict('superset._feature_flags', {}, clear=True)
     def test_nonexistent_feature_flags(self):
         self.assertFalse(is_feature_enabled('FOO'))
+
+    def test_feature_flags(self):
+        self.assertEquals(is_feature_enabled('foo'), 'bar')
+        self.assertEquals(is_feature_enabled('super'), 'set')
diff --git a/tests/superset_test_config.py b/tests/superset_test_config.py
index 757b8d7..d22414b 100644
--- a/tests/superset_test_config.py
+++ b/tests/superset_test_config.py
@@ -15,6 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 # flake8: noqa
+from copy import copy
 from superset.config import *
 
 AUTH_USER_REGISTRATION_ROLE = 'alpha'
@@ -29,6 +30,14 @@ if 'SUPERSET__SQLALCHEMY_DATABASE_URI' in os.environ:
 
 SQL_SELECT_AS_CTA = True
 SQL_MAX_ROW = 666
+FEATURE_FLAGS = {
+    'foo': 'bar',
+}
+def GET_FEATURE_FLAGS_FUNC(ff):
+    ff_copy = copy(ff)
+    ff_copy['super'] = 'set'
+    return ff_copy
+
 
 TESTING = True
 SECRET_KEY = 'thisismyscretkey'


Mime
View raw message