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
29 from .compat.odict import odict as OrderedDict
30 from .util.convert import to_string, from_string
34 '/usr/share/hooke/hooke.cfg',
35 '/etc/hooke/hooke.cfg',
39 """We start with the system files, and work our way to the more
40 specific user files, so the user can override the sysadmin who
41 in turn overrides the developer defaults.
44 class Setting (object):
45 """An entry (section or option) in HookeConfigParser.
47 def __init__(self, section, option=None, value=None, type='string',
48 count=1, help=None, wrap=True):
49 self.section = section
57 def __eq__(self, other):
58 for attr in ['__class__', 'section', 'option', 'value', 'help']:
59 value = getattr(self, attr)
60 o_value = getattr(other, attr)
66 return self.option == None
69 return not self.is_section()
71 def write(self, fp, value=None, comments=True, wrapper=None):
72 if comments == True and self.help != None:
76 wrapper = textwrap.TextWrapper(
78 subsequent_indent='# ')
79 text = wrapper.fill(text)
81 text = '# ' + '\n# '.join(text.splitlines())
82 fp.write(text.rstrip()+'\n')
84 fp.write("[%s]\n" % self.section)
86 fp.write("%s = %s\n" % (self.option,
87 str(value).replace('\n', '\n\t')))
90 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.'),
91 Setting('conditions', 'temperature', value='301', type='float', help='Temperature in Kelvin'),
93 Setting('loggers', help='Configure loggers, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
94 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.'),
95 Setting('handlers', help='Configure log handlers, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
96 Setting('handlers', 'keys', 'hand1'),
97 Setting('formatters', help='Configure log formatters, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
98 Setting('formatters', 'keys', 'form1'),
99 Setting('logger_root', help='Configure the root logger, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
100 Setting('logger_root', 'level', 'NOTSET'),
101 Setting('logger_root', 'handlers', 'hand1'),
102 Setting('logger_hooke', help='Configure the hooke logger, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
103 Setting('logger_hooke', 'level', 'DEBUG'),
104 Setting('logger_hooke', 'handlers', 'hand1', help='No specific handlers here, just propagate up to the root logger'),
105 Setting('logger_hooke', 'propagate', '0'),
106 Setting('logger_hooke', 'qualname', 'hooke'),
107 Setting('handler_hand1', help='Configure the default log handler, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
108 Setting('handler_hand1', 'class', 'StreamHandler'),
109 Setting('handler_hand1', 'level', 'WARN'),
110 Setting('handler_hand1', 'formatter', 'form1'),
111 Setting('handler_hand1', 'args', '(sys.stderr,)'),
112 Setting('formatter_form1', help='Configure the default log formatter, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
113 Setting('formatter_form1', 'format', '%(asctime)s %(levelname)s %(message)s'),
114 Setting('formatter_form1', 'datefmt', '', help='Leave blank for ISO8601, e.g. "2003-01-23 00:29:50,411".'),
115 Setting('formatter_form1', 'class', 'logging.Formatter'),
118 def get_setting(settings, match):
119 """Return the first Setting object matching both match.section and
123 if s.section == match.section and s.option == match.option:
127 class HookeConfigParser (configparser.RawConfigParser):
128 """A wrapper around configparser.RawConfigParser.
130 You will probably only need .read and .write.
137 >>> c = HookeConfigParser(default_settings=DEFAULT_SETTINGS)
138 >>> c.write(sys.stdout) # doctest: +ELLIPSIS
139 # Default environmental conditions in case they are not specified in
140 # the force curve data. Configuration options in this section are
141 # available to every plugin.
143 # Temperature in Kelvin
146 # Configure loggers, see
147 # http://docs.python.org/library/logging.html#configuration-file-format
149 # Hooke only uses the hooke logger, but other included modules may
150 # also use logging and you can configure their loggers here as well.
154 class:`HookeConfigParser` automatically converts typed settings.
156 >>> section = 'test conversion'
157 >>> c = HookeConfigParser(default_settings=[
158 ... Setting(section),
159 ... Setting(section, option='my string', value='Lorem ipsum', type='string'),
160 ... Setting(section, option='my bool', value=True, type='bool'),
161 ... Setting(section, option='my int', value=13, type='int'),
162 ... Setting(section, option='my float', value=3.14159, type='float'),
164 >>> pprint.pprint(c.items(section)) # doctest: +ELLIPSIS
165 [('my string', u'Lorem ipsum'),
168 ('my float', 3.1415...)]
170 However, the regular `.get()` is not typed. Users are encouraged
171 to use the standard `.get*()` methods.
173 >>> c.get('test conversion', 'my bool')
175 >>> c.getboolean('test conversion', 'my bool')
178 def __init__(self, paths=None, default_settings=None, defaults=None,
179 dict_type=OrderedDict, indent='# ', **kwargs):
180 # Can't use super() because RawConfigParser is a classic class
181 #super(HookeConfigParser, self).__init__(defaults, dict_type)
182 configparser.RawConfigParser.__init__(self, defaults, dict_type)
185 self._config_paths = paths
186 if default_settings == None:
187 default_settings = []
188 self._default_settings = default_settings
189 self._default_settings_dict = {}
190 for key in ['initial_indent', 'subsequent_indent']:
191 if key not in kwargs:
193 self._comment_textwrap = textwrap.TextWrapper(**kwargs)
194 self._setup_default_settings()
196 def _setup_default_settings(self):
197 for setting in self._default_settings: #reversed(self._default_settings):
198 # reversed cause: http://docs.python.org/library/configparser.html
199 # "When adding sections or items, add them in the reverse order of
200 # how you want them to be displayed in the actual file."
201 self._default_settings_dict[
202 (setting.section, setting.option)] = setting
203 if setting.section not in self.sections():
204 self.add_section(setting.section)
205 if setting.option != None:
206 self.set(setting.section, setting.option, setting.value)
208 def read(self, filenames=None):
209 """Read and parse a filename or a list of filenames.
211 If filenames is None, it defaults to ._config_paths. If a
212 filename is not in ._config_paths, it gets appended to the
213 list. We also run os.path.expanduser() on the input filenames
214 internally so you don't have to worry about it.
216 Files that cannot be opened are silently ignored; this is
217 designed so that you can specify a list of potential
218 configuration file locations (e.g. current directory, user's
219 home directory, systemwide directory), and all existing
220 configuration files in the list will be read. A single
221 filename may also be given.
223 Return list of successfully read files.
225 if filenames == None:
226 filenames = [os.path.expanduser(p) for p in self._config_paths]
228 if isinstance(filenames, basestring):
229 filenames = [filenames]
230 paths = [os.path.abspath(os.path.expanduser(p))
231 for p in self._config_paths]
232 for filename in filenames:
233 if os.path.abspath(os.path.expanduser(filename)) not in paths:
234 self._config_paths.append(filename)
235 # Can't use super() because RawConfigParser is a classic class
236 #return super(HookeConfigParser, self).read(filenames)
237 return configparser.RawConfigParser.read(self, self._config_paths)
239 def _write_setting(self, fp, section=None, option=None, value=None,
241 """Print, if known, a nicely wrapped comment form of a
244 match = get_setting(self._default_settings, Setting(section, option))
246 match = Setting(section, option, value, **kwargs)
247 match.write(fp, value=value, wrapper=self._comment_textwrap)
249 def write(self, fp=None, comments=True):
250 """Write an .ini-format representation of the configuration state.
252 This expands on RawConfigParser.write() by optionally adding
253 comments when they are known (i.e. for ._default_settings).
254 However, comments are not read in during a read, so .read(x)
255 .write(x) may `not preserve comments`_.
257 .. _not preserve comments: http://bugs.python.org/issue1410680
262 >>> import sys, StringIO
263 >>> c = HookeConfigParser()
270 >>> c._read(StringIO.StringIO(instring), 'example.cfg')
271 >>> c.write(sys.stdout)
276 local_fp = fp == None
278 fp = open(os.path.expanduser(self._config_paths[-1]), 'w')
280 self._write_setting(fp, configparser.DEFAULTSECT,
281 help="Miscellaneous options")
282 for (key, value) in self._defaults.items():
283 self._write_setting(fp, configparser.DEFAULTSECT, key, value)
285 for section in self._sections:
286 self._write_setting(fp, section)
287 for (key, value) in self._sections[section].items():
288 if key != "__name__":
289 self._write_setting(fp, section, key, value)
294 def items(self, section, *args, **kwargs):
295 """Return a list of tuples with (name, value) for each option
298 # Can't use super() because RawConfigParser is a classic class
299 #return super(HookeConfigParser, self).items(section, *args, **kwargs)
300 items = configparser.RawConfigParser.items(
301 self, section, *args, **kwargs)
302 for i,kv in enumerate(items):
304 log = logging.getLogger('hooke')
306 setting = self._default_settings_dict[(section, key)]
308 log.error('unknown setting %s/%s: %s' % (section, key, e))
311 items[i] = (key, from_string(value=value, type=setting.type,
312 count=setting.count))
313 except ValueError, e:
314 log.error("could not convert '%s' (%s) for %s/%s: %s"
315 % (value, type(value), section, key, e))
319 def set(self, section, option, value):
321 setting = self._default_settings_dict[(section, option)]
322 value = to_string(value=value, type=setting.type, count=setting.count)
323 # Can't use super() because RawConfigParser is a classic class
324 #return super(HookeConfigParser, self).set(section, option, value)
325 configparser.RawConfigParser.set(self, section, option, value)
327 def optionxform(self, option):
330 Overrides lowercasing behaviour of
331 :meth:`ConfigParser.RawConfigParser.optionxform`.
336 class TestHookeConfigParser (unittest.TestCase):
337 def test_queue_safe(self):
338 """Ensure :class:`HookeConfigParser` is Queue-safe.
340 from multiprocessing import Queue
342 a = HookeConfigParser(default_settings=DEFAULT_SETTINGS)
345 for attr in ['_dict', '_defaults', '_sections', '_config_paths',
346 '_default_settings']:
347 a_value = getattr(a, attr)
348 b_value = getattr(b, attr)
351 'HookeConfigParser.%s did not survive Queue: %s != %s'
352 % (attr, a_value, b_value))