- bump version to 0.130.1
[opensuse:osc.git] / osc / OscConfigParser.py
1 # Copyright 2008,2009 Marcus Huewe <suse-tux@gmx.de>
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License version 2
5 # as published by the Free Software Foundation;
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU General Public License
13 # along with this program; if not, write to the Free Software
14 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
15
16
17 import ConfigParser
18 import re
19
20 # inspired from http://code.google.com/p/iniparse/ - although their implementation is
21 # quite different
22
23 class ConfigLineOrder:
24     """
25     A ConfigLineOrder() instance task is to preserve the order of a config file.
26     It keeps track of all lines (including comments) in the _lines list. This list
27     either contains SectionLine() instances or CommentLine() instances.
28     """
29     def __init__(self):
30         self._lines = []
31
32     def _append(self, line_obj):
33         self._lines.append(line_obj)
34
35     def _find_section(self, section):
36         for line in self._lines:
37             if line.type == 'section' and line.name == section:
38                 return line
39         return None
40
41     def add_section(self, sectname):
42         self._append(SectionLine(sectname))
43
44     def get_section(self, sectname):
45         section = self._find_section(sectname)
46         if section:
47             return section
48         section = SectionLine(sectname)
49         self._append(section)
50         return section
51
52     def add_other(self, sectname, line):
53         if sectname:
54             self.get_section(sectname).add_other(line)
55         else:
56             self._append(CommentLine(line))
57
58     def keys(self):
59         return [ i.name for i in self._lines if i.type == 'section' ]
60
61     def __setitem__(self, key, value):
62         section = SectionLine(key)
63         self._append(section)
64
65     def __getitem__(self, key):
66         section = self._find_section(key)
67         if not section:
68             raise KeyError()
69         return section
70
71     def __delitem__(self, key):
72         line = self._find(line)
73         if not line:
74             raise KeyError(key)
75         self._lines.remove(line)
76
77     def __iter__(self):
78         #return self._lines.__iter__()
79         for line in self._lines:
80             if line.type == 'section':
81                 yield line.name
82         raise StopIteration()
83
84 class Line:
85     """Base class for all line objects"""
86     def __init__(self, name, type):
87         self.name = name
88         self.type = type
89
90 class SectionLine(Line):
91     """
92     This class represents a [section]. It stores all lines which belongs to
93     this certain section in the _lines list. The _lines list either contains
94     CommentLine() or OptionLine() instances.
95     """
96     def __init__(self, sectname, dict = {}):
97         Line.__init__(self, sectname, 'section')
98         self._lines = []
99         self._dict = dict
100
101     def _find(self, name):
102         for line in self._lines:
103             if line.name == name:
104                 return line
105         return None
106
107     def _add_option(self, optname, value = None, line = None, sep = '='):
108         if value is None and line is None:
109             raise ConfigParser.Error('Either value or line must be passed in')
110         elif value and line:
111             raise ConfigParser.Error('value and line are mutually exclusive')
112
113         if value is not None:
114             line = '%s%s%s' % (optname, sep, value)
115         opt = self._find(optname)
116         if opt:
117             opt.format(line)
118         else:
119             self._lines.append(OptionLine(optname, line))
120
121     def add_other(self, line):
122         self._lines.append(CommentLine(line))
123
124     def copy(self):
125         return dict(self.items())
126
127     def items(self):
128         return [ (i.name, i.value) for i in self._lines if i.type == 'option' ]
129
130     def keys(self):
131         return [ i.name for i in self._lines ]
132
133     def __setitem__(self, key, val):
134         self._add_option(key, val)
135
136     def __getitem__(self, key):
137         line = self._find(key)
138         if not line:
139             raise KeyError(key)
140         return str(line)
141
142     def __delitem__(self, key):
143         line = self._find(key)
144         if not line:
145             raise KeyError(key)
146         self._lines.remove(line)
147
148     def __str__(self):
149         return self.name
150
151     # XXX: needed to support 'x' in cp._sections['sectname']
152     def __iter__(self):
153         for line in self._lines:
154             yield line.name
155         raise StopIteration()
156
157
158 class CommentLine(Line):
159     """Store a commentline"""
160     def __init__(self, line):
161         Line.__init__(self, line.strip('\n'), 'comment')
162
163     def __str__(self):
164         return self.name
165
166 class OptionLine(Line):
167     """
168     This class represents an option. The class' "name" attribute is used
169     to store the option's name and the "value" attribute contains the option's
170     value. The "frmt" attribute preserves the format which was used in the configuration
171     file.
172     Example:
173         optionx:<SPACE><SPACE>value
174         => self.frmt = '%s:<SPACE><SPACE>%s'
175         optiony<SPACE>=<SPACE>value<SPACE>;<SPACE>some_comment
176         => self.frmt = '%s<SPACE>=<SPACE><SPACE>%s<SPACE>;<SPACE>some_comment
177     """
178
179     def __init__(self, optname, line):
180         Line.__init__(self, optname, 'option')
181         self.name = optname
182         self.format(line)
183
184     def format(self, line):
185         mo = ConfigParser.ConfigParser.OPTCRE.match(line.strip())
186         key, val = mo.group('option', 'value')
187         self.frmt = line.replace(key.strip(), '%s', 1)
188         pos = val.find(' ;')
189         if pos >= 0:
190             val = val[:pos]
191         self.value = val
192         self.frmt = self.frmt.replace(val.strip(), '%s', 1).rstrip('\n')
193
194     def __str__(self):
195         return self.value
196
197
198 class OscConfigParser(ConfigParser.SafeConfigParser):
199     """
200     OscConfigParser() behaves like a normal ConfigParser() object. The
201     only differences is that it preserves the order+format of configuration entries
202     and that it stores comments.
203     In order to keep the order and the format it makes use of the ConfigLineOrder()
204     class.
205     """
206     def __init__(self, defaults={}):
207         ConfigParser.SafeConfigParser.__init__(self, defaults)
208         self._sections = ConfigLineOrder()
209
210     # XXX: unfortunately we have to override the _read() method from the ConfigParser()
211     #      class because a) we need to store comments b) the original version doesn't use
212     #      the its set methods to add and set sections, options etc. instead they use a
213     #      dictionary (this makes it hard for subclasses to use their own objects, IMHO
214     #      a bug) and c) in case of an option we need the complete line to store the format.
215     #      This all sounds complicated but it isn't - we only needed some slight changes
216     def _read(self, fp, fpname):
217         """Parse a sectioned setup file.
218
219         The sections in setup file contains a title line at the top,
220         indicated by a name in square brackets (`[]'), plus key/value
221         options lines, indicated by `name: value' format lines.
222         Continuations are represented by an embedded newline then
223         leading whitespace.  Blank lines, lines beginning with a '#',
224         and just about everything else are ignored.
225         """
226         cursect = None                            # None, or a dictionary
227         optname = None
228         lineno = 0
229         e = None                                  # None, or an exception
230         while True:
231             line = fp.readline()
232             if not line:
233                 break
234             lineno = lineno + 1
235             # comment or blank line?
236             if line.strip() == '' or line[0] in '#;':
237                 self._sections.add_other(cursect, line)
238                 continue
239             if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
240                 # no leading whitespace
241                 continue
242             # continuation line?
243             if line[0].isspace() and cursect is not None and optname:
244                 value = line.strip()
245                 if value:
246                     #cursect[optname] = "%s\n%s" % (cursect[optname], value)
247                     #self.set(cursect, optname, "%s\n%s" % (self.get(cursect, optname), value))
248                     if cursect == ConfigParser.DEFAULTSECT:
249                         self._defaults[optname] = "%s\n%s" % (self._defaults[optname], value)
250                     else:
251                         # use the raw value here (original version uses raw=False)
252                         self._sections[cursect]._find(optname).value = '%s\n%s' % (self.get(cursect, optname, raw=True), value)
253             # a section header or option header?
254             else:
255                 # is it a section header?
256                 mo = self.SECTCRE.match(line)
257                 if mo:
258                     sectname = mo.group('header')
259                     if sectname in self._sections:
260                         cursect = self._sections[sectname]
261                     elif sectname == ConfigParser.DEFAULTSECT:
262                         cursect = self._defaults
263                     else:
264                         #cursect = {'__name__': sectname}
265                         #self._sections[sectname] = cursect
266                         self.add_section(sectname)
267                         self.set(sectname, '__name__', sectname)
268                     # So sections can't start with a continuation line
269                     cursect = sectname
270                     optname = None
271                 # no section header in the file?
272                 elif cursect is None:
273                     raise ConfigParser.MissingSectionHeaderError(fpname, lineno, line)
274                 # an option line?
275                 else:
276                     mo = self.OPTCRE.match(line)
277                     if mo:
278                         optname, vi, optval = mo.group('option', 'vi', 'value')
279                         if vi in ('=', ':') and ';' in optval:
280                             # ';' is a comment delimiter only if it follows
281                             # a spacing character
282                             pos = optval.find(';')
283                             if pos != -1 and optval[pos-1].isspace():
284                                 optval = optval[:pos]
285                         optval = optval.strip()
286                         # allow empty values
287                         if optval == '""':
288                             optval = ''
289                         optname = self.optionxform(optname.rstrip())
290                         if cursect == ConfigParser.DEFAULTSECT:
291                             self._defaults[optname] = optval
292                         else:
293                             self._sections[cursect]._add_option(optname, line=line)
294                     else:
295                         # a non-fatal parsing error occurred.  set up the
296                         # exception but keep going. the exception will be
297                         # raised at the end of the file and will contain a
298                         # list of all bogus lines
299                         if not e:
300                             e = ConfigParser.ParsingError(fpname)
301                         e.append(lineno, repr(line))
302         # if any parsing errors occurred, raise an exception
303         if e:
304             raise e
305
306     def write(self, fp, comments = False):
307         """
308         write the configuration file. If comments is True all comments etc.
309         will be written to fp otherwise the ConfigParsers' default write method
310         will be called.
311         """
312         if comments:
313             fp.write(str(self))
314             fp.write('\n')
315         else:
316             ConfigParser.SafeConfigParser.write(self, fp)
317
318     # XXX: simplify!
319     def __str__(self):
320         ret = []
321         first = True
322         for line in self._sections._lines:
323             if line.type == 'section':
324                 if first:
325                     first = False
326                 else:
327                     ret.append('')
328                 ret.append('[%s]' % line.name)
329                 for sline in line._lines:
330                     if sline.name == '__name__':
331                         continue
332                     if sline.type == 'option':
333                         ret.append(sline.frmt % (sline.name, sline.value))
334                     elif str(sline) != '':
335                         ret.append(str(sline))
336             else:
337                 ret.append(str(line))
338         return '\n'.join(ret)
339
340 # vim: sw=4 et