3 Broken out from the main modules to make it easy to override if you
4 wish to use a different configuration file format.
6 The HDF5- and YAML-backed config file classes are created dynamically:
8 >>> print '\\n'.join([obj for obj in sorted(locals().keys())
9 ... if obj.endswith('Config')
10 ... and not obj.startswith('_')])
14 HDF5_InputChannelConfig
15 HDF5_OutputChannelConfig
19 YAML_InputChannelConfig
20 YAML_OutputChannelConfig
22 The first time you use them, the file they create will probably be
27 >>> fd,filename = tempfile.mkstemp(suffix='.h5', prefix='pypiezo-')
30 >>> c = HDF5_BaseConfig(filename=filename, group='/base')
33 Loading will create a stub group group if it hadn't existed before.
35 >>> pprint_HDF5(filename)
38 >>> print c.dump(from_file=True)
41 Saving fills in all the config values.
43 >>> c['syslog'] = True
45 >>> pprint_HDF5(filename) # doctest: +REPORT_UDIFF
48 <HDF5 dataset "log-level": shape (), type "|S4">
50 <HDF5 dataset "matplotlib": shape (), type "|S2">
52 <HDF5 dataset "syslog": shape (), type "|S3">
54 >>> print c.dump(from_file=True)
59 If you want more details, you can dump with help strings.
61 >>> print c.dump(help=True, from_file=True) # doctest: +NORMALIZE_WHITESPACE
62 log-level: warn\t(Module logging level. Default: warn. Choices:
63 \t critical, error, warn, info, debug)
64 matplotlib: no\t(Plot piezo motion using `matplotlib`. Default: no.
66 syslog: yes\t(Log to syslog (otherwise log to stderr). Default: no.
69 Settings also support `None`, even if they have numeric types.
71 >>> c = HDF5_AxisConfig(filename=filename, group='/z-axis')
75 >>> print c.dump(from_file=True)
80 >>> print (c['minimum'] == None)
83 Cleanup our temporary config file.
85 >>> os.remove(filename)
88 import logging as _logging
89 import os.path as _os_path
95 from . import LOG as _LOG
98 class _Setting (object):
99 "A named setting with arbitrart text values."
100 def __init__(self, name, help='', default=None):
103 self.default = default
106 return '<%s %s>' % (self.__class__.__name__, self.name)
109 return self.__str__()
112 ret = '%s Default: %s.' % (
113 self._help, self.convert_to_text(self.default))
116 def convert_from_text(self, value):
119 def convert_to_text(self, value):
123 class _ChoiceSetting (_Setting):
124 """A named setting with a limited number of possible values.
126 `choices` should be a list of `(config_file_value, Python value)`
129 >>> s = _ChoiceSetting(name='bool',
130 ... choices=[('yes', True), ('no', False)])
131 >>> s.convert_from_text('yes')
133 >>> s.convert_to_text(True)
135 >>> s.convert_to_text('invalid')
136 Traceback (most recent call last):
140 'Default: yes. Choices: yes, no'
142 def __init__(self, choices=None, **kwargs):
143 if 'default' not in kwargs:
144 if None not in [keyval[1] for keyval in choices]:
145 kwargs['default'] = choices[0][1]
146 super(_ChoiceSetting, self).__init__(**kwargs)
149 self.choices = choices
152 ret = '%s Choices: %s' % (
153 super(_ChoiceSetting, self).help(),
154 ', '.join([key for key,value in self.choices]))
157 def convert_from_text(self, value):
158 return dict(self.choices)[value]
160 def convert_to_text(self, value):
161 for keyval in self.choices:
165 raise ValueError(value)
168 class _BooleanSetting (_ChoiceSetting):
169 """A named settubg that can be either true or false.
171 >>> s = _BooleanSetting(name='bool')
173 >>> s.convert_from_text('yes')
175 >>> s.convert_to_text(True)
177 >>> s.convert_to_text('invalid')
178 Traceback (most recent call last):
182 'Default: no. Choices: yes, no'
184 def __init__(self, **kwargs):
185 assert 'choices' not in kwargs
186 if 'default' not in kwargs:
187 kwargs['default'] = False
188 super(_BooleanSetting, self).__init__(
189 choices=[('yes', True), ('no', False)], **kwargs)
192 class _NumericSetting (_Setting):
193 """A named setting with numeric values.
195 >>> s = _NumericSetting(name='float')
198 >>> s.convert_to_text(13)
203 def __init__(self, **kwargs):
204 if 'default' not in kwargs:
205 kwargs['default'] = self._default_value
206 super(_NumericSetting, self).__init__(**kwargs)
208 def convert_to_text(self, value):
211 def convert_from_text(self, value):
212 if value in [None, 'None']:
214 return self._convert_from_text(value)
216 def _convert_from_text(self, value):
217 raise NotImplementedError()
220 class _IntegerSetting (_NumericSetting):
221 """A named setting with integer values.
223 >>> s = _IntegerSetting(name='int')
226 >>> s.convert_from_text('8')
231 def _convert_from_text(self, value):
235 class _FloatSetting (_NumericSetting):
236 """A named setting with floating point values.
238 >>> s = _FloatSetting(name='float')
241 >>> s.convert_from_text('8')
243 >>> s.convert_from_text('invalid')
244 Traceback (most recent call last):
246 ValueError: invalid literal for float(): invalid
250 def _convert_from_text(self, value):
254 class _FloatListSetting (_Setting):
255 """A named setting with a list of floating point values.
257 >>> s = _FloatListSetting(name='floatlist')
260 >>> s.convert_to_text([1, 2.3])
262 >>> s.convert_from_text('4.5, -6.7') # doctest: +ELLIPSIS
264 >>> s.convert_to_text([])
266 >>> s.convert_from_text('')
269 def __init__(self, **kwargs):
270 if 'default' not in kwargs:
271 kwargs['default'] = []
272 super(_FloatListSetting, self).__init__(**kwargs)
274 def _convert_from_text(self, value):
279 def convert_from_text(self, value):
284 return [self._convert_from_text(x) for x in value.split(',')]
286 def convert_to_text(self, value):
289 return ', '.join([str(x) for x in value])
292 class _Config (dict):
293 "A class with a list `._keys` of `_Setting`\s."
297 for s in self.settings:
298 self[s.name] = s.default
300 def dump(self, help=False):
301 """Return all settings and their values as a string
303 >>> b = _BaseConfig()
308 >>> print b.dump(help=True) # doctest: +NORMALIZE_WHITESPACE
309 syslog: no (Log to syslog (otherwise log to stderr).
310 Default: no. Choices: yes, no)
311 matplotlib: no (Plot piezo motion using `matplotlib`.
312 Default: no. Choices: yes, no)
313 log-level: warn (Module logging level. Default: warn.
314 Choices: critical, error, warn, info, debug)
317 settings = dict([(s.name, s) for s in self.settings])
318 for key,value in self.iteritems():
320 setting = settings[key]
321 value_string = setting.convert_to_text(value)
323 help_string = '\t(%s)' % setting.help()
326 lines.append('%s: %s%s' % (key, value_string, help_string))
327 return '\n'.join(lines)
330 class _BackedConfig (_Config):
331 "A `_Config` instance with some kind of storage interface"
333 raise NotImplementedError()
336 raise NotImplementedError()
339 class _BaseConfig (_Config):
340 """Configure `pypiezo` module operation
342 >>> b = _BaseConfig()
343 >>> b.settings # doctest: +NORMALIZE_WHITESPACE
344 [<_ChoiceSetting log-level>, <_BooleanSetting syslog>,
345 <_BooleanSetting matplotlib>]
346 >>> print b['log-level'] == _logging.WARN
352 help='Module logging level.',
353 default=_logging.WARN,
355 ('critical', _logging.CRITICAL),
356 ('error', _logging.ERROR),
357 ('warn', _logging.WARN),
358 ('info', _logging.INFO),
359 ('debug', _logging.DEBUG),
363 help='Log to syslog (otherwise log to stderr).',
367 help='Plot piezo motion using `matplotlib`.',
372 class _AxisConfig (_Config):
373 "Configure a single piezo axis"
378 'Volts applied at piezo per volt output from the DAQ card '
379 '(e.g. if your DAQ output is amplified before driving the '
383 help='Meters of piezo deflection per volt applied to the piezo.'),
386 help='Set a lower limit on allowed output voltage',
390 help='Set an upper limit on allowed output voltage',
395 class _ChannelConfig (_Config):
399 help='Comedi device.',
400 default='/dev/comedi0'),
403 help='Comedi subdevice index. -1 for automatic detection.',
407 help='Subdevice channel index.',
411 help="Channel's maximum bit value."),
414 help="Channel's selected range index."),
416 name='conversion-coefficients',
417 help=('Bit to physical unit conversion coefficients starting with '
418 'the constant coefficient.')),
420 name='conversion-origin',
421 help=('Origin (bit offset) of bit to physical polynomial '
424 name='inverse-conversion-coefficients',
425 help=('Physical unit to bit conversion coefficients starting with '
426 'the constant coefficient.')),
428 name='inverse-conversion-origin',
429 help=('Origin (physical unit offset) of physical to bit '
430 'polynomial expansion.')),
434 class _OutputChannelConfig (_ChannelConfig):
438 class _InputChannelConfig (_ChannelConfig):
442 def pprint_HDF5(*args, **kwargs):
443 print pformat_HDF5(*args, **kwargs)
445 def pformat_HDF5(filename, group='/'):
446 f = _h5py.File(filename, 'r')
448 return '\n'.join(_pformat_hdf5(cwg))
450 def _pformat_hdf5(cwg, depth=0):
452 lines.append(' '*depth + cwg.name)
454 for key,value in cwg.iteritems():
455 if isinstance(value, _h5py.Group):
456 lines.extend(_pformat_hdf5(value, depth))
457 elif isinstance(value, _h5py.Dataset):
458 lines.append(' '*depth + str(value))
459 lines.append(' '*(depth+1) + str(value[...]))
461 lines.append(' '*depth + str(value))
464 def h5_create_group(cwg, path):
465 "Create the group where the settings are stored (if necessary)."
467 for group in path.strip('/').split('/'):
469 if group not in cwg.keys():
470 _LOG.debug('creating group %s in %s'
471 % ('/'.join(gpath), cwg.file))
472 cwg.create_group(group)
475 class _HDF5Config (_BackedConfig):
476 """Mixin to back a `_Config` class with an HDF5 file.
478 TODO: Special handling for Choice (enums), FloatList (arrays), etc.?
480 The `.save` and `.load` methods have an optional `group` argument
481 that allows you to save and load settings from an externally
482 opened HDF5 file. This can make it easier to stash several
483 related `_Config` classes in a single file. For example
487 >>> fd,filename = tempfile.mkstemp(suffix='.h5', prefix='pypiezo-')
490 >>> f = _h5py.File(filename, 'a')
491 >>> c = HDF5_BaseConfig(filename='untouched_file.h5',
492 ... group='/untouched/group')
493 >>> c['syslog'] = True
494 >>> group = f.create_group('base')
496 >>> pprint_HDF5(filename)
499 <HDF5 dataset "log-level": shape (), type "|S4">
501 <HDF5 dataset "matplotlib": shape (), type "|S2">
503 <HDF5 dataset "syslog": shape (), type "|S3">
505 >>> d = HDF5_BaseConfig(filename='untouched_file.h5',
506 ... group='/untouched/group')
512 >>> os.remove(filename)
514 def __init__(self, filename, group='/', **kwargs):
515 super(_HDF5Config, self).__init__(**kwargs)
516 self.filename = filename
517 assert group.startswith('/'), group
518 if not group.endswith('/'):
521 self._file_checked = False
523 def _check_file(self):
524 if self._file_checked:
527 self._file_checked = True
529 def _setup_file(self):
530 f = _h5py.File(self.filename, 'a')
531 cwg = f # current working group
532 h5_create_group(cwg, self.group)
535 def dump(self, help=False, from_file=False):
536 """Return the relevant group in `self.filename` as a string
538 Extends the base :meth:`dump` by adding the `from_file`
539 option. If `from_file` is true, dump all entries that
540 currently exist in the relevant group, rather than listing all
541 settings defined in the instance dictionary.
545 f = _h5py.File(self.filename, 'r')
548 settings = dict([(s.name, s) for s in self.settings])
549 for key,value in cwg.iteritems():
550 if help and key in settings:
551 help_string = '\t(%s)' % settings[key].help()
554 lines.append('%s: %s%s' % (key, value[...], help_string))
555 return '\n'.join(lines)
556 return super(_HDF5Config, self).dump(help=help)
558 def load(self, group=None):
561 f = _h5py.File(self.filename, 'r')
562 group = f[self.group]
565 for s in self.settings:
566 if s.name not in group.keys():
568 self[s.name] = s.convert_from_text(group[s.name][...])
572 def save(self, group=None):
575 f = _h5py.File(self.filename, 'a')
576 group = f[self.group]
579 for s in self.settings:
580 value = s.convert_to_text(self[s.name])
585 group[s.name] = value
590 class _YAMLDumper (_yaml.SafeDumper):
591 def represent_bool(self, data):
592 "Use yes/no instead of the default true/false"
597 return self.represent_scalar(u'tag:yaml.org,2002:bool', value)
600 _YAMLDumper.add_representer(bool, _YAMLDumper.represent_bool)
603 class _YAMLConfig (_BackedConfig):
604 """Mixin to back a `_Config` class with a YAML file.
606 TODO: Special handling for Choice (enums), FloatList (arrays), etc.?
611 >>> fd,filename = tempfile.mkstemp(suffix='.yaml', prefix='pypiezo-')
614 >>> c = YAML_BaseConfig(filename=filename)
617 Saving writes all the config values to disk.
619 >>> c['syslog'] = True
621 >>> print open(c.filename, 'r').read()
627 Loading reads the config files from disk.
629 >>> c = YAML_BaseConfig(filename=filename)
636 Cleanup our temporary config file.
638 >>> os.remove(filename)
642 def __init__(self, filename, **kwargs):
643 super(_YAMLConfig, self).__init__(**kwargs)
644 self.filename = filename
647 if not _os_path.exists(self.filename):
648 open(self.filename, 'a').close()
649 with open(self.filename, 'r') as f:
650 data = _yaml.safe_load(f)
653 settings = dict([(s.name, s) for s in self.settings])
654 for key,value in data.iteritems():
655 setting = settings[key]
656 if isinstance(setting, _BooleanSetting):
659 v = setting.convert_from_text(value)
664 settings = dict([(s.name, s) for s in self.settings])
665 for key,value in self.iteritems():
667 setting = settings[key]
668 if isinstance(setting, _BooleanSetting):
671 v = setting.convert_to_text(value)
673 with open(self.filename, 'w') as f:
674 _yaml.dump(data, stream=f, Dumper=self.dumper,
675 default_flow_style=False)
678 # Define HDF5- and YAML-backed subclasses of the basic _Config types.
679 for name,obj in locals().items():
680 if (obj != _Config and
681 type(obj) == type and
682 issubclass(obj, _Config) and
683 not issubclass(obj, _BackedConfig)):
684 for prefix,base in [('HDF5', _HDF5Config), ('YAML', _YAMLConfig)]:
685 _name = '%s%s' % (prefix, name)
688 _class = type(_name, _bases, _dict)
689 setattr(_sys.modules[__name__], _name, _class)
691 del name, obj, prefix, base, _name, _bases, _dict, _class
694 def find_base_config():
695 "Return the best `_BaseConfig` match after scanning the filesystem"
696 _LOG.info('looking for base_config file')
697 user_basepath = _os_path.join(_os_path.expanduser('~'), '.pypiezorc')
698 system_basepath = _os_path.join('/etc', 'pypiezo', 'config')
699 distributed_basepath = _os_path.join('/usr', 'share', 'pypiezo', 'config')
700 for basepath in [user_basepath, system_basepath, distributed_basepath]:
701 for (extension, config) in [('.h5', HDF5_BaseConfig),
702 ('.yaml', YAML_BaseConfig)]:
703 filename = basepath + extension
704 if _os_path.exists(filename):
705 _LOG.info('base_config file found at %s' % filename)
706 base_config = config(filename)
710 _LOG.debug('no base_config file at %s' % filename)
711 _LOG.info('new base_config file at %s' % filename)
712 basepath = user_basepath
713 filename = basepath + extension
714 return config(filename)