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