1""" brain-dead simple parser for ini-style files.
2(C) Ronny Pfannschmidt, Holger Krekel -- MIT licensed
3"""
4__version__ = "0.2.dev2"
5
6__all__ = ['IniConfig', 'ParseError']
7
8COMMENTCHARS = "#;"
9
10class ParseError(Exception):
11    def __init__(self, path, lineno, msg):
12        Exception.__init__(self, path, lineno, msg)
13        self.path = path
14        self.lineno = lineno
15        self.msg = msg
16
17    def __str__(self):
18        return "%s:%s: %s" %(self.path, self.lineno+1, self.msg)
19
20class SectionWrapper(object):
21    def __init__(self, config, name):
22        self.config = config
23        self.name = name
24
25    def lineof(self, name):
26        return self.config.lineof(self.name, name)
27
28    def get(self, key, default=None, convert=str):
29        return self.config.get(self.name, key, convert=convert, default=default)
30
31    def __getitem__(self, key):
32        return self.config.sections[self.name][key]
33
34    def __iter__(self):
35        section = self.config.sections.get(self.name, [])
36        def lineof(key):
37            return self.config.lineof(self.name, key)
38        for name in sorted(section, key=lineof):
39            yield name
40
41    def items(self):
42        for name in self:
43            yield name, self[name]
44
45
46class IniConfig(object):
47    def __init__(self, path, data=None):
48        self.path = str(path) # convenience
49        if data is None:
50            f = open(self.path)
51            try:
52                tokens = self._parse(iter(f))
53            finally:
54                f.close()
55        else:
56            tokens = self._parse(data.splitlines(True))
57
58        self._sources = {}
59        self.sections = {}
60
61        for lineno, section, name, value in tokens:
62            if section is None:
63                self._raise(lineno, 'no section header defined')
64            self._sources[section, name] = lineno
65            if name is None:
66                if section in self.sections:
67                    self._raise(lineno, 'duplicate section %r'%(section, ))
68                self.sections[section] = {}
69            else:
70                if name in self.sections[section]:
71                    self._raise(lineno, 'duplicate name %r'%(name, ))
72                self.sections[section][name] = value
73
74    def _raise(self, lineno, msg):
75        raise ParseError(self.path, lineno, msg)
76
77    def _parse(self, line_iter):
78        result = []
79        section = None
80        for lineno, line in enumerate(line_iter):
81            name, data = self._parseline(line, lineno)
82            # new value
83            if name is not None and data is not None:
84                result.append((lineno, section, name, data))
85            # new section
86            elif name is not None and data is None:
87                if not name:
88                    self._raise(lineno, 'empty section name')
89                section = name
90                result.append((lineno, section, None, None))
91            # continuation
92            elif name is None and data is not None:
93                if not result:
94                    self._raise(lineno, 'unexpected value continuation')
95                last = result.pop()
96                last_name, last_data = last[-2:]
97                if last_name is None:
98                    self._raise(lineno, 'unexpected value continuation')
99
100                if last_data:
101                    data = '%s\n%s' % (last_data, data)
102                result.append(last[:-1] + (data,))
103        return result
104
105    def _parseline(self, line, lineno):
106        # blank lines
107        if iscommentline(line):
108            line = ""
109        else:
110            line = line.rstrip()
111        if not line:
112            return None, None
113        # section
114        if line[0] == '[':
115            realline = line
116            for c in COMMENTCHARS:
117                line = line.split(c)[0].rstrip()
118            if line[-1] == "]":
119                return line[1:-1], None
120            return None, realline.strip()
121        # value
122        elif not line[0].isspace():
123            try:
124                name, value = line.split('=', 1)
125                if ":" in name:
126                    raise ValueError()
127            except ValueError:
128                try:
129                    name, value = line.split(":", 1)
130                except ValueError:
131                    self._raise(lineno, 'unexpected line: %r' % line)
132            return name.strip(), value.strip()
133        # continuation
134        else:
135            return None, line.strip()
136
137    def lineof(self, section, name=None):
138        lineno = self._sources.get((section, name))
139        if lineno is not None:
140            return lineno + 1
141
142    def get(self, section, name, default=None, convert=str):
143        try:
144            return convert(self.sections[section][name])
145        except KeyError:
146            return default
147
148    def __getitem__(self, name):
149        if name not in self.sections:
150            raise KeyError(name)
151        return SectionWrapper(self, name)
152
153    def __iter__(self):
154        for name in sorted(self.sections, key=self.lineof):
155            yield SectionWrapper(self, name)
156
157    def __contains__(self, arg):
158        return arg in self.sections
159
160def iscommentline(line):
161    c = line.lstrip()[:1]
162    return c in COMMENTCHARS
163