ConfParser.py
Jump to navigation
Jump to search
#!/usr/bin/python '''Configuration file parser for files similar to MS-Windows .ini files. Meant as a replacement for the broken ConfigParser module in the Python standard library. Copyright (C) 2000 Charles Cazabon <getmail @ discworld.dyndns.org> This program is free software; you can redistribute it and/or modify it under the terms of version 2 of the GNU General Public License as published by the Free Software Foundation. A copy of this license should be included in the file COPYING. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. For documentation, see the Python standard module ConfigParser documentation. This module is similar, except: o Options are supplied with option_name=option_value pairs only (not ':'). o Comments are allowed on the same lines as data, with '#' as the comment delimiter. o Leading and trailing whitespace is ignored. o Whitespace surrounding the '=' sign is ignored. o Option values can be quoted with single or double quotes, to preserve leading or trailing whitespace, or if they contain a whitespace or the "#" symbol which would otherwise mark the start of a comment. o Empty option values must be quoted; use the empty string ("" or '') o Option values are returned as either: o a string, if the option name occurs once in the section o a list of strings, if the option name occurs multiple times o All the limitations on what characters can be in section headers and option values are gone, except that '#' is forbidden (because it starts a comment), and option names cannot contain '=' (because that starts a value). I welcome questions and comments at <software @ discworld.dyndns.org>. ''' __version__ = '3.1' __author__ = 'Charles Cazabon <software @ discworld.dyndns.org>' # # Imports # import string import UserDict import sys import shlex import cStringIO from types import * # # ConfParser exception classes # # Base class for all ConfParser exceptions class ConfParserException (Exception): pass # Specific exceptions class NoSectionError (ConfParserException): '''Exception raised when a specified section is not found. ''' pass class DuplicateSectionError (ConfParserException): '''Exception raised when mutliple sections with the same name are found, or if add_section() is called with the name of a section that is already present. ''' pass class NoOptionError (ConfParserException): '''Exception raised when a specified option is not found in the specified section. ''' pass class InterpolationError (ConfParserException): '''Exception raised when problems occur performing string interpolation. ''' pass class MissingSectionHeaderError (ConfParserException): '''Exception raised when attempting to parse a file which has no section headers. ''' pass class ParsingError (ConfParserException): '''Exception raised when errors occur attempting to parse a file. Also raised if defaults is not a dictionary, or when reading a file fails. These errors are not covered by exceptions in the standard Python ConfigParser module. ''' pass # # Globals # debug = 0 # # Helper functions # ####################################### def log (msg): if not debug: return sys.stderr.write (msg + '\n') sys.stderr.flush () # # ConfParser SmartDict class # ####################################### class SmartDict (UserDict.UserDict): '''Dictionary class which handles lists and singletons intelligently. ''' ####################################### def __init__ (self, initialdata = {}): '''Constructor. ''' UserDict.UserDict.__init__ (self, {}) for (key, value) in initialdata.items (): self.__setitem (key, value) ####################################### def __getitem__ (self, key): ''' ''' try: value = self.data[key] if len (value) == 1: return value[0] return value except KeyError, txt: raise KeyError, txt ####################################### def __setitem__ (self, key, value): ''' ''' if type (value) in (ListType, TupleType): self.data[key] = list (value) else: self.data[key] = [value] # # Main ConfParser class # ####################################### class ConfParser: '''Class to parse a configuration file without all the limitations in ConfigParser.py, but without the dictionary formatting options either. ''' ####################################### def __init__ (self, defaults = {}): '''Constructor. ''' self.__rawdata = [] self.__sectionlist = [] self.__sections = [] self.__defaults = SmartDict () try: for key in defaults.keys (): self.__defaults[key] = defaults[key] except AttributeError: raise ParsingError, 'defaults not a dictionary (%s)' % defaults ####################################### def read (self, filelist): '''Read configuration file(s) from list of 1 or more filenames. ''' if type (filelist) not in (ListType, TupleType): filelist = [filelist] try: for filename in filelist: log ('Reading configuration file "%s"' % filename) f = open (filename, 'r') self.__rawdata = self.__rawdata + f.readlines () f.close () except IOError, txt: raise ParsingError, 'error reading configuration file (%s)' % txt self.__parse () return self ####################################### def __parse (self): '''Parse the read-in configuration file. ''' config = string.join (self.__rawdata, '\n') f = cStringIO.StringIO (config) lex = shlex.shlex (f) lex.wordchars = lex.wordchars + '|/.,$^\\():;@-+?<>!%&*`~' section_name = '' option_name = '' option_value = '' while 1: token = lex.get_token () if token == '': break if not (section_name): if token != '[': raise ParsingError, 'expected section start, got %s' % token section_name = '' while 1: token = lex.get_token () if token == ']': break if token == '': raise ParsingError, 'expected section end, hit EOF' if section_name: section_name = section_name + ' ' section_name = section_name + token if not section_name: raise ParsingError, 'expected section name, got nothing' section = SmartDict () # Collapse case on section names section_name = string.lower (section_name) if section_name in self.__sectionlist: raise DuplicateSectionError, \ 'duplicate section (%s)' % section_name section['__name__'] = section_name continue if token == '=': raise ParsingError, 'expected option name, got =' if token == '[': # Start new section lex.push_token (token) if section_name in self.__sectionlist: raise DuplicateSectionError, \ 'duplicate section (%s)' % section_name if section['__name__'] == 'default': self.__defaults.update (section) self.__sectionlist.append (section_name) self.__sections.append (section.copy ()) section_name = '' continue if not option_name: option_name = token token = lex.get_token () if token != '=': raise ParsingError, 'Expected =, got %s' % token token = lex.get_token () if token in ('[', '='): raise ParsingError, 'expected option value, got %s' % token option_value = token if option_value[0] in ('"', "'") and option_value[0] == option_value[-1]: option_value = option_value[1:-1] if section.has_key (option_name): if type (section[option_name]) == ListType: section[option_name].append (option_value) else: section[option_name] = [section[option_name], option_value] else: section[option_name] = option_value option_name = '' # Done parsing if section_name: if section_name in self.__sectionlist: raise DuplicateSectionError, \ 'duplicate section (%s)' % section_name if section['__name__'] == 'default': self.__defaults.update (section) self.__sectionlist.append (section_name) self.__sections.append (section.copy ()) if not self.__sectionlist: raise MissingSectionHeaderError, 'no section headers in file' ####################################### def defaults (self): '''Return a dictionary containing the passed-in instance-wide defaults. ''' return self.__defaults.copy () ####################################### def has_section (self, section): '''Indicates whether the named section is present in the configuration. The default section is not acknowledged. ''' section = string.lower (section) if section not in self.sections (): return 0 return 1 ####################################### def sections (self): '''Return a list of sections in the configuration file. ''' s = self.__sectionlist[:] try: # Remove 'default' section from returned list i = s.index ('default') del s[i] except ValueError: # No default section pass return s ####################################### def options (self, section): '''Return list of options in section. ''' try: s = self.__sectionlist.index (string.lower (section)) except ValueError: raise NoSectionError, 'missing section: "%s"' % section return self.__sections[s].keys () ####################################### def get (self, section, option, raw=0, _vars={}): '''Get an option value for the provided section. All the "%" interpolations are expanded in the return values, based on the defaults passed into the constructor, as well as the options _vars provided, unless the raw argument is true. __vars contents must be lists. ''' try: s = self.__sectionlist.index (string.lower (section)) options = self.__sections[s] except ValueError: raise NoSectionError, 'missing section (%s)' % section expand = self.__defaults.copy () expand.update (_vars) if not options.has_key (option): if expand.has_key (option): return expand[option] raise NoOptionError, 'section [%s] missing option (%s)' \ % (section, option) rawval = options[option] if raw: return rawval try: value = [] if type (rawval) != ListType: rawval = [rawval] for part in rawval: try: part = part % expand except: raise value.append (part) if len (value) == 1: return value[0] return value except KeyError, txt: raise NoOptionError, 'section [%s] missing option (%s)' \ % (section, option) except TypeError, txt: raise InterpolationError, 'invalid conversion or specification' \ ' for option %s (%s (%s))' % (option, rawval, txt) ####################################### def getint (self, section, option): '''A convenience method which coerces the option in the specified section to an integer. ''' val = self.get (section, option) try: return int (val) except ValueError: raise InterpolationError, 'option %s not an integer (%s)' \ % (option, val) ####################################### def getfloat (self, section, option): '''A convenience method which coerces the option in the specified section to a floating point number. ''' val = self.get (section, option) try: return float (val) except ValueError: raise InterpolationError, 'option %s not a float (%s)' \ % (option, val) ####################################### def getboolean (self, section, option): '''A convenience method which coerces the option in the specified section to a boolean value. Note that the only accepted values for the option are "0" and "1", any others will raise ValueError. ''' val = self.getint (section, option) return val != 0 ####################################### def dump (self): '''Dump the parsed contents of the configuration file. ''' sys.stderr.write ('ConfParser dump:\n\n') sections = self.__sectionlist[:] sections.sort () for section in sections: sys.stderr.write (' Section [%s]:\n' % section) options = self.options (section) options.sort () for option in options: values = self.get (section, option) if type (values) == ListType: sys.stderr.write (' %s:\n' % option) for value in values: sys.stderr.write (' %s\n' % value) else: sys.stderr.write (' %s: %s\n' % (option, values)) sys.stderr.write ('\n')