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', 'NOTSET'),
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.
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', '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, filenames)
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 setting = self._default_settings_dict[(section, key)]
305 items[i] = (key, from_string(value=value, type=setting.type,
306 count=setting.count))
307 except ValueError, e:
308 log = logging.getLogger('hooke')
309 log.error("could not convert '%s' (%s) for %s/%s: %s"
310 % (value, type(value), section, key, e))
314 def set(self, section, option, value):
316 setting = self._default_settings_dict[(section, option)]
317 value = to_string(value=value, type=setting.type, count=setting.count)
318 # Can't use super() because RawConfigParser is a classic class
319 #return super(HookeConfigParser, self).set(section, option, value)
320 configparser.RawConfigParser.set(self, section, option, value)
322 def optionxform(self, option):
325 Overrides lowercasing behaviour of
326 :meth:`ConfigParser.RawConfigParser.optionxform`.
331 class TestHookeConfigParser (unittest.TestCase):
332 def test_queue_safe(self):
333 """Ensure :class:`HookeConfigParser` is Queue-safe.
335 from multiprocessing import Queue
337 a = HookeConfigParser(default_settings=DEFAULT_SETTINGS)
340 for attr in ['_dict', '_defaults', '_sections', '_config_paths',
341 '_default_settings']:
342 a_value = getattr(a, attr)
343 b_value = getattr(b, attr)
346 'HookeConfigParser.%s did not survive Queue: %s != %s'
347 % (attr, a_value, b_value))