Added hooke.config.TestHookeConfigParser
[hooke.git] / hooke / config.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 '''
5 config.py
6
7 Configuration defaults, read/write, and template file creation for Hooke.
8
9 COPYRIGHT
10 '''
11
12 import ConfigParser as configparser
13 import os.path
14 import textwrap
15 import unittest
16
17 from .compat.odict import odict as OrderedDict
18
19
20 DEFAULT_PATHS = [
21     '/usr/share/hooke/hooke.cfg',
22     '/etc/hooke/hooke.cfg',
23     '~/.hooke.cfg',
24     '.hooke.cfg',
25     ]
26 """We start with the system files, and work our way to the more
27 specific user files, so the user can override the sysadmin who
28 in turn overrides the developer defaults.
29 """
30
31 class Setting (object):
32     """An entry (section or option) in HookeConfigParser.
33     """
34     def __init__(self, section, option=None, value=None, help=None, wrap=True):
35         self.section = section
36         self.option = option
37         self.value = value
38         self.help = help
39         self.wrap = wrap
40
41     def __eq__(self, other):
42         for attr in ['__class__', 'section', 'option', 'value', 'help']:
43             value = getattr(self, attr)
44             o_value = getattr(other, attr)
45             if o_value != value:
46                 return False
47         return True
48
49     def is_section(self):
50         return self.option == None
51
52     def is_option(self):
53         return not self.is_section()
54
55     def write(self, fp, value=None, comments=True, wrapper=None):
56         if comments == True and self.help != None:
57             text = self.help
58             if self.wrap == True:
59                 if wrapper == None:
60                     wrapper = textwrap.TextWrapper(
61                         initial_indent="# ",
62                         subsequent_indent="# ")
63                 text = wrapper.fill(text)
64             fp.write(text.rstrip()+'\n')
65         if self.is_section():
66             fp.write("[%s]\n" % self.section)
67         else:
68             fp.write("%s = %s\n" % (self.option,
69                                     str(value).replace('\n', '\n\t')))
70
71 DEFAULT_SETTINGS = [
72     Setting('display', help='Control display appearance: colour, ???, etc.'),
73     Setting('display', 'colour_ext', 'None', help=None),
74     Setting('display', 'colour_ret', 'None', help=None),
75     Setting('display', 'ext', '1', help=None),
76     Setting('display', 'ret', '1', help=None),
77
78     Setting('display', 'correct', '1', help=None),
79     Setting('display', 'colout_correct', 'None', help=None),
80     Setting('display', 'contact_point', '0', help=None),
81     Setting('display', 'medfilt', '0', help=None),
82
83     Setting('display', 'xaxes', '0', help=None),
84     Setting('display', 'yaxes', '0', help=None),
85     Setting('display', 'flatten', '1', help=None),
86
87     Setting('conditions', 'temperature', '301', help=None),
88
89     Setting('fitting', 'auto_fit_points', '50', help=None),
90     Setting('fitting', 'auto_slope_span', '20', help=None),
91     Setting('fitting', 'auto_delta_force', '1-', help=None),
92     Setting('fitting', 'auto_fit_nm', '5', help=None),
93     Setting('fitting', 'auto_min_p', '0.005', help=None),
94     Setting('fitting', 'auto_max_p', '10', help=None),
95
96     Setting('?', 'baseline_clicks', '0', help=None),
97     Setting('fitting', 'auto_left_baseline', '20', help=None),
98     Setting('fitting', 'auto_right_baseline', '20', help=None),
99     Setting('fitting', 'force_multiplier', '1', help=None),
100     
101     Setting('display', 'fc_showphase', '0', help=None),
102     Setting('display', 'fc_showimposed', '0', help=None),
103     Setting('display', 'fc_interesting', '0', help=None),
104     Setting('?', 'tccd_threshold', '0', help=None),
105     Setting('?', 'tccd_coincident', '0', help=None),
106     Setting('display', '', '', help=None),
107     Setting('display', '', '', help=None),
108
109     Setting('filesystem', 'filterindex', '0', help=None),
110     Setting('filesystem', 'filters',
111             "Playlist files (*.hkp)|*.hkp|Text files (*.txt)|*.txt|All files (*.*)|*.*')",
112             help=None),
113     Setting('filesystem', 'workdir', 'test',
114             help='\n'.join(['# Substitute your work directory',
115                             '#workdir = D:\hooke']),
116             wrap=False),
117     Setting('filesystem', 'playlist', 'test.hkp', help=None),
118     ]
119
120 def get_setting(settings, match):
121     """Return the first Setting object matching both match.section and
122     match.option.
123     """
124     for s in settings:
125         if s.section == match.section and s.option == match.option:
126             return s
127     return None
128
129 class HookeConfigParser (configparser.SafeConfigParser):
130     """A wrapper around configparser.SafeConfigParser.
131
132     You will probably only need .read and .write.
133
134     Examples
135     --------
136
137     >>> import sys
138     >>> c = HookeConfigParser(paths=DEFAULT_PATHS,
139     ...                       default_settings=DEFAULT_SETTINGS)
140     >>> c.write(sys.stdout) # doctest: +ELLIPSIS
141     # Control display appearance: colour, ???, etc.
142     [display]
143     colour_ext = None
144     colour_ret = None
145     ...
146     """
147     def __init__(self, paths=None, default_settings=None, defaults=None,
148                  dict_type=OrderedDict, indent='# ', **kwargs):
149         # Can't use super() because SafeConfigParser is a classic class
150         #super(HookeConfigParser, self).__init__(defaults, dict_type)
151         configparser.SafeConfigParser.__init__(self, defaults, dict_type)
152         if paths == None:
153             paths = []
154         self._config_paths = paths
155         if default_settings == None:
156             default_settings = []
157         self._default_settings = default_settings
158         for key in ['initial_indent', 'subsequent_indent']:
159             if key not in kwargs:
160                 kwargs[key] = indent
161         self._comment_textwrap = textwrap.TextWrapper(**kwargs)
162         self._setup_default_settings()
163
164     def _setup_default_settings(self):
165         for setting in self._default_settings: #reversed(self._default_settings):
166             # reversed cause: http://docs.python.org/library/configparser.html
167             # "When adding sections or items, add them in the reverse order of
168             # how you want them to be displayed in the actual file."
169             if setting.section not in self.sections():
170                 self.add_section(setting.section)
171             if setting.option != None:
172                 self.set(setting.section, setting.option, setting.value)
173
174     def read(self, filenames=None):
175         """Read and parse a filename or a list of filenames.
176
177         If filenames is None, it defaults to ._config_paths.  If a
178         filename is not in ._config_paths, it gets appended to the
179         list.  We also run os.path.expanduser() on the input filenames
180         internally so you don't have to worry about it.
181
182         Files that cannot be opened are silently ignored; this is
183         designed so that you can specify a list of potential
184         configuration file locations (e.g. current directory, user's
185         home directory, systemwide directory), and all existing
186         configuration files in the list will be read.  A single
187         filename may also be given.
188
189         Return list of successfully read files.
190         """
191         if filenames == None:
192             filenames = [os.path.expanduser(p) for p in self._config_paths]
193         else:
194             if isinstance(filenames, basestring):
195                 filenames = [filenames]
196             paths = [os.path.abspath(os.path.expanduser(p))
197                      for p in self._config_paths]
198             for filename in filenames:
199                 if os.path.abspath(os.path.expanduser(filename)) not in paths:
200                     self._config_paths.append(filename)
201         # Can't use super() because SafeConfigParser is a classic class
202         #return super(HookeConfigParser, self).read(filenames)
203         return configparser.SafeConfigParser.read(self, filenames)
204
205     def _write_setting(self, fp, section=None, option=None, value=None,
206                        **kwargs):
207         """Print, if known, a nicely wrapped comment form of a
208         setting's .help.
209         """
210         match = get_setting(self._default_settings, Setting(section, option))
211         if match == None:
212             match = Setting(section, option, value, **kwargs)
213         match.write(fp, value=value, wrapper=self._comment_textwrap)
214
215     def write(self, fp=None, comments=True):
216         """Write an .ini-format representation of the configuration state.
217
218         This expands on RawConfigParser.write() by optionally adding
219         comments when they are known (i.e. for ._default_settings).
220         However, comments are not read in during a read, so .read(x)
221         .write(x) may `not preserve comments`_.
222
223         .. _not preserve comments: http://bugs.python.org/issue1410680
224
225         Examples
226         --------
227
228         >>> import sys, StringIO
229         >>> c = HookeConfigParser()
230         >>> instring = '''
231         ... # Some comment
232         ... [section]
233         ... option = value
234         ...
235         ... '''
236         >>> c._read(StringIO.StringIO(instring), 'example.cfg')
237         >>> c.write(sys.stdout)
238         [section]
239         option = value
240         <BLANKLINE>
241         """
242         local_fp = fp == None
243         if local_fp:
244             fp = open(os.path.expanduser(self._config_paths[-1]), 'w')
245         if self._defaults:
246             self._write_setting(fp, configparser.DEFAULTSECT,
247                                 help="Miscellaneous options")
248             for (key, value) in self._defaults.items():
249                 self._write_setting(fp, configparser.DEFAULTSECT, key, value)
250             fp.write("\n")
251         for section in self._sections:
252             self._write_setting(fp, section)
253             for (key, value) in self._sections[section].items():
254                 if key != "__name__":
255                     self._write_setting(fp, section, key, value)
256             fp.write("\n")
257         if local_fp:
258             fp.close()
259
260 class TestHookeConfigParser (unittest.TestCase):
261     def test_queue_safe(self):
262         """Ensure :class:`HookeConfigParser` is Queue-safe.
263         """
264         from multiprocessing import Queue
265         q = Queue()
266         a = HookeConfigParser(
267             paths=DEFAULT_PATHS, default_settings=DEFAULT_SETTINGS)
268         q.put(a)
269         b = q.get(a)
270         for attr in ['_dict', '_defaults', '_sections', '_config_paths',
271                      '_default_settings']:
272             a_value = getattr(a, attr)
273             b_value = getattr(b, attr)
274             self.failUnless(
275                 a_value == b_value,
276                 'HookeConfigParser.%s did not survive Queue: %s != %s'
277                 % (attr, a_value, b_value))