-"""Piezo configuration
+# Copyright (C) 2011 W. Trevor King <wking@drexel.edu>
+#
+# This file is part of pypiezo.
+#
+# pypiezo is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# pypiezo is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with pypiezo. If not, see <http://www.gnu.org/licenses/>.
+
+"Piezo configuration"
-Broken out from the main modules to make it easy to override if you
-wish to use a different configuration file format.
-
-The HDF5- and YAML-backed config file classes are created dynamically:
-
->>> print '\\n'.join([obj for obj in sorted(locals().keys())
-... if obj.endswith('Config')
-... and not obj.startswith('_')])
-HDF5_AxisConfig
-HDF5_BaseConfig
-HDF5_ChannelConfig
-HDF5_InputChannelConfig
-HDF5_OutputChannelConfig
-YAML_AxisConfig
-YAML_BaseConfig
-YAML_ChannelConfig
-YAML_InputChannelConfig
-YAML_OutputChannelConfig
-
-The first time you use them, the file they create will probably be
-empty or not exist.
-
->>> import os
->>> import tempfile
->>> fd,filename = tempfile.mkstemp(suffix='.h5', prefix='pypiezo-')
->>> os.close(fd)
-
->>> c = HDF5_BaseConfig(filename=filename, group='/base')
->>> c.load()
-
-Loading will create a stub group group if it hadn't existed before.
-
->>> pprint_HDF5(filename)
-/
- /base
->>> print c.dump(from_file=True)
-<BLANKLINE>
-
-Saving fills in all the config values.
-
->>> c['syslog'] = True
->>> c.save()
->>> pprint_HDF5(filename) # doctest: +REPORT_UDIFF
-/
- /base
- <HDF5 dataset "log-level": shape (), type "|S4">
- warn
- <HDF5 dataset "matplotlib": shape (), type "|S2">
- no
- <HDF5 dataset "syslog": shape (), type "|S3">
- yes
->>> print c.dump(from_file=True)
-log-level: warn
-matplotlib: no
-syslog: yes
-
-If you want more details, you can dump with help strings.
-
->>> print c.dump(help=True, from_file=True) # doctest: +NORMALIZE_WHITESPACE
-log-level: warn\t(Module logging level. Default: warn. Choices:
- \t critical, error, warn, info, debug)
-matplotlib: no\t(Plot piezo motion using `matplotlib`. Default: no.
- \t Choices: yes, no)
-syslog: yes\t(Log to syslog (otherwise log to stderr). Default: no.
- \t Choices: yes, no)
-
-Settings also support `None`, even if they have numeric types.
-
->>> c = HDF5_AxisConfig(filename=filename, group='/z-axis')
->>> c.load()
->>> c.save()
->>> c.load()
->>> print c.dump(from_file=True)
-gain: 1.0
-maximum: None
-minimum: None
-sensitivity: 1.0
->>> print (c['minimum'] == None)
-True
-
-Cleanup our temporary config file.
-
->>> os.remove(filename)
-"""
-
-import logging as _logging
-import os.path as _os_path
import sys as _sys
-import h5py as _h5py
-import yaml as _yaml
-
-from . import LOG as _LOG
-
-
-class _Setting (object):
- "A named setting with arbitrart text values."
- def __init__(self, name, help='', default=None):
- self.name = name
- self._help = help
- self.default = default
-
- def __str__(self):
- return '<%s %s>' % (self.__class__.__name__, self.name)
-
- def __repr__(self):
- return self.__str__()
-
- def help(self):
- ret = '%s Default: %s.' % (
- self._help, self.convert_to_text(self.default))
- return ret.strip()
-
- def convert_from_text(self, value):
- return value
-
- def convert_to_text(self, value):
- return value
-
-
-class _ChoiceSetting (_Setting):
- """A named setting with a limited number of possible values.
-
- `choices` should be a list of `(config_file_value, Python value)`
- pairs. For example
-
- >>> s = _ChoiceSetting(name='bool',
- ... choices=[('yes', True), ('no', False)])
- >>> s.convert_from_text('yes')
- True
- >>> s.convert_to_text(True)
- 'yes'
- >>> s.convert_to_text('invalid')
- Traceback (most recent call last):
- ...
- ValueError: invalid
- >>> s.help()
- 'Default: yes. Choices: yes, no'
- """
- def __init__(self, choices=None, **kwargs):
- if 'default' not in kwargs:
- if None not in [keyval[1] for keyval in choices]:
- kwargs['default'] = choices[0][1]
- super(_ChoiceSetting, self).__init__(**kwargs)
- if choices == None:
- choices = []
- self.choices = choices
-
- def help(self):
- ret = '%s Choices: %s' % (
- super(_ChoiceSetting, self).help(),
- ', '.join([key for key,value in self.choices]))
- return ret.strip()
-
- def convert_from_text(self, value):
- return dict(self.choices)[value]
-
- def convert_to_text(self, value):
- for keyval in self.choices:
- key,val = keyval
- if val == value:
- return key
- raise ValueError(value)
-
-
-class _BooleanSetting (_ChoiceSetting):
- """A named settubg that can be either true or false.
-
- >>> s = _BooleanSetting(name='bool')
-
- >>> s.convert_from_text('yes')
- True
- >>> s.convert_to_text(True)
- 'yes'
- >>> s.convert_to_text('invalid')
- Traceback (most recent call last):
- ...
- ValueError: invalid
- >>> s.help()
- 'Default: no. Choices: yes, no'
- """
- def __init__(self, **kwargs):
- assert 'choices' not in kwargs
- if 'default' not in kwargs:
- kwargs['default'] = False
- super(_BooleanSetting, self).__init__(
- choices=[('yes', True), ('no', False)], **kwargs)
-
-
-class _NumericSetting (_Setting):
- """A named setting with numeric values.
-
- >>> s = _NumericSetting(name='float')
- >>> s.default
- 0
- >>> s.convert_to_text(13)
- '13'
- """
- _default_value = 0
-
- def __init__(self, **kwargs):
- if 'default' not in kwargs:
- kwargs['default'] = self._default_value
- super(_NumericSetting, self).__init__(**kwargs)
-
- def convert_to_text(self, value):
- return str(value)
-
- def convert_from_text(self, value):
- if value in [None, 'None']:
- return None
- return self._convert_from_text(value)
-
- def _convert_from_text(self, value):
- raise NotImplementedError()
-
-
-class _IntegerSetting (_NumericSetting):
- """A named setting with integer values.
-
- >>> s = _IntegerSetting(name='int')
- >>> s.default
- 1
- >>> s.convert_from_text('8')
- 8
- """
- _default_value = 1
-
- def _convert_from_text(self, value):
- return int(value)
-
-
-class _FloatSetting (_NumericSetting):
- """A named setting with floating point values.
-
- >>> s = _FloatSetting(name='float')
- >>> s.default
- 1.0
- >>> s.convert_from_text('8')
- 8.0
- >>> s.convert_from_text('invalid')
- Traceback (most recent call last):
- ...
- ValueError: invalid literal for float(): invalid
- """
- _default_value = 1.0
-
- def _convert_from_text(self, value):
- return float(value)
-
-
-class _FloatListSetting (_Setting):
- """A named setting with a list of floating point values.
+import h5config.config as _config
+import h5config.tools as _h5config_tools
+import h5config.util as _util
- >>> s = _FloatListSetting(name='floatlist')
- >>> s.default
- []
- >>> s.convert_to_text([1, 2.3])
- '1, 2.3'
- >>> s.convert_from_text('4.5, -6.7') # doctest: +ELLIPSIS
- [4.5, -6.700...]
- >>> s.convert_to_text([])
- ''
- >>> s.convert_from_text('')
- []
- """
- def __init__(self, **kwargs):
- if 'default' not in kwargs:
- kwargs['default'] = []
- super(_FloatListSetting, self).__init__(**kwargs)
- def _convert_from_text(self, value):
- if value is None:
- return value
- return float(value)
-
- def convert_from_text(self, value):
- if value is None:
- return None
- elif value == '':
- return []
- return [self._convert_from_text(x) for x in value.split(',')]
-
- def convert_to_text(self, value):
- if value is None:
- return None
- return ', '.join([str(x) for x in value])
-
-
-class _Config (dict):
- "A class with a list `._keys` of `_Setting`\s."
- settings = []
-
- def __init__(self):
- for s in self.settings:
- self[s.name] = s.default
-
- def dump(self, help=False):
- """Return all settings and their values as a string
-
- >>> b = _BaseConfig()
- >>> print b.dump()
- syslog: no
- matplotlib: no
- log-level: warn
- >>> print b.dump(help=True) # doctest: +NORMALIZE_WHITESPACE
- syslog: no (Log to syslog (otherwise log to stderr).
- Default: no. Choices: yes, no)
- matplotlib: no (Plot piezo motion using `matplotlib`.
- Default: no. Choices: yes, no)
- log-level: warn (Module logging level. Default: warn.
- Choices: critical, error, warn, info, debug)
- """
- lines = []
- settings = dict([(s.name, s) for s in self.settings])
- for key,value in self.iteritems():
- if key in settings:
- setting = settings[key]
- value_string = setting.convert_to_text(value)
- if help:
- help_string = '\t(%s)' % setting.help()
- else:
- help_string = ''
- lines.append('%s: %s%s' % (key, value_string, help_string))
- return '\n'.join(lines)
-
-
-class _BackedConfig (_Config):
- "A `_Config` instance with some kind of storage interface"
- def load(self):
- raise NotImplementedError()
-
- def save(self):
- raise NotImplementedError()
-
-
-class _BaseConfig (_Config):
- """Configure `pypiezo` module operation
-
- >>> b = _BaseConfig()
- >>> b.settings # doctest: +NORMALIZE_WHITESPACE
- [<_ChoiceSetting log-level>, <_BooleanSetting syslog>,
- <_BooleanSetting matplotlib>]
- >>> print b['log-level'] == _logging.WARN
- True
- """
- settings = [
- _ChoiceSetting(
- name='log-level',
- help='Module logging level.',
- default=_logging.WARN,
- choices=[
- ('critical', _logging.CRITICAL),
- ('error', _logging.ERROR),
- ('warn', _logging.WARN),
- ('info', _logging.INFO),
- ('debug', _logging.DEBUG),
- ]),
- _BooleanSetting(
- name='syslog',
- help='Log to syslog (otherwise log to stderr).',
- default=False),
- _BooleanSetting(
+class PackageConfig (_h5config_tools.PackageConfig):
+ "Configure `pypiezo` module operation"
+ settings = _h5config_tools.PackageConfig.settings + [
+ _config.BooleanSetting(
name='matplotlib',
help='Plot piezo motion using `matplotlib`.',
default=False),
]
-class _AxisConfig (_Config):
+class AxisConfig (_config.Config):
"Configure a single piezo axis"
settings = [
- _FloatSetting(
+ _config.FloatSetting(
name='gain',
help=(
'Volts applied at piezo per volt output from the DAQ card '
'(e.g. if your DAQ output is amplified before driving the '
'piezo),')),
- _FloatSetting(
+ _config.FloatSetting(
name='sensitivity',
help='Meters of piezo deflection per volt applied to the piezo.'),
- _FloatSetting(
+ _config.FloatSetting(
name='minimum',
help='Set a lower limit on allowed output voltage',
default=None),
- _FloatSetting(
+ _config.FloatSetting(
name='maximum',
help='Set an upper limit on allowed output voltage',
default=None),
]
-class _ChannelConfig (_Config):
+class ChannelConfig (_config.Config):
settings = [
- _Setting(
+ _config.Setting(
name='device',
help='Comedi device.',
default='/dev/comedi0'),
- _IntegerSetting(
+ _config.IntegerSetting(
name='subdevice',
help='Comedi subdevice index. -1 for automatic detection.',
default=-1),
- _IntegerSetting(
+ _config.IntegerSetting(
name='channel',
help='Subdevice channel index.',
default=0),
- _IntegerSetting(
+ _config.IntegerSetting(
name='maxdata',
help="Channel's maximum bit value."),
- _IntegerSetting(
+ _config.IntegerSetting(
name='range',
help="Channel's selected range index."),
- _FloatListSetting(
+ _config.FloatListSetting(
name='conversion-coefficients',
help=('Bit to physical unit conversion coefficients starting with '
'the constant coefficient.')),
- _FloatSetting(
+ _config.FloatSetting(
name='conversion-origin',
help=('Origin (bit offset) of bit to physical polynomial '
'expansion.')),
- _FloatListSetting(
+ _config.FloatListSetting(
name='inverse-conversion-coefficients',
help=('Physical unit to bit conversion coefficients starting with '
'the constant coefficient.')),
- _FloatSetting(
+ _config.FloatSetting(
name='inverse-conversion-origin',
help=('Origin (physical unit offset) of physical to bit '
'polynomial expansion.')),
]
-class _OutputChannelConfig (_ChannelConfig):
+class OutputChannelConfig (ChannelConfig):
pass
-class _InputChannelConfig (_ChannelConfig):
+class InputChannelConfig (ChannelConfig):
pass
-def pprint_HDF5(*args, **kwargs):
- print pformat_HDF5(*args, **kwargs)
-
-def pformat_HDF5(filename, group='/'):
- f = _h5py.File(filename, 'r')
- cwg = f[group]
- return '\n'.join(_pformat_hdf5(cwg))
-
-def _pformat_hdf5(cwg, depth=0):
- lines = []
- lines.append(' '*depth + cwg.name)
- depth += 1
- for key,value in cwg.iteritems():
- if isinstance(value, _h5py.Group):
- lines.extend(_pformat_hdf5(value, depth))
- elif isinstance(value, _h5py.Dataset):
- lines.append(' '*depth + str(value))
- lines.append(' '*(depth+1) + str(value[...]))
- else:
- lines.append(' '*depth + str(value))
- return lines
-
-def h5_create_group(cwg, path):
- "Create the group where the settings are stored (if necessary)."
- if path == '/':
- return cwg
- gpath = ['']
- for group in path.strip('/').split('/'):
- gpath.append(group)
- if group not in cwg.keys():
- _LOG.debug('creating group %s in %s'
- % ('/'.join(gpath), cwg.file))
- cwg.create_group(group)
- cwg = cwg[group]
- return cwg
-
-class _HDF5Config (_BackedConfig):
- """Mixin to back a `_Config` class with an HDF5 file.
-
- TODO: Special handling for Choice (enums), FloatList (arrays), etc.?
-
- The `.save` and `.load` methods have an optional `group` argument
- that allows you to save and load settings from an externally
- opened HDF5 file. This can make it easier to stash several
- related `_Config` classes in a single file. For example
-
- >>> import os
- >>> import tempfile
- >>> fd,filename = tempfile.mkstemp(suffix='.h5', prefix='pypiezo-')
- >>> os.close(fd)
-
- >>> f = _h5py.File(filename, 'a')
- >>> c = HDF5_BaseConfig(filename='untouched_file.h5',
- ... group='/untouched/group')
- >>> c['syslog'] = True
- >>> group = f.create_group('base')
- >>> c.save(group)
- >>> pprint_HDF5(filename)
- /
- /base
- <HDF5 dataset "log-level": shape (), type "|S4">
- warn
- <HDF5 dataset "matplotlib": shape (), type "|S2">
- no
- <HDF5 dataset "syslog": shape (), type "|S3">
- yes
- >>> d = HDF5_BaseConfig(filename='untouched_file.h5',
- ... group='/untouched/group')
- >>> d.load(group)
- >>> d['syslog']
- True
-
- >>> f.close()
- >>> os.remove(filename)
- """
- def __init__(self, filename, group='/', **kwargs):
- super(_HDF5Config, self).__init__(**kwargs)
- self.filename = filename
- assert group.startswith('/'), group
- if not group.endswith('/'):
- group += '/'
- self.group = group
- self._file_checked = False
-
- def _check_file(self):
- if self._file_checked:
- return
- self._setup_file()
- self._file_checked = True
-
- def _setup_file(self):
- f = _h5py.File(self.filename, 'a')
- cwg = f # current working group
- h5_create_group(cwg, self.group)
- f.close()
-
- def dump(self, help=False, from_file=False):
- """Return the relevant group in `self.filename` as a string
-
- Extends the base :meth:`dump` by adding the `from_file`
- option. If `from_file` is true, dump all entries that
- currently exist in the relevant group, rather than listing all
- settings defined in the instance dictionary.
- """
- if from_file:
- self._check_file()
- f = _h5py.File(self.filename, 'r')
- cwg = f[self.group]
- lines = []
- settings = dict([(s.name, s) for s in self.settings])
- for key,value in cwg.iteritems():
- if help and key in settings:
- help_string = '\t(%s)' % settings[key].help()
- else:
- help_string = ''
- lines.append('%s: %s%s' % (key, value[...], help_string))
- return '\n'.join(lines)
- return super(_HDF5Config, self).dump(help=help)
-
- def load(self, group=None):
- if group is None:
- self._check_file()
- f = _h5py.File(self.filename, 'r')
- group = f[self.group]
- else:
- f = None
- for s in self.settings:
- if s.name not in group.keys():
- continue
- self[s.name] = s.convert_from_text(group[s.name][...])
- if f:
- f.close()
-
- def save(self, group=None):
- if group is None:
- self._check_file()
- f = _h5py.File(self.filename, 'a')
- group = f[self.group]
- else:
- f = None
- for s in self.settings:
- value = s.convert_to_text(self[s.name])
- try:
- del group[s.name]
- except KeyError:
- pass
- group[s.name] = value
- if f:
- f.close()
-
-
-class _YAMLDumper (_yaml.SafeDumper):
- def represent_bool(self, data):
- "Use yes/no instead of the default true/false"
- if data:
- value = u'yes'
- else:
- value = u'no'
- return self.represent_scalar(u'tag:yaml.org,2002:bool', value)
-
-
-_YAMLDumper.add_representer(bool, _YAMLDumper.represent_bool)
-
-
-class _YAMLConfig (_BackedConfig):
- """Mixin to back a `_Config` class with a YAML file.
-
- TODO: Special handling for Choice (enums), FloatList (arrays), etc.?
-
- >>> import os
- >>> import os.path
- >>> import tempfile
- >>> fd,filename = tempfile.mkstemp(suffix='.yaml', prefix='pypiezo-')
- >>> os.close(fd)
-
- >>> c = YAML_BaseConfig(filename=filename)
- >>> c.load()
-
- Saving writes all the config values to disk.
-
- >>> c['syslog'] = True
- >>> c.save()
- >>> print open(c.filename, 'r').read()
- log-level: warn
- matplotlib: no
- syslog: yes
- <BLANKLINE>
-
- Loading reads the config files from disk.
-
- >>> c = YAML_BaseConfig(filename=filename)
- >>> c.load()
- >>> print c.dump()
- syslog: yes
- matplotlib: no
- log-level: warn
-
- Cleanup our temporary config file.
-
- >>> os.remove(filename)
- """
- dumper = _YAMLDumper
-
- def __init__(self, filename, **kwargs):
- super(_YAMLConfig, self).__init__(**kwargs)
- self.filename = filename
-
- def load(self):
- if not _os_path.exists(self.filename):
- open(self.filename, 'a').close()
- with open(self.filename, 'r') as f:
- data = _yaml.safe_load(f)
- if data == None:
- return # empty file
- settings = dict([(s.name, s) for s in self.settings])
- for key,value in data.iteritems():
- setting = settings[key]
- if isinstance(setting, _BooleanSetting):
- v = value
- else:
- v = setting.convert_from_text(value)
- self[key] = v
-
- def save(self):
- data = {}
- settings = dict([(s.name, s) for s in self.settings])
- for key,value in self.iteritems():
- if key in settings:
- setting = settings[key]
- if isinstance(setting, _BooleanSetting):
- v = value
- else:
- v = setting.convert_to_text(value)
- data[key] = v
- with open(self.filename, 'w') as f:
- _yaml.dump(data, stream=f, Dumper=self.dumper,
- default_flow_style=False)
-
-
-# Define HDF5- and YAML-backed subclasses of the basic _Config types.
-for name,obj in locals().items():
- if (obj != _Config and
- type(obj) == type and
- issubclass(obj, _Config) and
- not issubclass(obj, _BackedConfig)):
- for prefix,base in [('HDF5', _HDF5Config), ('YAML', _YAMLConfig)]:
- _name = '%s%s' % (prefix, name)
- _bases = (base, obj)
- _dict = {}
- _class = type(_name, _bases, _dict)
- setattr(_sys.modules[__name__], _name, _class)
-
-del name, obj, prefix, base, _name, _bases, _dict, _class
-
-
-def find_base_config():
- "Return the best `_BaseConfig` match after scanning the filesystem"
- _LOG.info('looking for base_config file')
- user_basepath = _os_path.join(_os_path.expanduser('~'), '.pypiezorc')
- system_basepath = _os_path.join('/etc', 'pypiezo', 'config')
- distributed_basepath = _os_path.join('/usr', 'share', 'pypiezo', 'config')
- for basepath in [user_basepath, system_basepath, distributed_basepath]:
- for (extension, config) in [('.h5', HDF5_BaseConfig),
- ('.yaml', YAML_BaseConfig)]:
- filename = basepath + extension
- if _os_path.exists(filename):
- _LOG.info('base_config file found at %s' % filename)
- base_config = config(filename)
- base_config.load()
- return base_config
- else:
- _LOG.debug('no base_config file at %s' % filename)
- _LOG.info('new base_config file at %s' % filename)
- basepath = user_basepath
- filename = basepath + extension
- return config(filename)
+_util.build_backend_classes(_sys.modules[__name__])