1 # Copyright (C) 2011-2012 W. Trevor King <wking@tremily.us>
3 # This file is part of h5config.
5 # h5config is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
10 # h5config is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License along with
15 # h5config. If not, see <http://www.gnu.org/licenses/>.
17 """The basic h5config classes
22 from . import LOG as _LOG
25 class Setting (object):
26 "A named setting with arbitrary text values."
27 def __init__(self, name, help='', default=None):
30 self.default = default
33 return '<%s %s>' % (self.__class__.__name__, self.name)
39 ret = '%s Default: %s.' % (
40 self._help, self.convert_to_text(self.default))
43 def convert_from_text(self, value):
46 def convert_to_text(self, value):
50 class ChoiceSetting (Setting):
51 """A named setting with a limited number of possible values.
53 `choices` should be a list of `(config_file_value, Python value)`
56 >>> s = ChoiceSetting(name='bool',
57 ... choices=[('yes', True), ('no', False)])
58 >>> s.convert_from_text('yes')
60 >>> s.convert_to_text(True)
62 >>> s.convert_to_text('invalid')
63 Traceback (most recent call last):
67 'Default: yes. Choices: yes, no'
69 def __init__(self, choices=None, **kwargs):
70 if 'default' not in kwargs:
71 if None not in [keyval[1] for keyval in choices]:
72 kwargs['default'] = choices[0][1]
73 super(ChoiceSetting, self).__init__(**kwargs)
76 self.choices = choices
79 ret = '%s Choices: %s' % (
80 super(ChoiceSetting, self).help(),
81 ', '.join([key for key,value in self.choices]))
84 def convert_from_text(self, value):
85 return dict(self.choices)[value]
87 def convert_to_text(self, value):
88 for keyval in self.choices:
92 raise ValueError(value)
95 class BooleanSetting (ChoiceSetting):
96 """A named settubg that can be either true or false.
98 >>> s = BooleanSetting(name='bool')
100 >>> s.convert_from_text('yes')
102 >>> s.convert_to_text(True)
104 >>> s.convert_to_text('invalid')
105 Traceback (most recent call last):
109 'Default: no. Choices: yes, no'
111 def __init__(self, default=False, **kwargs):
112 super(BooleanSetting, self).__init__(
113 choices=[('yes', True), ('no', False)], default=default, **kwargs)
116 class NumericSetting (Setting):
117 """A named setting with numeric values.
119 Don't use this setting class. Use a more specific subclass, such
122 >>> s = NumericSetting(name='float')
125 >>> s.convert_to_text(13)
128 def __init__(self, default=0, **kwargs):
129 super(NumericSetting, self).__init__(default=default, **kwargs)
131 def convert_to_text(self, value):
134 def convert_from_text(self, value):
135 if value in [None, 'None']:
137 return self._convert_from_text(value)
139 def _convert_from_text(self, value):
140 raise NotImplementedError()
143 class IntegerSetting (NumericSetting):
144 """A named setting with integer values.
146 >>> s = IntegerSetting(name='int')
149 >>> s.convert_from_text('8')
152 def __init__(self, default=1, **kwargs):
153 super(IntegerSetting, self).__init__(default=default, **kwargs)
155 def _convert_from_text(self, value):
159 class FloatSetting (NumericSetting):
160 """A named setting with floating point values.
162 >>> s = FloatSetting(name='float')
165 >>> s.convert_from_text('8')
168 ... s.convert_from_text('invalid')
169 ... except ValueError as e:
170 ... print('caught a ValueError')
173 def __init__(self, default=1.0, **kwargs):
174 super(FloatSetting, self).__init__(default=default, **kwargs)
176 def _convert_from_text(self, value):
180 class ListSetting(Setting):
181 """A named setting with a list of string values.
183 >>> s = ListSetting(name='list')
186 >>> s.convert_to_text(['abc', 'def'])
188 >>> s.convert_from_text('uvw, xyz')
190 >>> s.convert_to_text([])
192 >>> s.convert_from_text('')
195 def __init__(self, default=None, **kwargs):
198 super(ListSetting, self).__init__(default=default, **kwargs)
200 def _convert_from_text(self, value):
203 def convert_from_text(self, value):
206 elif value in ['', []]:
208 return [self._convert_from_text(x) for x in value.split(',')]
210 def convert_to_text(self, value):
213 return ','.join([str(x) for x in value])
215 class IntegerListSetting (ListSetting):
216 """A named setting with a list of integer point values.
218 >>> s = IntegerListSetting(name='integerlist')
221 >>> s.convert_to_text([1, 3])
223 >>> s.convert_from_text('4, -6')
225 >>> s.convert_to_text([])
227 >>> s.convert_from_text('')
230 def _convert_from_text(self, value):
236 class FloatListSetting (ListSetting):
237 """A named setting with a list of floating point values.
239 >>> s = FloatListSetting(name='floatlist')
242 >>> s.convert_to_text([1, 2.3])
244 >>> s.convert_from_text('4.5, -6.7') # doctest: +ELLIPSIS
246 >>> s.convert_to_text([])
248 >>> s.convert_from_text('')
251 def _convert_from_text(self, value):
257 class ConfigSetting (Setting):
258 """A setting that holds a pointer to a child `Config` class
260 This allows you to nest `Config`\s, which is a useful way to
261 contain complexity. In order to save such a config, the backend
262 must be able to handle hierarchical storage (possibly via
265 For example, a configurable AFM may contain a configurable piezo
266 scanner, as well as a configurable stepper motor.
268 def __init__(self, config_class=None, **kwargs):
269 super(ConfigSetting, self).__init__(**kwargs)
270 self.config_class = config_class
273 class ConfigListSetting (ConfigSetting):
274 """A setting that holds a list of child `Config` classes
276 For example, a piezo scanner with several axes.
278 def __init__(self, default=[], **kwargs):
279 super(ConfigListSetting, self).__init__(**kwargs)
283 """A class with a list of `Setting`\s.
285 `Config` instances store their values just like dictionaries, but
286 the attached list of `Settings`\s allows them to intelligently
287 dump, save, and load those values.
291 def __init__(self, storage=None):
292 super(Config, self).__init__()
294 self.set_storage(storage=storage)
297 return '<{} {}>'.format(self.__class__.__name__, id(self))
299 def set_storage(self, storage):
300 self._storage = storage
303 super(Config, self).clear()
304 for s in self.settings:
305 # copy to avoid ambiguity with mutable defaults
306 self[s.name] = _copy.deepcopy(s.default)
308 def load(self, merge=False, **kwargs):
309 self._storage.load(config=self, merge=merge, **kwargs)
311 def save(self, merge=False, **kwargs):
312 self._storage.save(config=self, merge=merge, **kwargs)
314 def dump(self, help=False, prefix=''):
315 """Return all settings and their values as a string
317 >>> class MyConfig (Config):
321 ... help='I have a number behind my back...',
323 ... choices=[('one', 1), ('two', 2),
327 ... help='The number behind my back is odd.',
331 ... help='Number of guesses before epic failure.',
339 >>> print(c.dump(help=True)) # doctest: +NORMALIZE_WHITESPACE
340 number: one (I have a number behind my back... Default: one.
342 odd: yes (The number behind my back is odd. Default: yes.
344 guesses: 2 (Number of guesses before epic failure. Default: 2.)
347 for setting in self.settings:
351 if isinstance(setting, ConfigListSetting):
353 lines.append('{}{}:'.format(prefix, name))
354 for i,config in enumerate(value):
355 lines.append('{} {}:'.format(prefix, i))
357 config.dump(help=help, prefix=prefix+' '))
359 elif isinstance(setting, ConfigSetting):
360 if value is not None:
361 lines.append('{}{}:'.format(prefix, name))
362 lines.append(value.dump(help=help, prefix=prefix+' '))
364 value_string = setting.convert_to_text(self[name])
366 help_string = '\t({})'.format(setting.help())
369 lines.append('{}{}: {}{}'.format(
370 prefix, name, value_string, help_string))
372 _LOG.error('could not dump {} ({!r})'.format(name, value))
374 return '\n'.join(lines)
376 def select_config(self, setting_name, attribute_value=None,
377 get_attribute=lambda value : value['name']):
378 """Select a `Config` instance from `ConfigListSetting` values
380 If your don't want to select `ConfigListSetting` items by
381 index, you can select them by matching another attribute. For
384 >>> from .test import TestConfig
386 >>> c['children'] = []
387 >>> for name in ['Jack', 'Jill']:
388 ... child = TestConfig()
389 ... child['name'] = name
390 ... c['children'].append(child)
391 >>> child = c.select_config('children', 'Jack')
392 >>> child # doctest: +ELLIPSIS
397 `get_attribute` defaults to returning `value['name']`, because
398 I expect that to be the most common case, but you can use
399 another function if neccessary, for example to drill down more
400 than one level. Here we select by grandchild name:
402 >>> for name in ['Jack', 'Jill']:
403 ... grandchild = TestConfig()
404 ... grandchild['name'] = name + ' Junior'
405 ... child = c.select_config('children', name)
406 ... child['children'] = [grandchild]
407 >>> get_grandchild = lambda value : value['children'][0]['name']
408 >>> get_grandchild(child)
410 >>> child = c.select_config('children', 'Jack Junior',
411 ... get_attribute=get_grandchild)
412 >>> child # doctest: +ELLIPSIS
417 setting_value = self.get(setting_name, None)
418 if not setting_value:
419 raise KeyError(setting_name)
420 for item in setting_value:
421 if get_attribute(item) == attribute_value:
423 raise KeyError(attribute_value)