Fix nested dumping output (using prefix and nested config names).
[h5config.git] / h5config / config.py
index a5a137e6de11c7b2a9491749b9905aa55d1146a6..65687603917fdb31cd19394d6fa855d746b2a1a5 100644 (file)
 """The basic h5config classes
 """
 
+import copy as _copy
 
-class _Setting (object):
-    "A named setting with arbitrart text values."
+from . import LOG as _LOG
+
+
+class Setting (object):
+    "A named setting with arbitrary text values."
     def __init__(self, name, help='', default=None):
         self.name = name
         self._help = help
@@ -38,20 +42,20 @@ class _Setting (object):
         return ret.strip()
 
     def convert_from_text(self, value):
-        return value
+        return value or None
 
     def convert_to_text(self, value):
-        return value
+        return value or ''
 
 
-class _ChoiceSetting (_Setting):
+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 = ChoiceSetting(name='bool',
+    ...                   choices=[('yes', True), ('no', False)])
     >>> s.convert_from_text('yes')
     True
     >>> s.convert_to_text(True)
@@ -67,14 +71,14 @@ class _ChoiceSetting (_Setting):
         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)
+        super(ChoiceSetting, self).__init__(**kwargs)
         if choices == None:
             choices = []
         self.choices = choices
 
     def help(self):
         ret = '%s  Choices: %s' % (
-            super(_ChoiceSetting, self).help(),
+            super(ChoiceSetting, self).help(),
             ', '.join([key for key,value in self.choices]))
         return ret.strip()
 
@@ -89,10 +93,10 @@ class _ChoiceSetting (_Setting):
         raise ValueError(value)
 
 
-class _BooleanSetting (_ChoiceSetting):
+class BooleanSetting (ChoiceSetting):
     """A named settubg that can be either true or false.
 
-    >>> s = _BooleanSetting(name='bool')
+    >>> s = BooleanSetting(name='bool')
 
     >>> s.convert_from_text('yes')
     True
@@ -105,32 +109,25 @@ class _BooleanSetting (_ChoiceSetting):
     >>> 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)
+    def __init__(self, default=False, **kwargs):
+        super(BooleanSetting, self).__init__(
+            choices=[('yes', True), ('no', False)], default=default, **kwargs)
 
 
-class _NumericSetting (_Setting):
+class NumericSetting (Setting):
     """A named setting with numeric values.
 
     Don't use this setting class.  Use a more specific subclass, such
-    as `_IntegerSetting`.
+    as `IntegerSetting`.
 
-    >>> s = _NumericSetting(name='float')
+    >>> 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 __init__(self, default=0, **kwargs):
+        super(NumericSetting, self).__init__(default=default, **kwargs)
 
     def convert_to_text(self, value):
         return str(value)
@@ -144,25 +141,26 @@ class _NumericSetting (_Setting):
         raise NotImplementedError()
 
 
-class _IntegerSetting (_NumericSetting):
+class IntegerSetting (NumericSetting):
     """A named setting with integer values.
 
-    >>> s = _IntegerSetting(name='int')
+    >>> s = IntegerSetting(name='int')
     >>> s.default
     1
     >>> s.convert_from_text('8')
     8
     """
-    _default_value = 1
+    def __init__(self, default=1, **kwargs):
+        super(IntegerSetting, self).__init__(default=default, **kwargs)
 
     def _convert_from_text(self, value):
         return int(value)
 
 
-class _FloatSetting (_NumericSetting):
+class FloatSetting (NumericSetting):
     """A named setting with floating point values.
 
-    >>> s = _FloatSetting(name='float')
+    >>> s = FloatSetting(name='float')
     >>> s.default
     1.0
     >>> s.convert_from_text('8')
@@ -173,16 +171,17 @@ class _FloatSetting (_NumericSetting):
     ...     print 'caught a ValueError'
     caught a ValueError
     """
-    _default_value = 1.0
+    def __init__(self, default=1.0, **kwargs):
+        super(FloatSetting, self).__init__(default=default, **kwargs)
 
     def _convert_from_text(self, value):
         return float(value)
 
 
-class _FloatListSetting (_Setting):
+class FloatListSetting (Setting):
     """A named setting with a list of floating point values.
 
-    >>> s = _FloatListSetting(name='floatlist')
+    >>> s = FloatListSetting(name='floatlist')
     >>> s.default
     []
     >>> s.convert_to_text([1, 2.3])
@@ -194,10 +193,8 @@ class _FloatListSetting (_Setting):
     >>> s.convert_from_text('')
     []
     """
-    def __init__(self, **kwargs):
-        if 'default' not in kwargs:
-            kwargs['default'] = []
-        super(_FloatListSetting, self).__init__(**kwargs)
+    def __init__(self, default=[], **kwargs):
+        super(FloatListSetting, self).__init__(default=default, **kwargs)
 
     def _convert_from_text(self, value):
         if value is None:
@@ -207,7 +204,7 @@ class _FloatListSetting (_Setting):
     def convert_from_text(self, value):
         if value is None:
             return None
-        elif value == '':
+        elif value in ['', []]:
             return []
         return [self._convert_from_text(x) for x in value.split(',')]
 
@@ -217,35 +214,76 @@ class _FloatListSetting (_Setting):
         return ', '.join([str(x) for x in value])
 
 
-class _Config (dict):
-    "A class with a list `._keys` of `_Setting`\s."
+class ConfigSetting (Setting):
+    """A setting that holds a pointer to a child `Config` class
+
+    This allows you to nest `Config`\s, which is a useful way to
+    contain complexity.  In order to save such a config, the backend
+    must be able to handle hierarchical storage (possibly via
+    references).
+
+    For example, a configurable AFM may contain a configurable piezo
+    scanner, as well as a configurable stepper motor.
+    """
+    def __init__(self, config_class=None, **kwargs):
+        super(ConfigSetting, self).__init__(**kwargs)
+        self.config_class = config_class
+
+
+class ConfigListSetting (ConfigSetting):
+    """A setting that holds a list of child `Config` classes
+
+    For example, a piezo scanner with several axes.
+    """
+    def __init__(self, default=[], **kwargs):
+        super(ConfigListSetting, self).__init__(**kwargs)
+
+
+class Config (dict):
+    "A class with a list `._keys` of `Setting`\s."
     settings = []
 
-    def __init__(self):
+    def __init__(self, storage=None):
+        super(Config, self).__init__()
+        self.clear()
+        self._storage = storage
+
+    def __repr__(self):
+        return '<{} {}>'.format(self.__class__.__name__, id(self))
+
+    def clear(self):
+        super(Config, self).clear()
         for s in self.settings:
-            self[s.name] = s.default
+            # copy to avoid ambiguity with mutable defaults
+            self[s.name] = _copy.deepcopy(s.default)
+
+    def load(self, merge=False, **kwargs):
+        self._storage.load(config=self, merge=merge, **kwargs)
 
-    def dump(self, help=False):
+    def save(self, merge=False, **kwargs):
+        self._storage.save(config=self, merge=merge, **kwargs)
+
+    def dump(self, help=False, prefix=''):
         """Return all settings and their values as a string
 
-        >>> class _MyConfig (_Config):
+        >>> class MyConfig (Config):
         ...     settings = [
-        ...         _ChoiceSetting(
+        ...         ChoiceSetting(
         ...             name='number',
         ...             help='I have a number behind my back...',
         ...             default=1,
         ...             choices=[('one', 1), ('two', 2),
         ...                 ]),
-        ...         _BooleanSetting(
+        ...         BooleanSetting(
         ...             name='odd',
         ...             help='The number behind my back is odd.',
         ...             default=True),
-        ...         _IntegerSetting(
+        ...         IntegerSetting(
         ...             name='guesses',
         ...             help='Number of guesses before epic failure.',
         ...             default=2),
         ...         ]
-        >>> c = _MyConfig()
+        >>> c = MyConfig()
         >>> print c.dump()
         number: one
         odd: yes
@@ -260,19 +298,29 @@ class _Config (dict):
         lines = []
         for setting in self.settings:
             name = setting.name
-            value_string = setting.convert_to_text(self[name])
-            if help:
-                help_string = '\t({})'.format(setting.help())
-            else:
-                help_string = ''
-            lines.append('{}: {}{}'.format(name, value_string, help_string))
+            value = self[name]
+            try:
+                if isinstance(setting, ConfigListSetting):
+                    if value:
+                        lines.append('{}{}:'.format(prefix, name))
+                        for i,config in enumerate(value):
+                            lines.append('{}  {}:'.format(prefix, i))
+                            lines.append(
+                                config.dump(help=help, prefix=prefix+'    '))
+                        continue
+                elif isinstance(setting, ConfigSetting):
+                    if value is not None:
+                        lines.append('{}{}:'.format(prefix, name))
+                        lines.append(value.dump(help=help, prefix=prefix+'  '))
+                        continue
+                value_string = setting.convert_to_text(self[name])
+                if help:
+                    help_string = '\t({})'.format(setting.help())
+                else:
+                    help_string = ''
+                lines.append('{}{}: {}{}'.format(
+                        prefix, name, value_string, help_string))
+            except Exception:
+                _LOG.error('could not dump {} ({!r})'.format(name, value))
+                raise
         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()