Ran update-copyright.py.
[h5config.git] / h5config / config.py
1 # Copyright (C) 2011-2012 W. Trevor King <wking@tremily.us>
2 #
3 # This file is part of h5config.
4 #
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
8 # version.
9 #
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.
13 #
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/>.
16
17 """The basic h5config classes
18 """
19
20 import copy as _copy
21
22 from . import LOG as _LOG
23
24
25 class Setting (object):
26     "A named setting with arbitrary text values."
27     def  __init__(self, name, help='', default=None):
28         self.name = name
29         self._help = help
30         self.default = default
31
32     def __str__(self):
33         return '<%s %s>' % (self.__class__.__name__, self.name)
34
35     def __repr__(self):
36         return self.__str__()
37
38     def help(self):
39         ret = '%s  Default: %s.' % (
40             self._help, self.convert_to_text(self.default))
41         return ret.strip()
42
43     def convert_from_text(self, value):
44         return value or None
45
46     def convert_to_text(self, value):
47         return value or ''
48
49
50 class ChoiceSetting (Setting):
51     """A named setting with a limited number of possible values.
52
53     `choices` should be a list of `(config_file_value, Python value)`
54     pairs.  For example
55
56     >>> s = ChoiceSetting(name='bool',
57     ...                   choices=[('yes', True), ('no', False)])
58     >>> s.convert_from_text('yes')
59     True
60     >>> s.convert_to_text(True)
61     'yes'
62     >>> s.convert_to_text('invalid')
63     Traceback (most recent call last):
64       ...
65     ValueError: invalid
66     >>> s.help()
67     'Default: yes.  Choices: yes, no'
68     """
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)
74         if choices == None:
75             choices = []
76         self.choices = choices
77
78     def help(self):
79         ret = '%s  Choices: %s' % (
80             super(ChoiceSetting, self).help(),
81             ', '.join([key for key,value in self.choices]))
82         return ret.strip()
83
84     def convert_from_text(self, value):
85         return dict(self.choices)[value]
86
87     def convert_to_text(self, value):
88         for keyval in self.choices:
89             key,val = keyval
90             if val == value:
91                 return key
92         raise ValueError(value)
93
94
95 class BooleanSetting (ChoiceSetting):
96     """A named settubg that can be either true or false.
97
98     >>> s = BooleanSetting(name='bool')
99
100     >>> s.convert_from_text('yes')
101     True
102     >>> s.convert_to_text(True)
103     'yes'
104     >>> s.convert_to_text('invalid')
105     Traceback (most recent call last):
106       ...
107     ValueError: invalid
108     >>> s.help()
109     'Default: no.  Choices: yes, no'
110     """
111     def __init__(self, default=False, **kwargs):
112         super(BooleanSetting, self).__init__(
113             choices=[('yes', True), ('no', False)], default=default, **kwargs)
114
115
116 class NumericSetting (Setting):
117     """A named setting with numeric values.
118
119     Don't use this setting class.  Use a more specific subclass, such
120     as `IntegerSetting`.
121
122     >>> s = NumericSetting(name='float')
123     >>> s.default
124     0
125     >>> s.convert_to_text(13)
126     '13'
127     """
128     def __init__(self, default=0, **kwargs):
129         super(NumericSetting, self).__init__(default=default, **kwargs)
130
131     def convert_to_text(self, value):
132         return str(value)
133
134     def convert_from_text(self, value):
135         if value in [None, 'None']:
136             return None
137         return self._convert_from_text(value)
138
139     def _convert_from_text(self, value):
140         raise NotImplementedError()
141
142
143 class IntegerSetting (NumericSetting):
144     """A named setting with integer values.
145
146     >>> s = IntegerSetting(name='int')
147     >>> s.default
148     1
149     >>> s.convert_from_text('8')
150     8
151     """
152     def __init__(self, default=1, **kwargs):
153         super(IntegerSetting, self).__init__(default=default, **kwargs)
154
155     def _convert_from_text(self, value):
156         return int(value)
157
158
159 class FloatSetting (NumericSetting):
160     """A named setting with floating point values.
161
162     >>> s = FloatSetting(name='float')
163     >>> s.default
164     1.0
165     >>> s.convert_from_text('8')
166     8.0
167     >>> try:
168     ...     s.convert_from_text('invalid')
169     ... except ValueError as e:
170     ...     print('caught a ValueError')
171     caught a ValueError
172     """
173     def __init__(self, default=1.0, **kwargs):
174         super(FloatSetting, self).__init__(default=default, **kwargs)
175
176     def _convert_from_text(self, value):
177         return float(value)
178
179
180 class ListSetting(Setting):
181     """A named setting with a list of string values.
182
183     >>> s = ListSetting(name='list')
184     >>> s.default
185     []
186     >>> s.convert_to_text(['abc', 'def'])
187     'abc,def'
188     >>> s.convert_from_text('uvw, xyz')
189     ['uvw', ' xyz']
190     >>> s.convert_to_text([])
191     ''
192     >>> s.convert_from_text('')
193     []
194     """
195     def __init__(self, default=None, **kwargs):
196         if default is None:
197             default = []
198         super(ListSetting, self).__init__(default=default, **kwargs)
199
200     def _convert_from_text(self, value):
201         return value
202
203     def convert_from_text(self, value):
204         if value is None:
205             return None
206         elif value in ['', []]:
207             return []
208         return [self._convert_from_text(x) for x in value.split(',')]
209
210     def convert_to_text(self, value):
211         if value is None:
212             return None
213         return ','.join([str(x) for x in value])
214
215 class IntegerListSetting (ListSetting):
216     """A named setting with a list of integer point values.
217
218     >>> s = IntegerListSetting(name='integerlist')
219     >>> s.default
220     []
221     >>> s.convert_to_text([1, 3])
222     '1,3'
223     >>> s.convert_from_text('4, -6')
224     [4, -6]
225     >>> s.convert_to_text([])
226     ''
227     >>> s.convert_from_text('')
228     []
229     """
230     def _convert_from_text(self, value):
231         if value is None:
232             return value
233         return int(value)
234
235
236 class FloatListSetting (ListSetting):
237     """A named setting with a list of floating point values.
238
239     >>> s = FloatListSetting(name='floatlist')
240     >>> s.default
241     []
242     >>> s.convert_to_text([1, 2.3])
243     '1,2.3'
244     >>> s.convert_from_text('4.5, -6.7')  # doctest: +ELLIPSIS
245     [4.5, -6.7...]
246     >>> s.convert_to_text([])
247     ''
248     >>> s.convert_from_text('')
249     []
250     """
251     def _convert_from_text(self, value):
252         if value is None:
253             return value
254         return float(value)
255
256
257 class ConfigSetting (Setting):
258     """A setting that holds a pointer to a child `Config` class
259
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
263     references).
264
265     For example, a configurable AFM may contain a configurable piezo
266     scanner, as well as a configurable stepper motor.
267     """
268     def __init__(self, config_class=None, **kwargs):
269         super(ConfigSetting, self).__init__(**kwargs)
270         self.config_class = config_class
271
272
273 class ConfigListSetting (ConfigSetting):
274     """A setting that holds a list of child `Config` classes
275
276     For example, a piezo scanner with several axes.
277     """
278     def __init__(self, default=[], **kwargs):
279         super(ConfigListSetting, self).__init__(**kwargs)
280
281
282 class Config (dict):
283     """A class with a list of `Setting`\s.
284
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.
288     """
289     settings = []
290
291     def __init__(self, storage=None):
292         super(Config, self).__init__()
293         self.clear()
294         self.set_storage(storage=storage)
295
296     def __repr__(self):
297         return '<{} {}>'.format(self.__class__.__name__, id(self))
298
299     def set_storage(self, storage):
300         self._storage = storage
301
302     def clear(self):
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)
307
308     def load(self, merge=False, **kwargs):
309         self._storage.load(config=self, merge=merge, **kwargs)
310
311     def save(self, merge=False, **kwargs):
312         self._storage.save(config=self, merge=merge, **kwargs)
313
314     def dump(self, help=False, prefix=''):
315         """Return all settings and their values as a string
316
317         >>> class MyConfig (Config):
318         ...     settings = [
319         ...         ChoiceSetting(
320         ...             name='number',
321         ...             help='I have a number behind my back...',
322         ...             default=1,
323         ...             choices=[('one', 1), ('two', 2),
324         ...                 ]),
325         ...         BooleanSetting(
326         ...             name='odd',
327         ...             help='The number behind my back is odd.',
328         ...             default=True),
329         ...         IntegerSetting(
330         ...             name='guesses',
331         ...             help='Number of guesses before epic failure.',
332         ...             default=2),
333         ...         ]
334         >>> c = MyConfig()
335         >>> print(c.dump())
336         number: one
337         odd: yes
338         guesses: 2
339         >>> print(c.dump(help=True))  # doctest: +NORMALIZE_WHITESPACE
340         number: one (I have a number behind my back...  Default: one.
341                      Choices: one, two)
342         odd: yes    (The number behind my back is odd.  Default: yes.
343                      Choices: yes, no)
344         guesses: 2  (Number of guesses before epic failure.  Default: 2.)
345         """
346         lines = []
347         for setting in self.settings:
348             name = setting.name
349             value = self[name]
350             try:
351                 if isinstance(setting, ConfigListSetting):
352                     if value:
353                         lines.append('{}{}:'.format(prefix, name))
354                         for i,config in enumerate(value):
355                             lines.append('{}  {}:'.format(prefix, i))
356                             lines.append(
357                                 config.dump(help=help, prefix=prefix+'    '))
358                         continue
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+'  '))
363                         continue
364                 value_string = setting.convert_to_text(self[name])
365                 if help:
366                     help_string = '\t({})'.format(setting.help())
367                 else:
368                     help_string = ''
369                 lines.append('{}{}: {}{}'.format(
370                         prefix, name, value_string, help_string))
371             except Exception:
372                 _LOG.error('could not dump {} ({!r})'.format(name, value))
373                 raise
374         return '\n'.join(lines)
375
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
379
380         If your don't want to select `ConfigListSetting` items by
381         index, you can select them by matching another attribute.  For
382         example:
383
384         >>> from .test import TestConfig
385         >>> c = 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
393         <TestConfig ...>
394         >>> child['name']
395         'Jack'
396
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:
401
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)
409         'Jill Junior'
410         >>> child = c.select_config('children', 'Jack Junior',
411         ...     get_attribute=get_grandchild)
412         >>> child  # doctest: +ELLIPSIS
413         <TestConfig ...>
414         >>> child['name']
415         'Jack'
416         """
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:
422                 return item
423         raise KeyError(attribute_value)