Merge lp:~tvansteenburgh/charm-helpers/config-wrapper into lp:charm-helpers

Proposed by Tim Van Steenburgh
Status: Merged
Merged at revision: 152
Proposed branch: lp:~tvansteenburgh/charm-helpers/config-wrapper
Merge into: lp:charm-helpers
Diff against target: 241 lines (+199/-1)
2 files modified
charmhelpers/core/hookenv.py (+98/-1)
tests/core/test_hookenv.py (+101/-0)
To merge this branch: bzr merge lp:~tvansteenburgh/charm-helpers/config-wrapper
Reviewer Review Type Date Requested Status
Marco Ceppi Approve
Benjamin Saller (community) Approve
Review via email: mp+218497@code.launchpad.net

Description of the change

Add Config wrapper around charm config dictionary.

Backwards-compatible with previous config dictionary. New wrapper
tracks which config values have changed since the previous hook
invocation and allows the user to query for the previous value.

To post a comment you must log in.
Revision history for this message
Benjamin Saller (bcsaller) wrote :

LGTM +1 Thanks for adding this.

Some optional minors, prev_dict might be _prev_dict. It is private.

in changed
   call self.previous(key) rather than directly access prev_dict

use CONFIG_FILE_NAME as the default path in __init__, but allow it to be modified, that might make some of the patching in the tests go away

review: Approve
146. By Tim Van Steenburgh

Minor changes per code review.

Revision history for this message
Marco Ceppi (marcoceppi) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmhelpers/core/hookenv.py'
2--- charmhelpers/core/hookenv.py 2014-01-27 10:29:55 +0000
3+++ charmhelpers/core/hookenv.py 2014-05-06 20:43:25 +0000
4@@ -155,6 +155,100 @@
5 return os.path.basename(sys.argv[0])
6
7
8+class Config(dict):
9+ """A Juju charm config dictionary that can write itself to
10+ disk (as json) and track which values have changed since
11+ the previous hook invocation.
12+
13+ Do not instantiate this object directly - instead call
14+ ``hookenv.config()``
15+
16+ Example usage::
17+
18+ >>> # inside a hook
19+ >>> from charmhelpers.core import hookenv
20+ >>> config = hookenv.config()
21+ >>> config['foo']
22+ 'bar'
23+ >>> config['mykey'] = 'myval'
24+ >>> config.save()
25+
26+
27+ >>> # user runs `juju set mycharm foo=baz`
28+ >>> # now we're inside subsequent config-changed hook
29+ >>> config = hookenv.config()
30+ >>> config['foo']
31+ 'baz'
32+ >>> # test to see if this val has changed since last hook
33+ >>> config.changed('foo')
34+ True
35+ >>> # what was the previous value?
36+ >>> config.previous('foo')
37+ 'bar'
38+ >>> # keys/values that we add are preserved across hooks
39+ >>> config['mykey']
40+ 'myval'
41+ >>> # don't forget to save at the end of hook!
42+ >>> config.save()
43+
44+ """
45+ CONFIG_FILE_NAME = '.juju-persistent-config'
46+
47+ def __init__(self, *args, **kw):
48+ super(Config, self).__init__(*args, **kw)
49+ self._prev_dict = None
50+ self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
51+ if os.path.exists(self.path):
52+ self.load_previous()
53+
54+ def load_previous(self, path=None):
55+ """Load previous copy of config from disk so that current values
56+ can be compared to previous values.
57+
58+ :param path:
59+
60+ File path from which to load the previous config. If `None`,
61+ config is loaded from the default location. If `path` is
62+ specified, subsequent `save()` calls will write to the same
63+ path.
64+
65+ """
66+ self.path = path or self.path
67+ with open(self.path) as f:
68+ self._prev_dict = json.load(f)
69+
70+ def changed(self, key):
71+ """Return true if the value for this key has changed since
72+ the last save.
73+
74+ """
75+ if self._prev_dict is None:
76+ return True
77+ return self.previous(key) != self.get(key)
78+
79+ def previous(self, key):
80+ """Return previous value for this key, or None if there
81+ is no "previous" value.
82+
83+ """
84+ if self._prev_dict:
85+ return self._prev_dict.get(key)
86+ return None
87+
88+ def save(self):
89+ """Save this config to disk.
90+
91+ Preserves items in _prev_dict that do not exist in self.
92+
93+ """
94+ if self._prev_dict:
95+ for k, v in self._prev_dict.iteritems():
96+ if k not in self:
97+ self[k] = v
98+ with open(self.path, 'w') as f:
99+ json.dump(self, f)
100+
101+
102 @cached
103 def config(scope=None):
104 """Juju charm configuration"""
105@@ -163,7 +257,10 @@
106 config_cmd_line.append(scope)
107 config_cmd_line.append('--format=json')
108 try:
109- return json.loads(subprocess.check_output(config_cmd_line))
110+ config_data = json.loads(subprocess.check_output(config_cmd_line))
111+ if scope is not None:
112+ return config_data
113+ return Config(config_data)
114 except ValueError:
115 return None
116
117
118=== modified file 'tests/core/test_hookenv.py'
119--- tests/core/test_hookenv.py 2013-11-06 03:30:37 +0000
120+++ tests/core/test_hookenv.py 2014-05-06 20:43:25 +0000
121@@ -1,5 +1,8 @@
122+import os
123 import json
124 from subprocess import CalledProcessError
125+import shutil
126+import tempfile
127
128 import cPickle as pickle
129 from mock import patch, call, mock_open
130@@ -25,6 +28,92 @@
131 """
132
133
134+class ConfigTest(TestCase):
135+ def setUp(self):
136+ super(ConfigTest, self).setUp()
137+
138+ self.charm_dir = tempfile.mkdtemp()
139+ self.addCleanup(lambda: shutil.rmtree(self.charm_dir))
140+
141+ patcher = patch.object(hookenv, 'charm_dir', lambda: self.charm_dir)
142+ self.addCleanup(patcher.stop)
143+ patcher.start()
144+
145+ def test_init(self):
146+ d = dict(foo='bar')
147+ c = hookenv.Config(d)
148+
149+ self.assertEqual(c['foo'], 'bar')
150+ self.assertEqual(c._prev_dict, None)
151+
152+ def test_load_previous(self):
153+ d = dict(foo='bar')
154+ c = hookenv.Config()
155+
156+ with open(c.path, 'w') as f:
157+ json.dump(d, f)
158+
159+ c.load_previous()
160+ self.assertEqual(c._prev_dict, d)
161+
162+ def test_load_previous_alternate_path(self):
163+ d = dict(foo='bar')
164+ c = hookenv.Config()
165+
166+ alt_path = os.path.join(self.charm_dir, '.alt-config')
167+ with open(alt_path, 'w') as f:
168+ json.dump(d, f)
169+
170+ c.load_previous(path=alt_path)
171+ self.assertEqual(c._prev_dict, d)
172+ self.assertEqual(c.path, alt_path)
173+
174+ def test_changed_without_prev_dict(self):
175+ d = dict(foo='bar')
176+ c = hookenv.Config(d)
177+
178+ self.assertTrue(c.changed('foo'))
179+
180+ def test_changed_with_prev_dict(self):
181+ c = hookenv.Config(dict(foo='bar', a='b'))
182+ c.save()
183+ c = hookenv.Config(dict(foo='baz', a='b'))
184+
185+ self.assertTrue(c.changed('foo'))
186+ self.assertFalse(c.changed('a'))
187+
188+ def test_previous_without_prev_dict(self):
189+ c = hookenv.Config()
190+
191+ self.assertEqual(c.previous('foo'), None)
192+
193+ def test_previous_with_prev_dict(self):
194+ c = hookenv.Config(dict(foo='bar'))
195+ c.save()
196+ c = hookenv.Config(dict(foo='baz', a='b'))
197+
198+ self.assertEqual(c.previous('foo'), 'bar')
199+ self.assertEqual(c.previous('a'), None)
200+
201+ def test_save_without_prev_dict(self):
202+ c = hookenv.Config(dict(foo='bar'))
203+ c.save()
204+
205+ with open(c.path, 'r') as f:
206+ self.assertEqual(c, json.load(f))
207+ self.assertEqual(c, dict(foo='bar'))
208+
209+ def test_save_with_prev_dict(self):
210+ c = hookenv.Config(dict(foo='bar'))
211+ c.save()
212+ c = hookenv.Config(dict(a='b'))
213+ c.save()
214+
215+ with open(c.path, 'r') as f:
216+ self.assertEqual(c, json.load(f))
217+ self.assertEqual(c, dict(foo='bar', a='b'))
218+
219+
220 class SerializableTest(TestCase):
221 def test_serializes_object_to_json(self):
222 foo = {
223@@ -165,6 +254,18 @@
224 self.assertEqual(result, None)
225 check_output.assert_called_with(['config-get', 'baz', '--format=json'])
226
227+ @patch('charmhelpers.core.hookenv.charm_dir')
228+ @patch('subprocess.check_output')
229+ def test_gets_config_without_scope(self, check_output, charm_dir):
230+ check_output.return_value = json.dumps(dict(foo='bar'))
231+ charm_dir.side_effect = tempfile.mkdtemp
232+
233+ result = hookenv.config()
234+
235+ self.assertIsInstance(result, hookenv.Config)
236+ self.assertEqual(result['foo'], 'bar')
237+ check_output.assert_called_with(['config-get', '--format=json'])
238+
239 @patch('charmhelpers.core.hookenv.os')
240 def test_gets_the_local_unit(self, os_):
241 os_.environ = {

Subscribers

People subscribed via source and target branches