3 from astdicts import OrderedDict
4 from astdicts import MultiOrderedDict
6 def merge_values(left, right, key):
7 """Merges values from right into left."""
8 if isinstance(left, list):
10 else: # assume dictionary
11 vals0 = left[key] if key in left else []
12 vals1 = right[key] if key in right else []
14 return vals0 + [i for i in vals1 if i not in vals0]
16 ###############################################################################
18 class Section(MultiOrderedDict):
19 """A Section is a MultiOrderedDict itself that maintains a list of
20 key/value options. However, in the case of an Asterisk config
21 file a section may have other defaults sections that is can pull
22 data from (i.e. templates). So when an option is looked up by key
23 it first checks the base section and if not found looks in the
24 added default sections. If not found at that point then a 'KeyError'
29 def __init__(self, defaults=None, templates=None):
30 MultiOrderedDict.__init__(self)
31 # track an ordered id of sections
33 self.id = Section.count
34 self._defaults = [] if defaults is None else defaults
35 self._templates = [] if templates is None else templates
37 def __cmp__(self, other):
38 return cmp(self.id, other.id)
40 def get(self, key, from_self=True, from_templates=True, from_defaults=True):
41 if from_self and key in self:
42 return MultiOrderedDict.__getitem__(self, key)
45 if self in self._templates:
47 for t in self._templates:
49 # fail if not found on the search - doing it this way
50 # allows template's templates to be searched.
51 return t.get(key, True, from_templates, from_defaults)
56 for d in self._defaults:
58 return d.get(key, True, from_templates, from_defaults)
64 def __getitem__(self, key):
65 """Get the value for the given key. If it is not found in the 'self'
66 then check inside templates and defaults before declaring raising
71 def keys(self, self_only=False):
72 res = MultiOrderedDict.keys(self)
76 for d in self._templates:
81 for d in self._defaults:
87 def add_defaults(self, defaults):
90 self._defaults.insert(0, i)
92 def add_templates(self, templates):
93 templates.sort(reverse=True);
94 self._templates.extend(templates)
96 def get_merged(self, key):
97 """Return a list of values for a given key merged from default(s)"""
98 # first merge key/values from defaults together
100 for i in reversed(self._defaults):
104 merged = merge_values(merged, i, key)
106 for i in reversed(self._templates):
110 merged = merge_values(merged, i, key)
113 return merge_values(merged, self, key)
115 ###############################################################################
118 COMMENT_START = ';--'
121 DEFAULTSECT = 'general'
123 def remove_comment(line, is_comment):
124 """Remove any commented elements from the line."""
125 if not line: return line, is_comment
128 part = line.partition(COMMENT_END)
130 # found multi-line comment end check string after it
131 return remove_comment(part[2], False)
134 part = line.partition(COMMENT_START)
136 # found multi-line comment start check string before
137 # it to make sure there wasn't an eol comment in it
138 has_comment = part[0].partition(COMMENT)
140 # eol comment found return anything before it
141 return has_comment[0], False
143 # check string after it to see if the comment ends
144 line, is_comment = remove_comment(part[2], True)
146 # return possible string data before comment
147 return part[0].strip(), True
149 # otherwise it was an embedded comment so combine
150 return ''.join([part[0].strip(), ' ', line]).rstrip(), False
152 # check for eol comment
153 return line.partition(COMMENT)[0].strip(), False
155 def try_include(line):
156 """Checks to see if the given line is an include. If so return the
157 included filename, otherwise None.
159 if not line.startswith('#'):
162 # it is an include - get file name
164 return line[line.index('"') + 1:line.rindex('"')]
166 print "Invalid include - could not parse filename."
169 def try_section(line):
170 """Checks to see if the given line is a section. If so return the section
171 name, otherwise return 'None'.
173 # leading spaces were stripped when checking for comments
174 if not line.startswith('['):
175 return None, False, []
177 section, delim, templates = line.partition(']')
179 return section[1:], False, []
181 # strip out the parens and parse into an array
182 templates = templates.replace('(', "").replace(')', "").split(',')
183 # go ahead and remove extra whitespace
184 templates = [i.strip() for i in templates]
186 templates.remove('!')
187 return section[1:], True, templates
189 return section[1:], False, templates
191 def try_option(line):
192 """Parses the line as an option, returning the key/value pair."""
193 data = re.split('=>?', line)
194 # should split in two (key/val), but either way use first two elements
195 return data[0].rstrip(), data[1].lstrip()
197 ###############################################################################
199 def find_value(sections, key):
200 """Given a list of sections, try to find value(s) for the given key."""
201 # always start looking in the last one added
202 sections.sort(reverse=True);
205 # try to find in section and section's templates
206 return s.get(key, from_defaults=False)
210 # wasn't found in sections or a section's templates so check in defaults
213 # try to find in section's defaultsects
214 return s.get(key, from_self=False, from_templates=False)
220 def find_dict(mdicts, key, val):
221 """Given a list of mult-dicts, return the multi-dict that contains
222 the given key/value pair."""
225 return key in d and val in d[key]
228 return [d for d in mdicts if found(d)][0]
230 raise LookupError("Dictionary not located for key = %s, value = %s"
233 def get_sections(parser, key, attr='_sections', searched=None):
236 if parser is None or parser in searched:
240 sections = getattr(parser, attr)
241 res = sections[key] if key in sections else []
242 searched.append(parser)
243 return res + get_sections(parser._includes, key, attr, searched) \
244 + get_sections(parser._parent, key, attr, searched)
246 # assume ordereddict of parsers
248 for p in parser.itervalues():
249 res.extend(get_sections(p, key, attr, searched))
252 def get_defaults(parser, key):
253 return get_sections(parser, key, '_defaults')
255 def write_dicts(file, mdicts):
256 for section, sect_list in mdicts.iteritems():
257 # every section contains a list of dictionaries
258 for sect in sect_list:
259 file.write("[%s]\n" % section)
260 for key, val_list in sect.iteritems():
261 # every value is also a list
265 key_val += " = " + str(v)
266 file.write("%s\n" % (key_val))
269 ###############################################################################
271 class MultiOrderedConfigParser:
272 def __init__(self, parent=None):
273 self._parent = parent
274 self._defaults = MultiOrderedDict()
275 self._sections = MultiOrderedDict()
276 self._includes = OrderedDict()
279 return self._defaults
281 def default(self, key):
282 """Retrieves a list of dictionaries for a default section."""
283 return get_defaults(self, key)
285 def add_default(self, key, template_keys=None):
286 """Adds a default section to defaults, returning the
287 default Section object.
289 if template_keys is None:
291 return self.add_section(key, template_keys, self._defaults)
294 return self._sections
296 def section(self, key):
297 """Retrieves a list of dictionaries for a section."""
298 return get_sections(self, key)
300 def add_section(self, key, template_keys=None, mdicts=None):
301 if template_keys is None:
304 mdicts = self._sections
306 for t in template_keys:
307 res.add_templates(get_defaults(self, t))
308 res.add_defaults(get_defaults(self, DEFAULTSECT))
309 mdicts.insert(0, key, res)
313 return self._includes
315 def add_include(self, filename, parser=None):
316 if filename in self._includes:
317 return self._includes[filename]
319 self._includes[filename] = res = \
320 MultiOrderedConfigParser(self) if parser is None else parser
323 def get(self, section, key):
324 """Retrieves the list of values from a section for a key."""
326 # search for the value in the list of sections
327 return find_value(self.section(section), key)
332 # section may be a default section so, search
333 # for the value in the list of defaults
334 return find_value(self.default(section), key)
336 raise LookupError("key %r not found for section %r"
339 def set(self, section, key, val):
340 """Sets an option in the given section."""
341 # TODO - set in multiple sections? (for now set in first)
342 # TODO - set in both sections and defaults?
343 if section in self._sections:
344 self.section(section)[0][key] = val
346 self.defaults(section)[0][key] = val
348 def read(self, filename):
350 with open(filename, 'rt') as file:
351 self._read(file, filename)
353 print "Could not open file ", filename, " for reading"
355 def _read(self, file, filename):
356 is_comment = False # used for multi-lined comments
358 line, is_comment = remove_comment(line, is_comment)
360 # line was empty or was a comment
363 include_name = try_include(line)
365 parser = self.add_include(include_name)
366 parser.read(include_name)
369 section, is_template, templates = try_section(line)
371 if section == DEFAULTSECT or is_template:
372 sect = self.add_default(section, templates)
374 sect = self.add_section(section, templates)
377 key, val = try_option(line)
382 for key, val in self._includes.iteritems():
384 f.write('#include "%s"\n' % key)
387 write_dicts(f, self._defaults)
388 write_dicts(f, self._sections)
391 with open(f, 'wt') as fp:
394 print "Could not open file ", f, " for writing"