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