1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
3 # This file is part of Hooke.
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.
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.
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/>.
19 """Configuration defaults, read/write, and template file creation for
23 import ConfigParser as configparser
28 from .compat.odict import odict as OrderedDict
32 '/usr/share/hooke/hooke.cfg',
33 '/etc/hooke/hooke.cfg',
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.
42 CONVERT_FROM_STRING = {
43 'string': lambda x: x,
44 'bool': lambda x: x == 'True',
45 'int': lambda x: int(x),
46 'float': lambda x: float(x),
48 """Functions converting strings to values, keyed by type.
52 'string': lambda x: x,
53 'bool': lambda x: str(x),
54 'int': lambda x: str(x),
55 'float': lambda x: str(x),
57 """Functions converting values to strings, keyed by type.
60 class Setting (object):
61 """An entry (section or option) in HookeConfigParser.
63 def __init__(self, section, option=None, value=None, type='string',
64 help=None, wrap=True):
65 self.section = section
67 self.value = CONVERT_TO_STRING[type](value)
72 def __eq__(self, other):
73 for attr in ['__class__', 'section', 'option', 'value', 'help']:
74 value = getattr(self, attr)
75 o_value = getattr(other, attr)
81 return self.option == None
84 return not self.is_section()
86 def write(self, fp, value=None, comments=True, wrapper=None):
87 if comments == True and self.help != None:
91 wrapper = textwrap.TextWrapper(
93 subsequent_indent='# ')
94 text = wrapper.fill(text)
96 text = '# ' + '\n# '.join(text.splitlines())
97 fp.write(text.rstrip()+'\n')
99 fp.write("[%s]\n" % self.section)
101 fp.write("%s = %s\n" % (self.option,
102 str(value).replace('\n', '\n\t')))
105 Setting('conditions', help='Default environmental conditions in case they are not specified in the force curve data.'),
106 Setting('conditions', 'temperature', value='301', type='float', help='Temperature in Kelvin'),
108 Setting('loggers', help='Configure loggers, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
109 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.'),
110 Setting('handlers', help='Configure log handlers, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
111 Setting('handlers', 'keys', 'hand1'),
112 Setting('formatters', help='Configure log formatters, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
113 Setting('formatters', 'keys', 'form1'),
114 Setting('logger_root', help='Configure the root logger, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
115 Setting('logger_root', 'level', 'NOTSET'),
116 Setting('logger_root', 'handlers', 'hand1'),
117 Setting('logger_hooke', help='Configure the hooke logger, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
118 Setting('logger_hooke', 'level', 'DEBUG'),
119 Setting('logger_hooke', 'handlers', 'hand1', help='No specific handlers here, just propagate up to the root logger'),
120 Setting('logger_hooke', 'propagate', '0'),
121 Setting('logger_hooke', 'qualname', 'hooke'),
122 Setting('handler_hand1', help='Configure the default log handler, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
123 Setting('handler_hand1', 'class', 'StreamHandler'),
124 Setting('handler_hand1', 'level', 'NOTSET'),
125 Setting('handler_hand1', 'formatter', 'form1'),
126 Setting('handler_hand1', 'args', '(sys.stderr,)'),
127 Setting('formatter_form1', help='Configure the default log formatter, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
128 Setting('formatter_form1', 'format', '%(asctime)s %(levelname)s %(message)s'),
129 Setting('formatter_form1', 'datefmt', '', help='Leave blank for ISO8601, e.g. "2003-01-23 00:29:50,411".'),
130 Setting('formatter_form1', 'class', 'logging.Formatter'),
133 def get_setting(settings, match):
134 """Return the first Setting object matching both match.section and
138 if s.section == match.section and s.option == match.option:
142 class HookeConfigParser (configparser.RawConfigParser):
143 """A wrapper around configparser.RawConfigParser.
145 You will probably only need .read and .write.
152 >>> c = HookeConfigParser(default_settings=DEFAULT_SETTINGS)
153 >>> c.write(sys.stdout) # doctest: +ELLIPSIS
154 # Default environmental conditions in case they are not specified in
155 # the force curve data.
157 # Temperature in Kelvin
160 # Configure loggers, see
161 # http://docs.python.org/library/logging.html#configuration-file-format
163 # Hooke only uses the hooke logger, but other included modules may
164 # also use logging and you can configure their loggers here as well.
168 class:`HookeConfigParser` automatically converts typed settings.
170 >>> section = 'test conversion'
171 >>> c = HookeConfigParser(default_settings=[
172 ... Setting(section),
173 ... Setting(section, option='my string', value='Lorem ipsum', type='string'),
174 ... Setting(section, option='my bool', value=True, type='bool'),
175 ... Setting(section, option='my int', value=13, type='int'),
176 ... Setting(section, option='my float', value=3.14159, type='float'),
178 >>> pprint.pprint(c.items(section)) # doctest: +ELLIPSIS
179 [('my string', 'Lorem ipsum'),
182 ('my float', 3.1415...)]
184 However, the regular `.get()` is not typed. Users are encouraged
185 to use the standard `.get*()` methods.
187 >>> c.get('test conversion', 'my bool')
189 >>> c.getboolean('test conversion', 'my bool')
192 def __init__(self, paths=None, default_settings=None, defaults=None,
193 dict_type=OrderedDict, indent='# ', **kwargs):
194 # Can't use super() because RawConfigParser is a classic class
195 #super(HookeConfigParser, self).__init__(defaults, dict_type)
196 configparser.RawConfigParser.__init__(self, defaults, dict_type)
199 self._config_paths = paths
200 if default_settings == None:
201 default_settings = []
202 self._default_settings = default_settings
203 self._default_settings_dict = {}
204 for key in ['initial_indent', 'subsequent_indent']:
205 if key not in kwargs:
207 self._comment_textwrap = textwrap.TextWrapper(**kwargs)
208 self._setup_default_settings()
210 def _setup_default_settings(self):
211 for setting in self._default_settings: #reversed(self._default_settings):
212 # reversed cause: http://docs.python.org/library/configparser.html
213 # "When adding sections or items, add them in the reverse order of
214 # how you want them to be displayed in the actual file."
215 self._default_settings_dict[
216 (setting.section, setting.option)] = setting
217 if setting.section not in self.sections():
218 self.add_section(setting.section)
219 if setting.option != None:
220 self.set(setting.section, setting.option, setting.value)
222 def read(self, filenames=None):
223 """Read and parse a filename or a list of filenames.
225 If filenames is None, it defaults to ._config_paths. If a
226 filename is not in ._config_paths, it gets appended to the
227 list. We also run os.path.expanduser() on the input filenames
228 internally so you don't have to worry about it.
230 Files that cannot be opened are silently ignored; this is
231 designed so that you can specify a list of potential
232 configuration file locations (e.g. current directory, user's
233 home directory, systemwide directory), and all existing
234 configuration files in the list will be read. A single
235 filename may also be given.
237 Return list of successfully read files.
239 if filenames == None:
240 filenames = [os.path.expanduser(p) for p in self._config_paths]
242 if isinstance(filenames, basestring):
243 filenames = [filenames]
244 paths = [os.path.abspath(os.path.expanduser(p))
245 for p in self._config_paths]
246 for filename in filenames:
247 if os.path.abspath(os.path.expanduser(filename)) not in paths:
248 self._config_paths.append(filename)
249 # Can't use super() because RawConfigParser is a classic class
250 #return super(HookeConfigParser, self).read(filenames)
251 return configparser.RawConfigParser.read(self, filenames)
253 def _write_setting(self, fp, section=None, option=None, value=None,
255 """Print, if known, a nicely wrapped comment form of a
258 match = get_setting(self._default_settings, Setting(section, option))
260 match = Setting(section, option, value, **kwargs)
261 match.write(fp, value=value, wrapper=self._comment_textwrap)
263 def write(self, fp=None, comments=True):
264 """Write an .ini-format representation of the configuration state.
266 This expands on RawConfigParser.write() by optionally adding
267 comments when they are known (i.e. for ._default_settings).
268 However, comments are not read in during a read, so .read(x)
269 .write(x) may `not preserve comments`_.
271 .. _not preserve comments: http://bugs.python.org/issue1410680
276 >>> import sys, StringIO
277 >>> c = HookeConfigParser()
284 >>> c._read(StringIO.StringIO(instring), 'example.cfg')
285 >>> c.write(sys.stdout)
290 local_fp = fp == None
292 fp = open(os.path.expanduser(self._config_paths[-1]), 'w')
294 self._write_setting(fp, configparser.DEFAULTSECT,
295 help="Miscellaneous options")
296 for (key, value) in self._defaults.items():
297 self._write_setting(fp, configparser.DEFAULTSECT, key, value)
299 for section in self._sections:
300 self._write_setting(fp, section)
301 for (key, value) in self._sections[section].items():
302 if key != "__name__":
303 self._write_setting(fp, section, key, value)
308 def items(self, section, *args, **kwargs):
309 """Return a list of tuples with (name, value) for each option
312 # Can't use super() because RawConfigParser is a classic class
313 #return super(HookeConfigParser, self).items(section, *args, **kwargs)
314 items = configparser.RawConfigParser.items(
315 self, section, *args, **kwargs)
316 for i,kv in enumerate(items):
318 setting = self._default_settings_dict[(section, key)]
319 items[i] = (key, CONVERT_FROM_STRING[setting.type](value))
322 def set(self, section, option, value):
324 setting = self._default_settings_dict[(section, option)]
325 value = CONVERT_TO_STRING[setting.type](value)
326 # Can't use super() because RawConfigParser is a classic class
327 #return super(HookeConfigParser, self).set(section, option, value)
328 configparser.RawConfigParser.set(self, section, option, value)
330 def optionxform(self, option):
333 Overrides lowercasing behaviour of
334 :meth:`ConfigParser.RawConfigParser.optionxform`.
339 class TestHookeConfigParser (unittest.TestCase):
340 def test_queue_safe(self):
341 """Ensure :class:`HookeConfigParser` is Queue-safe.
343 from multiprocessing import Queue
345 a = HookeConfigParser(default_settings=DEFAULT_SETTINGS)
348 for attr in ['_dict', '_defaults', '_sections', '_config_paths',
349 '_default_settings']:
350 a_value = getattr(a, attr)
351 b_value = getattr(b, attr)
354 'HookeConfigParser.%s did not survive Queue: %s != %s'
355 % (attr, a_value, b_value))