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