X-Git-Url: http://git.tremily.us/?p=hooke.git;a=blobdiff_plain;f=hooke%2Fconfig.py;h=5948974f101c4ef647612f121fb3c6c69cd34af6;hp=7f10c1527ad557b16550abc69e3a21e8c9a12b90;hb=b90995fb4b6d8151df862d40edc8c369d7052cfa;hpb=676be70ebc00b0ae33fbe237f9a67525c553aa5b diff --git a/hooke/config.py b/hooke/config.py index 7f10c15..5948974 100644 --- a/hooke/config.py +++ b/hooke/config.py @@ -1,24 +1,40 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -''' -config.py - -Configuration defaults, read/write, and template file creation for Hooke. - -COPYRIGHT -''' +# Copyright (C) 2010 W. Trevor King +# +# 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 +# . + +"""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 = [ '/usr/share/hooke/hooke.cfg', '/etc/hooke/hooke.cfg', '~/.hooke.cfg', + '.hooke.cfg', ] """We start with the system files, and work our way to the more specific user files, so the user can override the sysadmin who @@ -28,12 +44,23 @@ 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 = True + self.wrap = wrap + + def __eq__(self, other): + for attr in ['__class__', 'section', 'option', 'value', 'help']: + value = getattr(self, attr) + o_value = getattr(other, attr) + if o_value != value: + return False + return True def is_section(self): return self.option == None @@ -47,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) @@ -58,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): @@ -115,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 + + # 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 @@ -155,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: @@ -187,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): @@ -230,9 +274,8 @@ class HookeConfigParser (configparser.SafeConfigParser): """ local_fp = fp == None if local_fp: - fp = open(self._config_paths[-1], 'w') + fp = open(os.path.expanduser(self._config_paths[-1]), 'w') if self._defaults: - print self._defaults self._write_setting(fp, configparser.DEFAULTSECT, help="Miscellaneous options") for (key, value) in self._defaults.items(): @@ -246,3 +289,63 @@ class HookeConfigParser (configparser.SafeConfigParser): fp.write("\n") 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(default_settings=DEFAULT_SETTINGS) + q.put(a) + b = q.get(a) + for attr in ['_dict', '_defaults', '_sections', '_config_paths', + '_default_settings']: + a_value = getattr(a, attr) + b_value = getattr(b, attr) + self.failUnless( + a_value == b_value, + 'HookeConfigParser.%s did not survive Queue: %s != %s' + % (attr, a_value, b_value))