1 # Copyright (C) 2011 W. Trevor King <wking@drexel.edu>
3 # This file is part of h5config.
5 # h5config is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation, either version 3 of the License, or (at your
8 # option) any later version.
10 # h5config is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 # General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with h5config. If not, see <http://www.gnu.org/licenses/>.
18 """The basic h5config classes
23 from . import LOG as _LOG
26 class Setting (object):
27 "A named setting with arbitrary text values."
28 def __init__(self, name, help='', default=None):
31 self.default = default
34 return '<%s %s>' % (self.__class__.__name__, self.name)
40 ret = '%s Default: %s.' % (
41 self._help, self.convert_to_text(self.default))
44 def convert_from_text(self, value):
47 def convert_to_text(self, value):
51 class ChoiceSetting (Setting):
52 """A named setting with a limited number of possible values.
54 `choices` should be a list of `(config_file_value, Python value)`
57 >>> s = ChoiceSetting(name='bool',
58 ... choices=[('yes', True), ('no', False)])
59 >>> s.convert_from_text('yes')
61 >>> s.convert_to_text(True)
63 >>> s.convert_to_text('invalid')
64 Traceback (most recent call last):
68 'Default: yes. Choices: yes, no'
70 def __init__(self, choices=None, **kwargs):
71 if 'default' not in kwargs:
72 if None not in [keyval[1] for keyval in choices]:
73 kwargs['default'] = choices[0][1]
74 super(ChoiceSetting, self).__init__(**kwargs)
77 self.choices = choices
80 ret = '%s Choices: %s' % (
81 super(ChoiceSetting, self).help(),
82 ', '.join([key for key,value in self.choices]))
85 def convert_from_text(self, value):
86 return dict(self.choices)[value]
88 def convert_to_text(self, value):
89 for keyval in self.choices:
93 raise ValueError(value)
96 class BooleanSetting (ChoiceSetting):
97 """A named settubg that can be either true or false.
99 >>> s = BooleanSetting(name='bool')
101 >>> s.convert_from_text('yes')
103 >>> s.convert_to_text(True)
105 >>> s.convert_to_text('invalid')
106 Traceback (most recent call last):
110 'Default: no. Choices: yes, no'
112 def __init__(self, default=False, **kwargs):
113 super(BooleanSetting, self).__init__(
114 choices=[('yes', True), ('no', False)], default=default, **kwargs)
117 class NumericSetting (Setting):
118 """A named setting with numeric values.
120 Don't use this setting class. Use a more specific subclass, such
123 >>> s = NumericSetting(name='float')
126 >>> s.convert_to_text(13)
129 def __init__(self, default=0, **kwargs):
130 super(NumericSetting, self).__init__(default=default, **kwargs)
132 def convert_to_text(self, value):
135 def convert_from_text(self, value):
136 if value in [None, 'None']:
138 return self._convert_from_text(value)
140 def _convert_from_text(self, value):
141 raise NotImplementedError()
144 class IntegerSetting (NumericSetting):
145 """A named setting with integer values.
147 >>> s = IntegerSetting(name='int')
150 >>> s.convert_from_text('8')
153 def __init__(self, default=1, **kwargs):
154 super(IntegerSetting, self).__init__(default=default, **kwargs)
156 def _convert_from_text(self, value):
160 class FloatSetting (NumericSetting):
161 """A named setting with floating point values.
163 >>> s = FloatSetting(name='float')
166 >>> s.convert_from_text('8')
169 ... s.convert_from_text('invalid')
170 ... except ValueError, e:
171 ... print 'caught a ValueError'
174 def __init__(self, default=1.0, **kwargs):
175 super(FloatSetting, self).__init__(default=default, **kwargs)
177 def _convert_from_text(self, value):
181 class ListSetting(Setting):
182 """A named setting with a list of string values.
184 >>> s = ListSetting(name='list')
187 >>> s.convert_to_text(['abc', 'def'])
189 >>> s.convert_from_text('uvw, xyz')
191 >>> s.convert_to_text([])
193 >>> s.convert_from_text('')
196 def __init__(self, default=None, **kwargs):
199 super(ListSetting, self).__init__(default=default, **kwargs)
201 def _convert_from_text(self, value):
204 def convert_from_text(self, value):
207 elif value in ['', []]:
209 return [self._convert_from_text(x) for x in value.split(',')]
211 def convert_to_text(self, value):
214 return ','.join([str(x) for x in value])
216 class IntegerListSetting (ListSetting):
217 """A named setting with a list of integer point values.
219 >>> s = IntegerListSetting(name='integerlist')
222 >>> s.convert_to_text([1, 3])
224 >>> s.convert_from_text('4, -6')
226 >>> s.convert_to_text([])
228 >>> s.convert_from_text('')
231 def _convert_from_text(self, value):
237 class FloatListSetting (ListSetting):
238 """A named setting with a list of floating point values.
240 >>> s = FloatListSetting(name='floatlist')
243 >>> s.convert_to_text([1, 2.3])
245 >>> s.convert_from_text('4.5, -6.7') # doctest: +ELLIPSIS
247 >>> s.convert_to_text([])
249 >>> s.convert_from_text('')
252 def _convert_from_text(self, value):
258 class ConfigSetting (Setting):
259 """A setting that holds a pointer to a child `Config` class
261 This allows you to nest `Config`\s, which is a useful way to
262 contain complexity. In order to save such a config, the backend
263 must be able to handle hierarchical storage (possibly via
266 For example, a configurable AFM may contain a configurable piezo
267 scanner, as well as a configurable stepper motor.
269 def __init__(self, config_class=None, **kwargs):
270 super(ConfigSetting, self).__init__(**kwargs)
271 self.config_class = config_class
274 class ConfigListSetting (ConfigSetting):
275 """A setting that holds a list of child `Config` classes
277 For example, a piezo scanner with several axes.
279 def __init__(self, default=[], **kwargs):
280 super(ConfigListSetting, self).__init__(**kwargs)
284 """A class with a list of `Setting`\s.
286 `Config` instances store their values just like dictionaries, but
287 the attached list of `Settings`\s allows them to intelligently
288 dump, save, and load those values.
292 def __init__(self, storage=None):
293 super(Config, self).__init__()
295 self.set_storage(storage=storage)
298 return '<{} {}>'.format(self.__class__.__name__, id(self))
300 def set_storage(self, storage):
301 self._storage = storage
304 super(Config, self).clear()
305 for s in self.settings:
306 # copy to avoid ambiguity with mutable defaults
307 self[s.name] = _copy.deepcopy(s.default)
309 def load(self, merge=False, **kwargs):
310 self._storage.load(config=self, merge=merge, **kwargs)
312 def save(self, merge=False, **kwargs):
313 self._storage.save(config=self, merge=merge, **kwargs)
315 def dump(self, help=False, prefix=''):
316 """Return all settings and their values as a string
318 >>> class MyConfig (Config):
322 ... help='I have a number behind my back...',
324 ... choices=[('one', 1), ('two', 2),
328 ... help='The number behind my back is odd.',
332 ... help='Number of guesses before epic failure.',
340 >>> print c.dump(help=True) # doctest: +NORMALIZE_WHITESPACE
341 number: one (I have a number behind my back... Default: one.
343 odd: yes (The number behind my back is odd. Default: yes.
345 guesses: 2 (Number of guesses before epic failure. Default: 2.)
348 for setting in self.settings:
352 if isinstance(setting, ConfigListSetting):
354 lines.append('{}{}:'.format(prefix, name))
355 for i,config in enumerate(value):
356 lines.append('{} {}:'.format(prefix, i))
358 config.dump(help=help, prefix=prefix+' '))
360 elif isinstance(setting, ConfigSetting):
361 if value is not None:
362 lines.append('{}{}:'.format(prefix, name))
363 lines.append(value.dump(help=help, prefix=prefix+' '))
365 value_string = setting.convert_to_text(self[name])
367 help_string = '\t({})'.format(setting.help())
370 lines.append('{}{}: {}{}'.format(
371 prefix, name, value_string, help_string))
373 _LOG.error('could not dump {} ({!r})'.format(name, value))
375 return '\n'.join(lines)
377 def select_config(self, setting_name, attribute_value=None,
378 get_attribute=lambda value : value['name']):
379 """Select a `Config` instance from `ConfigListSetting` values
381 If your don't want to select `ConfigListSetting` items by
382 index, you can select them by matching another attribute. For
385 >>> from .test import TestConfig
387 >>> c['children'] = []
388 >>> for name in ['Jack', 'Jill']:
389 ... child = TestConfig()
390 ... child['name'] = name
391 ... c['children'].append(child)
392 >>> child = c.select_config('children', 'Jack')
393 >>> child # doctest: +ELLIPSIS
398 `get_attribute` defaults to returning `value['name']`, because
399 I expect that to be the most common case, but you can use
400 another function if neccessary, for example to drill down more
401 than one level. Here we select by grandchild name:
403 >>> for name in ['Jack', 'Jill']:
404 ... grandchild = TestConfig()
405 ... grandchild['name'] = name + ' Junior'
406 ... child = c.select_config('children', name)
407 ... child['children'] = [grandchild]
408 >>> get_grandchild = lambda value : value['children'][0]['name']
409 >>> get_grandchild(child)
411 >>> child = c.select_config('children', 'Jack Junior',
412 ... get_attribute=get_grandchild)
413 >>> child # doctest: +ELLIPSIS
418 setting_value = self.get(setting_name, None)
419 if not setting_value:
420 raise KeyError(setting_name)
421 for item in setting_value:
422 if get_attribute(item) == attribute_value:
424 raise KeyError(attribute_value)