4
Option - Holds information about an option
5
OptionsClass - A collection of options
9
This module is used to manage "options" managed in user editable files.
10
This is the implementation of the Options.options globally shared options
11
object for the SpamBayes project, but is also able to be used to manage
12
other options required by each application.
14
The Option class holds information about an option - the name of the
15
option, a nice name (to display), documentation, default value,
16
possible values (a tuple or a regex pattern), whether multiple values
17
are allowed, and whether the option should be reset when restoring to
18
defaults (options like server names should *not* be).
20
The OptionsClass class provides facility for a collection of Options.
21
It is expected that manipulation of the options will be carried out
22
via an instance of this class.
24
Experimental or deprecated options are prefixed with 'x-', borrowing the
25
practice from RFC-822 mail. If the user sets an option like:
30
and an 'x-transmogrify' or 'transmogrify' option exists, it is set silently
31
to the value given by the user. If the user sets an option like:
36
and no 'transmogrify' option exists, but an 'x-transmogrify' option does,
37
the latter is set to the value given by the users and a deprecation message
38
is printed to standard error.
41
o Stop allowing invalid options in configuration files
42
o Find a regex expert to come up with *good* patterns for domains,
43
email addresses, and so forth.
44
o str(Option) should really call Option.unconvert since this is what
45
it does. Try putting that in and running all the tests.
46
o [See also the __issues__ string.]
51
# This module is part of the spambayes project, which is Copyright 2002-3
52
# The Python Software Foundation and is covered by the Python Software
55
__credits__ = "All the Spambayes folk."
56
# blame for the new format: Tony Meyer <ta-meyer@ihug.co.nz>
58
__issues__ = """Things that should be considered further and by
61
We are very generous in checking validity when multiple values are
62
allowed and the check is a regex (rather than a tuple). Any sequence
63
that does not match the regex may be used to delimit the values.
64
For example, if the regex was simply r"[\d]*" then these would all
67
"123abced234" -> 123, 234
68
"123XST234xas" -> 123, 234
70
"123~!@$%^&@234!" -> 123, 234
72
If this is a problem, my recommendation would be to change the
73
multiple_values_allowed attribute from a boolean to a regex/None
74
i.e. if multiple is None, then only one value is allowed. Otherwise
75
multiple is used in a re.split() to separate the input.
81
from tempfile import TemporaryFile
84
import cStringIO as StringIO
95
# Maintain compatibility with Python 2.2
100
__all__ = ['OptionsClass',
101
'HEADER_NAME', 'HEADER_VALUE',
102
'INTEGER', 'REAL', 'BOOLEAN',
103
'SERVER', 'PORT', 'EMAIL_ADDRESS',
104
'PATH', 'VARIABLE_PATH', 'FILE', 'FILE_WITH_PATH',
105
'IMAP_FOLDER', 'IMAP_ASTRING',
106
'RESTORE', 'DO_NOT_RESTORE', 'IP_LIST',
109
MultiContainerTypes = (types.TupleType, types.ListType)
111
class Option(object):
112
def __init__(self, name, nice_name="", default=None,
113
help_text="", allowed=None, restore=True):
115
self.nice_name = nice_name
116
self.default_value = default
117
self.explanation_text = help_text
118
self.allowed_values = allowed
119
self.restore = restore
120
self.delimiter = None
121
# start with default value
124
def display_name(self):
125
'''A name for the option suitable for display to a user.'''
126
return self.nice_name
128
'''The default value for the option.'''
129
return self.default_value
131
'''Documentation for the option.'''
132
return self.explanation_text
133
def valid_input(self):
134
'''Valid values for the option.'''
135
return self.allowed_values
136
def no_restore(self):
137
'''Do not restore this option when restoring to defaults.'''
138
return not self.restore
140
'''Set option to value.'''
143
'''Get option value.'''
145
def multiple_values_allowed(self):
146
'''Multiple values are allowed for this option.'''
147
return type(self.default_value) in MultiContainerTypes
149
def is_valid(self, value):
150
'''Check if this is a valid value for this option.'''
151
if self.allowed_values is None:
154
if self.multiple_values_allowed():
155
return self.is_valid_multiple(value)
157
return self.is_valid_single(value)
159
def is_valid_multiple(self, value):
160
'''Return True iff value is a valid value for this option.
161
Use if multiple values are allowed.'''
162
if type(value) in MultiContainerTypes:
164
if not self.is_valid_single(val):
167
return self.is_valid_single(value)
169
def is_valid_single(self, value):
170
'''Return True iff value is a valid value for this option.
171
Use when multiple values are not allowed.'''
172
if type(self.allowed_values) == types.TupleType:
173
if value in self.allowed_values:
178
# special handling for booleans, thanks to Python 2.2
179
if self.is_boolean and (value == True or value == False):
181
if type(value) != type(self.value) and \
182
type(self.value) not in MultiContainerTypes:
183
# This is very strict! If the value is meant to be
184
# a real number and an integer is passed in, it will fail.
185
# (So pass 1. instead of 1, for example)
188
# A blank string is always ok.
190
avals = self._split_values(value)
191
# in this case, allowed_values must be a regex, and
192
# _split_values must match once and only once
196
# either no match or too many matches
199
def _split_values(self, value):
200
# do the regex mojo here
201
if not self.allowed_values:
204
r = re.compile(self.allowed_values)
206
print >> sys.stderr, self.allowed_values
216
delimiter = s[i:i + m.start()]
217
if self.delimiter is None and delimiter != "":
218
self.delimiter = delimiter
222
def as_nice_string(self, section=None):
223
'''Summarise the option in a user-readable format.'''
227
strval = "[%s] " % (section)
228
strval += "%s - \"%s\"\nDefault: %s\nDo not restore: %s\n" \
229
% (self.name, self.display_name(),
230
str(self.default()), str(self.no_restore()))
231
strval += "Valid values: %s\nMultiple values allowed: %s\n" \
232
% (str(self.valid_input()),
233
str(self.multiple_values_allowed()))
234
strval += "\"%s\"\n\n" % (str(self.doc()))
237
def write_config(self, file):
238
'''Output value in configuration file format.'''
239
file.write(self.name)
241
file.write(self.unconvert())
244
def convert(self, value):
245
'''Convert value from a string to the appropriate type.'''
246
svt = type(self.value)
247
if svt == type(value):
248
# already the correct type
250
if type(self.allowed_values) == types.TupleType and \
251
value in self.allowed_values:
252
# already correct type
254
if self.is_boolean():
255
if str(value) == "True" or value == 1:
257
elif str(value) == "False" or value == 0:
259
raise TypeError, self.name + " must be True or False"
260
if self.multiple_values_allowed():
261
# This will fall apart if the allowed_value is a tuple,
262
# but not a homogenous one...
263
if isinstance(self.allowed_values, types.StringTypes):
264
vals = list(self._split_values(value))
266
if isinstance(value, types.TupleType):
270
if len(self.default_value) > 0:
271
to_type = type(self.default_value[0])
273
to_type = types.StringType
274
for i in range(0, len(vals)):
275
vals[i] = self._convert(vals[i], to_type)
278
return self._convert(value, svt)
279
raise TypeError, self.name + " has an invalid type."
281
def _convert(self, value, to_type):
282
'''Convert an int, float or string to the specified type.'''
283
if to_type == type(value):
284
# already the correct type
286
if to_type == types.IntType:
287
return locale.atoi(value)
288
if to_type == types.FloatType:
289
return locale.atof(value)
290
if to_type in types.StringTypes:
292
raise TypeError, "Invalid type."
295
'''Convert value from the appropriate type to a string.'''
296
if type(self.value) in types.StringTypes:
299
if self.is_boolean():
300
# A wee bit extra for Python 2.2
301
if self.value == True:
305
if type(self.value) == types.TupleType:
306
if len(self.value) == 0:
308
if len(self.value) == 1:
310
if type(v) == types.FloatType:
311
return locale.str(self.value[0])
313
# We need to separate out the items
315
# We use a character that is invalid as the separator
316
# so that it will reparse correctly. We could try all
317
# characters, but we make do with this set of commonly
318
# used ones - note that the first one that works will
319
# be used. Perhaps a nicer solution than this would be
320
# to specifiy a valid delimiter for all options that
321
# can have multiple values. Note that we have None at
322
# the end so that this will crash and die if none of
323
# the separators works <wink>.
324
if self.delimiter is None:
325
if type(self.allowed_values) == types.TupleType:
330
for sep in [' ', ',', ':', ';', '/', '\\', None]:
331
# we know at this point that len(self.value) is at
332
# least two, because len==0 and len==1 were dealt
333
# with as special cases
334
test_str = str(v0) + sep + str(v1)
335
test_tuple = self._split_values(test_str)
336
if test_tuple[0] == str(v0) and \
337
test_tuple[1] == str(v1) and \
338
len(test_tuple) == 2:
340
# cache this so we don't always need to do the above
343
if type(v) == types.FloatType:
347
strval += v + self.delimiter
348
strval = strval[:-len(self.delimiter)] # trailing seperator
350
# Otherwise, we just hope str() will do the job
351
strval = str(self.value)
354
def is_boolean(self):
355
'''Return True iff the option is a boolean value.'''
356
# This is necessary because of the Python 2.2 True=1, False=0
357
# cheat. The valid values are returned as 0 and 1, even if
358
# they are actually False and True - but 0 and 1 are not
359
# considered valid input (and 0 and 1 don't look as nice)
360
# So, just for the 2.2 people, we have this helper function
362
if type(self.allowed_values) == types.TupleType and \
363
len(self.allowed_values) > 0 and \
364
type(self.allowed_values[0]) == types.BooleanType:
367
except AttributeError:
368
# If the user has Python 2.2 and an option has valid values
369
# of (0, 1) - i.e. integers, then this function will return
370
# the wrong value. I don't know what to do about that without
371
# explicitly stating which options are boolean
372
if self.allowed_values == (False, True):
377
class OptionsClass(object):
381
self.conversion_table = {} # set by creator if they need it.
383
# Regular expressions for parsing section headers and options.
384
# Lifted straight from ConfigParser
386
SECTCRE = re.compile(
388
r'(?P<header>[^]]+)' # very permissive!
392
r'(?P<option>[^:=\s][^:=]*)' # very permissive!
393
r'\s*(?P<vi>[:=])\s*' # any number of space/tab,
394
# followed by separator
395
# (either : or =), followed
397
r'(?P<value>.*)$' # everything up to EOL
400
def update_file(self, filename):
401
'''Update the specified configuration file.'''
404
out = TemporaryFile()
405
if os.path.exists(filename):
406
f = file(filename, "r")
408
# doesn't exist, so create it - all the changed options will
411
print >> sys.stderr, "Creating new configuration file",
412
print >> sys.stderr, filename
413
f = file(filename, "w")
415
f = file(filename, "r")
417
vi = ": " # default; uses the one from the file where possible
422
# comment or blank line?
423
if line.strip() == '' or line[0] in '#;':
426
if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
427
# no leading whitespace
431
if line[0].isspace() and sectname is not None and optname:
433
# a section header or option header?
435
# is it a section header?
436
mo = self.SECTCRE.match(line)
438
# Add any missing from the previous section
439
if sectname is not None:
440
self._add_missing(out, written, sectname, vi, False)
441
sectname = mo.group('header')
442
# So sections can't start with a continuation line
444
if sectname in self.sections():
448
mo = self.OPTCRE.match(line)
450
optname, vi, optval = mo.group('option', 'vi', 'value')
451
if vi in ('=', ':') and ';' in optval:
452
# ';' is a comment delimiter only if it follows
453
# a spacing character
454
pos = optval.find(';')
455
if pos != -1 and optval[pos-1].isspace():
456
optval = optval[:pos]
457
optval = optval.strip()
461
optname = optname.rstrip().lower()
462
if self._options.has_key((sectname, optname)):
465
newval = self.unconvert(sectname, optname)
466
out.write(newval.replace("\n", "\n\t"))
468
written.append((sectname, optname))
469
for sect in self.sections():
470
self._add_missing(out, written, sect, vi)
474
# save a backup of the old file
475
shutil.copyfile(filename, filename + ".bak")
476
# copy the new file across
477
f = file(filename, "w")
479
shutil.copyfileobj(out, f)
483
def _add_missing(self, out, written, sect, vi, label=True):
484
# add any missing ones, where the value does not equal the default
485
for opt in self.options_in_section(sect):
486
if not (sect, opt) in written and \
487
self.get(sect, opt) != self.default(sect, opt):
495
newval = self.unconvert(sect, opt)
496
out.write(newval.replace("\n", "\n\t"))
498
written.append((sect, opt))
500
def load_defaults(self, defaults):
501
'''Load default values (stored in this module).'''
502
for section, opts in defaults.items():
504
# If first item of the tuple is a sub-class of Option, then
505
# instantiate that (with the rest as args). Otherwise,
506
# assume standard Options class.
510
if issubclass(opt[0], Option):
513
except TypeError: # opt[0] not a class
517
self._options[section, o.name] = o
519
def merge_files(self, file_list):
523
def convert_and_set(self, section, option, value):
524
value = self.convert(section, option, value)
525
self.set(section, option, value)
527
def merge_file(self, filename):
529
c = ConfigParser.ConfigParser()
531
for sect in c.sections():
532
for opt in c.options(sect):
533
value = c.get(sect, opt)
536
if not self._options.has_key((section, option)):
537
if option.startswith('x-'):
538
# try setting option without the x- prefix
540
if self._options.has_key((section, option)):
541
self.convert_and_set(section, option, value)
542
# not an error if an X- option is missing
544
option = 'x-' + option
545
# going the other way, if the option has been
546
# deprecated, set its x-prefixed version and
548
if self._options.has_key((section, option)):
549
self.convert_and_set(section, option, value)
550
self._report_deprecated_error(section, opt)
552
print >> sys.stderr, (
553
"warning: Invalid option %s in"
554
" section %s in file %s" %
555
(opt, sect, filename))
557
self.convert_and_set(section, option, value)
559
# not strictly necessary, but convenient shortcuts to self._options
560
def display_name(self, sect, opt):
561
'''A name for the option suitable for display to a user.'''
562
return self._options[sect, opt.lower()].display_name()
563
def default(self, sect, opt):
564
'''The default value for the option.'''
565
return self._options[sect, opt.lower()].default()
566
def doc(self, sect, opt):
567
'''Documentation for the option.'''
568
return self._options[sect, opt.lower()].doc()
569
def valid_input(self, sect, opt):
570
'''Valid values for the option.'''
571
return self._options[sect, opt.lower()].valid_input()
572
def no_restore(self, sect, opt):
573
'''Do not restore this option when restoring to defaults.'''
574
return self._options[sect, opt.lower()].no_restore()
575
def is_valid(self, sect, opt, value):
576
'''Check if this is a valid value for this option.'''
577
return self._options[sect, opt.lower()].is_valid(value)
578
def multiple_values_allowed(self, sect, opt):
579
'''Multiple values are allowed for this option.'''
580
return self._options[sect, opt.lower()].multiple_values_allowed()
582
def is_boolean(self, sect, opt):
583
'''The option is a boolean value. (Support for Python 2.2).'''
584
return self._options[sect, opt.lower()].is_boolean()
586
def convert(self, sect, opt, value):
587
'''Convert value from a string to the appropriate type.'''
588
return self._options[sect, opt.lower()].convert(value)
590
def unconvert(self, sect, opt):
591
'''Convert value from the appropriate type to a string.'''
592
return self._options[sect, opt.lower()].unconvert()
594
def get_option(self, sect, opt):
596
if self.conversion_table.has_key((sect, opt)):
597
sect, opt = self.conversion_table[sect, opt]
598
return self._options[sect, opt.lower()]
600
def get(self, sect, opt):
601
'''Get an option value.'''
602
if self.conversion_table.has_key((sect, opt.lower())):
603
sect, opt = self.conversion_table[sect, opt.lower()]
604
return self.get_option(sect, opt.lower()).get()
606
def __getitem__(self, key):
607
return self.get(key[0], key[1])
609
def set(self, sect, opt, val=None):
611
if self.conversion_table.has_key((sect, opt.lower())):
612
sect, opt = self.conversion_table[sect, opt.lower()]
613
if self.is_valid(sect, opt, val):
614
self._options[sect, opt.lower()].set(val)
616
print >> sys.stderr, ("Attempted to set [%s] %s with invalid"
618
(sect, opt.lower(), val, type(val)))
620
def set_from_cmdline(self, arg, stream=None):
621
"""Set option from colon-separated sect:opt:val string.
623
If optional stream arg is not None, error messages will be displayed
624
on stream, otherwise KeyErrors will be propagated up the call chain.
626
sect, opt, val = arg.split(':', 2)
629
val = self.convert(sect, opt, val)
630
except (KeyError, TypeError), msg:
631
if stream is not None:
632
self._report_option_error(sect, opt, val, stream, msg)
636
self.set(sect, opt, val)
638
def _report_deprecated_error(self, sect, opt):
639
print >> sys.stderr, (
640
"Warning: option %s in section %s is deprecated" %
643
def _report_option_error(self, sect, opt, val, stream, msg):
645
if sect in self.sections():
646
vopts = self.options(True)
647
vopts = [v.split(']', 1)[1] for v in vopts
648
if v.startswith('[%s]'%sect)]
650
print >> stream, "Invalid option:", opt
651
print >> stream, "Valid options for", sect, "are:"
652
vopts = ', '.join(vopts)
653
vopts = textwrap.wrap(vopts)
655
print >> stream, ' ', line
657
print >> stream, "Invalid value:", msg
659
print >> stream, "Invalid section:", sect
660
print >> stream, "Valid sections are:"
661
vsects = ', '.join(self.sections())
662
vsects = textwrap.wrap(vsects)
664
print >> stream, ' ', line
666
def __setitem__(self, key, value):
667
self.set(key[0], key[1], value)
670
'''Return an alphabetical list of all the sections.'''
672
for sect, opt in self._options.keys():
678
def options_in_section(self, section):
679
'''Return an alphabetical list of all the options in this section.'''
681
for sect, opt in self._options.keys():
687
def options(self, prepend_section_name=False):
688
'''Return an alphabetical list of all the options, optionally
689
prefixed with [section_name]'''
691
for sect, opt in self._options.keys():
692
if prepend_section_name:
693
all.append('[' + sect + ']' + opt)
700
'''Display options in a config file form.'''
701
output = StringIO.StringIO()
702
keys = self._options.keys()
704
currentSection = None
705
for sect, opt in keys:
706
if sect != currentSection:
707
if currentSection is not None:
712
currentSection = sect
713
self._options[sect, opt].write_config(output)
714
return output.getvalue()
716
def display_full(self, section=None, option=None):
717
'''Display options including all information.'''
718
# Given that the Options class is no longer as nice looking
719
# as it once was, this returns all the information, i.e.
720
# the doc, default values, and so on
721
output = StringIO.StringIO()
723
# when section and option are both specified, this
724
# is nothing more than a call to as_nice_string
725
if section is not None and option is not None:
726
output.write(self._options[section,
727
option.lower()].as_nice_string(section))
728
return output.getvalue()
730
all = self._options.keys()
732
for sect, opt in all:
733
if section is not None and sect != section:
735
output.write(self._options[sect, opt.lower()].as_nice_string(sect))
736
return output.getvalue()
738
# These are handy references to commonly used regex/tuples defining
739
# permitted values. Although the majority of options use one of these,
740
# you may use any regex or tuple you wish.
741
HEADER_NAME = r"[\w\.\-\*]+"
743
INTEGER = r"[\d]+" # actually, a *positive* integer
744
REAL = r"[\d]+[\.]?[\d]*" # likewise, a *positive* real
745
BOOLEAN = (False, True)
746
SERVER = r"([\w\.\-]+(:[\d]+)?)" # in the form server:port
748
EMAIL_ADDRESS = r"[\w\-\.]+@[\w\-\.]+"
749
PATH = r"[\w \$\.\-~:\\/\*\@\=]+"
750
VARIABLE_PATH = PATH + r"%"
752
FILE_WITH_PATH = PATH
753
IP_LIST = r"\*|localhost|((\*|[01]?\d\d?|2[04]\d|25[0-5])\.(\*|[01]?\d" \
754
r"\d?|2[04]\d|25[0-5])\.(\*|[01]?\d\d?|2[04]\d|25[0-5])\.(\*" \
755
r"|[01]?\d\d?|2[04]\d|25[0-5]),?)+"
756
# IMAP seems to allow any character at all in a folder name,
757
# but we want to use the comma as a delimiter for lists, so
758
# we don't allow this. If anyone has folders with commas in the
759
# names, please let us know and we'll figure out something else.
760
# ImapUI.py prints out a warning if this is the case.
761
IMAP_FOLDER = r"[^,]+"
763
# IMAP's astring should also be valid in the form:
764
# "{" number "}" CRLF *CHAR8
765
# where number represents the number of CHAR8 octets
766
# but this is too complex for us at the moment.
768
for i in range(1, 128):
769
if not chr(i) in ['"', '\\', '\n', '\r']:
770
IMAP_ASTRING += chr(i)
771
IMAP_ASTRING = r"\"?\\?[" + re.escape(IMAP_ASTRING) + r"]+\"?"
773
# Similarly, each option must specify whether it should be reset to
774
# this value on a "reset to defaults" command. Most should, but with some
775
# like a server name that defaults to "", this would be pointless.
776
# Again, for ease of reading, we define these here:
778
DO_NOT_RESTORE = False