Version 0.3.0
mp3togo/options.py
1 # - options.py -
2 # This file is part of mp3togo
3
4 # Manage command line and config file program options
5 #
6 # (c) Simeon Veldstra 2006 <reallifesim@gmail.com>
7 #
8 # This software is free.
9 #
10 # This program 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 General Public License for more details.
14 #
15 # You may redistribute this program under the terms of the
16 # GNU General Public Licence version 2
17 # Available in this package or at http://www.fsf.org
18
19 # requires python-pyvorbis and python-id3 and lame
20 # and mpg321
21
22 """Configuration manager for command line programs.
23
24 Instantiate Options with argv as an argument and command
25 line options will be read in. With no argv a default set
26 of options will be initialized. If ~/.mp3togo exists, it
27 will be read in before command line options are
28 processed. The command line takes precedence over the
29 config file unless a config file is specified on the
30 command line or through the conffile argument to the
31 constructor. In this case, the file is processed last
32 and overrides any command line options. To totaly
33 disable reading in config files, set readconf=False in
34 the constructor."""
35
36
37
38 import sys
39 import os
40 import UserDict
41 import getopt
42 #import dummy_threading as threading
43 import threading
44
45 class Fail(Exception):
46 def __init__(self, msg):
47 self.msg = msg
48
49 def __str__(self):
50 return self.msg + "\n"
51
52 class Options(UserDict.DictMixin):
53 """Class: mp3togo.conf.Options
54 The Options class obtains configuration information and
55 makes it available to the running program in a dictionary
56 interface.
57 """
58 def __init__(self, config_file):
59 # The lock
60 self._mutex = threading.Lock()
61
62 # The name of the program
63 self.name = ''
64 self.version = ''
65
66 # The option dictionary
67 self._d = {}
68
69 # Add program options here
70 # ((name, short option, long option, default value, hook, help text), ...)
71 self._set = [
72 ('conffile', 'c', 'config', config_file, self._conffile,
73 '''The location of the config file. (If it exists.)'''),
74 ('saveconffile', 's', 'save-config', False, self._saveconffile,
75 '''Write out a new config file with the options given and exit. To save the configuration to a different file, set --config to the desired filename. The file, if it exists, will be overwritten.'''),
76 ('noconffile', '', 'no-config-file', False, self._noconffile,
77 '''Disable reading from and writing to the config file.'''),
78 ('verbosity', 'v', 'verbose', 1, self._verbosity,
79 '''Set the verbosity level. A value of 0 will produce no output (except option parsing errors).'''),
80 ('help', 'h', 'help', False, self._help,
81 '''Print this message.'''),
82 ('arg_files', '', '', [], self._arg_files, '')]
83
84 # Binaries to check for:
85 self.bin = {}
86
87 self._help_preamble = ""
88
89 # Don't save these options to the config file:
90 self._nosave_options = ['saveconffile', 'conffile', 'arg_files', 'help']
91
92
93 def _lock(self):
94 self._mutex.acquire()
95
96 def _unlock(self):
97 self._mutex.release()
98
99 def _locked(self):
100 return self._mutex.locked()
101
102 def getconf(self, argv=None, conffile=None, readconf=True):
103 self._lock()
104
105 #Do any pre configuration chores
106 try:
107 self._pre_hook()
108 except:
109 self._unlock()
110 raise
111 self._readconf = readconf
112
113 #Default values:
114 self._unlock()
115 for i in self._set:
116 self[i[0]] = i[3] # hooks run
117 self._lock()
118
119 #Read comand line options
120 olist, loose = {}, []
121 if argv:
122 self.name = os.path.basename(argv[0])
123 argv = argv[1:]
124 sopts = ''
125 lopts = []
126 for i in self._set:
127 if i[1]:
128 sopts += i[1]
129 if i[3] is not False:
130 sopts += ':'
131 if i[2]:
132 if i[3] is not False:
133 lopts.append(i[2] + '=')
134 else:
135 lopts.append(i[2])
136 try:
137 olist, loose = getopt.getopt(argv, sopts, lopts)
138 except getopt.GetoptError:
139 self._unlock()
140 raise Fail, "Bad arguments."
141 if '--no-config-file' in olist:
142 self._readconf = False
143
144 #Read in config file
145 if conffile:
146 self._d['conffile'] = conffile
147 oldconf = self._d['conffile']
148 self._unlock()
149 self.load_config_file()
150 self._lock()
151
152 #Sort out loose files from the command line
153 if loose:
154 files = []
155 for f in loose:
156 absf = os.path.expanduser(f)
157 absf = os.path.abspath(absf)
158 if os.path.exists(absf):
159 files.append(absf)
160 self._d['arg_files'] = files
161
162 #Process options.
163 self._unlock() # may execute a hook function
164 if olist:
165 for opt, val in olist:
166 opt = opt.strip('-')
167 val = val.strip()
168 for line in self._set:
169 if opt == line[1] or opt == line[2]:
170 opttype = type(line[3])
171 try:
172 if type(val) == opttype:
173 self[line[0]] = val
174 elif opttype == type(1):
175 self[line[0]] = int(val)
176 elif opttype == type(1.0):
177 self[line[0]] = float(val)
178 elif opttype == type(False):
179 self[line[0]] = True
180 except ValueError:
181 raise Fail, "Expecting Numeric argument!"
182 break
183
184 #Offer help, if asked:
185 #unlocked
186 if self.opt('help'):
187 print self.usage()
188 sys.exit(0)
189
190 #Save the conf file and exit if so instructed:
191 #unlocked
192 if self.opt('saveconffile'):
193 self.log(1, "Saving config to '%s'" % self._d['conffile'])
194 self.save_config_file()
195 self.log(1, "Exiting.")
196 sys.exit(0)
197
198 #Read in new config file if a new one has been specified:
199 #unlocked
200 if self.opt('conffile') != oldconf:
201 self.load_config_file()
202
203 #Check for binaries:
204 self._lock()
205 search = os.environ['PATH'].split(':')
206 for bin in self.bin.keys():
207 if not self.bin[bin]:
208 for spot in search:
209 if os.path.exists(os.path.join(spot, bin)):
210 self.bin[bin] = os.path.join(spot, bin)
211 break
212 if self.bin[bin]:
213 self.log(3, "%s binary found at %s" % (bin, self.bin[bin]))
214 else:
215 self.log(2, "%s binary not found" % bin)
216
217 #Do any post configuration chores
218 self._post_hook()
219 self._unlock()
220
221 def save_config_file(self):
222 if self.opt('conffile') and self._readconf:
223 self._lock()
224 try:
225 fp = file(self._d['conffile'], 'w')
226 for key in self._d.keys():
227 if key in self._nosave_options:
228 continue
229 fp.write("%s=%s\n" % (key, self._d[key]))
230 fp.close()
231 except IOError:
232 self.log(1, "Failed to save config file")
233 self._unlock()
234
235 def load_config_file(self):
236 """Load the config file specified by the 'conffile' option, if it exists."""
237 # All state access through __getitem__ and __setitem__
238 # Don't lock, except to directly manipulate state
239 if self.opt('conffile') and \
240 os.path.exists(os.path.expanduser(self['conffile'])) and self._readconf:
241 filename = os.path.expanduser(self['conffile'])
242 try:
243 fp = file(filename, 'r')
244 lines = fp.readlines()
245 fp.close()
246 conf = {}
247 for line in lines:
248 line = line[:-1]
249 line = line.split('=', 1)
250 if len(line) == 2:
251 conf[line[0].lower()] = line[1].strip()
252 for key in conf.keys():
253 if self.has_key(key):
254 if type(self[key]) == type(conf[key]):
255 self[key] = conf[key]
256 elif type(self[key]) == type(1):
257 self[key] = int(conf[key])
258 elif type(self[key]) == type(1L):
259 self[key] = long(conf[key])
260 elif type(self[key]) == type(1.0):
261 self[key] = float(conf[key])
262 elif type(self[key]) == type(True):
263 if conf[key].lower() in ('no', 'off', 'false', 'disable', '0') or\
264 not conf[key]:
265 self[key] = False
266 else:
267 self[key] = True
268 else:
269 self.log(2, "Unknown entry in config file: %s" % key)
270 except:
271 self.log(1, "Error reading config file")
272
273 def opt(self, name):
274 self._lock()
275 if self._d.has_key(name):
276 out = self._d[name]
277 else:
278 out = None
279 self._unlock()
280 return out
281
282 def __getitem__(self, name):
283 self._lock()
284 if self._d.has_key(name):
285 self._unlock()
286 return self._d[name]
287 else:
288 self._unlock()
289 raise KeyError
290
291 def __setitem__(self, name, value):
292 """Set value if key exists and type is correct."""
293 for i in self._set:
294 if i[0] == name:
295 if type(i[3]) != type(value):
296 raise ValueError
297 if i[4]:
298 try:
299 self._lock()
300 apply(i[4], [value])
301 finally:
302 self._unlock()
303 else:
304 self._lock()
305 self._d[name] = value
306 self._unlock()
307 return
308 raise IndexError
309
310 def keys(self):
311 self._lock()
312 out = self._d.keys()
313 self._unlock()
314 return out
315
316 def __delitem__(self, item):
317 """conf items shouldn't be deleted. Object methods shouldn't
318 raise an exception when called. Item deletion will fail fairly
319 silently."""
320 self.log(4, "Warning, attempt to delete conf item was foiled.")
321
322 def log(self, level, msg):
323 """Print a log message. Level must be at least 1. If the level
324 is below the current verbosity level, nothing happens."""
325 # Don't lock
326 if level < 1:
327 raise Fail, "log() called with insufficiently high level."
328 if level <= self._d['verbosity']:
329 print >>sys.stderr, msg
330
331 def usage(self):
332 """Returns a pretty printed usage message with the available
333 command line options in a string."""
334 msg = "%s version %s\n" % (self.name, self.version)
335 msg += self._help_preamble
336 msg += "\nOptions:\n"
337 for option in self._set:
338 if option[1] or option[2]:
339 if option[3] is not False:
340 if option [1]:
341 line = " -%s %s" % (option[1], option[3])
342 if option[2]:
343 line += ", "
344 if option[2]:
345 line += " --%s %s" % (option[2], option[3])
346 else:
347 if option[1]:
348 line = " -%s" % option[1]
349 else:
350 line = ""
351 if option[1] and option[2]:
352 line += ", "
353 if option[2]:
354 line += " --%s" % option[2]
355 msg += line + "\n"
356 line = ""
357 if option[5]:
358 desc = option[5].replace('\n', ' ').split()
359 while len(desc):
360 line = " "
361 while 1:
362 if len(line) + len(desc[0]) > 60:
363 msg += line + "\n"
364 line = ""
365 break
366 else:
367 line += " " + desc[0]
368 del desc[0]
369 if len(desc) == 0:
370 msg += line + "\n\n"
371 line = ""
372 break
373 return msg
374
375 def manpage(self):
376 """Like usage, but formatted as a docbook sgml fragment for inclusion
377 in a Debian man page.""" # Cheating!
378 msg = "\n<variablelist>\n"
379 for option in self._set:
380 if option[1] or option[2]:
381 msg += " <varlistentry>\n"
382 msg += " <term>\n"
383 if option[1]:
384 msg += " <option>-%s</option>\n" % option[1]
385 if option[2]:
386 msg += " <option>--%s</option>\n" % option[2]
387 msg += " </term>\n"
388 msg += " <listitem>\n"
389 if option[5]:
390 desc = option[5].replace('\n', ' ').split()
391 line = " <para>"
392 while len(desc):
393 while 1:
394 if len(line) + len(desc[0]) > 70:
395 break
396 else:
397 line += " " + desc[0]
398 del desc[0]
399 if len(desc) == 0:
400 break
401 msg += line + '\n'
402 line = " "
403 msg += " </para>\n"
404 msg += " </listitem>\n"
405 msg += " </varlistentry>\n"
406 msg += "</variablelist>\n\n"
407 return msg
408
409
410
411
412
413 # These are stubs to be overridden in subclasses:
414 def _post_hook(self):
415 pass
416
417 def _pre_hook(self):
418 pass
419
420 def _arg_files(self, value):
421 self._d['arg_files'] = value
422
423 def _conffile(self, value):
424 self._d['conffile'] = value
425
426 def _saveconffile(self, value):
427 self._d['saveconffile'] = value
428
429 def _noconffile(self, value):
430 self._d['noconffile'] = value
431
432 def _verbosity(self, value):
433 self._d['verbosity'] = value
434
435 def _help(self, value):
436 self._d['help'] = value
437