~ubuntu-branches/ubuntu/karmic/spambayes/karmic

« back to all changes in this revision

Viewing changes to spambayes/OptionsClass.py

  • Committer: Bazaar Package Importer
  • Author(s): Jorge Bernal
  • Date: 2005-04-07 14:02:02 UTC
  • Revision ID: james.westby@ubuntu.com-20050407140202-mgyh6t7gn2dlrrw5
Tags: upstream-1.0.1
ImportĀ upstreamĀ versionĀ 1.0.1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""OptionsClass
 
2
 
 
3
Classes:
 
4
    Option - Holds information about an option
 
5
    OptionsClass - A collection of options
 
6
 
 
7
Abstract:
 
8
 
 
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.
 
13
 
 
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).
 
19
 
 
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.
 
23
 
 
24
Experimental or deprecated options are prefixed with 'x-', borrowing the
 
25
practice from RFC-822 mail.  If the user sets an option like:
 
26
 
 
27
    [Tokenizer]
 
28
    x-transmogrify: True
 
29
 
 
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:
 
32
 
 
33
    [Tokenizer]
 
34
    transmogrify: True
 
35
 
 
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.
 
39
 
 
40
To Do:
 
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.]
 
47
 o Suggestions?
 
48
 
 
49
"""
 
50
 
 
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
 
53
# Foundation license.
 
54
 
 
55
__credits__ = "All the Spambayes folk."
 
56
# blame for the new format: Tony Meyer <ta-meyer@ihug.co.nz>
 
57
 
 
58
__issues__ = """Things that should be considered further and by
 
59
other people:
 
60
 
 
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
 
65
be considered valid:
 
66
"123a234" -> 123, 234
 
67
"123abced234" -> 123, 234
 
68
"123XST234xas" -> 123, 234
 
69
"123 234" -> 123, 234
 
70
"123~!@$%^&@234!" -> 123, 234
 
71
 
 
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.
 
76
"""
 
77
 
 
78
import sys
 
79
import os
 
80
import shutil
 
81
from tempfile import TemporaryFile
 
82
 
 
83
try:
 
84
    import cStringIO as StringIO
 
85
except ImportError:
 
86
    import StringIO
 
87
 
 
88
import re
 
89
import types
 
90
import locale
 
91
 
 
92
try:
 
93
    True, False, bool
 
94
except NameError:
 
95
    # Maintain compatibility with Python 2.2
 
96
    True, False = 1, 0
 
97
    def bool(val):
 
98
        return not not val
 
99
 
 
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',
 
107
          ]
 
108
 
 
109
MultiContainerTypes = (types.TupleType, types.ListType)
 
110
 
 
111
class Option(object):
 
112
    def __init__(self, name, nice_name="", default=None,
 
113
                 help_text="", allowed=None, restore=True):
 
114
        self.name = name
 
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
 
122
        self.set(default)
 
123
 
 
124
    def display_name(self):
 
125
        '''A name for the option suitable for display to a user.'''
 
126
        return self.nice_name
 
127
    def default(self):
 
128
        '''The default value for the option.'''
 
129
        return self.default_value
 
130
    def doc(self):
 
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
 
139
    def set(self, val):
 
140
        '''Set option to value.'''
 
141
        self.value = val
 
142
    def get(self):
 
143
        '''Get option value.'''
 
144
        return self.value
 
145
    def multiple_values_allowed(self):
 
146
        '''Multiple values are allowed for this option.'''
 
147
        return type(self.default_value) in MultiContainerTypes
 
148
 
 
149
    def is_valid(self, value):
 
150
        '''Check if this is a valid value for this option.'''
 
151
        if self.allowed_values is None:
 
152
            return False
 
153
 
 
154
        if self.multiple_values_allowed():
 
155
            return self.is_valid_multiple(value)
 
156
        else:
 
157
            return self.is_valid_single(value)
 
158
 
 
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:
 
163
            for val in value:
 
164
                if not self.is_valid_single(val):
 
165
                    return False
 
166
            return True
 
167
        return self.is_valid_single(value)
 
168
 
 
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:
 
174
                return True
 
175
            else:
 
176
                return False
 
177
        else:
 
178
            # special handling for booleans, thanks to Python 2.2
 
179
            if self.is_boolean and (value == True or value == False):
 
180
                return True
 
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)
 
186
                return False
 
187
            if value == "":
 
188
                # A blank string is always ok.
 
189
                return True
 
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
 
193
            if len(avals) == 1:
 
194
                return True
 
195
            else:
 
196
                # either no match or too many matches
 
197
                return False
 
198
 
 
199
    def _split_values(self, value):
 
200
        # do the regex mojo here
 
201
        if not self.allowed_values:
 
202
            return ('',)
 
203
        try:
 
204
            r = re.compile(self.allowed_values)
 
205
        except:
 
206
            print >> sys.stderr, self.allowed_values
 
207
            raise
 
208
        s = str(value)
 
209
        i = 0
 
210
        vals = ()
 
211
        while True:
 
212
            m = r.search(s[i:])
 
213
            if m is None:
 
214
                break
 
215
            vals += (m.group(),)
 
216
            delimiter = s[i:i + m.start()]
 
217
            if self.delimiter is None and delimiter != "":
 
218
                self.delimiter = delimiter
 
219
            i += m.end()
 
220
        return vals
 
221
 
 
222
    def as_nice_string(self, section=None):
 
223
        '''Summarise the option in a user-readable format.'''
 
224
        if section is None:
 
225
            strval = ""
 
226
        else:
 
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()))
 
235
        return strval
 
236
 
 
237
    def write_config(self, file):
 
238
        '''Output value in configuration file format.'''
 
239
        file.write(self.name)
 
240
        file.write(': ')
 
241
        file.write(self.unconvert())
 
242
        file.write('\n')
 
243
 
 
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
 
249
            return value
 
250
        if type(self.allowed_values) == types.TupleType and \
 
251
           value in self.allowed_values:
 
252
            # already correct type
 
253
            return value
 
254
        if self.is_boolean():
 
255
            if str(value) == "True" or value == 1:
 
256
                return True
 
257
            elif str(value) == "False" or value == 0:
 
258
                return False
 
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))
 
265
            else:
 
266
                if isinstance(value, types.TupleType):
 
267
                    vals = list(value)
 
268
                else:
 
269
                    vals = value.split()
 
270
            if len(self.default_value) > 0:
 
271
                to_type = type(self.default_value[0])
 
272
            else:
 
273
                to_type = types.StringType
 
274
            for i in range(0, len(vals)):
 
275
                vals[i] = self._convert(vals[i], to_type)
 
276
            return tuple(vals)
 
277
        else:
 
278
            return self._convert(value, svt)
 
279
        raise TypeError, self.name + " has an invalid type."
 
280
 
 
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
 
285
            return value
 
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:
 
291
            return str(value)
 
292
        raise TypeError, "Invalid type."
 
293
 
 
294
    def unconvert(self):
 
295
        '''Convert value from the appropriate type to a string.'''
 
296
        if type(self.value) in types.StringTypes:
 
297
            # nothing to do
 
298
            return self.value
 
299
        if self.is_boolean():
 
300
            # A wee bit extra for Python 2.2
 
301
            if self.value == True:
 
302
                return "True"
 
303
            else:
 
304
                return "False"
 
305
        if type(self.value) == types.TupleType:
 
306
            if len(self.value) == 0:
 
307
                return ""
 
308
            if len(self.value) == 1:
 
309
                v = self.value[0]
 
310
                if type(v) == types.FloatType:
 
311
                    return locale.str(self.value[0])
 
312
                return str(v)
 
313
            # We need to separate out the items
 
314
            strval = ""
 
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:
 
326
                    self.delimiter = ' '
 
327
                else:
 
328
                    v0 = self.value[0]
 
329
                    v1 = self.value[1]
 
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:
 
339
                            break
 
340
                    # cache this so we don't always need to do the above
 
341
                    self.delimiter = sep
 
342
            for v in self.value:
 
343
                if type(v) == types.FloatType:
 
344
                    v = locale.str(v)
 
345
                else:
 
346
                    v = str(v)
 
347
                strval += v + self.delimiter
 
348
            strval = strval[:-len(self.delimiter)] # trailing seperator
 
349
        else:
 
350
            # Otherwise, we just hope str() will do the job
 
351
            strval = str(self.value)
 
352
        return strval
 
353
 
 
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
 
361
        try:
 
362
            if type(self.allowed_values) == types.TupleType and \
 
363
               len(self.allowed_values) > 0 and \
 
364
               type(self.allowed_values[0]) == types.BooleanType:
 
365
                return True
 
366
            return False
 
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):
 
373
                return True
 
374
            return False
 
375
 
 
376
 
 
377
class OptionsClass(object):
 
378
    def __init__(self):
 
379
        self.verbose = None
 
380
        self._options = {}
 
381
        self.conversion_table = {} # set by creator if they need it.
 
382
    #
 
383
    # Regular expressions for parsing section headers and options.
 
384
    # Lifted straight from ConfigParser
 
385
    #
 
386
    SECTCRE = re.compile(
 
387
        r'\['                                 # [
 
388
        r'(?P<header>[^]]+)'                  # very permissive!
 
389
        r'\]'                                 # ]
 
390
        )
 
391
    OPTCRE = re.compile(
 
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
 
396
                                              # by any # space/tab
 
397
        r'(?P<value>.*)$'                     # everything up to EOL
 
398
        )
 
399
 
 
400
    def update_file(self, filename):
 
401
        '''Update the specified configuration file.'''
 
402
        sectname = None
 
403
        optname = None
 
404
        out = TemporaryFile()
 
405
        if os.path.exists(filename):
 
406
            f = file(filename, "r")
 
407
        else:
 
408
            # doesn't exist, so create it - all the changed options will
 
409
            # be added to it
 
410
            if self.verbose:
 
411
                print >> sys.stderr, "Creating new configuration file",
 
412
                print >> sys.stderr, filename
 
413
            f = file(filename, "w")
 
414
            f.close()
 
415
            f = file(filename, "r")
 
416
        written = []
 
417
        vi = ": " # default; uses the one from the file where possible
 
418
        while True:
 
419
            line = f.readline()
 
420
            if not line:
 
421
                break
 
422
            # comment or blank line?
 
423
            if line.strip() == '' or line[0] in '#;':
 
424
                out.write(line)
 
425
                continue
 
426
            if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
 
427
                # no leading whitespace
 
428
                out.write(line)
 
429
                continue
 
430
            # continuation line?
 
431
            if line[0].isspace() and sectname is not None and optname:
 
432
                continue
 
433
            # a section header or option header?
 
434
            else:
 
435
                # is it a section header?
 
436
                mo = self.SECTCRE.match(line)
 
437
                if mo:
 
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
 
443
                    optname = None
 
444
                    if sectname in self.sections():
 
445
                        out.write(line)
 
446
                # an option line?
 
447
                else:
 
448
                    mo = self.OPTCRE.match(line)
 
449
                    if mo:
 
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()
 
458
                        # allow empty values
 
459
                        if optval == '""':
 
460
                            optval = ''
 
461
                        optname = optname.rstrip().lower()
 
462
                        if self._options.has_key((sectname, optname)):
 
463
                            out.write(optname)
 
464
                            out.write(vi)
 
465
                            newval = self.unconvert(sectname, optname)
 
466
                            out.write(newval.replace("\n", "\n\t"))
 
467
                            out.write('\n')
 
468
                            written.append((sectname, optname))
 
469
        for sect in self.sections():
 
470
            self._add_missing(out, written, sect, vi)
 
471
        f.close()
 
472
        out.flush()
 
473
        if self.verbose:
 
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")
 
478
        out.seek(0)
 
479
        shutil.copyfileobj(out, f)
 
480
        out.close()
 
481
        f.close()
 
482
 
 
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):
 
488
                if label:
 
489
                    out.write('[')
 
490
                    out.write(sect)
 
491
                    out.write("]\n")
 
492
                    label = False
 
493
                out.write(opt)
 
494
                out.write(vi)
 
495
                newval = self.unconvert(sect, opt)
 
496
                out.write(newval.replace("\n", "\n\t"))
 
497
                out.write('\n')
 
498
                written.append((sect, opt))
 
499
 
 
500
    def load_defaults(self, defaults):
 
501
        '''Load default values (stored in this module).'''
 
502
        for section, opts in defaults.items():
 
503
            for opt in opts:
 
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.
 
507
                klass = Option
 
508
                args = opt
 
509
                try:
 
510
                    if issubclass(opt[0], Option):
 
511
                        klass = opt[0]
 
512
                        args = opt[1:]
 
513
                except TypeError: # opt[0] not a class
 
514
                    pass
 
515
 
 
516
                o = klass(*args)
 
517
                self._options[section, o.name] = o
 
518
 
 
519
    def merge_files(self, file_list):
 
520
        for f in file_list:
 
521
            self.merge_file(f)
 
522
 
 
523
    def convert_and_set(self, section, option, value):
 
524
        value = self.convert(section, option, value)
 
525
        self.set(section, option, value)
 
526
 
 
527
    def merge_file(self, filename):
 
528
        import ConfigParser
 
529
        c = ConfigParser.ConfigParser()
 
530
        c.read(filename)
 
531
        for sect in c.sections():
 
532
            for opt in c.options(sect):
 
533
                value = c.get(sect, opt)
 
534
                section = sect
 
535
                option = opt
 
536
                if not self._options.has_key((section, option)):
 
537
                    if option.startswith('x-'):
 
538
                        # try setting option without the x- prefix
 
539
                        option = option[2:]
 
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
 
543
                    else:
 
544
                        option = 'x-' + option
 
545
                        # going the other way, if the option has been
 
546
                        # deprecated, set its x-prefixed version and
 
547
                        # emit a warning
 
548
                        if self._options.has_key((section, option)):
 
549
                            self.convert_and_set(section, option, value)
 
550
                            self._report_deprecated_error(section, opt)
 
551
                        else:
 
552
                            print >> sys.stderr, (
 
553
                                "warning: Invalid option %s in"
 
554
                                " section %s in file %s" %
 
555
                                (opt, sect, filename))
 
556
                else:
 
557
                    self.convert_and_set(section, option, value)
 
558
 
 
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()
 
581
 
 
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()
 
585
 
 
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)
 
589
 
 
590
    def unconvert(self, sect, opt):
 
591
        '''Convert value from the appropriate type to a string.'''
 
592
        return self._options[sect, opt.lower()].unconvert()
 
593
 
 
594
    def get_option(self, sect, opt):
 
595
        '''Get an option.'''
 
596
        if self.conversion_table.has_key((sect, opt)):
 
597
            sect, opt = self.conversion_table[sect, opt]
 
598
        return self._options[sect, opt.lower()]
 
599
 
 
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()
 
605
 
 
606
    def __getitem__(self, key):
 
607
        return self.get(key[0], key[1])
 
608
 
 
609
    def set(self, sect, opt, val=None):
 
610
        '''Set an option.'''
 
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)
 
615
        else:
 
616
            print >> sys.stderr, ("Attempted to set [%s] %s with invalid"
 
617
                                  " value %s (%s)" %
 
618
                                  (sect, opt.lower(), val, type(val)))
 
619
 
 
620
    def set_from_cmdline(self, arg, stream=None):
 
621
        """Set option from colon-separated sect:opt:val string.
 
622
 
 
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.
 
625
        """
 
626
        sect, opt, val = arg.split(':', 2)
 
627
        opt = opt.lower()
 
628
        try:
 
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)
 
633
            else:
 
634
                raise
 
635
        else:
 
636
            self.set(sect, opt, val)
 
637
 
 
638
    def _report_deprecated_error(self, sect, opt):
 
639
        print >> sys.stderr, (
 
640
            "Warning: option %s in section %s is deprecated" %
 
641
            (opt, sect))
 
642
 
 
643
    def _report_option_error(self, sect, opt, val, stream, msg):
 
644
        import textwrap
 
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)]
 
649
            if opt not in vopts:
 
650
                print >> stream, "Invalid option:", opt
 
651
                print >> stream, "Valid options for", sect, "are:"
 
652
                vopts = ', '.join(vopts)
 
653
                vopts = textwrap.wrap(vopts)
 
654
                for line in vopts:
 
655
                    print >> stream, '  ', line
 
656
            else:
 
657
                print >> stream, "Invalid value:", msg
 
658
        else:
 
659
            print >> stream, "Invalid section:", sect
 
660
            print >> stream, "Valid sections are:"
 
661
            vsects = ', '.join(self.sections())
 
662
            vsects = textwrap.wrap(vsects)
 
663
            for line in vsects:
 
664
                print >> stream, '  ', line
 
665
 
 
666
    def __setitem__(self, key, value):
 
667
        self.set(key[0], key[1], value)
 
668
 
 
669
    def sections(self):
 
670
        '''Return an alphabetical list of all the sections.'''
 
671
        all = []
 
672
        for sect, opt in self._options.keys():
 
673
            if sect not in all:
 
674
                all.append(sect)
 
675
        all.sort()
 
676
        return all
 
677
 
 
678
    def options_in_section(self, section):
 
679
        '''Return an alphabetical list of all the options in this section.'''
 
680
        all = []
 
681
        for sect, opt in self._options.keys():
 
682
            if sect == section:
 
683
                all.append(opt)
 
684
        all.sort()
 
685
        return all
 
686
 
 
687
    def options(self, prepend_section_name=False):
 
688
        '''Return an alphabetical list of all the options, optionally
 
689
        prefixed with [section_name]'''
 
690
        all = []
 
691
        for sect, opt in self._options.keys():
 
692
            if prepend_section_name:
 
693
                all.append('[' + sect + ']' + opt)
 
694
            else:
 
695
                all.append(opt)
 
696
        all.sort()
 
697
        return all
 
698
 
 
699
    def display(self):
 
700
        '''Display options in a config file form.'''
 
701
        output = StringIO.StringIO()
 
702
        keys = self._options.keys()
 
703
        keys.sort()
 
704
        currentSection = None
 
705
        for sect, opt in keys:
 
706
            if sect != currentSection:
 
707
                if currentSection is not None:
 
708
                    output.write('\n')
 
709
                output.write('[')
 
710
                output.write(sect)
 
711
                output.write("]\n")
 
712
                currentSection = sect
 
713
            self._options[sect, opt].write_config(output)
 
714
        return output.getvalue()
 
715
 
 
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()
 
722
 
 
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()
 
729
 
 
730
        all = self._options.keys()
 
731
        all.sort()
 
732
        for sect, opt in all:
 
733
            if section is not None and sect != section:
 
734
                continue
 
735
            output.write(self._options[sect, opt.lower()].as_nice_string(sect))
 
736
        return output.getvalue()
 
737
 
 
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\.\-\*]+"
 
742
HEADER_VALUE = r".+"
 
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
 
747
PORT = r"[\d]+"
 
748
EMAIL_ADDRESS = r"[\w\-\.]+@[\w\-\.]+"
 
749
PATH = r"[\w \$\.\-~:\\/\*\@\=]+"
 
750
VARIABLE_PATH = PATH + r"%"
 
751
FILE = r"[\S]+"
 
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"[^,]+"
 
762
 
 
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.
 
767
IMAP_ASTRING = ""
 
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"]+\"?"
 
772
 
 
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:
 
777
RESTORE = True
 
778
DO_NOT_RESTORE = False