fix typo
[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                         self._sections[cursect]._find(optname).value = '%s\n%s' % (self.get(cursect, optname), value)
252             # a section header or option header?
253             else:
254                 # is it a section header?
255                 mo = self.SECTCRE.match(line)
256                 if mo:
257                     sectname = mo.group('header')
258                     if sectname in self._sections:
259                         cursect = self._sections[sectname]
260                     elif sectname == ConfigParser.DEFAULTSECT:
261                         cursect = self._defaults
262                     else:
263                         #cursect = {'__name__': sectname}
264                         #self._sections[sectname] = cursect
265                         self.add_section(sectname)
266                         self.set(sectname, '__name__', sectname)
267                     # So sections can't start with a continuation line
268                     cursect = sectname
269                     optname = None
270                 # no section header in the file?
271                 elif cursect is None:
272                     raise ConfigParser.MissingSectionHeaderError(fpname, lineno, line)
273                 # an option line?
274                 else:
275                     mo = self.OPTCRE.match(line)
276                     if mo:
277                         optname, vi, optval = mo.group('option', 'vi', 'value')
278                         if vi in ('=', ':') and ';' in optval:
279                             # ';' is a comment delimiter only if it follows
280                             # a spacing character
281                             pos = optval.find(';')
282                             if pos != -1 and optval[pos-1].isspace():
283                                 optval = optval[:pos]
284                         optval = optval.strip()
285                         # allow empty values
286                         if optval == '""':
287                             optval = ''
288                         optname = self.optionxform(optname.rstrip())
289                         if cursect == ConfigParser.DEFAULTSECT:
290                             self._defaults[optname] = optval
291                         else:
292                             self._sections[cursect]._add_option(optname, line=line)
293                     else:
294                         # a non-fatal parsing error occurred.  set up the
295                         # exception but keep going. the exception will be
296                         # raised at the end of the file and will contain a
297                         # list of all bogus lines
298                         if not e:
299                             e = ConfigParser.ParsingError(fpname)
300                         e.append(lineno, repr(line))
301         # if any parsing errors occurred, raise an exception
302         if e:
303             raise e
304
305     def write(self, fp, comments = False):
306         """
307         write the configuration file. If comments is True all comments etc.
308         will be written to fp otherwise the ConfigParsers' default write method
309         will be called.
310         """
311         if comments:
312             fp.write(str(self))
313             fp.write('\n')
314         else:
315             ConfigParser.SafeConfigParser.write(self, fp)
316
317     # XXX: simplify!
318     def __str__(self):
319         ret = []
320         for line in self._sections._lines:
321             if line.type == 'section':
322                 ret.append('[%s]' % line.name)
323                 for sline in line._lines:
324                     if sline.name == '__name__':
325                         continue
326                     if sline.type == 'option':
327                         ret.append(sline.frmt % (sline.name, sline.value))
328                     else:
329                         ret.append(str(sline))
330             else:
331                 ret.append(str(line))
332         return '\n'.join(ret)
333
334 # vim: sw=4 et