Skip to content

Commit bb17099

Browse files
authored
Merge pull request #371 from informatics-lab/dynamodb-flags
Feature flag plugin architecture
2 parents cf50811 + 680601a commit bb17099

File tree

6 files changed

+92
-3
lines changed

6 files changed

+92
-3
lines changed

doc/source/howto-feature-toggles.rst

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ ready for a wider audience. Feature toggles give developers and
77
user experience wizards the ability to test features early in
88
the cycle and give them insight into performance and usability.
99

10+
**Static feature flags**
1011

1112
.. code-block:: yaml
1213
@@ -18,8 +19,29 @@ To access these settings in main.py use the following syntax.
1819

1920
.. code-block:: python
2021
21-
if config.features['foo']:
22+
if forest.data.FEATURE_FLAGS['foo']:
2223
# Do foo feature
2324
2425
2526
As easy as that.
27+
28+
**Dynamic feature flags**
29+
30+
To add more sophisticated dynamic feature toggles it is possible to
31+
specify an ``entry_point`` that runs general purpose Python code to
32+
determine the feature flags dictionary.
33+
34+
35+
.. code-block:: yaml
36+
37+
plugins:
38+
feature:
39+
entry_point: lib.mod.func
40+
41+
The string ``lib.mod.func`` is parsed into an import statement to
42+
import ``lib.mod`` and a call of the ``func`` method. This is very
43+
similar to how setup.py wires up commands.
44+
45+
46+
.. warning:: Since the entry_point could point to arbitrary Python code
47+
make sure this feature is only used with trusted source code

forest/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
.. automodule:: forest.presets
2424
2525
"""
26-
__version__ = '0.15.7'
26+
__version__ = '0.16.0'
2727

2828
from .config import *
2929
from . import (

forest/config.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,43 @@
2323
import os
2424
import string
2525
import yaml
26+
from dataclasses import dataclass
2627
from collections import defaultdict
28+
from collections.abc import Mapping
2729
from forest.export import export
2830

2931

3032
__all__ = []
3133

3234

35+
@dataclass
36+
class PluginSpec:
37+
"""Data representation of plugin"""
38+
entry_point: str
39+
40+
41+
class Plugins(Mapping):
42+
"""Specialist mapping between allowed keys and specs"""
43+
def __init__(self, data):
44+
allowed = ("feature",)
45+
self.data = {}
46+
for key, value in data.items():
47+
if key in allowed:
48+
self.data[key] = PluginSpec(**value)
49+
else:
50+
msg = f"{key} not in {allowed}"
51+
raise Exception(msg)
52+
53+
def __getitem__(self, *args, **kwargs):
54+
return self.data.__getitem__(*args, **kwargs)
55+
56+
def __len__(self, *args, **kwargs):
57+
return self.data.__len__(*args, **kwargs)
58+
59+
def __iter__(self, *args, **kwargs):
60+
return self.data.__iter__(*args, **kwargs)
61+
62+
3363
class Viewport:
3464
def __init__(self, lon_range, lat_range):
3565
self.lon_range = lon_range
@@ -68,6 +98,7 @@ class Config(object):
6898
"""
6999
def __init__(self, data):
70100
self.data = data
101+
self.plugins = Plugins(self.data.get("plugins", {}))
71102

72103
def __repr__(self):
73104
return "{}({})".format(

forest/main.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
layers,
1717
db,
1818
keys,
19+
plugin,
1920
presets,
2021
redux,
2122
rx,
@@ -45,7 +46,11 @@ def main(argv=None):
4546
args.variables))
4647

4748
# Feature toggles
48-
data.FEATURE_FLAGS = config.features
49+
if "feature" in config.plugins:
50+
features = plugin.call(config.plugins["feature"].entry_point)
51+
else:
52+
features = config.features
53+
data.FEATURE_FLAGS = features
4954

5055
# Full screen map
5156
viewport = config.default_viewport

forest/plugin.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Simple plugin architecture"""
2+
import importlib
3+
4+
5+
def call(entry_point):
6+
"""Call entry_point to run plugin"""
7+
*parts, method = entry_point.split(".")
8+
module = importlib.import_module(".".join(parts))
9+
return getattr(module, method)()

test/test_config.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,25 @@ def test_config_parser_use_web_map_tiles(data, expect):
217217
def test_config_parser_features(data, expect):
218218
config = forest.config.Config(data)
219219
assert config.features["example"] == expect
220+
221+
222+
def test_config_parser_plugin_entry_points():
223+
config = forest.config.Config({
224+
"plugins": {
225+
"feature": {
226+
"entry_point": "module.main"
227+
}
228+
}
229+
})
230+
assert config.plugins["feature"].entry_point == "module.main"
231+
232+
233+
def test_config_parser_plugin_given_unsupported_key():
234+
with pytest.raises(Exception):
235+
forest.config.Config({
236+
"plugins": {
237+
"not_a_key": {
238+
"entry_point": "module.main"
239+
}
240+
}
241+
})

0 commit comments

Comments
 (0)