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