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 FloatListSetting (Setting):
182 """A named setting with a list of floating point values.
184 >>> s = FloatListSetting(name='floatlist')
187 >>> s.convert_to_text([1, 2.3])
189 >>> s.convert_from_text('4.5, -6.7') # doctest: +ELLIPSIS
191 >>> s.convert_to_text([])
193 >>> s.convert_from_text('')
196 def __init__(self, default=[], **kwargs):
197 super(FloatListSetting, self).__init__(default=default, **kwargs)
199 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])
217 class ConfigSetting (Setting):
218 """A setting that holds a pointer to a child `Config` class
220 This allows you to nest `Config`\s, which is a useful way to
221 contain complexity. In order to save such a config, the backend
222 must be able to handle hierarchical storage (possibly via
225 For example, a configurable AFM may contain a configurable piezo
226 scanner, as well as a configurable stepper motor.
228 def __init__(self, config_class=None, **kwargs):
229 super(ConfigSetting, self).__init__(**kwargs)
230 self.config_class = config_class
233 class ConfigListSetting (ConfigSetting):
234 """A setting that holds a list of child `Config` classes
236 For example, a piezo scanner with several axes.
238 def __init__(self, default=[], **kwargs):
239 super(ConfigListSetting, self).__init__(**kwargs)
243 """A class with a list of `Setting`\s.
245 `Config` instances store their values just like dictionaries, but
246 the attached list of `Settings`\s allows them to intelligently
247 dump, save, and load those values.
251 def __init__(self, storage=None):
252 super(Config, self).__init__()
254 self.set_storage(storage=storage)
257 return '<{} {}>'.format(self.__class__.__name__, id(self))
259 def set_storage(self, storage):
260 self._storage = storage
263 super(Config, self).clear()
264 for s in self.settings:
265 # copy to avoid ambiguity with mutable defaults
266 self[s.name] = _copy.deepcopy(s.default)
268 def load(self, merge=False, **kwargs):
269 self._storage.load(config=self, merge=merge, **kwargs)
271 def save(self, merge=False, **kwargs):
272 self._storage.save(config=self, merge=merge, **kwargs)
274 def dump(self, help=False, prefix=''):
275 """Return all settings and their values as a string
277 >>> class MyConfig (Config):
281 ... help='I have a number behind my back...',
283 ... choices=[('one', 1), ('two', 2),
287 ... help='The number behind my back is odd.',
291 ... help='Number of guesses before epic failure.',
299 >>> print c.dump(help=True) # doctest: +NORMALIZE_WHITESPACE
300 number: one (I have a number behind my back... Default: one.
302 odd: yes (The number behind my back is odd. Default: yes.
304 guesses: 2 (Number of guesses before epic failure. Default: 2.)
307 for setting in self.settings:
311 if isinstance(setting, ConfigListSetting):
313 lines.append('{}{}:'.format(prefix, name))
314 for i,config in enumerate(value):
315 lines.append('{} {}:'.format(prefix, i))
317 config.dump(help=help, prefix=prefix+' '))
319 elif isinstance(setting, ConfigSetting):
320 if value is not None:
321 lines.append('{}{}:'.format(prefix, name))
322 lines.append(value.dump(help=help, prefix=prefix+' '))
324 value_string = setting.convert_to_text(self[name])
326 help_string = '\t({})'.format(setting.help())
329 lines.append('{}{}: {}{}'.format(
330 prefix, name, value_string, help_string))
332 _LOG.error('could not dump {} ({!r})'.format(name, value))
334 return '\n'.join(lines)
336 def select_config(self, setting_name, attribute_value=None,
337 get_attribute=lambda value : value['name']):
338 """Select a `Config` instance from `ConfigListSetting` values
340 If your don't want to select `ConfigListSetting` items by
341 index, you can select them by matching another attribute. For
344 >>> from .test import TestConfig
346 >>> c['children'] = []
347 >>> for name in ['Jack', 'Jill']:
348 ... child = TestConfig()
349 ... child['name'] = name
350 ... c['children'].append(child)
351 >>> child = c.select_config('children', 'Jack')
352 >>> child # doctest: +ELLIPSIS
357 `get_attribute` defaults to returning `value['name']`, because
358 I expect that to be the most common case, but you can use
359 another function if neccessary, for example to drill down more
360 than one level. Here we select by grandchild name:
362 >>> for name in ['Jack', 'Jill']:
363 ... grandchild = TestConfig()
364 ... grandchild['name'] = name + ' Junior'
365 ... child = c.select_config('children', name)
366 ... child['children'] = [grandchild]
367 >>> get_grandchild = lambda value : value['children'][0]['name']
368 >>> get_grandchild(child)
370 >>> child = c.select_config('children', 'Jack Junior',
371 ... get_attribute=get_grandchild)
372 >>> child # doctest: +ELLIPSIS
377 setting_value = self.get(setting_name, None)
378 if not setting_value:
379 raise KeyError(setting_name)
380 for item in setting_value:
381 if get_attribute(item) == attribute_value:
383 raise KeyError(attribute_value)