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 attribute_name='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 `attribute_value` defaults to `name`, because I expect that to
358 be the most common case.
360 setting_value = self[setting_name]
361 if not setting_value:
362 raise KeyError(setting_value)
363 for item in setting_value:
364 if item[attribute_name] == attribute_value:
366 raise KeyError(attribute_value)