"""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-2003 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.

"""

__version__ = '3.3'
__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 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

class ConversionError (ConfParserException):
    """Exception raised when a value conversion (i.e. to integer) fails.
    """
    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.
    By Charles Cazabon <software @ discworld.dyndns.org>
    Taken from the getmail package to be used in childsplay.
    Changes for childsplay:
        o In the function 'sections' removed the default section delete.
            The default section gets passed just like the other sections.
    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).
    """
    #######################################
    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)) #Original
            #s = self.__sectionlist.index (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))# original
            #s = self.__sectionlist.index (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, ValueError), txt:
            raise ConversionError, '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 (TypeError, ValueError), txt:
            raise ConversionError, '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 (TypeError, ValueError), txt:
            raise ConversionError, '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 getstring (self, section, option):
        """A convenience method which enforces that the option value is a 
        single string.  Multiple values will raise ValueError.
        """
        val = self.get (section, option)
        if type (val) == ListType:
            raise ConversionError, 'expected single value, got list ([%s] %s == "%s")' % (section, option, val)
        return val

    #######################################
    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')
            