Replace .config rather than reconstructing plugins, drivers, and UIs.
[hooke.git] / hooke / config.py
index 7218e5f740ef4128474750da8ce6d034d95b0407..5948974f101c4ef647612f121fb3c6c69cd34af6 100644 (file)
@@ -1,15 +1,33 @@
-# Copyright
+# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
+#
+# This file is part of Hooke.
+#
+# Hooke is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Hooke 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 Lesser General
+# Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with Hooke.  If not, see
+# <http://www.gnu.org/licenses/>.
 
 """Configuration defaults, read/write, and template file creation for
 Hooke.
 """
 
 import ConfigParser as configparser
+import logging
 import os.path
 import textwrap
 import unittest
 
 from .compat.odict import odict as OrderedDict
+from .util.convert import to_string, from_string
 
 
 DEFAULT_PATHS = [
@@ -26,10 +44,13 @@ in turn overrides the developer defaults.
 class Setting (object):
     """An entry (section or option) in HookeConfigParser.
     """
-    def __init__(self, section, option=None, value=None, help=None, wrap=True):
+    def __init__(self, section, option=None, value=None, type='string',
+                 count=1, help=None, wrap=True):
         self.section = section
         self.option = option
         self.value = value
+        self.type = type
+        self.count = count
         self.help = help
         self.wrap = wrap
 
@@ -53,9 +74,11 @@ class Setting (object):
             if self.wrap == True:
                 if wrapper == None:
                     wrapper = textwrap.TextWrapper(
-                        initial_indent="# ",
-                        subsequent_indent="# ")
+                        initial_indent='# ',
+                        subsequent_indent='# ')
                 text = wrapper.fill(text)
+            else:
+                text = '# ' + '\n# '.join(text.splitlines())
             fp.write(text.rstrip()+'\n')
         if self.is_section():
             fp.write("[%s]\n" % self.section)
@@ -64,52 +87,32 @@ class Setting (object):
                                     str(value).replace('\n', '\n\t')))
 
 DEFAULT_SETTINGS = [
-    Setting('display', help='Control display appearance: colour, ???, etc.'),
-    Setting('display', 'colour_ext', 'None', help=None),
-    Setting('display', 'colour_ret', 'None', help=None),
-    Setting('display', 'ext', '1', help=None),
-    Setting('display', 'ret', '1', help=None),
-
-    Setting('display', 'correct', '1', help=None),
-    Setting('display', 'colout_correct', 'None', help=None),
-    Setting('display', 'contact_point', '0', help=None),
-    Setting('display', 'medfilt', '0', help=None),
-
-    Setting('display', 'xaxes', '0', help=None),
-    Setting('display', 'yaxes', '0', help=None),
-    Setting('display', 'flatten', '1', help=None),
-
-    Setting('conditions', 'temperature', '301', help=None),
-
-    Setting('fitting', 'auto_fit_points', '50', help=None),
-    Setting('fitting', 'auto_slope_span', '20', help=None),
-    Setting('fitting', 'auto_delta_force', '1-', help=None),
-    Setting('fitting', 'auto_fit_nm', '5', help=None),
-    Setting('fitting', 'auto_min_p', '0.005', help=None),
-    Setting('fitting', 'auto_max_p', '10', help=None),
-
-    Setting('?', 'baseline_clicks', '0', help=None),
-    Setting('fitting', 'auto_left_baseline', '20', help=None),
-    Setting('fitting', 'auto_right_baseline', '20', help=None),
-    Setting('fitting', 'force_multiplier', '1', help=None),
-    
-    Setting('display', 'fc_showphase', '0', help=None),
-    Setting('display', 'fc_showimposed', '0', help=None),
-    Setting('display', 'fc_interesting', '0', help=None),
-    Setting('?', 'tccd_threshold', '0', help=None),
-    Setting('?', 'tccd_coincident', '0', help=None),
-    Setting('display', '', '', help=None),
-    Setting('display', '', '', help=None),
-
-    Setting('filesystem', 'filterindex', '0', help=None),
-    Setting('filesystem', 'filters',
-            "Playlist files (*.hkp)|*.hkp|Text files (*.txt)|*.txt|All files (*.*)|*.*')",
-            help=None),
-    Setting('filesystem', 'workdir', 'test',
-            help='\n'.join(['# Substitute your work directory',
-                            '#workdir = D:\hooke']),
-            wrap=False),
-    Setting('filesystem', 'playlist', 'test.hkp', help=None),
+    Setting('conditions', help='Default environmental conditions in case they are not specified in the force curve data.  Configuration options in this section are available to every plugin.'),
+    Setting('conditions', 'temperature', value='301', type='float', help='Temperature in Kelvin'),
+    # Logging settings
+    Setting('loggers', help='Configure loggers, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
+    Setting('loggers', 'keys', 'root, hooke', help='Hooke only uses the hooke logger, but other included modules may also use logging and you can configure their loggers here as well.'),
+    Setting('handlers', help='Configure log handlers, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
+    Setting('handlers', 'keys', 'hand1'),
+    Setting('formatters', help='Configure log formatters, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
+    Setting('formatters', 'keys', 'form1'),
+    Setting('logger_root', help='Configure the root logger, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
+    Setting('logger_root', 'level', 'NOTSET'),
+    Setting('logger_root', 'handlers', 'hand1'),
+    Setting('logger_hooke', help='Configure the hooke logger, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
+    Setting('logger_hooke', 'level', 'DEBUG'),
+    Setting('logger_hooke', 'handlers', 'hand1', help='No specific handlers here, just propagate up to the root logger'),
+    Setting('logger_hooke', 'propagate', '0'),
+    Setting('logger_hooke', 'qualname', 'hooke'),
+    Setting('handler_hand1', help='Configure the default log handler, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
+    Setting('handler_hand1', 'class', 'StreamHandler'),
+    Setting('handler_hand1', 'level', 'WARN'),
+    Setting('handler_hand1', 'formatter', 'form1'),
+    Setting('handler_hand1', 'args', '(sys.stderr,)'),
+    Setting('formatter_form1', help='Configure the default log formatter, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
+    Setting('formatter_form1', 'format', '%(asctime)s %(levelname)s %(message)s'),
+    Setting('formatter_form1', 'datefmt', '', help='Leave blank for ISO8601, e.g. "2003-01-23 00:29:50,411".'),
+    Setting('formatter_form1', 'class', 'logging.Formatter'),
     ]
 
 def get_setting(settings, match):
@@ -121,35 +124,68 @@ def get_setting(settings, match):
             return s
     return None
 
-class HookeConfigParser (configparser.SafeConfigParser):
-    """A wrapper around configparser.SafeConfigParser.
+class HookeConfigParser (configparser.RawConfigParser):
+    """A wrapper around configparser.RawConfigParser.
 
     You will probably only need .read and .write.
 
     Examples
     --------
 
+    >>> import pprint
     >>> import sys
-    >>> c = HookeConfigParser(paths=DEFAULT_PATHS,
-    ...                       default_settings=DEFAULT_SETTINGS)
+    >>> c = HookeConfigParser(default_settings=DEFAULT_SETTINGS)
     >>> c.write(sys.stdout) # doctest: +ELLIPSIS
-    # Control display appearance: colour, ???, etc.
-    [display]
-    colour_ext = None
-    colour_ret = None
+    # Default environmental conditions in case they are not specified in
+    # the force curve data.
+    [conditions]
+    # Temperature in Kelvin
+    temperature = 301
+    <BLANKLINE>
+    # Configure loggers, see
+    # http://docs.python.org/library/logging.html#configuration-file-format
+    [loggers]
+    # Hooke only uses the hooke logger, but other included modules may
+    # also use logging and you can configure their loggers here as well.
+    keys = root, hooke
     ...
+
+    class:`HookeConfigParser` automatically converts typed settings.
+
+    >>> section = 'test conversion'
+    >>> c = HookeConfigParser(default_settings=[
+    ...         Setting(section),
+    ...         Setting(section, option='my string', value='Lorem ipsum', type='string'),
+    ...         Setting(section, option='my bool', value=True, type='bool'),
+    ...         Setting(section, option='my int', value=13, type='int'),
+    ...         Setting(section, option='my float', value=3.14159, type='float'),
+    ...         ])
+    >>> pprint.pprint(c.items(section))  # doctest: +ELLIPSIS
+    [('my string', 'Lorem ipsum'),
+     ('my bool', True),
+     ('my int', 13),
+     ('my float', 3.1415...)]
+
+    However, the regular `.get()` is not typed.  Users are encouraged
+    to use the standard `.get*()` methods.
+
+    >>> c.get('test conversion', 'my bool')
+    'True'
+    >>> c.getboolean('test conversion', 'my bool')
+    True
     """
     def __init__(self, paths=None, default_settings=None, defaults=None,
                  dict_type=OrderedDict, indent='# ', **kwargs):
-        # Can't use super() because SafeConfigParser is a classic class
+        # Can't use super() because RawConfigParser is a classic class
         #super(HookeConfigParser, self).__init__(defaults, dict_type)
-        configparser.SafeConfigParser.__init__(self, defaults, dict_type)
+        configparser.RawConfigParser.__init__(self, defaults, dict_type)
         if paths == None:
             paths = []
         self._config_paths = paths
         if default_settings == None:
             default_settings = []
         self._default_settings = default_settings
+        self._default_settings_dict = {}
         for key in ['initial_indent', 'subsequent_indent']:
             if key not in kwargs:
                 kwargs[key] = indent
@@ -161,6 +197,8 @@ class HookeConfigParser (configparser.SafeConfigParser):
             # reversed cause: http://docs.python.org/library/configparser.html
             # "When adding sections or items, add them in the reverse order of
             # how you want them to be displayed in the actual file."
+            self._default_settings_dict[
+                (setting.section, setting.option)] = setting
             if setting.section not in self.sections():
                 self.add_section(setting.section)
             if setting.option != None:
@@ -193,9 +231,9 @@ class HookeConfigParser (configparser.SafeConfigParser):
             for filename in filenames:
                 if os.path.abspath(os.path.expanduser(filename)) not in paths:
                     self._config_paths.append(filename)
-        # Can't use super() because SafeConfigParser is a classic class
+        # Can't use super() because RawConfigParser is a classic class
         #return super(HookeConfigParser, self).read(filenames)
-        return configparser.SafeConfigParser.read(self, filenames)
+        return configparser.RawConfigParser.read(self, filenames)
 
     def _write_setting(self, fp, section=None, option=None, value=None,
                        **kwargs):
@@ -252,14 +290,55 @@ class HookeConfigParser (configparser.SafeConfigParser):
         if local_fp:
             fp.close()
 
+    def items(self, section, *args, **kwargs):
+        """Return a list of tuples with (name, value) for each option
+        in the section.
+        """
+        # Can't use super() because RawConfigParser is a classic class
+        #return super(HookeConfigParser, self).items(section, *args, **kwargs)
+        items = configparser.RawConfigParser.items(
+            self, section, *args, **kwargs)
+        for i,kv in enumerate(items):
+            key,value = kv
+            log = logging.getLogger('hooke') 
+            try:
+                setting = self._default_settings_dict[(section, key)]
+            except KeyError, e:
+                log.error('unknown setting %s/%s: %s' % (section, key, e))
+                raise
+            try:
+                items[i] = (key, from_string(value=value, type=setting.type,
+                                             count=setting.count))
+            except ValueError, e:
+                log.error("could not convert '%s' (%s) for %s/%s: %s"
+                          % (value, type(value), section, key, e))
+                raise
+        return items
+
+    def set(self, section, option, value):
+        """Set an option."""
+        setting = self._default_settings_dict[(section, option)]
+        value = to_string(value=value, type=setting.type, count=setting.count)
+        # Can't use super() because RawConfigParser is a classic class
+        #return super(HookeConfigParser, self).set(section, option, value)
+        configparser.RawConfigParser.set(self, section, option, value)
+
+    def optionxform(self, option):
+        """
+
+        Overrides lowercasing behaviour of
+        :meth:`ConfigParser.RawConfigParser.optionxform`.
+        """
+        return option
+
+
 class TestHookeConfigParser (unittest.TestCase):
     def test_queue_safe(self):
         """Ensure :class:`HookeConfigParser` is Queue-safe.
         """
         from multiprocessing import Queue
         q = Queue()
-        a = HookeConfigParser(
-            paths=DEFAULT_PATHS, default_settings=DEFAULT_SETTINGS)
+        a = HookeConfigParser(default_settings=DEFAULT_SETTINGS)
         q.put(a)
         b = q.get(a)
         for attr in ['_dict', '_defaults', '_sections', '_config_paths',