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