#!/usr/bin/env python

"""
An alternative config file parser written in response to the ConfigParser
shootout: http://www.python.org/moin/ConfigParserShootout

Author: Skip Montanaro (skip@pobox.com)

Significant points:

    * File format is indentation-based (where'd he get that idea???)

    * Both attribute-style and dictionary-style access supported

    * File format is *not* compatible with Windows INI file
      - section introduced by line ending with colon (another brilliant
      coup!)
      - values are specified by simple key = value lines

    * Nesting to arbitrary depths is supported

    * Read and write with round trip, though comments are not currently
    preserved.

    * Proof-of-concept only - there's not much of an api yet - no
    sections(), has_section(), as_dict() methods, etc.  That should be easy
    enough to add later.

    * Typeless - everything's a string.  Python has plenty of ways to
    convert strings to other types of objects.

"""

import cStringIO as StringIO
import re

class Configuration:
    def __init__(self):
        self.__dict__['_Configuration__values'] = []

    def __cmp__(self, other):
        v1 = self.__values[:]
        v1.sort()
        v2 = self.__values[:]
        v2.sort()
        result = cmp(v1, v2)
        if result:
            return result
        for attr in v1:
            result = cmp(self[attr], other[attr])
            if result:
                return result
        return 0

    def __eq__(self, other):
        if isinstance(other, Configuration):
            return self.__cmp__(other) == 0
        return False

    def __getitem__(self, key):
        if key in self.__values:
            return self.__dict__[key]
        self.__dict__[key] = Configuration()
        self.__values.append(key)
        return self.__dict__[key]
    __getattr__ = __getitem__

    def __setitem__(self, key, val):
        self.__dict__[key] = val
        if key not in self.__values:
            self.__values.append(key)
    __setattr__ = __setitem__
        
    def write(self, fp):
        for attr in self.__values:
            item = self[attr]
            if isinstance(item, Configuration):
                subfp = StringIO.StringIO()
                item.write(subfp)
                fp.write("%s:\n" % attr)
                subfp.seek(0)
                for line in subfp:
                    fp.write(" "*4)
                    fp.write(line)
            else:
                fp.write("%s = %s\n" % (attr, item))

    def read(self, fp):
        for v in self.__values:
            delattr(self, v)
        self.read_helper(PushBackFile(fp), "")

    def read_helper(self, fp, indent):
        #print ">", id(self)
        for line in fp:
            mat = re.match(r'([ \t]*)(.*)$', line)
            left, right = mat.groups()
            if len(left) < len(indent):
                fp.push(line)
                #print "<", id(self)
                return

            if right[-1:] == ':':
                # new section
                section = right[:-1].strip()
                if not section:
                    raise ValueError, "Empty section, line %d" % fp.lineno
                cfg = self[section] = Configuration()
                newline = fp.next()
                fp.push(newline)
                if newline:
                    mat = re.match(r'([ \t]*)', newline)
                    if len(mat.group(0)) > len(indent):
                        cfg.read_helper(fp, mat.group(0))
            else:
                if '=' in right:
                    key, val = right.split('=', 1)
                else:
                    key, val = right, ""
                self[key.strip()] = val.strip()

    def __str__(self):
        return "<Configuration @ 0x%x>" % id(self)
    __repr__ = __str__

class PushBackFile:
    def __init__(self, fp):
        self.fp = fp
        self.stack = []
        self.lineno = 0

    def __iter__(self):
        return self

    def next(self):
        while True:
            if self.stack:
                line = self.stack.pop()
            else:
                line = self.fp.next()
            if line.strip()[:1] != "#":
                break
        if line:
            self.lineno += 1
        line = self.untab(line.rstrip())
        #print "+", line
        return line

    def push(self, line):
        if line:
            self.lineno -= 1
        #print "-", line.rstrip()
        self.stack.append(line)

    def untab(self, line):
        "expand tabs in leading whitespace to spaces"
        newline = []
        line = list(line)
        while line:
            c = line[0]
            del line[0]
            if c == " ":
                newline.append(" ")
            elif c == "\t":
                newline.append(" ")
                while len(newline) % 8:
                    newline.append(" ")
            else:
                newline.append(c)
                newline.extend(line)
                line = []
        return "".join(newline)

if __name__ == "__main__":
    import sys
    sys.setrecursionlimit(40)

    import unittest

    class TestCase(unittest.TestCase):
        def test_cmp(self):
            cfg1 = Configuration()
            cfg1['level1'] = "new val"
            cfg2 = Configuration()
            cfg2['level1'] = "new val"
            self.assertEqual(cfg1, cfg2)
            cfg2['level1'] = "another val"
            self.assertNotEqual(cfg1, cfg2)
            
        def test_get_item(self):
            self.assertEqual(Configuration(), Configuration())
            cfg = Configuration()
            self.assertEqual(cfg['level1'], Configuration())

        def test_set_item(self):
            cfg = Configuration()
            cfg['level1'] = "new val"
            self.assertEqual(cfg['level1'], "new val")

        def test_set_item_2deep(self):
            cfg = Configuration()
            cfg['level1']['level2'] = "new val"
            self.assertEqual(cfg['level1']['level2'], "new val")

        def test_set_attr(self):
            cfg = Configuration()
            cfg.level1 = "new val"
            self.assertEqual(cfg.level1, "new val")

        def test_set_attr_2deep(self):
            cfg = Configuration()
            cfg.level1.level2 = "new val"
            self.assertEqual(cfg.level1.level2, "new val")

        def test_write(self):
            fp = StringIO.StringIO()
            cfg = Configuration()
            cfg.level1 = "new val"
            cfg.section1.item1 = "item 1"
            cfg.section1.subsection.item2 = "item 2"
            cfg.section2.subsection.item3 = "item 3"
            cfg['empty section1'] = Configuration()
            cfg['very last'] = "7"
            cfg.write(fp)
            self.assertEqual(fp.getvalue(),
                             ("level1 = new val\n"
                              "section1:\n"
                              "    item1 = item 1\n"
                              "    subsection:\n"
                              "        item2 = item 2\n"
                              "section2:\n"
                              "    subsection:\n"
                              "        item3 = item 3\n"
                              "empty section1:\n"
                              "very last = 7\n"))

        def test_read(self):
            fp = StringIO.StringIO("empty section1:\n"
                                   "level1 = new val\n"
                                   "section1:\n"
                                   "# this is a comment for section1.item1:\n"
                                   "    item1 = item 1\n"
                                   "          # this is another comment\n"
                                   "    subsection:  \n"
                                   "        item2 = item 2\n"
                                   "section2:\n"
                                   "    subsection:\n"
                                   "        item3 = item 3\n"
                                   "very last = 7\n")
            cfg = Configuration()
            cfg.read(fp)
            self.assertEqual(cfg._Configuration__values,
                             ['empty section1', 'level1',
                              'section1', 'section2', 'very last'])
            self.assertEqual(cfg.section1._Configuration__values,
                             ['item1', 'subsection'])
            self.assertEqual(cfg['empty section1'], Configuration())
            self.assertEqual(cfg.level1, "new val")
            self.assertEqual(cfg.section1.item1, "item 1")
            self.assertEqual(cfg.section1.subsection.item2, "item 2")
            self.assertEqual(cfg.section2.subsection.item3, "item 3")
            self.assertEqual(cfg['very last'], "7")

        def test_varying_indents(self):
            fp = StringIO.StringIO("empty section1:\n"
                                   "level1 = new val\n"
                                   "section1:\n"
                                   "\titem1 = item 1\n"
                                   "\tsubsection:  \n"
                                   "\t    item2 = item 2\n"
                                   "section2:\n"
                                   " subsection:\n"
                                   " \titem3 = item 3\n"
                                   "very last = 7\n")
            cfg = Configuration()
            cfg.read(fp)
            self.assertEqual(cfg._Configuration__values,
                             ['empty section1', 'level1',
                              'section1', 'section2', 'very last'])
            self.assertEqual(cfg.section1._Configuration__values,
                             ['item1', 'subsection'])
            self.assertEqual(cfg['empty section1'], Configuration())
            self.assertEqual(cfg.level1, "new val")
            self.assertEqual(cfg.section1.item1, "item 1")
            self.assertEqual(cfg.section1.subsection.item2, "item 2")
            self.assertEqual(cfg.section2.subsection.item3, "item 3")
            self.assertEqual(cfg['very last'], "7")

    unittest.main()
