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
6 # modify it under the terms of the GNU Lesser General Public
7 # License as published by the Free Software Foundation, either
8 # version 3 of the License, or (at your option) any later version.
10 # Hooke is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Lesser General 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 class Setting (object):
43 """An entry (section or option) in HookeConfigParser.
45 def __init__(self, section, option=None, value=None, help=None, wrap=True):
46 self.section = section
48 self.value = str(value)
52 def __eq__(self, other):
53 for attr in ['__class__', 'section', 'option', 'value', 'help']:
54 value = getattr(self, attr)
55 o_value = getattr(other, attr)
61 return self.option == None
64 return not self.is_section()
66 def write(self, fp, value=None, comments=True, wrapper=None):
67 if comments == True and self.help != None:
71 wrapper = textwrap.TextWrapper(
73 subsequent_indent='# ')
74 text = wrapper.fill(text)
76 text = '# ' + '\n# '.join(text.splitlines())
77 fp.write(text.rstrip()+'\n')
79 fp.write("[%s]\n" % self.section)
81 fp.write("%s = %s\n" % (self.option,
82 str(value).replace('\n', '\n\t')))
85 Setting('conditions', help='Default environmental conditions in case they are not specified in the force curve data.'),
86 Setting('conditions', 'temperature', '301', help='Temperature in Kelvin'),
88 Setting('loggers', help='Configure loggers, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
89 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.'),
90 Setting('handlers', help='Configure log handlers, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
91 Setting('handlers', 'keys', 'hand1'),
92 Setting('formatters', help='Configure log formatters, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
93 Setting('formatters', 'keys', 'form1'),
94 Setting('logger_root', help='Configure the root logger, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
95 Setting('logger_root', 'level', 'NOTSET'),
96 Setting('logger_root', 'handlers', 'hand1'),
97 Setting('logger_hooke', help='Configure the hooke logger, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
98 Setting('logger_hooke', 'level', 'DEBUG'),
99 Setting('logger_hooke', 'handlers', 'hand1', help='No specific handlers here, just propagate up to the root logger'),
100 Setting('logger_hooke', 'propagate', '0'),
101 Setting('logger_hooke', 'qualname', 'hooke'),
102 Setting('handler_hand1', help='Configure the default log handler, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
103 Setting('handler_hand1', 'class', 'StreamHandler'),
104 Setting('handler_hand1', 'level', 'NOTSET'),
105 Setting('handler_hand1', 'formatter', 'form1'),
106 Setting('handler_hand1', 'args', '(sys.stderr,)'),
107 Setting('formatter_form1', help='Configure the default log formatter, see\nhttp://docs.python.org/library/logging.html#configuration-file-format', wrap=False),
108 Setting('formatter_form1', 'format', '%(asctime)s %(levelname)s %(message)s'),
109 Setting('formatter_form1', 'datefmt', '', help='Leave blank for ISO8601, e.g. "2003-01-23 00:29:50,411".'),
110 Setting('formatter_form1', 'class', 'logging.Formatter'),
113 def get_setting(settings, match):
114 """Return the first Setting object matching both match.section and
118 if s.section == match.section and s.option == match.option:
122 class HookeConfigParser (configparser.SafeConfigParser):
123 """A wrapper around configparser.SafeConfigParser.
125 You will probably only need .read and .write.
131 >>> c = HookeConfigParser(paths=DEFAULT_PATHS,
132 ... default_settings=DEFAULT_SETTINGS)
133 >>> c.write(sys.stdout) # doctest: +ELLIPSIS
134 # Control display appearance: colour, ???, etc.
140 def __init__(self, paths=None, default_settings=None, defaults=None,
141 dict_type=OrderedDict, indent='# ', **kwargs):
142 # Can't use super() because SafeConfigParser is a classic class
143 #super(HookeConfigParser, self).__init__(defaults, dict_type)
144 configparser.SafeConfigParser.__init__(self, defaults, dict_type)
147 self._config_paths = paths
148 if default_settings == None:
149 default_settings = []
150 self._default_settings = default_settings
151 for key in ['initial_indent', 'subsequent_indent']:
152 if key not in kwargs:
154 self._comment_textwrap = textwrap.TextWrapper(**kwargs)
155 self._setup_default_settings()
157 def _setup_default_settings(self):
158 for setting in self._default_settings: #reversed(self._default_settings):
159 # reversed cause: http://docs.python.org/library/configparser.html
160 # "When adding sections or items, add them in the reverse order of
161 # how you want them to be displayed in the actual file."
162 if setting.section not in self.sections():
163 self.add_section(setting.section)
164 if setting.option != None:
165 self.set(setting.section, setting.option, setting.value)
167 def read(self, filenames=None):
168 """Read and parse a filename or a list of filenames.
170 If filenames is None, it defaults to ._config_paths. If a
171 filename is not in ._config_paths, it gets appended to the
172 list. We also run os.path.expanduser() on the input filenames
173 internally so you don't have to worry about it.
175 Files that cannot be opened are silently ignored; this is
176 designed so that you can specify a list of potential
177 configuration file locations (e.g. current directory, user's
178 home directory, systemwide directory), and all existing
179 configuration files in the list will be read. A single
180 filename may also be given.
182 Return list of successfully read files.
184 if filenames == None:
185 filenames = [os.path.expanduser(p) for p in self._config_paths]
187 if isinstance(filenames, basestring):
188 filenames = [filenames]
189 paths = [os.path.abspath(os.path.expanduser(p))
190 for p in self._config_paths]
191 for filename in filenames:
192 if os.path.abspath(os.path.expanduser(filename)) not in paths:
193 self._config_paths.append(filename)
194 # Can't use super() because SafeConfigParser is a classic class
195 #return super(HookeConfigParser, self).read(filenames)
196 return configparser.SafeConfigParser.read(self, filenames)
198 def _write_setting(self, fp, section=None, option=None, value=None,
200 """Print, if known, a nicely wrapped comment form of a
203 match = get_setting(self._default_settings, Setting(section, option))
205 match = Setting(section, option, value, **kwargs)
206 match.write(fp, value=value, wrapper=self._comment_textwrap)
208 def write(self, fp=None, comments=True):
209 """Write an .ini-format representation of the configuration state.
211 This expands on RawConfigParser.write() by optionally adding
212 comments when they are known (i.e. for ._default_settings).
213 However, comments are not read in during a read, so .read(x)
214 .write(x) may `not preserve comments`_.
216 .. _not preserve comments: http://bugs.python.org/issue1410680
221 >>> import sys, StringIO
222 >>> c = HookeConfigParser()
229 >>> c._read(StringIO.StringIO(instring), 'example.cfg')
230 >>> c.write(sys.stdout)
235 local_fp = fp == None
237 fp = open(os.path.expanduser(self._config_paths[-1]), 'w')
239 self._write_setting(fp, configparser.DEFAULTSECT,
240 help="Miscellaneous options")
241 for (key, value) in self._defaults.items():
242 self._write_setting(fp, configparser.DEFAULTSECT, key, value)
244 for section in self._sections:
245 self._write_setting(fp, section)
246 for (key, value) in self._sections[section].items():
247 if key != "__name__":
248 self._write_setting(fp, section, key, value)
253 class TestHookeConfigParser (unittest.TestCase):
254 def test_queue_safe(self):
255 """Ensure :class:`HookeConfigParser` is Queue-safe.
257 from multiprocessing import Queue
259 a = HookeConfigParser(
260 paths=DEFAULT_PATHS, default_settings=DEFAULT_SETTINGS)
263 for attr in ['_dict', '_defaults', '_sections', '_config_paths',
264 '_default_settings']:
265 a_value = getattr(a, attr)
266 b_value = getattr(b, attr)
269 'HookeConfigParser.%s did not survive Queue: %s != %s'
270 % (attr, a_value, b_value))