Skip to content

Commit 92e515b

Browse files
committed
add docs and utility methods to SettingsDict
* Document the new SettingsDict module that allows overriding MFR config via environment variables. * Add `get_bool` and `get_nullable` methods to cast stringy environment variables to Booleans and Nones.
1 parent b8c570d commit 92e515b

File tree

1 file changed

+60
-3
lines changed

1 file changed

+60
-3
lines changed

mfr/settings.py

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,77 @@
55

66

77
class SettingsDict(dict):
8+
"""Allow overriding on-disk config via environment variables. Normal config is done with a
9+
hierarchical dict::
10+
11+
"SERVER_CONFIG": {
12+
"HOST": "http://localhost:7777"
13+
}
14+
15+
``HOST`` can be retrieved in the python code with::
16+
17+
config = SettingsDict(json.load('local-config.json'))
18+
server_cfg = config.child('SERVER_CONFIG')
19+
host = server_cfg.get('HOST')
20+
21+
To override a value, join all of the parent keys and the child keys with an underscore::
22+
23+
$ SERVER_CONFIG_HOST='http://foo.bar.com' invoke server
24+
25+
Nested dicts can be handled with the ``.child()`` method. Config keys will be all parent keys
26+
joined by underscores::
27+
28+
"SERVER_CONFIG": {
29+
"ANALYTICS": {
30+
"PROJECT_ID": "foo"
31+
}
32+
}
33+
34+
The corresponding envvar for ``PROJECT_ID`` would be ``SERVER_CONFIG_ANALYTICS_PROJECT_ID``.
35+
"""
836

937
def __init__(self, *args, parent=None, **kwargs):
1038
self.parent = parent
1139
super().__init__(*args, **kwargs)
1240

1341
def get(self, key, default=None):
14-
env = '{}_{}'.format(self.parent, key) if self.parent else key
42+
"""Fetch a config value for ``key`` from the settings. First checks the env, then the
43+
on-disk config. If neither exists, returns ``default``."""
44+
env = self.full_key(key)
1545
if env in os.environ:
1646
return os.environ.get(env)
1747
return super().get(key, default)
1848

49+
def get_bool(self, key, default=None):
50+
"""Fetch a config value and interpret as a bool. Since envvars are always strings,
51+
interpret '0' and the empty string as False and '1' as True. Anything else is probably
52+
an acceident, so die screaming."""
53+
value = self.get(key, default)
54+
if value in [False, 0, '0', '']:
55+
retval = False
56+
elif value in [True, 1, '1']:
57+
retval = True
58+
else:
59+
raise Exception(
60+
'{} should be a truthy value, but instead we got {}'.format(
61+
self.full_key(key), value
62+
)
63+
)
64+
return retval
65+
66+
def get_nullable(self, key, default=None):
67+
"""Fetch a config value and interpret the empty string as None. Useful for external code
68+
that expects an explicit None."""
69+
value = self.get(key, default)
70+
return None if value == '' else value
71+
72+
def full_key(self, key):
73+
"""The name of the envvar which corresponds to this key."""
74+
return '{}_{}'.format(self.parent, key) if self.parent else key
75+
1976
def child(self, key):
20-
env = '{}_{}'.format(self.parent, key) if self.parent else key
21-
return SettingsDict(self.get(key, {}), parent=env)
77+
"""Fetch a sub-dict of the current dict."""
78+
return SettingsDict(self.get(key, {}), parent=self.full_key(key))
2279

2380

2481
PROJECT_NAME = 'mfr'

0 commit comments

Comments
 (0)