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
29 # For Python < 2.6, we use the "simplejson" add-on module
30 import simplejson
as json
32 # Python 2.6 already ships with a nice "json" module
36 class JsonConfigSubtree(object):
37 def __init__(self
, parent
, name
):
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
):
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
)
67 self
._parent
.__setattr
__(self
._attr
(name
), value
)
70 class JsonConfig(object):
73 def __init__(self
, data
=None, default
=None, on_key_changed
=None):
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)
95 callback: ('a.b', None, 10)
97 callback: ('a.b', 10, 11)
99 callback: ('x.y.z', None, [1, 2, 3])
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
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)
137 >>> c._restore(backup)
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
)
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
)]
158 data
, default
= work_queue
.pop()
159 for key
, value
in default
.iteritems():
161 # Copy defaults for missing key
162 data
[key
] = copy
.deepcopy(value
)
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
])
177 >>> c = JsonConfig('{"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
):
190 work_queue
.append(([], self
._data
))
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
]))
200 def __getattr__(self
, name
):
202 value
= self
._lookup
(name
)
203 if not isinstance(value
, dict):
208 return JsonConfigSubtree(self
, name
)
210 def __setattr__(self
, name
, value
):
211 if name
.startswith('_'):
212 object.__setattr
__(self
, name
, value
)
215 attrs
= name
.split('.')
216 target_dict
= self
._data
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
)
228 target
= target_dict
.get(attr
, None)
229 if target
is None or not isinstance(target
, dict):
230 target_dict
[attr
] = target
= {}