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