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