JSON Config: Convert float to int (bug 1590)
[gpodder.git] / src / gpodder / jsonconfig.py
blob419f63f924e051cef2f345fcf9adb4d6f2ac9479
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2012 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 # jsonconfig.py -- JSON Config Backend
23 # Thomas Perl <thp@gpodder.org> 2012-01-18
26 import copy
28 try:
29 # For Python < 2.6, we use the "simplejson" add-on module
30 import simplejson as json
31 except ImportError:
32 # Python 2.6 already ships with a nice "json" module
33 import json
36 class JsonConfigSubtree(object):
37 def __init__(self, parent, name):
38 self._parent = parent
39 self._name = name
41 def __repr__(self):
42 return '<Subtree %r of JsonConfig>' % (self._name,)
44 def _attr(self, name):
45 return '.'.join((self._name, name))
47 def __getitem__(self, name):
48 return self._parent._lookup(self._name).__getitem__(name)
50 def __delitem__(self, name):
51 self._parent._lookup(self._name).__delitem__(name)
53 def __setitem__(self, name, value):
54 self._parent._lookup(self._name).__setitem__(name, value)
56 def __getattr__(self, name):
57 if name == 'keys':
58 # Kludge for using dict() on a JsonConfigSubtree
59 return getattr(self._parent._lookup(self._name), name)
61 return getattr(self._parent, self._attr(name))
63 def __setattr__(self, name, value):
64 if name.startswith('_'):
65 object.__setattr__(self, name, value)
66 else:
67 self._parent.__setattr__(self._attr(name), value)
70 class JsonConfig(object):
71 _INDENT = 2
73 def __init__(self, data=None, default=None, on_key_changed=None):
74 """
75 Create a new JsonConfig object
77 data: A JSON string that contains the data to load (optional)
78 default: A dict that contains default config values (optional)
79 on_key_changed: Callback when a value changes (optional)
81 The signature of on_key_changed looks like this:
83 func(name, old_value, new_value)
85 name: The key name, e.g. "ui.gtk.show_toolbar"
86 old_value: The old value, e.g. False
87 new_value: The new value, e.g. True
89 For newly-set keys, on_key_changed is also called. In this case,
90 None will be the old_value:
92 >>> def callback(*args): print 'callback:', args
93 >>> c = JsonConfig(on_key_changed=callback)
94 >>> c.a.b = 10
95 callback: ('a.b', None, 10)
96 >>> c.a.b = 11
97 callback: ('a.b', 10, 11)
98 >>> c.x.y.z = [1,2,3]
99 callback: ('x.y.z', None, [1, 2, 3])
100 >>> c.x.y.z = 42
101 callback: ('x.y.z', [1, 2, 3], 42)
103 Please note that dict-style access will not call on_key_changed:
105 >>> def callback(*args): print 'callback:', args
106 >>> c = JsonConfig(on_key_changed=callback)
107 >>> c.a.b = 1 # This works as expected
108 callback: ('a.b', None, 1)
109 >>> c.a['c'] = 10 # This doesn't call on_key_changed!
110 >>> del c.a['c'] # This also doesn't call on_key_changed!
112 self._default = default
113 self._data = copy.deepcopy(self._default) or {}
114 self._on_key_changed = on_key_changed
115 if data is not None:
116 self._restore(data)
118 def _restore(self, backup):
120 Restore a previous state saved with repr()
122 This function allows you to "snapshot" the current values of
123 the configuration and reload them later on. Any missing
124 default values will be added on top of the restored config.
126 Returns True if new keys from the default config have been added,
127 False if no keys have been added (backup contains all default keys)
129 >>> c = JsonConfig()
130 >>> c.a.b = 10
131 >>> backup = repr(c)
132 >>> print c.a.b
134 >>> c.a.b = 11
135 >>> print c.a.b
137 >>> c._restore(backup)
138 False
139 >>> print c.a.b
142 self._data = json.loads(backup)
143 # Add newly-added default configuration options
144 if self._default is not None:
145 return self._merge_keys(self._default)
147 return False
149 def _merge_keys(self, merge_source):
150 """Merge keys from merge_source into this config object
152 Return True if new keys were merged, False otherwise
154 added_new_key = False
155 # Recurse into the data and add missing items
156 work_queue = [(self._data, merge_source)]
157 while work_queue:
158 data, default = work_queue.pop()
159 for key, value in default.iteritems():
160 if key not in data:
161 # Copy defaults for missing key
162 data[key] = copy.deepcopy(value)
163 added_new_key = True
164 elif isinstance(value, dict):
165 # Recurse into sub-dictionaries
166 work_queue.append((data[key], value))
167 elif type(value) != type(data[key]):
168 # Type mismatch of current value and default
169 if type(value) == int and type(data[key]) == float:
170 # Convert float to int if default value is int
171 data[key] = int(data[key])
173 return added_new_key
175 def __repr__(self):
177 >>> c = JsonConfig('{"a": 1}')
178 >>> print c
180 "a": 1
183 return json.dumps(self._data, indent=self._INDENT)
185 def _lookup(self, name):
186 return reduce(lambda d, k: d[k], name.split('.'), self._data)
188 def _keys_iter(self):
189 work_queue = []
190 work_queue.append(([], self._data))
191 while work_queue:
192 path, data = work_queue.pop(0)
194 if isinstance(data, dict):
195 for key in sorted(data.keys()):
196 work_queue.append((path + [key], data[key]))
197 else:
198 yield '.'.join(path)
200 def __getattr__(self, name):
201 try:
202 value = self._lookup(name)
203 if not isinstance(value, dict):
204 return value
205 except KeyError:
206 pass
208 return JsonConfigSubtree(self, name)
210 def __setattr__(self, name, value):
211 if name.startswith('_'):
212 object.__setattr__(self, name, value)
213 return
215 attrs = name.split('.')
216 target_dict = self._data
218 while attrs:
219 attr = attrs.pop(0)
220 if not attrs:
221 old_value = target_dict.get(attr, None)
222 if old_value != value or attr not in target_dict:
223 target_dict[attr] = value
224 if self._on_key_changed is not None:
225 self._on_key_changed(name, old_value, value)
226 break
228 target = target_dict.get(attr, None)
229 if target is None or not isinstance(target, dict):
230 target_dict[attr] = target = {}
231 target_dict = target