0001"""
0002A parser for .ini-syntax files.  INIParser should be subclassed to
0003create a data structure from the file.  See INIParser for more
0004"""
0005
0006class ParseError(Exception):
0007
0008    def __init__(self, message, filename=None, lineno=None, column=None, line=None):
0009        # Note: right now column is not used, but in some contexts it
0010        # could be used to print out a line like:
0011        # error, must be an integer:
0012        # a = one
0013        #     ^
0014        self.message = message
0015        self.filename = filename
0016        self.lineno = lineno
0017        self.column = column
0018        self.line = line
0019
0020    def __str__(self):
0021        msg = self.message
0022        if self.filename and self.lineno:
0023            msg += ' in %s:%s' % (self.filename, self.lineno)
0024        elif self.filename:
0025            msg += ' in %s' % self.filename
0026        elif self.lineno:
0027            msg += ' at line %s' % self.lineno
0028        return msg
0029
0030class INIParser(object):
0031
0032    """
0033    A parser for .ini-syntax files.
0034
0035    Implements all features I know of in .ini files:
0036
0037    * sections with a [ in the first column
0038    * assignment via a=b or a: b
0039    * rfc822-style continuation lines (i.e., start the next line
0040      with indentation to make it a continuation).
0041    * ; or # for comments
0042
0043    This class should be subclassed.  Subclasses may only need to
0044    implement the .assignment() method.  You may want to use the
0045    .section attribute, which holds the current section (or None if no
0046    section has been defined yet).
0047
0048    Use .parse_error(message) when you encounter a problem; this will
0049    create an exception that will note the filename and line in which
0050    the problem occurs.
0051    """
0052
0053    def __init__(self):
0054        self.reset()
0055
0056    def reset(self):
0057        pass
0058
0059    def load(self, filename, encoding=None):
0060        fileobj = open(filename, 'rb')
0061        self.loadfile(fileobj, filename=filename, encoding=encoding)
0062        fileobj.close()
0063
0064    def loadfile(self, fileobj, filename=None, encoding=None):
0065        self.start_lineno = 0
0066        if filename is None:
0067            filename = getattr(fileobj, 'name', None)
0068        self.filename = filename
0069        # lineno is what we are parsing, start_lineno is the last
0070        # assignment we processed (for multi-line assignments)
0071        self.start_lineno = 0
0072        self.lineno = 0
0073        self.encoding = encoding
0074        def strip_newline(l):
0075            if l.endswith('\n'):
0076                return l[:-1]
0077            else:
0078                return l
0079        self.stream = [strip_newline(l) for l in fileobj.readlines()]
0080        self.process_file()
0081        del self.filename
0082        del self.encoding
0083        del self.lineno
0084
0085    def loadstring(self, string, filename=None):
0086        self.stream = string.splitlines()
0087        self.filename = filename
0088        self.start_lineno = 0
0089        self.lineno = 0
0090        self.encoding = None
0091        self.process_file()
0092        del self.stream
0093        del self.filename
0094        del self.lineno
0095        del self.encoding
0096
0097    def process_file(self):
0098        self.section = None
0099        last_name = None
0100        accumulated_content = None
0101        for line in self.stream:
0102            self.lineno += 1
0103            # @@: should catch encoding error:
0104            if self.encoding:
0105                line = line.decode(self.encoding)
0106            end_assignment = False
0107            if not line.strip(): # empty line
0108                if last_name is not None:
0109                    self.process_assignment(
0110                        last_name,
0111                        accumulated_content)
0112                    last_name = accumulated_content = None
0113                    self.start_lineno = self.lineno
0114                continue
0115            elif line[0] in (' ', '\t'): # continuation line
0116                if not last_name:
0117                    self.error_continuation_without_assignment(line)
0118                else:
0119                    accumulated_content.append(line)
0120                continue
0121            elif self.get_section(line) is not None: # section line
0122                if last_name is not None:
0123                    self.process_assignment(
0124                        last_name,
0125                        accumulated_content)
0126                    last_name = accumulated_content = None
0127                    self.start_lineno = self.lineno
0128                self.new_section(self.get_section(line))
0129            elif self.get_comment(line) is not None: # comment line
0130                if last_name is not None:
0131                    self.process_assignment(
0132                        last_name,
0133                        accumulated_content)
0134                    last_name = accumulated_content = None
0135                    self.start_lineno = self.lineno
0136                self.add_comment(self.get_comment(line))
0137            else: # normal assignment
0138                if last_name is not None:
0139                    self.process_assignment(
0140                        last_name,
0141                        accumulated_content)
0142                last_name, accumulated_content = self.split_name_value(line)
0143                self.start_lineno = self.lineno
0144        if last_name is not None:
0145            self.process_assignment(
0146                last_name,
0147                accumulated_content)
0148
0149    def split_name_value(self, line):
0150        colon_pos = line.find(':')
0151        equal_pos = line.find('=')
0152        if colon_pos == -1 and equal_pos == -1:
0153            self.error_missing_equal(line)
0154            return None
0155        if (colon_pos == -1
0156            or (equal_pos != -1 and equal_pos < colon_pos)):
0157            pos = equal_pos
0158        else:
0159            pos = colon_pos
0160        return line[:pos], [line[pos+1:]]
0161
0162    def get_comment(self, line):
0163        """
0164        Returns None if not a comment
0165        """
0166        line = line.lstrip()
0167        if line.startswith(';') or line.startswith('#'):
0168            return line[1:]
0169        return None
0170
0171    def get_section(self, line):
0172        """
0173        Returns None if not a section
0174        """
0175        line = line.strip()
0176        if not line.startswith('['):
0177            return None
0178        if not line.endswith(']'):
0179            self.error_section_without_end_bracket(line)
0180            return None
0181        return line[1:-1]
0182
0183    def process_assignment(self, name, accumulated_content):
0184        content = '\n'.join([l.lstrip() for l in accumulated_content])
0185        self.assignment(name.strip(), content)
0186
0187    def assignment(self, name, content):
0188        raise NotImplementedError
0189
0190    def new_section(self, section):
0191        if not section:
0192            self.error_no_section_name()
0193        self.section = section
0194
0195    def add_comment(self, comment):
0196        pass
0197
0198    def error_continuation_without_assignment(self, line):
0199        self.parse_error('Invalid indentation', line)
0200
0201    def error_section_without_end_bracket(self, line):
0202        self.parse_error('Invalid section (must end with ])', line)
0203
0204    def error_missing_equal(self, line):
0205        self.parse_error(
0206            'Lines should look like "name=value" or "name: value"',
0207            line)
0208
0209    def error_no_section_name(self):
0210        self.parse_error(
0211            'Empty section name ([])')
0212
0213    def parse_error(self, msg, line=None, exception=None):
0214        if exception is None:
0215            exception = ParseError
0216        raise exception(
0217            msg,
0218            filename=self.filename,
0219            lineno=self.lineno,
0220            line=line)
0221
0222class BasicParser(INIParser):
0223
0224    """
0225    A simple subclass of INIParser; creates a nested data structure
0226    like ``{'section_name': {'variable': ['values']}}``
0227
0228    Usage::
0229
0230        >>> p = BasicParser()
0231        >>> p.load('config.ini')
0232        >>> data = p.data
0233    """
0234
0235    def reset(self):
0236        self.data = {}
0237        INIParser.reset(self)
0238
0239    def assignment(self, name, content):
0240        if not self.section:
0241            self.parse_error(
0242                'Assignments can only occur inside sections; no section has been defined yet')
0243        section = self.data.setdefault(self.section, {})
0244        section.setdefault(name, []).append(content)