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