1#!/usr/bin/env python
2
3"""
4ConfigManager
5
6ConfigManager is a combination command-line option parser and configuration
7file.  It essentially combines ConfigParser, getopt, and a lot of
8additional logic to parse the command-line the way you expect it to be
9parsed.  The ConfigManager class should be backwards compatible with the
10ConfigParser class, but contains much more functionality and a more natural
11dictionary-style interface to sections and options.
12
13See examples at the bottom of this file.  Try typing __init__.py
14followed by a bunch of imaginary command line options and arguments.
15
16"""
17
18import sys, string, re, urllib.request, copy, types, os
19from collections import UserList, UserDict, OrderedDict
20from textwrap import wrap
21
22__all__ = ['ConfigManager','BooleanOption','IntegerOption','CompoundOption',
23           'MultiOption','GenericOption','FloatOption','StringOption',
24           'InputFileOption','OutputFileOption','InputDirectoryOption',
25           'OutputDirectoryOption','CountedOption',
26           'BooleanArgument','IntegerArgument','CompoundArgument',
27           'MultiArgument','GenericArgument','FloatArgument','StringArgument',
28           'InputFileArgument','OutputFileArgument','InputDirectoryArgument',
29           'OutputDirectoryArgument','CountedArgument',
30           'BUILTIN','CODE','REGISTRY','CONFIG','CONFIGFILE','ENVIRON',
31           'ENVIRONMENT','COMMANDLINE','ALL','DEFAULTSECT',
32           'ON','OFF','TRUE','FALSE','YES','NO','CommandLineManager',
33           'GetoptError','ConfigError','NoOptionError']
34
35DEFAULTSECT = "DEFAULT"
36MAX_INTERPOLATION_DEPTH = 10
37ON = TRUE = YES = 1
38OFF = FALSE = NO = 0
39
40TERMINAL_WIDTH = 76   # Maximum width of terminal
41MAX_NAME_WIDTH_RATIO = 0.25  # Max fraction of terminal to use for option
42PREPAD = 2   # Padding before each option name in usage
43GUTTER = 4   # Space between option name and description in usage
44
45# Possible values for `source'.
46BUILTIN = 2
47CODE = 4
48REGISTRY = 8
49CONFIG = CONFIGFILE = 16
50ENVIRON = ENVIRONMENT = 32
51COMMANDLINE = 64
52ALL = 0xffffff
53
54# Exception classes
55class Error(Exception):
56    """ Generic exception """
57    def __init__(self, msg=''):
58        self.msg = msg
59        Exception.__init__(self, msg)
60
61    def __str__(self):
62        return self.msg
63    __repr__ = __str__
64
65
66# Exceptions while parsing command line
67class GetoptError(Error):
68    """ Generic command line exception """
69    def __init__(self, msg, opt):
70        self.msg = msg
71        self.opt = opt
72        Exception.__init__(self, msg, opt)
73
74    def __str__(self):
75        return self.msg
76    __repr__ = __str__
77
78class RequiresArgument(GetoptError):
79    """ Exception for a missing argument """
80
81class MandatoryOption(GetoptError):
82    """ Exception for a missing option """
83
84class UnspecifiedArgument(GetoptError):
85    """ Exception for an argument when none was expected """
86
87class UnrecognizedArgument(GetoptError):
88    """ Exception for an argument that is unrecognized """
89
90class NonUniquePrefix(GetoptError):
91    """ Exception for multiple option prefixes that match a given option """
92
93class UnknownCompoundGroup(GetoptError):
94    """ Exception for an unknown grouping character used for a compound """
95    def __init__(self, msg=''):
96        GetoptError.__init__(self, msg, '')
97
98
99# Exceptions while reading/parsing configuration files
100class ConfigError(Error):
101   """ Generic configuration file exception """
102
103class NoSectionError(ConfigError):
104    """ Exception for missing sections """
105    def __init__(self, section):
106        ConfigError.__init__(self, 'No section: %s' % section)
107        self.section = section
108
109class DuplicateSectionError(ConfigError):
110    """ Exception for duplicate sections """
111    def __init__(self, section):
112        ConfigError.__init__(self, "Section %s already exists" % section)
113        self.section = section
114
115class InvalidOptionError(GetoptError, ConfigError):
116    """ Exception for invalid values for a given option """
117    def __init__(self, option, value, msg='', type=''):
118        if type: type += ' '
119        if not msg:
120            msg="Invalid value for %soption `%s'" % (type, option)
121        ConfigError.__init__(self, msg+': %s' % value)
122        self.option = option
123        self.value = value
124
125class NoOptionError(ConfigError):
126    """ Exception for missing a missing option in a section """
127    def __init__(self, option, section):
128        ConfigError.__init__(self, "No option `%s' in section: %s" %
129                       (option, section))
130        self.option = option
131        self.section = section
132
133class InterpolationError(ConfigError):
134    """ Exception for message interpolation errors """
135    def __init__(self, reference, option, section, rawval):
136        ConfigError.__init__(self,
137                       "Bad value substitution:\n"
138                       "\tsection: [%s]\n"
139                       "\toption : %s\n"
140                       "\tkey    : %s\n"
141                       "\trawval : %s\n"
142                       % (section, option, reference, rawval))
143        self.reference = reference
144        self.option = option
145        self.section = section
146
147class InterpolationDepthError(ConfigError):
148    """ Exception for excessive recursion in interpolation """
149    def __init__(self, option, section, rawval):
150        ConfigError.__init__(self,
151                       "Value interpolation too deeply recursive:\n"
152                       "\tsection: [%s]\n"
153                       "\toption : %s\n"
154                       "\trawval : %s\n"
155                       % (section, option, rawval))
156        self.option = option
157        self.section = section
158
159class ParsingError(ConfigError):
160    """ Exception for errors occurring during parsing of a config file """
161    def __init__(self, filename):
162        ConfigError.__init__(self, 'File contains parsing errors: %s' % filename)
163        self.filename = filename
164        self.errors = []
165
166    def append(self, lineno, line):
167        self.errors.append((lineno, line))
168        self.msg = self.msg + '\n\t[line %2d]: %s' % (lineno, line)
169
170class TooFewValues(GetoptError):
171   """ Got fewer values than expected """
172   def __init__(self, msg):
173      GetoptError.__init__(self, msg, '')
174
175class TooManyValues(GetoptError):
176   """ Got more values than expected """
177   def __init__(self, msg):
178      GetoptError.__init__(self, msg, '')
179
180class MissingSectionHeaderError(ParsingError):
181    """ Exception for options that occur before a section heading """
182    def __init__(self, filename, lineno, line):
183        ConfigError.__init__(
184            self,
185            'File contains no section headers.\nfile: %s, line: %d\n%s' %
186            (filename, lineno, line))
187        self.filename = filename
188        self.lineno = lineno
189        self.line = line
190
191
192class ConfigSection(UserDict, object):
193    """ Section of a configuration object """
194
195    def __init__(self, name, data=None):
196        """
197        Initialize the section
198
199        Required Arguments:
200        name -- name of the section
201        data -- dictionary containing the initial set of options
202
203        """
204        UserDict.__init__(self, data or {})
205        self.name = name
206        self.parent = None
207
208    def copy(self):
209        """ Make a deep copy of self """
210        newcopy = self.__class__(self.name)
211        for key, value in list(vars(self).items()):
212            if key == 'data': continue
213            setattr(newcopy, key, value)
214        for key, value in list(self.data.items()):
215            newcopy.data[key] = value.copy()
216        return newcopy
217
218    def setParent(self, parent):
219        """ Set the parent ConfigManager instance """
220        self.parent = parent
221
222    def defaults(self):
223        """ Return the dictionary of defaults """
224        return self.parent.defaults()
225
226    def __getitem__(self, key):
227        """ Return the value of the option, not the option itself """
228        return self.get(key)
229
230    def set(self, option, value, source=BUILTIN):
231        """
232        Create the appropriate option type
233
234        If the value is already an Option instance, just plug it in.
235        If the value is anything else, try to figure out which option
236        type it corresponds to and create an option of that type.
237
238        Required Arguments:
239        option -- dictionary key where the option should be set
240        value -- option value to store
241        source -- flag to indicate source of option
242
243        Returns: None
244
245        """
246        typemap = {str:StringOption, int:IntegerOption,
247                   float:FloatOption, list:MultiOption, tuple:MultiOption}
248
249        if option in list(self.data.keys()):
250           if self.data[option].source <= source:
251              self.data[option].source = source
252              self.data[option].setValue(value)
253
254        else:
255           if isinstance(value, GenericOption):
256              value.setParent(self)
257              value.name = str(option)
258              self.data[option] = value
259
260           elif type(value) in list(typemap.keys()):
261              for key, opttype in list(typemap.items()):
262                 if isinstance(value, key):
263                    # Handle booleans this way until support for
264                    # true booleans shows up in Python.
265                    if type(value) == str and \
266                       value.lower().strip() in ['on','off','true','false','yes','no']:
267                       opttype = BooleanOption
268                    self.data[option] = opttype(name=option, source=source)
269                    self.data[option].setParent(self)
270                    self.data[option].name = str(option)
271                    self.data[option].setValue(value)
272                    break
273           else:
274              raise TypeError('Could not find valid option type for "%s"' % value)
275
276    def __setitem__(self, key, value):
277        """ Set the item in the dictionary """
278        self.set(key, value, source=BUILTIN)
279
280    def getint(self, option):
281        """ Get the option value and cast it to an integer """
282        return int(self[option])
283
284    def getfloat(self, option):
285        """ Get the option value and cast it to a float """
286        return float(self[option])
287
288    def getboolean(self, option):
289        """ Get the option value and cast it to a boolean """
290        v = self[option]
291        val = int(v)
292        if val not in (0, 1):
293            raise ValueError('Not a boolean: %s' % v)
294        return val
295
296    def get(self, option, raw=0, vars=None):
297        """
298        Get an option value for a given section.
299
300        All % interpolations are expanded in the return values, based on the
301        defaults passed into the constructor, unless the optional argument
302        `raw' is true.  Additional substitutions may be provided using the
303        `vars' argument, which must be a dictionary whose contents overrides
304        any pre-existing defaults.
305
306        Required Arguments:
307        option -- name of the option to retrieve
308
309        Keyword Arguments:
310        raw -- boolean flag that indicates whether string values should
311           be returned as a raw value or as a string with all variable
312           interpolation applied
313        vars -- dictionary of values to use in string interpolation
314
315        Returns:
316        value of the option
317
318        """
319        vars = vars or {}
320        value = self.getraw(option, vars)
321
322        # Raw was specified
323        if raw or not value: return value
324
325        # If we have a list, see if any strings need interpolation.
326        if type(value) in [list, tuple]:
327            strings = [s for s in value
328                         if isinstance(s,str) and s.find('%(')+1]
329            if not strings: return value
330
331        # If we have a string, but no interpolation is needed, bail out.
332        elif not(isinstance(value,str)) or value.find("%(") < 0:
333            return value
334
335        # otherwise needs interpolation...
336        var_dict = self.defaults().data.copy()
337        var_dict.update(self.data)
338        var_dict.update(vars)
339
340        # Handle lists of interpolations as well as single values.
341        if type(value) in [list, tuple]:
342            new_values = []
343            for i in value:
344                new_values.append(self.interpolate(option, var_dict, i))
345            return new_values
346        else:
347            return self.interpolate(option, var_dict, value)
348
349    def interpolate(self, option, vars, rawval):
350        """
351        Do the string interpolation
352
353        Required Arguments:
354        option -- name of the option
355        vars -- dictionary of values to use in interpolation
356        rawval -- raw value of the option
357
358        Returns:
359        string -- string with all variables interpolated
360
361        """
362        value = rawval
363        depth = 0
364        # Loop through this until it's done
365        while depth < MAX_INTERPOLATION_DEPTH:
366            depth = depth + 1
367            if value.find("%(") >= 0:
368                try:
369                    value = value % vars
370                except KeyError as key:
371                    raise InterpolationError(key, option, self.name, rawval)
372            else:
373                break
374        if value.find("%(") >= 0:
375            raise InterpolationDepthError(option, self.name, rawval)
376        return value
377
378    def getraw(self, option, vars=None):
379        """
380        Return raw value of option
381
382        Required Arguments:
383        option -- name of the option to retrieve
384
385        Keyword Arguments:
386        vars -- dictionary containing additional default values
387
388        """
389        vars = vars or {}
390        if option in list(vars.keys()):
391            return vars[option].getValue()
392
393        if option in list(self.keys()):
394            return self.data[option].getValue()
395
396        defaults = self.defaults()
397        if option in list(defaults.keys()):
398            return defaults.data[option].getValue()
399
400        raise NoOptionError(option, self.name)
401
402    def to_string(self, source=COMMANDLINE):
403        """
404        Convert the section back into INI format
405
406        Keyword Arguments:
407        source -- flag which indicates which source of information to print
408
409        Returns:
410        string -- string containing the section in INI format
411
412        """
413        s = ''
414        keys = list(self.keys())
415        keys.sort()
416        for key in keys:
417            if source & self.data[key].source:
418               raw = self.getraw(key)
419               option = self.data[key]
420
421               # Bypass unset options
422               if isinstance(option, MultiOption) and raw == []: continue
423               if not raw: continue
424
425               # Print description or summary of the option as well
426               comment = ''
427               if option.summary: comment = option.summary
428               if option.description: comment = option.description
429               if comment:
430                  comment = comment.strip() % option.names()
431                  comment = comment.split('\n')
432                  s += '\n; %s\n' % '\n; '.join(comment)
433
434               value = str(option).replace('\n', '\n    ')
435               if value.find('\n') + 1: value = '\n    ' + value
436               s += "%s %s %s\n" % (key, ConfigManager.OPTIONSEP, value)
437        return s
438
439    def __str__(self):
440        """ Return section in INI format without builtins """
441        return self.to_string()
442
443    def __repr__(self):
444        """ Return section in INI format with builtins """
445        return self.to_string(ALL)
446
447
448class ConfigManager(UserDict, object):
449
450    # Regular expressions for parsing section headers and options.
451    SECTCRE = re.compile(
452        r'\['                                 # [
453        r'(?P<header>[^]]+)'                  # very permissive!
454        r'\]'                                 # ]
455        )
456
457    OPTCRE = re.compile(
458        r'(?P<option>[]\-[\w_.*,(){}]+)'      # a lot of stuff found by IvL
459        r'[ \t]*(?P<vi>[:=])[ \t]*'           # any number of space/tab,
460                                              # followed by separator
461                                              # (either : or =), followed
462                                              # by any # space/tab
463        r'(?P<value>.*)$'                     # everything up to eol
464        )
465
466    # Option separator used in printing out INI format
467    OPTIONSEP = '='
468
469    # Set prefixes for options.  If these are the same, all options
470    # are treated as long options.  You can set either one to None
471    # to turn that type of option off as well.
472    short_prefix = '-'
473    long_prefix = '--'
474
475    def __init__(self, defaults=None):
476        """
477        Initialize ConfigManager
478
479        Keyword Arguments:
480        defaults -- dictionary of default values.  These values will
481           make up the section by the name DEFAULTSECT.
482
483        """
484        defaults = defaults or {}
485        UserDict.__init__(self)
486        self[DEFAULTSECT] = ConfigSection(DEFAULTSECT, defaults)
487        self.strict = 1     # Raise exception for unknown options
488        self._categories = {}  # Dictionary of option categories
489        self.unrecognized = []
490
491    def copy(self):
492        """ Make a deep copy of self """
493        newcopy = self.__class__()
494        for key, value in list(vars(self).items()):
495            if key == 'data': continue
496            setattr(newcopy, key, value)
497        for key, value in list(self.data.items()):
498            newcopy.data[key] = value.copy()
499        return newcopy
500
501    def set_prefixes(cls, arg1, arg2=None):
502        """
503        Set the command-line option prefixes
504
505        Arguments:
506        short - prefix to use for short command-line options.  If this
507           is set to 'None', then all options are treated as long options.
508        long - prefix to use for long options
509
510        """
511        if arg1 == arg2 == None:
512           raise ValueError('Short and long prefixes cannot both be None.')
513        if arg2 is None:
514           cls.long_prefix = arg1
515           cls.short_prefix = None
516        else:
517           cls.long_prefix = arg2
518           cls.short_prefix = arg1
519
520    set_prefixes = classmethod(set_prefixes)
521
522    def add_help_on_option(self, category=None):
523        """
524        Add a --help-on=LIST option for displaying specific option help
525
526        The --help-on= option can be added to give the command line
527        interactive help.  For example, if you had an option called
528        '--debug', you could type '--help-on debug' to get the full
529        description of the --debug option printed out.
530
531        Keyword Arguments:
532        category -- category to put the --help-on option in
533
534        """
535        self[DEFAULTSECT]['__help_on__'] = MultiOption(
536           """ Display help on listed option names """,
537           options = '%shelp-on' % self.long_prefix[0],
538           category = category,
539           callback = self.usage_on,
540        )
541
542    def remove_help_on_option(self):
543        """ Remove the --help-on option """
544        try: del self[DEFAULTSECT]['__help_on__']
545        except: pass
546
547    def add_category(self, key, title):
548        """
549        Add a category to the ConfigManager
550
551        Options can be grouped by categories for display in the usage
552        message.  Categories have both a key and a title.  The key is
553        what is used in the 'category' parameter when instantiating an
554        option.  The title is the heading to print in the usage message.
555
556        Required Arguments:
557        key -- name of the category used in instantiating an option
558        title -- title of the category to print in the usage message
559
560        Returns:
561        string -- the same key given as the first argument
562
563        """
564#       if not self._categories:
565#          self._categories['__categories__'] = 'Help Categories'
566#       if not self.has_key('__categories__'):
567#          self.add_section('__categories__')
568#       self['__categories__'][key] = BooleanOption(title,
569#            options='%shelp-%s' % (self.long_prefix, key),
570#            category='__categories__')
571        self._categories[key] = title
572        return key
573
574    def get_category(self, key):
575        """ Return the title of the given category """
576        if type(key) not in [list, tuple]:
577           key = [key]
578        if not key:
579           return ''
580        return self._categories[key[0]]
581
582    def categories(self):
583        """ Return the dictionary of categories """
584        return self._categories
585
586    def set_strict(self, strict=True):
587        """
588        Parse the command line strictly
589
590        If you do not want to be bothered with errors due to unrecognized
591        arguments, this method can be called with a boolean 'false'.
592        This is very useful if your program is actually a wrapper around
593        another program and you do not want to declare all of its options
594        in your ConfigManager.  The ConfigManager will simply make its best
595        guess as to whether the option accepts a value and what type the
596        option is.
597
598        Keyword Arguments:
599        strict -- flag indicating whether parsing should be strict or not
600
601        """
602        self.strict = bool(strict)
603
604    def defaults(self):
605        """ Return a dictionary of defaults """
606        return self[DEFAULTSECT]
607
608    def sections(self):
609        """ Return a list of section names """
610        return list(self.keys())
611
612    def add_section(self, section):
613        """
614        Create a new section in the configuration.
615
616        Do nothing if a section by the specified name already exists.
617
618        Required Arguments:
619        section -- name of the section to create
620
621        Returns:
622        instance -- a ConfigSection instance with the given name is returned
623
624        """
625        if section in list(self.keys()): return self[section]
626        self[section] = ConfigSection(section)
627        return self[section]
628
629    def has_section(self, section):
630        """ Indicate whether the named section is present """
631        return section in list(self.keys())
632
633    def options(self, section):
634        """ Return a list of option names for the given section name """
635        if section in list(self.keys()):
636            return list(self[section].keys())
637        else:
638            raise NoSectionError(section)
639
640    def read(self, filenames):
641        """
642        Read and parse a filename or a list of filenames
643
644        Files that cannot be opened are silently ignored; this is
645        designed so that you can specify a list of potential
646        configuration file locations (e.g. current directory, user's
647        home directory, systemwide directory), and all existing
648        configuration files in the list will be read.  A single
649        filename may also be given.
650
651        """
652        if isinstance(filenames, str):
653            filenames = [filenames]
654        for filename in filenames:
655            try:
656                if filename.startswith('~'):
657                    filename = os.path.expanduser(filename)
658
659                fp = open(filename)
660            except (OSError, IOError):
661                continue
662            self.__read(fp, filename)
663            fp.close()
664        return self
665
666    def readfp(self, fp, filename=None):
667        """
668        Like read() but the argument must be a file-like object.
669
670        The 'fp' argument must have a 'readline' method.  Optional
671        second argument is the 'filename', which if not given, is
672        taken from fp.name.  If fp has no 'name' attribute, '<???>' is
673        used.
674
675        Required Arguments:
676        fp -- file-type like object
677        filename -- name of the file in 'fp'
678
679        Returns:
680        string -- contents of the file pointer
681
682        """
683        if filename is None:
684            try:
685                filename = fp.name
686            except AttributeError:
687                filename = '<???>'
688        self.__read(fp, filename)
689        return self
690
691    def get(self, section, option, raw=0, vars=None):
692        """ Get an option value for a given section """
693        return self[section].get(option, raw, vars or {})
694
695    def set(self, section, option, value, source=BUILTIN):
696        """ Set an option value """
697        if not section or section == DEFAULTSECT:
698            sectdict = self[DEFAULTSECT]
699        else:
700            try:
701                sectdict = self[section]
702            except KeyError:
703                raise NoSectionError(section)
704        sectdict.set(option, value, source)
705
706    def __setitem__(self, key, value):
707        """ Add a section to the configuration """
708        if isinstance(value, ConfigSection):
709           self.data[key] = value
710           self.data[key].setParent(self)
711        else:
712           self.data[key] = ConfigSection(str(key))
713           self.data[key].setParent(self)
714
715    def __getitem__(self, key):
716        """
717        Return section with given name
718
719        Return the section or if it's not a section try to
720        return an option by that name in the 'default' section.
721
722        """
723        if key in list(self.data.keys()):
724           return self.data[key]
725        if key in list(self.data[DEFAULTSECT].keys()):
726           return self.data[DEFAULTSECT][key]
727        raise NoSectionError(key)
728
729    def getint(self, section, option):
730        """ Get an option value and cast it to an integer """
731        return self[section].getint(option)
732
733    def getfloat(self, section, option):
734        """ Get an option value and cast it to a float """
735        return self[section].getfloat(option)
736
737    def getboolean(self, section, option):
738        """ Get an option value and cast it to a boolean """
739        return self[section].get(option)
740
741    def getraw(self, section, option):
742        """ Get the raw (un-interpolated) value of an option """
743        return self[section].getraw(option)
744
745    def has_option(self, section, option):
746        """ Check for the existence of a given option in a given section """
747        if not section:
748            section=DEFAULTSECT
749        elif section not in list(self.keys()):
750            return 0
751        else:
752            return option in list(self[section].keys())
753
754    def write(self, fp):
755        """ Write an INI-format representation of the configuration state """
756        fp.write(str(self))
757
758    def __str__(self):
759        """ Return an INI formated string with builtins removed """
760        return self.to_string()
761
762    def __repr__(self):
763        """ Return an INI formated string with builtins included """
764        return self.to_string(source=COMMANDLINE|CONFIGFILE|CODE|BUILTIN|REGISTRY|ENVIRONMENT)
765
766    def to_string(self, source=COMMANDLINE|CONFIGFILE):
767        """
768        Build an INI formatted string based on the ConfigManager contents
769
770        Keyword Arguments:
771        source -- flag indicating which sources if information to print
772
773        Returns:
774        string -- INI formatted string
775
776        """
777        if source & BUILTIN: func = repr
778        else: func = str
779        s = ''
780        keys = [x for x in list(self.keys()) if x != DEFAULTSECT]
781        keys.sort()
782        if self[DEFAULTSECT]:
783            keys.insert(0, DEFAULTSECT)
784        for section in keys:
785            content = func(self[section]).strip()
786            if content:
787               s += "[%s]\n%s\n\n" % (section, content)
788        return s
789
790    def remove_option(self, section, option):
791        """ Remove an option from a section """
792        if not section or section == DEFAULTSECT:
793            sectdict = self[DEFAULTSECT]
794        else:
795            try:
796                sectdict = self[section]
797            except KeyError:
798                raise NoSectionError(section)
799        try:
800            del sectdict[option]
801            return 1
802        except KeyError:
803            return 0
804
805    def remove_section(self, section):
806        """ Remove the given section """
807        if section in list(self.keys()):
808            del self[section]
809            return 1
810        else:
811            return 0
812
813    def __read(self, fp, fpname):
814        """
815        Parse an INI formatted file
816
817        The sections in the file contain a title line at the top,
818        indicated by a name in square brackets (`[]'), plus key/value
819        options lines, indicated by `name: value' format lines.
820        Continuation are represented by an embedded newline then
821        leading whitespace.  Blank lines, lines beginning with a '#',
822        and just about everything else is ignored.
823
824        """
825        cursect = None                            # None, or a dictionary
826        optname = None
827        lineno = 0
828        e = None                                  # None, or an exception
829        while 1:
830            line = fp.readline()
831            if not line:
832                break
833            lineno = lineno + 1
834            # comment or blank line?
835            if line.strip() == '' or line[0] in '#;':
836                continue
837            if line.split()[0].lower() == 'rem' \
838               and line[0] in "rR":      # no leading whitespace
839                continue
840            # continuation line?
841            if line[0] in ' \t' and cursect is not None and optname:
842                value = line.strip()
843                if value and cursect.data[optname].source == CONFIGFILE:
844                    cursect.data[optname] += "%s" % value
845            # a section header or option header?
846            else:
847                # is it a section header?
848                mo = self.SECTCRE.match(line)
849                if mo:
850                    sectname = mo.group('header')
851                    if sectname in list(self.keys()):
852                        cursect = self[sectname]
853                    else:
854                        cursect = ConfigSection(sectname)
855                        self[sectname] = cursect
856                    # So sections can't start with a continuation line
857                    optname = None
858                # no section header in the file?
859                elif cursect is None:
860                    raise MissingSectionHeaderError(fpname, lineno, repr(line))
861                # an option line?
862                else:
863                    mo = self.OPTCRE.match(line)
864                    if mo:
865                        optname, vi, optval = mo.group('option', 'vi', 'value')
866                        if vi in ('=', ':') and ';' in optval:
867                            # ';' is a comment delimiter only if it follows
868                            # a spacing character
869                            pos = optval.find(';')
870                            if pos and optval[pos-1] in string.whitespace:
871                                optval = optval[:pos]
872                        optval = optval.strip()
873                        # allow empty values
874                        if optval == '""':
875                            optval = ''
876                        try:
877                            cursect.set(optname, optval, source=CONFIGFILE)
878                            cursect.data[optname].file = fpname
879                        except:
880                            print("Problem occurred in section '%s' while reading file %s." % (cursect.name, fpname))
881                            raise
882                    else:
883                        # a non-fatal parsing error occurred.  set up the
884                        # exception but keep going. the exception will be
885                        # raised at the end of the file and will contain a
886                        # list of all bogus lines
887                        if not e:
888                            e = ParsingError(fpname)
889                        e.append(lineno, repr(line))
890        # if any parsing errors occurred, raise an exception
891        if e:
892            raise e
893
894    def get_default_option(self, option):
895        """ Get the given option from the default section """
896        try:
897            return self[DEFAULTSECT][option]
898        except KeyError:
899            raise NoOptionError(option, DEFAULTSECT)
900
901    def get_opt(self, section, option):
902        """ Return the option with leading and trailing quotes removed """
903        optionstring = self[section][option].strip()
904        if (optionstring[0] == '\'' and optionstring[-1] == '\'') or \
905           (optionstring[0] == '\"' and optionstring[-1] == '\"'):
906            optionstring = optionstring[1:-1]
907        return optionstring
908
909    def get_optlist(self, section, option, delim=','):
910        """
911        Return the option split into a list
912
913        Required Arguments:
914        section -- name of the section
915        option -- name of the option
916
917        Keyword Arguments:
918        delim -- delimiter to use when splitting the option
919
920        Returns:
921        list -- option value split on 'delim' with whitespace trimmed
922
923        """
924        optionstring = self.get_opt( section, option )
925        return [x.strip() for x in optionstring.split(delim)]
926
927    def __add__(self, other):
928        """
929        Merge items from another ConfigManager
930
931        Sections in 'other' will overwrite sections in 'self'.
932
933        """
934        other = other.copy()
935        for key, value in list(other.items()):
936            self[key] = value
937        try:
938           for key, value in list(other._categories.items()):
939               self._categories[key] = value
940        except AttributeError: pass
941        return self
942
943    def __iadd__(self, other):
944        """ Merge items from another ConfigManager """
945        return self.__add__(other)
946
947    def __radd__(self, other):
948        """
949        Merge items from another ConfigManager
950
951        Sections already in 'self' will not be overwritten.
952
953        """
954        other = other.copy()
955        for key, value in list(other.items()):
956            if key not in list(self.keys()):
957                self[key] = value
958        try:
959           for key, values in list(other._categories.items()):
960               if key not in list(self._categories.keys()):
961                   self._categories[key] = value
962        except AttributeError: pass
963        return self
964
965    def get_options_from_config(self):
966        """
967        Locate all short and long options
968
969        Returns:
970        tuple -- two element tuple contain a list of short option
971           instances and a list of long option instances
972
973        """
974        short_prefixes, long_prefixes = type(self).get_prefixes()
975
976        longopts = []
977        shortopts = []
978        for section in list(self.values()):
979           for option in list(section.data.values()):
980              for opt in option.getPossibleOptions():
981                 opt = opt.replace('!','')
982
983                 # See if the option is a long option
984                 for prefix in long_prefixes:
985                    if prefix is None:
986                       pass
987                    elif not prefix.strip():
988                       pass
989                    elif opt.startswith(prefix):
990                       if option not in longopts:
991                          longopts.append(option)
992                       continue
993
994                 # See if the option is a short option
995                 for prefix in short_prefixes:
996                    if prefix is None:
997                       pass
998                    elif not prefix.strip():
999                       pass
1000                    elif opt.startswith(prefix):
1001                       if option not in shortopts:
1002                          shortopts.append(option)
1003
1004        return shortopts, longopts
1005
1006    def getopt(self, args=None, merge=1):
1007        """
1008        Parse the command line
1009
1010        Keyword Arguments:
1011        args -- list of strings containing the command line.  If this is
1012           not given, sys.argv[1:] is used.
1013        merge -- boolean flag indicating whether parsed options should
1014           be merged into the configuration or not
1015
1016        Returns:
1017        tuple -- two element tuple where the first element is a list of
1018           parsed options in the form '(option, value)' and the second
1019           element is a list of unparsed arguments
1020
1021        """
1022        if args is None: args = sys.argv[1:]
1023
1024        shortopts, longopts = self.get_options_from_config()
1025
1026        short_prefixes, long_prefixes = type(self).get_prefixes()
1027
1028        opts = []
1029        while args and args[0] not in short_prefixes:
1030            # If the argument is equal to one of the long prefixes,
1031            # throw it away and bail out.
1032            if args[0] in long_prefixes:
1033                args = args[1:]
1034                break
1035
1036            # Parse long options
1037            if [x for x in long_prefixes if args[0].startswith(x)]:
1038               try:
1039                  opts, args = self.do_longs(opts,args[0],longopts,args[1:])
1040               except UnrecognizedArgument as e:
1041                  if self.strict: raise
1042                  opts, args = self.handle_unrecognized(e[1],opts,args,'long')
1043                  if merge: self.unrecognized.append(opts[-1])
1044
1045            # Parse short options
1046            elif [x for x in short_prefixes if args[0].startswith(x)]:
1047               try:
1048                  opts, args = self.do_shorts(opts,args[0],shortopts,args[1:])
1049               except UnrecognizedArgument as e:
1050                  if self.strict: raise
1051                  opts, args = self.handle_unrecognized(e[1],opts,args,'short')
1052                  if merge: self.unrecognized.append(opts[-1])
1053
1054            # No option found.  We're done.
1055            else:
1056                break
1057
1058        # Merge command line options with configuration
1059        if merge:
1060           self.merge_options(opts)
1061           self.check_mandatory_options()
1062
1063        return opts, args
1064
1065    def check_mandatory_options(self):
1066        """
1067        Make sure that all mandatory options have been set
1068
1069        """
1070        for section in self.values():
1071           for option in section.data.values():
1072              if not option.mandatory: continue
1073              if option.getValue() in [None,[]]:
1074                 names = ', '.join(option.getPossibleOptions())
1075                 if not names:
1076                    names = option.name
1077                 raise MandatoryOption('The %s option is mandatory, but was not given' % names, names)
1078
1079    def handle_unrecognized(self, option, opts, args, type='long'):
1080        """
1081        Try to parse unrecognized options and their arguments
1082
1083        Required Arguments:
1084        option -- the actual option parsed from the command-line
1085           which was not recognized
1086        opts -- tuples of already parsed options and their values
1087        args -- remaining unparsed command-line arguments
1088
1089        Keyword Arguments:
1090        type -- flag indicating which type of argument to parse.
1091           This should be either "short" or "long".
1092
1093        """
1094        if type == 'long':
1095
1096           args.pop(0)
1097
1098           # Explicitly specified value
1099           if option.find('=') + 1:
1100              option, value = option.split('=',1)
1101              opts.append((option, value))
1102              return opts, args
1103
1104           # Implicitly specified value
1105           if self.has_following_argument(args):
1106              opts.append((option, args.pop(0)))
1107              return opts, args
1108
1109           # No argument found
1110           opts.append((option, None))
1111           return opts, args
1112
1113        elif type == 'short':
1114
1115           short_prefixes, long_prefixes = self.get_prefixes()
1116           prefix = [x for x in short_prefixes if option.startswith(x)][0]
1117
1118           start, end = args[0].split(option.replace(prefix,'',1),1)
1119           if end: args[0] = prefix + end
1120           else: args.pop(0)
1121
1122           opts.append((option, ''))
1123           return opts, args
1124
1125        raise ValueError('Expecting type of "short" or "long".')
1126
1127    def merge_options(self, options):
1128        """ Merge options parsed from the command line into configuration """
1129        # Multi options that have been cleared by a command line option.
1130        # Lists will only be cleared on the first command line option, all
1131        # consecutive options will append.
1132        for option, value in options:
1133
1134#           opts = self.getPossibleOptions()
1135#           negative = [x.replace('!','',1) for x in opts if x.startswith('!')]
1136#           positive = [x for x in opts if not x.startswith('!')]
1137
1138            if isinstance(option, GenericOption):
1139                option.source = COMMANDLINE
1140                option.file = None
1141                if type(value) in [list, tuple]:
1142                    if not isinstance(option, MultiOption):
1143                        value = ' '.join(value)
1144                        option.occurrences += 1
1145                        option.setValue(value)
1146                    else:
1147                        if option.occurrences:
1148                            option.occurrences += 1
1149                            option += value
1150                        else:
1151                            option.occurrences += 1
1152                            option.setValue(value)
1153                else:
1154                    option.occurrences += 1
1155                    option.setValue(value)
1156
1157    def get_prefixes(cls):
1158        """ Prepare option prefixes to make sure that they are always lists """
1159        long_prefixes = cls.long_prefix
1160        short_prefixes = cls.short_prefix
1161        if type(long_prefixes) not in [list, tuple]:
1162           long_prefixes = [long_prefixes]
1163        if type(short_prefixes) not in [list, tuple]:
1164           short_prefixes = [short_prefixes]
1165        return [x for x in short_prefixes if x],[x for x in long_prefixes if x]
1166
1167    get_prefixes = classmethod(get_prefixes)
1168
1169    def do_longs(self, opts, opt, longopts, args):
1170        """
1171        Parse a long option
1172
1173        Required Arguments:
1174        opts -- list of parsed options
1175        opt -- string containing current option
1176        longopts -- list of long option instances
1177        args -- remaining argument list
1178
1179        Returns:
1180        tuple -- two element tuple where the first argument is the
1181           'opts' list with the latest argument added and the second
1182           element is the 'args' list with the arguments from the
1183           current option removed
1184
1185        """
1186        forcedarg = False
1187        if opt.find('=') + 1:
1188            forcedarg = True
1189            opt, arg = opt.split('=', 1)
1190            args.insert(0, arg)
1191
1192        option = self.get_match(opt, longopts)
1193        option.actual = opt
1194        option.file = None
1195
1196        # If we have an argument, but the option doesn't accept one, bail out.
1197        if forcedarg and not(option.acceptsArgument()):
1198            raise UnspecifiedArgument('option %s must not have an argument' \
1199                                       % opt, opt)
1200
1201        elif forcedarg:
1202           optarg, args = option.getArgument(args, forcedarg=True)
1203
1204        elif not(option.acceptsArgument()):
1205           pass
1206
1207        # Parse the following arguments
1208        else:
1209
1210           # See if we have a possible following argument
1211           if not type(self).has_following_argument(args):
1212
1213              # No argument found, but we require one.
1214              if option.requiresArgument():
1215                 raise RequiresArgument('option %s requires argument' \
1216                                         % opt, opt)
1217
1218           # Parse the argument
1219           optarg, args = option.getArgument(args)
1220
1221        # Convert boolean options to proper value
1222        if not forcedarg and isinstance(option, BooleanOption):
1223           options = option.getPossibleOptions()
1224           negative = [x.replace('!','',1) for x in options
1225                                           if x.startswith('!')]
1226#          positive = [x for x in options if not x.startswith('!')]
1227           if opt in negative:
1228              optarg = 0
1229           else:
1230              optarg = 1
1231
1232        opts.append((option, optarg))
1233        return opts, args
1234
1235    def has_following_argument(cls, args):
1236        """
1237        Return boolean indicating the existence of a following argument
1238
1239        Required Arguments:
1240        args -- command-line arguments to inspect for following argument.
1241
1242        Returns:
1243        boolean -- true, if following argument exists
1244
1245        """
1246        short_prefixes, long_prefixes = cls.get_prefixes()
1247
1248        # No arguments at all
1249        if not(args):
1250           return 0
1251
1252        # The next argument has an option prefix and it doesn't consist
1253        # entirely of a prefix.
1254        if [x for x in long_prefixes+short_prefixes if args[0].startswith(x)] \
1255           and args[0] not in long_prefixes+short_prefixes:
1256           return 0
1257
1258        # All other cases fail.  This must be an argument.
1259        return 1
1260
1261    has_following_argument = classmethod(has_following_argument)
1262
1263    def get_match(self, opt, longopts):
1264        """
1265        Get possible matches for the given option
1266
1267        Required Arguments:
1268        opt -- name of the current option
1269        longopts -- list of all long option instances
1270
1271        Returns:
1272        instance -- an instance of the option which matches
1273
1274        """
1275        possibilities = []
1276        for o in longopts:
1277           match = o.matches(opt)
1278           if match:
1279              possibilities.append((match, o))
1280
1281        if not possibilities:
1282            raise UnrecognizedArgument('option %s not recognized' % opt, opt)
1283
1284        # Is there an exact match?
1285        option = [x for x in possibilities if opt == x[0]]
1286        if option:
1287            return option[0][1]
1288
1289        # No exact match, so better be unique.
1290        if len(possibilities) > 1:
1291            # XXX since possibilities contains all valid continuations,
1292            # might be nice to work them into the error msg
1293            raise NonUniquePrefix('option %s not a unique prefix' % opt, opt)
1294
1295        assert len(possibilities) == 1
1296
1297        return possibilities[0][1]
1298
1299    def do_shorts(self, opts, optstring, shortopts, args):
1300        """
1301        Parse a short argument
1302
1303        Required Arguments:
1304        opts -- list of parsed options
1305        optstring -- string containing current option
1306        shortopts -- list of short option instances
1307        args -- remaining argument list
1308
1309        Returns:
1310        tuple -- two element tuple where the first argument is the
1311           'opts' list with the latest argument added and the second
1312           element is the 'args' list with the arguments from the
1313           current option removed
1314
1315        """
1316        short_prefixes, long_prefixes = type(self).get_prefixes()
1317
1318        optstring.strip()
1319
1320        if not optstring:
1321           return [], args
1322
1323        prefix = optstring[0]
1324        optstring = optstring[1:]
1325
1326        while optstring != '':
1327            opt, optstring = optstring[0], optstring[1:]
1328
1329            option = self.get_match(prefix+opt, shortopts)
1330            option.actual = prefix+opt
1331            option.file = None
1332
1333            # See if we need to check for an argument
1334            if option.acceptsArgument():
1335
1336                if optstring == '':
1337
1338                   # Are there any following arguments?
1339                   if not type(self).has_following_argument(args):
1340
1341                      # No argument found, but we require one.
1342                      if option.requiresArgument():
1343                         raise RequiresArgument('option %s requires argument' \
1344                                                 % opt, opt)
1345
1346                   optarg, args = option.getArgument(args)
1347
1348                else:
1349                   optarg, args = option.getArgument([optstring]+args)
1350
1351                # No argument was found
1352                if args and args[0] == optstring:
1353                   optarg = None
1354                   optstring = args.pop(0)
1355                else:
1356                   optstring = ''
1357
1358            else:
1359                optarg = None
1360
1361            # Convert boolean options to proper value
1362            if optarg is None and isinstance(option, BooleanOption):
1363               options = option.getPossibleOptions()
1364               negative = [x.replace('!','',1) for x in options
1365                                               if x.startswith('!')]
1366#              positive = [x for x in options if not x.startswith('!')]
1367               if prefix+opt in negative:
1368                  optarg = 0
1369               else:
1370                  optarg = 1
1371
1372            opts.append((option, optarg))
1373
1374        return opts, args
1375
1376    def usage_on(self, options):
1377        """
1378        Print full help for listed options and exit
1379
1380        Required Arguments:
1381        options -- list of strings indicating which options to list
1382           help on (preceding '-' and '--' should be removed)
1383
1384        """
1385        display = []
1386        for sectkey, section in list(self.items()):
1387           for optkey, option in list(section.items()):
1388              if option.long in options or option.short in options:
1389                 display.append((sectkey, optkey, option))
1390
1391        display.reverse()
1392
1393        err = sys.stderr.write
1394
1395        while display:
1396           sectkey, optkey, opt = display.pop()
1397           err('Command Line: %s\n' % self._option_usage(opt))
1398           err('Configuration File: [%s] %s=\n' % (sectkey, optkey))
1399
1400           current = opt.names()['current']
1401           if not current:
1402               err('Current Value: %s\n\n' % current)
1403           else:
1404               err('\n')
1405
1406           err('%s\n\n' % opt.description)
1407           if display: err('\n')
1408
1409        sys.exit(1)
1410
1411    def _option_usage(self, option):
1412        """ Return the option the way it can be typed at the command line """
1413        if option.options.strip():
1414           short_prefixes, long_prefixes = self.get_prefixes()
1415           prefixes = long_prefixes + short_prefixes
1416           options = re.sub(r'\s+', r' ', option.options.replace('!',''))
1417           options = options.split()
1418           options = [(len(x),x) for x in options
1419                       if [x for p in prefixes if x.startswith(p)]]
1420           options.sort()
1421           options = [x[1] for x in options]
1422
1423           # Determine argument separator
1424           sep = ' '
1425           loptions = [x for x in options
1426                         if [x for p in long_prefixes if x.startswith(p)]]
1427           if loptions:
1428              sep = '='
1429
1430           options = ', '.join(options)
1431           if option.acceptsArgument() and option.requiresArgument():
1432              return '%s%s%s' % (options, sep, option.synopsis)
1433           elif option.acceptsArgument():
1434              return '%s[%s%s]' % (options, sep, option.synopsis)
1435           else:
1436              return options
1437        return ''
1438
1439    def usage(self, categories=None):
1440        """ Print descriptions of all command line options """
1441        categories = categories or []
1442        options = []
1443        for section in list(self.values()):
1444            for option in list(section.data.values()):
1445                if option.options:
1446                    options.append(option)
1447        options.sort()
1448
1449        if not options: return ''
1450
1451        name_len = 0
1452        summary_len = 0
1453
1454        # Put options into categories
1455        categorized = {}
1456        for option in options:
1457
1458            catname = self.get_category(option.category).strip()
1459            name = self._option_usage(option)
1460
1461            summary = ''
1462            if option.summary:
1463               summary = option.summary % option.names()
1464               default = option.defaultValue()
1465               if default is not None:
1466                   summary += ' [%s]' % default
1467
1468            if catname not in list(categorized.keys()):
1469               categorized[catname] = []
1470            categorized[catname].append((name,summary,option))
1471
1472            if summary:
1473               name_len = max(name_len,len(name))
1474               summary_len = max(summary_len,len(summary))
1475
1476        name_len = min(name_len, int(TERMINAL_WIDTH*MAX_NAME_WIDTH_RATIO))
1477        summary_len = TERMINAL_WIDTH - PREPAD - GUTTER - name_len
1478
1479        # Build the output string
1480        s = ''
1481        if categories:
1482           categories = [self.get_category(x) for x in list(categories.keys())]
1483        else:
1484           categories = list(categorized.keys())
1485           categories.sort()
1486        for category in categories:
1487           options = categorized[category]
1488           if not category:
1489              if len(categories) > 1:
1490                 category = 'General Options'
1491              else:
1492                 category = 'Options'
1493           s += '\n%s:\n' % category
1494           for name, summary, option in options:
1495              length = len(name)
1496              summary = wrap(summary, summary_len)
1497              summary = ('\n%s' % (' '*(PREPAD+name_len+GUTTER))).join(summary)
1498              # Be lenient on the gutter if we are really close to
1499              # fitting in the allocated space
1500              format = '%s%s%s%s\n'
1501              colspace = max(GUTTER + name_len - length, GUTTER)
1502              if summary and ((name_len + GUTTER) > length):
1503                 colspace = (name_len + GUTTER) - length
1504              elif summary and length > name_len:
1505                 colspace = PREPAD + name_len + GUTTER
1506                 format = '%s%s\n%s%s\n'
1507              s += format % (' '*PREPAD, name, ' '*colspace, summary)
1508        return s
1509
1510
1511class CommandLineManager(OrderedDict):
1512   """ Command-Line Argument Manager """
1513
1514   def __init__(self, data=None):
1515      OrderedDict.__init__(self, data or {})
1516      self._associations = {}
1517
1518   def usage(self):
1519      s = ''
1520      for item in list(self.values()):
1521         if isinstance(item, ConfigManager):
1522            s += item.usage()
1523         else:
1524            break
1525      return s
1526
1527   def requiresArgument(self):
1528      """ Return boolean indicating if config requires an argument """
1529      if not self: return 0
1530      for key in list(self.keys()):
1531         item = OrderedDict.__getitem__(self, key)
1532         if isinstance(item, GenericArgument):
1533             if item.mandatory:
1534                 return 1
1535
1536   def getopt(self, args=None):
1537      if not args:
1538         args = sys.argv[1:]
1539      else:
1540         args = args[:]
1541
1542      if not self: return self
1543
1544      for key in list(self.keys()):
1545         item = OrderedDict.__getitem__(self, key)
1546         association = self._associations.get(key, None)
1547         if isinstance(item, ConfigManager):
1548            if association:
1549               item.read(association)
1550            options, args = item.getopt(args)
1551         elif isinstance(item, GenericArgument):
1552            value, args = item.getArgument(args)
1553            item.setValue(value)
1554         else:
1555            raise ValueError("Unrecognized argument type.")
1556
1557      if args:
1558         raise TooManyValues('Too many command-line arguments: %s' % ' '.join(args))
1559
1560      return self
1561
1562   def __setitem__(self, key, value):
1563      item = value
1564      if type(value) in [tuple, list]:
1565         value = list(value)
1566         item = value.pop(0)
1567         self._associations[key] = value
1568      assert isinstance(item, (ConfigManager, GenericArgument)), \
1569             'Command-line parameters must be ConfigManagers or ' + \
1570             'subclasses of GenericArgument'
1571      if hasattr(item, 'name') and item.name is None:
1572         item.name = key
1573      OrderedDict.__setitem__(self, key, item)
1574
1575   def __getitem__(self, key):
1576      if type(key) == slice:
1577         return self.__getslice__(key.start, key.stop)
1578      item = OrderedDict.__getitem__(self, key)
1579      if isinstance(item, ConfigManager):
1580         return item
1581      else:
1582         return item.getValue()
1583
1584
1585
1586# These must be loaded last because they depend on variables
1587# assigned in this file.
1588from plasTeX.ConfigManager.Generic import GenericOption, GenericArgument
1589from plasTeX.ConfigManager.String import StringOption, StringArgument
1590from plasTeX.ConfigManager.Integer import IntegerOption, IntegerArgument
1591from plasTeX.ConfigManager.Float import FloatOption, FloatArgument
1592from plasTeX.ConfigManager.Multi import MultiOption, MultiArgument
1593from plasTeX.ConfigManager.Compound import CompoundOption, CompoundArgument
1594from plasTeX.ConfigManager.Boolean import BooleanOption, BooleanArgument
1595from plasTeX.ConfigManager.Files import OutputFileOption, InputFileOption
1596from plasTeX.ConfigManager.Directories import OutputDirectoryOption, InputDirectoryOption
1597from plasTeX.ConfigManager.Files import OutputFileArgument, InputFileArgument
1598from plasTeX.ConfigManager.Directories import OutputDirectoryArgument, InputDirectoryArgument
1599from plasTeX.ConfigManager.Counted import CountedOption, CountedArgument
1600