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
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 help=None, wrap=True):
48 self.section = section
50 self.value = to_string(value=value, type=type)
55 def __eq__(self, other):
56 for attr in ['__class__', 'section', 'option', 'value', 'help']:
57 value = getattr(self, attr)
58 o_value = getattr(other, attr)
64 return self.option == None
67 return not self.is_section()
69 def write(self, fp, value=None, comments=True, wrapper=None):
70 if comments == True and self.help != None:
74 wrapper = textwrap.TextWrapper(
76 subsequent_indent='# ')
77 text = wrapper.fill(text)
79 text = '# ' + '\n# '.join(text.splitlines())
80 fp.write(text.rstrip()+'\n')
82 fp.write("[%s]\n" % self.section)
84 fp.write("%s = %s\n" % (self.option,
85 str(value).replace('\n', '\n\t')))
88 Setting('conditions', help='Default environmental conditions in case they are not specified in the force curve data.'),
89 Setting('conditions', 'temperature', value='301', type='float', help='Temperature in Kelvin'),
91 Setting('loggers', help='Configure loggers, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
92 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.'),
93 Setting('handlers', help='Configure log handlers, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
94 Setting('handlers', 'keys', 'hand1'),
95 Setting('formatters', help='Configure log formatters, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
96 Setting('formatters', 'keys', 'form1'),
97 Setting('logger_root', help='Configure the root logger, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
98 Setting('logger_root', 'level', 'NOTSET'),
99 Setting('logger_root', 'handlers', 'hand1'),
100 Setting('logger_hooke', help='Configure the hooke logger, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
101 Setting('logger_hooke', 'level', 'DEBUG'),
102 Setting('logger_hooke', 'handlers', 'hand1', help='No specific handlers here, just propagate up to the root logger'),
103 Setting('logger_hooke', 'propagate', '0'),
104 Setting('logger_hooke', 'qualname', 'hooke'),
105 Setting('handler_hand1', help='Configure the default log handler, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
106 Setting('handler_hand1', 'class', 'StreamHandler'),
107 Setting('handler_hand1', 'level', 'NOTSET'),
108 Setting('handler_hand1', 'formatter', 'form1'),
109 Setting('handler_hand1', 'args', '(sys.stderr,)'),
110 Setting('formatter_form1', help='Configure the default log formatter, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
111 Setting('formatter_form1', 'format', '%(asctime)s %(levelname)s %(message)s'),
112 Setting('formatter_form1', 'datefmt', '', help='Leave blank for ISO8601, e.g. "2003-01-23 00:29:50,411".'),
113 Setting('formatter_form1', 'class', 'logging.Formatter'),
116 def get_setting(settings, match):
117 """Return the first Setting object matching both match.section and
121 if s.section == match.section and s.option == match.option:
125 class HookeConfigParser (configparser.RawConfigParser):
126 """A wrapper around configparser.RawConfigParser.
128 You will probably only need .read and .write.
135 >>> c = HookeConfigParser(default_settings=DEFAULT_SETTINGS)
136 >>> c.write(sys.stdout) # doctest: +ELLIPSIS
137 # Default environmental conditions in case they are not specified in
138 # the force curve data.
140 # Temperature in Kelvin
143 # Configure loggers, see
144 # http://docs.python.org/library/logging.html#configuration-file-format
146 # Hooke only uses the hooke logger, but other included modules may
147 # also use logging and you can configure their loggers here as well.
151 class:`HookeConfigParser` automatically converts typed settings.
153 >>> section = 'test conversion'
154 >>> c = HookeConfigParser(default_settings=[
155 ... Setting(section),
156 ... Setting(section, option='my string', value='Lorem ipsum', type='string'),
157 ... Setting(section, option='my bool', value=True, type='bool'),
158 ... Setting(section, option='my int', value=13, type='int'),
159 ... Setting(section, option='my float', value=3.14159, type='float'),
161 >>> pprint.pprint(c.items(section)) # doctest: +ELLIPSIS
162 [('my string', 'Lorem ipsum'),
165 ('my float', 3.1415...)]
167 However, the regular `.get()` is not typed. Users are encouraged
168 to use the standard `.get*()` methods.
170 >>> c.get('test conversion', 'my bool')
172 >>> c.getboolean('test conversion', 'my bool')
175 def __init__(self, paths=None, default_settings=None, defaults=None,
176 dict_type=OrderedDict, indent='# ', **kwargs):
177 # Can't use super() because RawConfigParser is a classic class
178 #super(HookeConfigParser, self).__init__(defaults, dict_type)
179 configparser.RawConfigParser.__init__(self, defaults, dict_type)
182 self._config_paths = paths
183 if default_settings == None:
184 default_settings = []
185 self._default_settings = default_settings
186 self._default_settings_dict = {}
187 for key in ['initial_indent', 'subsequent_indent']:
188 if key not in kwargs:
190 self._comment_textwrap = textwrap.TextWrapper(**kwargs)
191 self._setup_default_settings()
193 def _setup_default_settings(self):
194 for setting in self._default_settings: #reversed(self._default_settings):
195 # reversed cause: http://docs.python.org/library/configparser.html
196 # "When adding sections or items, add them in the reverse order of
197 # how you want them to be displayed in the actual file."
198 self._default_settings_dict[
199 (setting.section, setting.option)] = setting
200 if setting.section not in self.sections():
201 self.add_section(setting.section)
202 if setting.option != None:
203 self.set(setting.section, setting.option, setting.value)
205 def read(self, filenames=None):
206 """Read and parse a filename or a list of filenames.
208 If filenames is None, it defaults to ._config_paths. If a
209 filename is not in ._config_paths, it gets appended to the
210 list. We also run os.path.expanduser() on the input filenames
211 internally so you don't have to worry about it.
213 Files that cannot be opened are silently ignored; this is
214 designed so that you can specify a list of potential
215 configuration file locations (e.g. current directory, user's
216 home directory, systemwide directory), and all existing
217 configuration files in the list will be read. A single
218 filename may also be given.
220 Return list of successfully read files.
222 if filenames == None:
223 filenames = [os.path.expanduser(p) for p in self._config_paths]
225 if isinstance(filenames, basestring):
226 filenames = [filenames]
227 paths = [os.path.abspath(os.path.expanduser(p))
228 for p in self._config_paths]
229 for filename in filenames:
230 if os.path.abspath(os.path.expanduser(filename)) not in paths:
231 self._config_paths.append(filename)
232 # Can't use super() because RawConfigParser is a classic class
233 #return super(HookeConfigParser, self).read(filenames)
234 return configparser.RawConfigParser.read(self, filenames)
236 def _write_setting(self, fp, section=None, option=None, value=None,
238 """Print, if known, a nicely wrapped comment form of a
241 match = get_setting(self._default_settings, Setting(section, option))
243 match = Setting(section, option, value, **kwargs)
244 match.write(fp, value=value, wrapper=self._comment_textwrap)
246 def write(self, fp=None, comments=True):
247 """Write an .ini-format representation of the configuration state.
249 This expands on RawConfigParser.write() by optionally adding
250 comments when they are known (i.e. for ._default_settings).
251 However, comments are not read in during a read, so .read(x)
252 .write(x) may `not preserve comments`_.
254 .. _not preserve comments: http://bugs.python.org/issue1410680
259 >>> import sys, StringIO
260 >>> c = HookeConfigParser()
267 >>> c._read(StringIO.StringIO(instring), 'example.cfg')
268 >>> c.write(sys.stdout)
273 local_fp = fp == None
275 fp = open(os.path.expanduser(self._config_paths[-1]), 'w')
277 self._write_setting(fp, configparser.DEFAULTSECT,
278 help="Miscellaneous options")
279 for (key, value) in self._defaults.items():
280 self._write_setting(fp, configparser.DEFAULTSECT, key, value)
282 for section in self._sections:
283 self._write_setting(fp, section)
284 for (key, value) in self._sections[section].items():
285 if key != "__name__":
286 self._write_setting(fp, section, key, value)
291 def items(self, section, *args, **kwargs):
292 """Return a list of tuples with (name, value) for each option
295 # Can't use super() because RawConfigParser is a classic class
296 #return super(HookeConfigParser, self).items(section, *args, **kwargs)
297 items = configparser.RawConfigParser.items(
298 self, section, *args, **kwargs)
299 for i,kv in enumerate(items):
301 setting = self._default_settings_dict[(section, key)]
302 items[i] = (key, from_string(value=value, type=setting.type))
305 def set(self, section, option, value):
307 setting = self._default_settings_dict[(section, option)]
308 value = to_string(value=value, type=setting.type)
309 # Can't use super() because RawConfigParser is a classic class
310 #return super(HookeConfigParser, self).set(section, option, value)
311 configparser.RawConfigParser.set(self, section, option, value)
313 def optionxform(self, option):
316 Overrides lowercasing behaviour of
317 :meth:`ConfigParser.RawConfigParser.optionxform`.
322 class TestHookeConfigParser (unittest.TestCase):
323 def test_queue_safe(self):
324 """Ensure :class:`HookeConfigParser` is Queue-safe.
326 from multiprocessing import Queue
328 a = HookeConfigParser(default_settings=DEFAULT_SETTINGS)
331 for attr in ['_dict', '_defaults', '_sections', '_config_paths',
332 '_default_settings']:
333 a_value = getattr(a, attr)
334 b_value = getattr(b, attr)
337 'HookeConfigParser.%s did not survive Queue: %s != %s'
338 % (attr, a_value, b_value))