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 `._keys` of `Setting`\s."
246 def __init__(self, storage=None):
247 super(Config, self).__init__()
249 self._storage = storage
252 return '<{} {}>'.format(self.__class__.__name__, id(self))
255 super(Config, self).clear()
256 for s in self.settings:
257 # copy to avoid ambiguity with mutable defaults
258 self[s.name] = _copy.deepcopy(s.default)
260 def load(self, merge=False, **kwargs):
261 self._storage.load(config=self, merge=merge, **kwargs)
263 def save(self, merge=False, **kwargs):
264 self._storage.save(config=self, merge=merge, **kwargs)
266 def dump(self, help=False, prefix=''):
267 """Return all settings and their values as a string
269 >>> class MyConfig (Config):
273 ... help='I have a number behind my back...',
275 ... choices=[('one', 1), ('two', 2),
279 ... help='The number behind my back is odd.',
283 ... help='Number of guesses before epic failure.',
291 >>> print c.dump(help=True) # doctest: +NORMALIZE_WHITESPACE
292 number: one (I have a number behind my back... Default: one.
294 odd: yes (The number behind my back is odd. Default: yes.
296 guesses: 2 (Number of guesses before epic failure. Default: 2.)
299 for setting in self.settings:
303 if isinstance(setting, ConfigListSetting):
305 lines.append('{}{}:'.format(prefix, name))
306 for i,config in enumerate(value):
307 lines.append('{} {}:'.format(prefix, i))
309 config.dump(help=help, prefix=prefix+' '))
311 elif isinstance(setting, ConfigSetting):
312 if value is not None:
313 lines.append('{}{}:'.format(prefix, name))
314 lines.append(value.dump(help=help, prefix=prefix+' '))
316 value_string = setting.convert_to_text(self[name])
318 help_string = '\t({})'.format(setting.help())
321 lines.append('{}{}: {}{}'.format(
322 prefix, name, value_string, help_string))
324 _LOG.error('could not dump {} ({!r})'.format(name, value))
326 return '\n'.join(lines)