sip_to_pjsip: improve ability to parse input files
[asterisk/asterisk.git] / contrib / scripts / sip_to_pjsip / astconfigparser.py
1 import re
2 import itertools
3
4 from astdicts import OrderedDict
5 from astdicts import MultiOrderedDict
6
7
8 def merge_values(left, right, key):
9     """Merges values from right into left."""
10     if isinstance(left, list):
11         vals0 = left
12     else:  # assume dictionary
13         vals0 = left[key] if key in left else []
14     vals1 = right[key] if key in right else []
15
16     return vals0 + [i for i in vals1 if i not in vals0]
17
18 ###############################################################################
19
20
21 class Section(MultiOrderedDict):
22     """
23     A Section is a MultiOrderedDict itself that maintains a list of
24     key/value options.  However, in the case of an Asterisk config
25     file a section may have other defaults sections that is can pull
26     data from (i.e. templates).  So when an option is looked up by key
27     it first checks the base section and if not found looks in the
28     added default sections. If not found at that point then a 'KeyError'
29     exception is raised.
30     """
31     count = 0
32
33     def __init__(self, defaults=None, templates=None):
34         MultiOrderedDict.__init__(self)
35         # track an ordered id of sections
36         Section.count += 1
37         self.id = Section.count
38         self._defaults = [] if defaults is None else defaults
39         self._templates = [] if templates is None else templates
40
41     def __cmp__(self, other):
42         """
43         Use self.id as means of determining equality
44         """
45         return cmp(self.id, other.id)
46
47     def get(self, key, from_self=True, from_templates=True,
48             from_defaults=True):
49         """
50         Get the values corresponding to a given key. The parameters to this
51         function form a hierarchy that determines priority of the search.
52         from_self takes priority over from_templates, and from_templates takes
53         priority over from_defaults.
54
55         Parameters:
56         from_self - If True, search within the given section.
57         from_templates - If True, search in this section's templates.
58         from_defaults - If True, search within this section's defaults.
59         """
60         if from_self and key in self:
61             return MultiOrderedDict.__getitem__(self, key)
62
63         if from_templates:
64             if self in self._templates:
65                 return []
66             for t in self._templates:
67                 try:
68                     # fail if not found on the search - doing it this way
69                     # allows template's templates to be searched.
70                     return t.get(key, True, from_templates, from_defaults)
71                 except KeyError:
72                     pass
73
74         if from_defaults:
75             for d in self._defaults:
76                 try:
77                     return d.get(key, True, from_templates, from_defaults)
78                 except KeyError:
79                     pass
80
81         raise KeyError(key)
82
83     def __getitem__(self, key):
84         """
85         Get the value for the given key. If it is not found in the 'self'
86         then check inside templates and defaults before declaring raising
87         a KeyError exception.
88         """
89         return self.get(key)
90
91     def keys(self, self_only=False):
92         """
93         Get the keys from this section. If self_only is True, then
94         keys from this section's defaults and templates are not
95         included in the returned value
96         """
97         res = MultiOrderedDict.keys(self)
98         if self_only:
99             return res
100
101         for d in self._templates:
102             for key in d.keys():
103                 if key not in res:
104                     res.append(key)
105
106         for d in self._defaults:
107             for key in d.keys():
108                 if key not in res:
109                     res.append(key)
110         return res
111
112     def add_defaults(self, defaults):
113         """
114         Add a list of defaults to the section. Defaults are
115         sections such as 'general'
116         """
117         defaults.sort()
118         for i in defaults:
119             self._defaults.insert(0, i)
120
121     def add_templates(self, templates):
122         """
123         Add a list of templates to the section.
124         """
125         templates.sort()
126         for i in templates:
127             self._templates.insert(0, i)
128
129     def get_merged(self, key):
130         """Return a list of values for a given key merged from default(s)"""
131         # first merge key/values from defaults together
132         merged = []
133         for i in reversed(self._defaults):
134             if not merged:
135                 merged = i
136                 continue
137             merged = merge_values(merged, i, key)
138
139         for i in reversed(self._templates):
140             if not merged:
141                 merged = i
142                 continue
143             merged = merge_values(merged, i, key)
144
145         # then merge self in
146         return merge_values(merged, self, key)
147
148 ###############################################################################
149
150 COMMENT = ';'
151 COMMENT_START = ';--'
152 COMMENT_END = '--;'
153
154 DEFAULTSECT = 'general'
155
156
157 def remove_comment(line, is_comment):
158     """Remove any commented elements from the line."""
159     if not line:
160         return line, is_comment
161
162     if is_comment:
163         part = line.partition(COMMENT_END)
164         if part[1]:
165             # found multi-line comment end check string after it
166             return remove_comment(part[2], False)
167         return "", True
168
169     part = line.partition(COMMENT_START)
170     if part[1]:
171         # found multi-line comment start check string before
172         # it to make sure there wasn't an eol comment in it
173         has_comment = part[0].partition(COMMENT)
174         if has_comment[1]:
175             # eol comment found return anything before it
176             return has_comment[0], False
177
178         # check string after it to see if the comment ends
179         line, is_comment = remove_comment(part[2], True)
180         if is_comment:
181             # return possible string data before comment
182             return part[0].strip(), True
183
184         # otherwise it was an embedded comment so combine
185         return ''.join([part[0].strip(), ' ', line]).rstrip(), False
186
187     # check for eol comment
188     return line.partition(COMMENT)[0].strip(), False
189
190
191 def try_include(line):
192     """
193     Checks to see if the given line is an include.  If so return the
194     included filename, otherwise None.
195     """
196
197     match = re.match('^#include\s*[<"]?(.*)[>"]?$', line)
198     return match.group(1) if match else None
199
200
201 def try_section(line):
202     """
203     Checks to see if the given line is a section. If so return the section
204     name, otherwise return 'None'.
205     """
206     # leading spaces were stripped when checking for comments
207     if not line.startswith('['):
208         return None, False, []
209
210     section, delim, templates = line.partition(']')
211     if not templates:
212         return section[1:], False, []
213
214     # strip out the parens and parse into an array
215     templates = templates.replace('(', "").replace(')', "").split(',')
216     # go ahead and remove extra whitespace
217     templates = [i.strip() for i in templates]
218     try:
219         templates.remove('!')
220         return section[1:], True, templates
221     except:
222         return section[1:], False, templates
223
224
225 def try_option(line):
226     """Parses the line as an option, returning the key/value pair."""
227     data = re.split('=>?', line)
228     # should split in two (key/val), but either way use first two elements
229     return data[0].rstrip(), data[1].lstrip()
230
231 ###############################################################################
232
233
234 def find_dict(mdicts, key, val):
235     """
236     Given a list of mult-dicts, return the multi-dict that contains
237     the given key/value pair.
238     """
239
240     def found(d):
241         return key in d and val in d[key]
242
243     try:
244         return [d for d in mdicts if found(d)][0]
245     except IndexError:
246         raise LookupError("Dictionary not located for key = %s, value = %s"
247                           % (key, val))
248
249
250 def write_dicts(config_file, mdicts):
251     """Write the contents of the mdicts to the specified config file"""
252     for section, sect_list in mdicts.iteritems():
253         # every section contains a list of dictionaries
254         for sect in sect_list:
255             config_file.write("[%s]\n" % section)
256             for key, val_list in sect.iteritems():
257                 # every value is also a list
258                 for v in val_list:
259                     key_val = key
260                     if v is not None:
261                         key_val += " = " + str(v)
262                         config_file.write("%s\n" % (key_val))
263             config_file.write("\n")
264
265 ###############################################################################
266
267
268 class MultiOrderedConfigParser:
269     def __init__(self, parent=None):
270         self._parent = parent
271         self._defaults = MultiOrderedDict()
272         self._sections = MultiOrderedDict()
273         self._includes = OrderedDict()
274
275     def find_value(self, sections, key):
276         """Given a list of sections, try to find value(s) for the given key."""
277         # always start looking in the last one added
278         sections.sort(reverse=True)
279         for s in sections:
280             try:
281                 # try to find in section and section's templates
282                 return s.get(key, from_defaults=False)
283             except KeyError:
284                 pass
285
286         # wasn't found in sections or a section's templates so check in
287         # defaults
288         for s in sections:
289             try:
290                 # try to find in section's defaultsects
291                 return s.get(key, from_self=False, from_templates=False)
292             except KeyError:
293                 pass
294
295         raise KeyError(key)
296
297     def defaults(self):
298         return self._defaults
299
300     def default(self, key):
301         """Retrieves a list of dictionaries for a default section."""
302         return self.get_defaults(key)
303
304     def add_default(self, key, template_keys=None):
305         """
306         Adds a default section to defaults, returning the
307         default Section object.
308         """
309         if template_keys is None:
310             template_keys = []
311         return self.add_section(key, template_keys, self._defaults)
312
313     def sections(self):
314         return self._sections
315
316     def section(self, key):
317         """Retrieves a list of dictionaries for a section."""
318         return self.get_sections(key)
319
320     def get_sections(self, key, attr='_sections', searched=None):
321         """
322         Retrieve a list of sections that have values for the given key.
323         The attr parameter can be used to control what part of the parser
324         to retrieve values from.
325         """
326         if searched is None:
327             searched = []
328         if self in searched:
329             return []
330
331         sections = getattr(self, attr)
332         res = sections[key] if key in sections else []
333         searched.append(self)
334         if self._includes:
335             res.extend(list(itertools.chain(*[
336                 incl.get_sections(key, attr, searched)
337                 for incl in self._includes.itervalues()])))
338         if self._parent:
339             res += self._parent.get_sections(key, attr, searched)
340         return res
341
342     def get_defaults(self, key):
343         """
344         Retrieve a list of defaults that have values for the given key.
345         """
346         return self.get_sections(key, '_defaults')
347
348     def add_section(self, key, template_keys=None, mdicts=None):
349         """
350         Create a new section in the configuration. The name of the
351         new section is the 'key' parameter.
352         """
353         if template_keys is None:
354             template_keys = []
355         if mdicts is None:
356             mdicts = self._sections
357         res = Section()
358         for t in template_keys:
359             res.add_templates(self.get_defaults(t))
360         res.add_defaults(self.get_defaults(DEFAULTSECT))
361         mdicts.insert(0, key, res)
362         return res
363
364     def includes(self):
365         return self._includes
366
367     def add_include(self, filename, parser=None):
368         """
369         Add a new #include file to the configuration.
370         """
371         if filename in self._includes:
372             return self._includes[filename]
373
374         self._includes[filename] = res = \
375             MultiOrderedConfigParser(self) if parser is None else parser
376         return res
377
378     def get(self, section, key):
379         """Retrieves the list of values from a section for a key."""
380         try:
381             # search for the value in the list of sections
382             return self.find_value(self.section(section), key)
383         except KeyError:
384             pass
385
386         try:
387             # section may be a default section so, search
388             # for the value in the list of defaults
389             return self.find_value(self.default(section), key)
390         except KeyError:
391             raise LookupError("key %r not found for section %r"
392                               % (key, section))
393
394     def multi_get(self, section, key_list):
395         """
396         Retrieves the list of values from a section for a list of keys.
397         This method is intended to be used for equivalent keys. Thus, as soon
398         as any match is found for any key in the key_list, the match is
399         returned. This does not concatenate the lookups of all of the keys
400         together.
401         """
402         for i in key_list:
403             try:
404                 return self.get(section, i)
405             except LookupError:
406                 pass
407
408         # Making it here means all lookups failed.
409         raise LookupError("keys %r not found for section %r" %
410                           (key_list, section))
411
412     def set(self, section, key, val):
413         """Sets an option in the given section."""
414         # TODO - set in multiple sections? (for now set in first)
415         # TODO - set in both sections and defaults?
416         if section in self._sections:
417             self.section(section)[0][key] = val
418         else:
419             self.defaults(section)[0][key] = val
420
421     def read(self, filename, sect=None):
422         """Parse configuration information from a file"""
423         try:
424             with open(filename, 'rt') as config_file:
425                 self._read(config_file, sect)
426         except IOError:
427             print "Could not open file ", filename, " for reading"
428
429     def _read(self, config_file, sect):
430         """Parse configuration information from the config_file"""
431         is_comment = False  # used for multi-lined comments
432         for line in config_file:
433             line, is_comment = remove_comment(line, is_comment)
434             if not line:
435                 # line was empty or was a comment
436                 continue
437
438             include_name = try_include(line)
439             if include_name:
440                 parser = self.add_include(include_name)
441                 parser.read(include_name, sect)
442                 continue
443
444             section, is_template, templates = try_section(line)
445             if section:
446                 if section == DEFAULTSECT or is_template:
447                     sect = self.add_default(section, templates)
448                 else:
449                     sect = self.add_section(section, templates)
450                 continue
451
452             key, val = try_option(line)
453             if sect is None:
454                 raise Exception("Section not defined before assignment")
455             sect[key] = val
456
457     def write(self, config_file):
458         """Write configuration information out to a file"""
459         try:
460             for key, val in self._includes.iteritems():
461                 val.write(key)
462                 config_file.write('#include "%s"\n' % key)
463
464             config_file.write('\n')
465             write_dicts(config_file, self._defaults)
466             write_dicts(config_file, self._sections)
467         except:
468             try:
469                 with open(config_file, 'wt') as fp:
470                     self.write(fp)
471             except IOError:
472                 print "Could not open file ", config_file, " for writing"