3 from astdicts import OrderedDict
4 from astdicts import MultiOrderedDict
7 def merge_values(left, right, key):
8 """Merges values from right into left."""
9 if isinstance(left, list):
11 else: # assume dictionary
12 vals0 = left[key] if key in left else []
13 vals1 = right[key] if key in right else []
15 return vals0 + [i for i in vals1 if i not in vals0]
17 ###############################################################################
20 class Section(MultiOrderedDict):
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'
32 def __init__(self, defaults=None, templates=None):
33 MultiOrderedDict.__init__(self)
34 # track an ordered id of sections
36 self.id = Section.count
37 self._defaults = [] if defaults is None else defaults
38 self._templates = [] if templates is None else templates
40 def __cmp__(self, other):
42 Use self.id as means of determining equality
44 return cmp(self.id, other.id)
46 def get(self, key, from_self=True, from_templates=True,
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.
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.
59 if from_self and key in self:
60 return MultiOrderedDict.__getitem__(self, key)
63 if self in self._templates:
65 for t in self._templates:
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)
74 for d in self._defaults:
76 return d.get(key, True, from_templates, from_defaults)
82 def __getitem__(self, key):
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
90 def keys(self, self_only=False):
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
96 res = MultiOrderedDict.keys(self)
100 for d in self._templates:
105 for d in self._defaults:
111 def add_defaults(self, defaults):
113 Add a list of defaults to the section. Defaults are
114 sections such as 'general'
118 self._defaults.insert(0, i)
120 def add_templates(self, templates):
122 Add a list of templates to the section.
126 self._templates.insert(0, i)
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
132 for i in reversed(self._defaults):
136 merged = merge_values(merged, i, key)
138 for i in reversed(self._templates):
142 merged = merge_values(merged, i, key)
145 return merge_values(merged, self, key)
147 ###############################################################################
150 COMMENT_START = ';--'
153 DEFAULTSECT = 'general'
156 def remove_comment(line, is_comment):
157 """Remove any commented elements from the line."""
159 return line, is_comment
162 part = line.partition(COMMENT_END)
164 # found multi-line comment end check string after it
165 return remove_comment(part[2], False)
168 part = line.partition(COMMENT_START)
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)
174 # eol comment found return anything before it
175 return has_comment[0], False
177 # check string after it to see if the comment ends
178 line, is_comment = remove_comment(part[2], True)
180 # return possible string data before comment
181 return part[0].strip(), True
183 # otherwise it was an embedded comment so combine
184 return ''.join([part[0].strip(), ' ', line]).rstrip(), False
186 # check for eol comment
187 return line.partition(COMMENT)[0].strip(), False
190 def try_include(line):
192 Checks to see if the given line is an include. If so return the
193 included filename, otherwise None.
196 match = re.match('^#include\s*[<"]?(.*)[>"]?$', line)
197 return match.group(1) if match else None
200 def try_section(line):
202 Checks to see if the given line is a section. If so return the section
203 name, otherwise return 'None'.
205 # leading spaces were stripped when checking for comments
206 if not line.startswith('['):
207 return None, False, []
209 section, delim, templates = line.partition(']')
211 return section[1:], False, []
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]
218 templates.remove('!')
219 return section[1:], True, templates
221 return section[1:], False, templates
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()
230 ###############################################################################
233 def find_dict(mdicts, key, val):
235 Given a list of mult-dicts, return the multi-dict that contains
236 the given key/value pair.
240 return key in d and val in d[key]
243 return [d for d in mdicts if found(d)][0]
245 raise LookupError("Dictionary not located for key = %s, value = %s"
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
260 key_val += " = " + str(v)
261 config_file.write("%s\n" % (key_val))
262 config_file.write("\n")
264 ###############################################################################
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()
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)
280 # try to find in section and section's templates
281 return s.get(key, from_defaults=False)
285 # wasn't found in sections or a section's templates so check in
289 # try to find in section's defaultsects
290 return s.get(key, from_self=False, from_templates=False)
297 return self._defaults
299 def default(self, key):
300 """Retrieves a list of dictionaries for a default section."""
301 return self.get_defaults(key)
303 def add_default(self, key, template_keys=None):
305 Adds a default section to defaults, returning the
306 default Section object.
308 if template_keys is None:
310 return self.add_section(key, template_keys, self._defaults)
313 return self._sections
315 def section(self, key):
316 """Retrieves a list of dictionaries for a section."""
317 return self.get_sections(key)
319 def get_sections(self, key, attr='_sections', searched=None):
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.
330 sections = getattr(self, attr)
331 res = sections[key] if key in sections else []
332 searched.append(self)
334 res += self._includes.get_sections(key, attr, searched)
336 res += self._parent.get_sections(key, attr, searched)
339 def get_defaults(self, key):
341 Retrieve a list of defaults that have values for the given key.
343 return self.get_sections(key, '_defaults')
345 def add_section(self, key, template_keys=None, mdicts=None):
347 Create a new section in the configuration. The name of the
348 new section is the 'key' parameter.
350 if template_keys is None:
353 mdicts = self._sections
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)
362 return self._includes
364 def add_include(self, filename, parser=None):
366 Add a new #include file to the configuration.
368 if filename in self._includes:
369 return self._includes[filename]
371 self._includes[filename] = res = \
372 MultiOrderedConfigParser(self) if parser is None else parser
375 def get(self, section, key):
376 """Retrieves the list of values from a section for a key."""
378 # search for the value in the list of sections
379 return self.find_value(self.section(section), key)
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)
388 raise LookupError("key %r not found for section %r"
391 def multi_get(self, section, key_list):
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
401 return self.get(section, i)
405 # Making it here means all lookups failed.
406 raise LookupError("keys %r not found for section %r" %
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
416 self.defaults(section)[0][key] = val
418 def read(self, filename):
419 """Parse configuration information from a file"""
421 with open(filename, 'rt') as config_file:
422 self._read(config_file)
424 print "Could not open file ", filename, " for reading"
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)
432 # line was empty or was a comment
435 include_name = try_include(line)
437 parser = self.add_include(include_name)
438 parser.read(include_name)
441 section, is_template, templates = try_section(line)
443 if section == DEFAULTSECT or is_template:
444 sect = self.add_default(section, templates)
446 sect = self.add_section(section, templates)
449 key, val = try_option(line)
452 def write(self, config_file):
453 """Write configuration information out to a file"""
455 for key, val in self._includes.iteritems():
457 config_file.write('#include "%s"\n' % key)
459 config_file.write('\n')
460 write_dicts(config_file, self._defaults)
461 write_dicts(config_file, self._sections)
464 with open(config_file, 'wt') as fp:
467 print "Could not open file ", config_file, " for writing"