Broke type conversion out into hooke.util.convert and expanded its use.
[hooke.git] / hooke / config.py
1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of Hooke.
4 #
5 # Hooke is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU Lesser General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
13 # Public License for more details.
14 #
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with Hooke.  If not, see
17 # <http://www.gnu.org/licenses/>.
18
19 """Configuration defaults, read/write, and template file creation for
20 Hooke.
21 """
22
23 import ConfigParser as configparser
24 import os.path
25 import textwrap
26 import unittest
27
28 from .compat.odict import odict as OrderedDict
29 from .util.convert import to_string, from_string
30
31
32 DEFAULT_PATHS = [
33     '/usr/share/hooke/hooke.cfg',
34     '/etc/hooke/hooke.cfg',
35     '~/.hooke.cfg',
36     '.hooke.cfg',
37     ]
38 """We start with the system files, and work our way to the more
39 specific user files, so the user can override the sysadmin who
40 in turn overrides the developer defaults.
41 """
42
43 class Setting (object):
44     """An entry (section or option) in HookeConfigParser.
45     """
46     def __init__(self, section, option=None, value=None, type='string',
47                  help=None, wrap=True):
48         self.section = section
49         self.option = option
50         self.value = to_string(value=value, type=type)
51         self.type = type
52         self.help = help
53         self.wrap = wrap
54
55     def __eq__(self, other):
56         for attr in ['__class__', 'section', 'option', 'value', 'help']:
57             value = getattr(self, attr)
58             o_value = getattr(other, attr)
59             if o_value != value:
60                 return False
61         return True
62
63     def is_section(self):
64         return self.option == None
65
66     def is_option(self):
67         return not self.is_section()
68
69     def write(self, fp, value=None, comments=True, wrapper=None):
70         if comments == True and self.help != None:
71             text = self.help
72             if self.wrap == True:
73                 if wrapper == None:
74                     wrapper = textwrap.TextWrapper(
75                         initial_indent='# ',
76                         subsequent_indent='# ')
77                 text = wrapper.fill(text)
78             else:
79                 text = '# ' + '\n# '.join(text.splitlines())
80             fp.write(text.rstrip()+'\n')
81         if self.is_section():
82             fp.write("[%s]\n" % self.section)
83         else:
84             fp.write("%s = %s\n" % (self.option,
85                                     str(value).replace('\n', '\n\t')))
86
87 DEFAULT_SETTINGS = [
88     Setting('conditions', help='Default environmental conditions in case they are not specified in the force curve data.'),
89     Setting('conditions', 'temperature', value='301', type='float', help='Temperature in Kelvin'),
90     # Logging settings
91     Setting('loggers', help='Configure loggers, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
92     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.'),
93     Setting('handlers', help='Configure log handlers, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
94     Setting('handlers', 'keys', 'hand1'),
95     Setting('formatters', help='Configure log formatters, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
96     Setting('formatters', 'keys', 'form1'),
97     Setting('logger_root', help='Configure the root logger, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
98     Setting('logger_root', 'level', 'NOTSET'),
99     Setting('logger_root', 'handlers', 'hand1'),
100     Setting('logger_hooke', help='Configure the hooke logger, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
101     Setting('logger_hooke', 'level', 'DEBUG'),
102     Setting('logger_hooke', 'handlers', 'hand1', help='No specific handlers here, just propagate up to the root logger'),
103     Setting('logger_hooke', 'propagate', '0'),
104     Setting('logger_hooke', 'qualname', 'hooke'),
105     Setting('handler_hand1', help='Configure the default log handler, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
106     Setting('handler_hand1', 'class', 'StreamHandler'),
107     Setting('handler_hand1', 'level', 'NOTSET'),
108     Setting('handler_hand1', 'formatter', 'form1'),
109     Setting('handler_hand1', 'args', '(sys.stderr,)'),
110     Setting('formatter_form1', help='Configure the default log formatter, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
111     Setting('formatter_form1', 'format', '%(asctime)s %(levelname)s %(message)s'),
112     Setting('formatter_form1', 'datefmt', '', help='Leave blank for ISO8601, e.g. "2003-01-23 00:29:50,411".'),
113     Setting('formatter_form1', 'class', 'logging.Formatter'),
114     ]
115
116 def get_setting(settings, match):
117     """Return the first Setting object matching both match.section and
118     match.option.
119     """
120     for s in settings:
121         if s.section == match.section and s.option == match.option:
122             return s
123     return None
124
125 class HookeConfigParser (configparser.RawConfigParser):
126     """A wrapper around configparser.RawConfigParser.
127
128     You will probably only need .read and .write.
129
130     Examples
131     --------
132
133     >>> import pprint
134     >>> import sys
135     >>> c = HookeConfigParser(default_settings=DEFAULT_SETTINGS)
136     >>> c.write(sys.stdout) # doctest: +ELLIPSIS
137     # Default environmental conditions in case they are not specified in
138     # the force curve data.
139     [conditions]
140     # Temperature in Kelvin
141     temperature = 301
142     <BLANKLINE>
143     # Configure loggers, see
144     # http://docs.python.org/library/logging.html#configuration-file-format
145     [loggers]
146     # Hooke only uses the hooke logger, but other included modules may
147     # also use logging and you can configure their loggers here as well.
148     keys = root, hooke
149     ...
150
151     class:`HookeConfigParser` automatically converts typed settings.
152
153     >>> section = 'test conversion'
154     >>> c = HookeConfigParser(default_settings=[
155     ...         Setting(section),
156     ...         Setting(section, option='my string', value='Lorem ipsum', type='string'),
157     ...         Setting(section, option='my bool', value=True, type='bool'),
158     ...         Setting(section, option='my int', value=13, type='int'),
159     ...         Setting(section, option='my float', value=3.14159, type='float'),
160     ...         ])
161     >>> pprint.pprint(c.items(section))  # doctest: +ELLIPSIS
162     [('my string', 'Lorem ipsum'),
163      ('my bool', True),
164      ('my int', 13),
165      ('my float', 3.1415...)]
166
167     However, the regular `.get()` is not typed.  Users are encouraged
168     to use the standard `.get*()` methods.
169
170     >>> c.get('test conversion', 'my bool')
171     'True'
172     >>> c.getboolean('test conversion', 'my bool')
173     True
174     """
175     def __init__(self, paths=None, default_settings=None, defaults=None,
176                  dict_type=OrderedDict, indent='# ', **kwargs):
177         # Can't use super() because RawConfigParser is a classic class
178         #super(HookeConfigParser, self).__init__(defaults, dict_type)
179         configparser.RawConfigParser.__init__(self, defaults, dict_type)
180         if paths == None:
181             paths = []
182         self._config_paths = paths
183         if default_settings == None:
184             default_settings = []
185         self._default_settings = default_settings
186         self._default_settings_dict = {}
187         for key in ['initial_indent', 'subsequent_indent']:
188             if key not in kwargs:
189                 kwargs[key] = indent
190         self._comment_textwrap = textwrap.TextWrapper(**kwargs)
191         self._setup_default_settings()
192
193     def _setup_default_settings(self):
194         for setting in self._default_settings: #reversed(self._default_settings):
195             # reversed cause: http://docs.python.org/library/configparser.html
196             # "When adding sections or items, add them in the reverse order of
197             # how you want them to be displayed in the actual file."
198             self._default_settings_dict[
199                 (setting.section, setting.option)] = setting
200             if setting.section not in self.sections():
201                 self.add_section(setting.section)
202             if setting.option != None:
203                 self.set(setting.section, setting.option, setting.value)
204
205     def read(self, filenames=None):
206         """Read and parse a filename or a list of filenames.
207
208         If filenames is None, it defaults to ._config_paths.  If a
209         filename is not in ._config_paths, it gets appended to the
210         list.  We also run os.path.expanduser() on the input filenames
211         internally so you don't have to worry about it.
212
213         Files that cannot be opened are silently ignored; this is
214         designed so that you can specify a list of potential
215         configuration file locations (e.g. current directory, user's
216         home directory, systemwide directory), and all existing
217         configuration files in the list will be read.  A single
218         filename may also be given.
219
220         Return list of successfully read files.
221         """
222         if filenames == None:
223             filenames = [os.path.expanduser(p) for p in self._config_paths]
224         else:
225             if isinstance(filenames, basestring):
226                 filenames = [filenames]
227             paths = [os.path.abspath(os.path.expanduser(p))
228                      for p in self._config_paths]
229             for filename in filenames:
230                 if os.path.abspath(os.path.expanduser(filename)) not in paths:
231                     self._config_paths.append(filename)
232         # Can't use super() because RawConfigParser is a classic class
233         #return super(HookeConfigParser, self).read(filenames)
234         return configparser.RawConfigParser.read(self, filenames)
235
236     def _write_setting(self, fp, section=None, option=None, value=None,
237                        **kwargs):
238         """Print, if known, a nicely wrapped comment form of a
239         setting's .help.
240         """
241         match = get_setting(self._default_settings, Setting(section, option))
242         if match == None:
243             match = Setting(section, option, value, **kwargs)
244         match.write(fp, value=value, wrapper=self._comment_textwrap)
245
246     def write(self, fp=None, comments=True):
247         """Write an .ini-format representation of the configuration state.
248
249         This expands on RawConfigParser.write() by optionally adding
250         comments when they are known (i.e. for ._default_settings).
251         However, comments are not read in during a read, so .read(x)
252         .write(x) may `not preserve comments`_.
253
254         .. _not preserve comments: http://bugs.python.org/issue1410680
255
256         Examples
257         --------
258
259         >>> import sys, StringIO
260         >>> c = HookeConfigParser()
261         >>> instring = '''
262         ... # Some comment
263         ... [section]
264         ... option = value
265         ...
266         ... '''
267         >>> c._read(StringIO.StringIO(instring), 'example.cfg')
268         >>> c.write(sys.stdout)
269         [section]
270         option = value
271         <BLANKLINE>
272         """
273         local_fp = fp == None
274         if local_fp:
275             fp = open(os.path.expanduser(self._config_paths[-1]), 'w')
276         if self._defaults:
277             self._write_setting(fp, configparser.DEFAULTSECT,
278                                 help="Miscellaneous options")
279             for (key, value) in self._defaults.items():
280                 self._write_setting(fp, configparser.DEFAULTSECT, key, value)
281             fp.write("\n")
282         for section in self._sections:
283             self._write_setting(fp, section)
284             for (key, value) in self._sections[section].items():
285                 if key != "__name__":
286                     self._write_setting(fp, section, key, value)
287             fp.write("\n")
288         if local_fp:
289             fp.close()
290
291     def items(self, section, *args, **kwargs):
292         """Return a list of tuples with (name, value) for each option
293         in the section.
294         """
295         # Can't use super() because RawConfigParser is a classic class
296         #return super(HookeConfigParser, self).items(section, *args, **kwargs)
297         items = configparser.RawConfigParser.items(
298             self, section, *args, **kwargs)
299         for i,kv in enumerate(items):
300             key,value = kv
301             setting = self._default_settings_dict[(section, key)]
302             items[i] = (key, from_string(value=value, type=setting.type))
303         return items
304
305     def set(self, section, option, value):
306         """Set an option."""
307         setting = self._default_settings_dict[(section, option)]
308         value = to_string(value=value, type=setting.type)
309         # Can't use super() because RawConfigParser is a classic class
310         #return super(HookeConfigParser, self).set(section, option, value)
311         configparser.RawConfigParser.set(self, section, option, value)
312
313     def optionxform(self, option):
314         """
315
316         Overrides lowercasing behaviour of
317         :meth:`ConfigParser.RawConfigParser.optionxform`.
318         """
319         return option
320
321
322 class TestHookeConfigParser (unittest.TestCase):
323     def test_queue_safe(self):
324         """Ensure :class:`HookeConfigParser` is Queue-safe.
325         """
326         from multiprocessing import Queue
327         q = Queue()
328         a = HookeConfigParser(default_settings=DEFAULT_SETTINGS)
329         q.put(a)
330         b = q.get(a)
331         for attr in ['_dict', '_defaults', '_sections', '_config_paths',
332                      '_default_settings']:
333             a_value = getattr(a, attr)
334             b_value = getattr(b, attr)
335             self.failUnless(
336                 a_value == b_value,
337                 'HookeConfigParser.%s did not survive Queue: %s != %s'
338                 % (attr, a_value, b_value))