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