Skip to content

Commit

Permalink
Merge pull request #371 from informatics-lab/dynamodb-flags
Browse files Browse the repository at this point in the history
Feature flag plugin architecture
  • Loading branch information
andrewgryan authored May 6, 2020
2 parents cf50811 + 680601a commit bb17099
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 3 deletions.
24 changes: 23 additions & 1 deletion doc/source/howto-feature-toggles.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ ready for a wider audience. Feature toggles give developers and
user experience wizards the ability to test features early in
the cycle and give them insight into performance and usability.

**Static feature flags**

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

.. code-block:: python
if config.features['foo']:
if forest.data.FEATURE_FLAGS['foo']:
# Do foo feature
As easy as that.

**Dynamic feature flags**

To add more sophisticated dynamic feature toggles it is possible to
specify an ``entry_point`` that runs general purpose Python code to
determine the feature flags dictionary.


.. code-block:: yaml
plugins:
feature:
entry_point: lib.mod.func
The string ``lib.mod.func`` is parsed into an import statement to
import ``lib.mod`` and a call of the ``func`` method. This is very
similar to how setup.py wires up commands.


.. warning:: Since the entry_point could point to arbitrary Python code
make sure this feature is only used with trusted source code
2 changes: 1 addition & 1 deletion forest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
.. automodule:: forest.presets
"""
__version__ = '0.15.7'
__version__ = '0.16.0'

from .config import *
from . import (
Expand Down
31 changes: 31 additions & 0 deletions forest/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,43 @@
import os
import string
import yaml
from dataclasses import dataclass
from collections import defaultdict
from collections.abc import Mapping
from forest.export import export


__all__ = []


@dataclass
class PluginSpec:
"""Data representation of plugin"""
entry_point: str


class Plugins(Mapping):
"""Specialist mapping between allowed keys and specs"""
def __init__(self, data):
allowed = ("feature",)
self.data = {}
for key, value in data.items():
if key in allowed:
self.data[key] = PluginSpec(**value)
else:
msg = f"{key} not in {allowed}"
raise Exception(msg)

def __getitem__(self, *args, **kwargs):
return self.data.__getitem__(*args, **kwargs)

def __len__(self, *args, **kwargs):
return self.data.__len__(*args, **kwargs)

def __iter__(self, *args, **kwargs):
return self.data.__iter__(*args, **kwargs)


class Viewport:
def __init__(self, lon_range, lat_range):
self.lon_range = lon_range
Expand Down Expand Up @@ -68,6 +98,7 @@ class Config(object):
"""
def __init__(self, data):
self.data = data
self.plugins = Plugins(self.data.get("plugins", {}))

def __repr__(self):
return "{}({})".format(
Expand Down
7 changes: 6 additions & 1 deletion forest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
layers,
db,
keys,
plugin,
presets,
redux,
rx,
Expand Down Expand Up @@ -45,7 +46,11 @@ def main(argv=None):
args.variables))

# Feature toggles
data.FEATURE_FLAGS = config.features
if "feature" in config.plugins:
features = plugin.call(config.plugins["feature"].entry_point)
else:
features = config.features
data.FEATURE_FLAGS = features

# Full screen map
viewport = config.default_viewport
Expand Down
9 changes: 9 additions & 0 deletions forest/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Simple plugin architecture"""
import importlib


def call(entry_point):
"""Call entry_point to run plugin"""
*parts, method = entry_point.split(".")
module = importlib.import_module(".".join(parts))
return getattr(module, method)()
22 changes: 22 additions & 0 deletions test/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,25 @@ def test_config_parser_use_web_map_tiles(data, expect):
def test_config_parser_features(data, expect):
config = forest.config.Config(data)
assert config.features["example"] == expect


def test_config_parser_plugin_entry_points():
config = forest.config.Config({
"plugins": {
"feature": {
"entry_point": "module.main"
}
}
})
assert config.plugins["feature"].entry_point == "module.main"


def test_config_parser_plugin_given_unsupported_key():
with pytest.raises(Exception):
forest.config.Config({
"plugins": {
"not_a_key": {
"entry_point": "module.main"
}
}
})

0 comments on commit bb17099

Please sign in to comment.