#!/usr/pkg/bin/python3.13
'''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-filter3-20180804'
__author__ = 'Charles Cazabon <software @ discworld.dyndns.org>'

#
# Imports
#

import string
import sys
import shlex
import io
from types import *
try:
    from UserDict import UserDict
except ImportError:
    from collections import UserDict


#
# 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):
    '''Dictionary class which handles lists and singletons intelligently.
    '''
    #######################################
    def __init__ (self, initialdata = {}):
        '''Constructor.
        '''
        UserDict.__init__ (self, {})
        for (key, value) in list(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 as txt:
            raise KeyError(txt)

    #######################################
    def __setitem__ (self, key, value):
        '''
        '''
        if type (value) in (list, tuple):
            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 list(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 (list, tuple):
            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 as txt:
            raise ParsingError('error reading configuration file (%s)' % txt)

        self.__parse ()
        return self

    #######################################
    def __parse (self):
        '''Parse the read-in configuration file.
        '''
        config = u'\n'.join (self.__rawdata)
        f = io.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 = section_name.lower ()
                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 option_name in section:
                    if type (section[option_name]) == list:
                        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 = section.lower ()
        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 (section.lower ())

        except ValueError:
            raise NoSectionError('missing section:  "%s"' % section)

        return list(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 (section.lower ())
            options = self.__sections[s]
        except ValueError:
            raise NoSectionError('missing section (%s)' % section)

        expand = self.__defaults.copy ()
        expand.update (_vars)
        
        if option not in options:
            if option in expand:
                return expand[option]
            raise NoOptionError('section [%s] missing option (%s)' \
                % (section, option))

        rawval = options[option]
        
        if raw:
            return rawval

        try:
            value = []
            if type (rawval) != list:
                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 as txt:
            raise NoOptionError('section [%s] missing option (%s)' \
                % (section, option))
        except TypeError as 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) == list:
                    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')

