1 # Copyright (C) 2010-2012 W. Trevor King <wking@tremily.us>
3 # This file is part of Hooke.
5 # Hooke is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU Lesser General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option) any
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
15 # You should have received a copy of the GNU Lesser General Public License
16 # along with Hooke. If not, see <http://www.gnu.org/licenses/>.
18 """Configuration defaults, read/write, and template file creation for
22 import ConfigParser as configparser
28 from .compat.odict import odict as OrderedDict
29 from .util.convert import to_string, from_string
33 '/usr/share/hooke/hooke.cfg',
34 '/etc/hooke/hooke.cfg',
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.
43 class Setting (object):
44 """An entry (section or option) in HookeConfigParser.
46 def __init__(self, section, option=None, value=None, type='string',
47 count=1, help=None, wrap=True):
48 self.section = section
56 def __eq__(self, other):
57 for attr in ['__class__', 'section', 'option', 'value', 'help']:
58 value = getattr(self, attr)
59 o_value = getattr(other, attr)
65 return self.option == None
68 return not self.is_section()
70 def write(self, fp, value=None, comments=True, wrapper=None):
71 if comments == True and self.help != None:
75 wrapper = textwrap.TextWrapper(
77 subsequent_indent='# ')
78 text = wrapper.fill(text)
80 text = '# ' + '\n# '.join(text.splitlines())
81 fp.write(text.rstrip()+'\n')
83 fp.write("[%s]\n" % self.section)
85 fp.write("%s = %s\n" % (self.option,
86 str(value).replace('\n', '\n\t')))
89 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.'),
90 Setting('conditions', 'temperature', value='301', type='float', help='Temperature in Kelvin'),
92 Setting('loggers', help='Configure loggers, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
93 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.'),
94 Setting('handlers', help='Configure log handlers, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
95 Setting('handlers', 'keys', 'hand1'),
96 Setting('formatters', help='Configure log formatters, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
97 Setting('formatters', 'keys', 'form1'),
98 Setting('logger_root', help='Configure the root logger, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
99 Setting('logger_root', 'level', 'NOTSET'),
100 Setting('logger_root', 'handlers', 'hand1'),
101 Setting('logger_hooke', help='Configure the hooke logger, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
102 Setting('logger_hooke', 'level', 'DEBUG'),
103 Setting('logger_hooke', 'handlers', 'hand1', help='No specific handlers here, just propagate up to the root logger'),
104 Setting('logger_hooke', 'propagate', '0'),
105 Setting('logger_hooke', 'qualname', 'hooke'),
106 Setting('handler_hand1', help='Configure the default log handler, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
107 Setting('handler_hand1', 'class', 'StreamHandler'),
108 Setting('handler_hand1', 'level', 'WARN'),
109 Setting('handler_hand1', 'formatter', 'form1'),
110 Setting('handler_hand1', 'args', '(sys.stderr,)'),
111 Setting('formatter_form1', help='Configure the default log formatter, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
112 Setting('formatter_form1', 'format', '%(asctime)s %(levelname)s %(message)s'),
113 Setting('formatter_form1', 'datefmt', '', help='Leave blank for ISO8601, e.g. "2003-01-23 00:29:50,411".'),
114 Setting('formatter_form1', 'class', 'logging.Formatter'),
117 def get_setting(settings, match):
118 """Return the first Setting object matching both match.section and
122 if s.section == match.section and s.option == match.option:
126 class HookeConfigParser (configparser.RawConfigParser):
127 """A wrapper around configparser.RawConfigParser.
129 You will probably only need .read and .write.
136 >>> c = HookeConfigParser(default_settings=DEFAULT_SETTINGS)
137 >>> c.write(sys.stdout) # doctest: +ELLIPSIS
138 # Default environmental conditions in case they are not specified in
139 # the force curve data. Configuration options in this section are
140 # available to every plugin.
142 # Temperature in Kelvin
145 # Configure loggers, see
146 # http://docs.python.org/library/logging.html#configuration-file-format
148 # Hooke only uses the hooke logger, but other included modules may
149 # also use logging and you can configure their loggers here as well.
153 class:`HookeConfigParser` automatically converts typed settings.
155 >>> section = 'test conversion'
156 >>> c = HookeConfigParser(default_settings=[
157 ... Setting(section),
158 ... Setting(section, option='my string', value='Lorem ipsum', type='string'),
159 ... Setting(section, option='my bool', value=True, type='bool'),
160 ... Setting(section, option='my int', value=13, type='int'),
161 ... Setting(section, option='my float', value=3.14159, type='float'),
163 >>> pprint.pprint(c.items(section)) # doctest: +ELLIPSIS
164 [('my string', u'Lorem ipsum'),
167 ('my float', 3.1415...)]
169 However, the regular `.get()` is not typed. Users are encouraged
170 to use the standard `.get*()` methods.
172 >>> c.get('test conversion', 'my bool')
174 >>> c.getboolean('test conversion', 'my bool')
177 def __init__(self, paths=None, default_settings=None, defaults=None,
178 dict_type=OrderedDict, indent='# ', **kwargs):
179 # Can't use super() because RawConfigParser is a classic class
180 #super(HookeConfigParser, self).__init__(defaults, dict_type)
181 configparser.RawConfigParser.__init__(self, defaults, dict_type)
184 self._config_paths = paths
185 if default_settings == None:
186 default_settings = []
187 self._default_settings = default_settings
188 self._default_settings_dict = {}
189 for key in ['initial_indent', 'subsequent_indent']:
190 if key not in kwargs:
192 self._comment_textwrap = textwrap.TextWrapper(**kwargs)
193 self._setup_default_settings()
195 def _setup_default_settings(self):
196 for setting in self._default_settings: #reversed(self._default_settings):
197 # reversed cause: http://docs.python.org/library/configparser.html
198 # "When adding sections or items, add them in the reverse order of
199 # how you want them to be displayed in the actual file."
200 self._default_settings_dict[
201 (setting.section, setting.option)] = setting
202 if setting.section not in self.sections():
203 self.add_section(setting.section)
204 if setting.option != None:
205 self.set(setting.section, setting.option, setting.value)
207 def read(self, filenames=None):
208 """Read and parse a filename or a list of filenames.
210 If filenames is None, it defaults to ._config_paths. If a
211 filename is not in ._config_paths, it gets appended to the
212 list. We also run os.path.expanduser() on the input filenames
213 internally so you don't have to worry about it.
215 Files that cannot be opened are silently ignored; this is
216 designed so that you can specify a list of potential
217 configuration file locations (e.g. current directory, user's
218 home directory, systemwide directory), and all existing
219 configuration files in the list will be read. A single
220 filename may also be given.
222 Return list of successfully read files.
224 if filenames == None:
225 filenames = [os.path.expanduser(p) for p in self._config_paths]
227 if isinstance(filenames, basestring):
228 filenames = [filenames]
229 paths = [os.path.abspath(os.path.expanduser(p))
230 for p in self._config_paths]
231 for filename in filenames:
232 if os.path.abspath(os.path.expanduser(filename)) not in paths:
233 self._config_paths.append(filename)
234 # Can't use super() because RawConfigParser is a classic class
235 #return super(HookeConfigParser, self).read(filenames)
236 return configparser.RawConfigParser.read(self, self._config_paths)
238 def _write_setting(self, fp, section=None, option=None, value=None,
240 """Print, if known, a nicely wrapped comment form of a
243 match = get_setting(self._default_settings, Setting(section, option))
245 match = Setting(section, option, value, **kwargs)
246 match.write(fp, value=value, wrapper=self._comment_textwrap)
248 def write(self, fp=None, comments=True):
249 """Write an .ini-format representation of the configuration state.
251 This expands on RawConfigParser.write() by optionally adding
252 comments when they are known (i.e. for ._default_settings).
253 However, comments are not read in during a read, so .read(x)
254 .write(x) may `not preserve comments`_.
256 .. _not preserve comments: http://bugs.python.org/issue1410680
261 >>> import sys, StringIO
262 >>> c = HookeConfigParser()
269 >>> c._read(StringIO.StringIO(instring), 'example.cfg')
270 >>> c.write(sys.stdout)
275 local_fp = fp == None
277 fp = open(os.path.expanduser(self._config_paths[-1]), 'w')
279 self._write_setting(fp, configparser.DEFAULTSECT,
280 help="Miscellaneous options")
281 for (key, value) in self._defaults.items():
282 self._write_setting(fp, configparser.DEFAULTSECT, key, value)
284 for section in self._sections:
285 self._write_setting(fp, section)
286 for (key, value) in self._sections[section].items():
287 if key != "__name__":
288 self._write_setting(fp, section, key, value)
293 def items(self, section, *args, **kwargs):
294 """Return a list of tuples with (name, value) for each option
297 # Can't use super() because RawConfigParser is a classic class
298 #return super(HookeConfigParser, self).items(section, *args, **kwargs)
299 items = configparser.RawConfigParser.items(
300 self, section, *args, **kwargs)
301 for i,kv in enumerate(items):
303 log = logging.getLogger('hooke')
305 setting = self._default_settings_dict[(section, key)]
307 log.error('unknown setting %s/%s: %s' % (section, key, e))
310 items[i] = (key, from_string(value=value, type=setting.type,
311 count=setting.count))
312 except ValueError, e:
313 log.error("could not convert '%s' (%s) for %s/%s: %s"
314 % (value, type(value), section, key, e))
318 def set(self, section, option, value):
320 setting = self._default_settings_dict[(section, option)]
321 value = to_string(value=value, type=setting.type, count=setting.count)
322 # Can't use super() because RawConfigParser is a classic class
323 #return super(HookeConfigParser, self).set(section, option, value)
324 configparser.RawConfigParser.set(self, section, option, value)
326 def optionxform(self, option):
329 Overrides lowercasing behaviour of
330 :meth:`ConfigParser.RawConfigParser.optionxform`.
335 class TestHookeConfigParser (unittest.TestCase):
336 def test_queue_safe(self):
337 """Ensure :class:`HookeConfigParser` is Queue-safe.
339 from multiprocessing import Queue
341 a = HookeConfigParser(default_settings=DEFAULT_SETTINGS)
344 for attr in ['_dict', '_defaults', '_sections', '_config_paths',
345 '_default_settings']:
346 a_value = getattr(a, attr)
347 b_value = getattr(b, attr)
350 'HookeConfigParser.%s did not survive Queue: %s != %s'
351 % (attr, a_value, b_value))