astconfigparser.py

Go to the documentation of this file.
00001 import re
00002 import itertools
00003 
00004 from astdicts import OrderedDict
00005 from astdicts import MultiOrderedDict
00006 
00007 
00008 def merge_values(left, right, key):
00009     """Merges values from right into left."""
00010     if isinstance(left, list):
00011         vals0 = left
00012     else:  # assume dictionary
00013         vals0 = left[key] if key in left else []
00014     vals1 = right[key] if key in right else []
00015 
00016     return vals0 + [i for i in vals1 if i not in vals0]
00017 
00018 ###############################################################################
00019 
00020 
00021 class Section(MultiOrderedDict):
00022     """
00023     A Section is a MultiOrderedDict itself that maintains a list of
00024     key/value options.  However, in the case of an Asterisk config
00025     file a section may have other defaults sections that is can pull
00026     data from (i.e. templates).  So when an option is looked up by key
00027     it first checks the base section and if not found looks in the
00028     added default sections. If not found at that point then a 'KeyError'
00029     exception is raised.
00030     """
00031     count = 0
00032 
00033     def __init__(self, defaults=None, templates=None):
00034         MultiOrderedDict.__init__(self)
00035         # track an ordered id of sections
00036         Section.count += 1
00037         self.id = Section.count
00038         self._defaults = [] if defaults is None else defaults
00039         self._templates = [] if templates is None else templates
00040 
00041     def __cmp__(self, other):
00042         """
00043         Use self.id as means of determining equality
00044         """
00045         return cmp(self.id, other.id)
00046 
00047     def get(self, key, from_self=True, from_templates=True,
00048             from_defaults=True):
00049         """
00050         Get the values corresponding to a given key. The parameters to this
00051         function form a hierarchy that determines priority of the search.
00052         from_self takes priority over from_templates, and from_templates takes
00053         priority over from_defaults.
00054 
00055         Parameters:
00056         from_self - If True, search within the given section.
00057         from_templates - If True, search in this section's templates.
00058         from_defaults - If True, search within this section's defaults.
00059         """
00060         if from_self and key in self:
00061             return MultiOrderedDict.__getitem__(self, key)
00062 
00063         if from_templates:
00064             if self in self._templates:
00065                 return []
00066             for t in self._templates:
00067                 try:
00068                     # fail if not found on the search - doing it this way
00069                     # allows template's templates to be searched.
00070                     return t.get(key, True, from_templates, from_defaults)
00071                 except KeyError:
00072                     pass
00073 
00074         if from_defaults:
00075             for d in self._defaults:
00076                 try:
00077                     return d.get(key, True, from_templates, from_defaults)
00078                 except KeyError:
00079                     pass
00080 
00081         raise KeyError(key)
00082 
00083     def __getitem__(self, key):
00084         """
00085         Get the value for the given key. If it is not found in the 'self'
00086         then check inside templates and defaults before declaring raising
00087         a KeyError exception.
00088         """
00089         return self.get(key)
00090 
00091     def keys(self, self_only=False):
00092         """
00093         Get the keys from this section. If self_only is True, then
00094         keys from this section's defaults and templates are not
00095         included in the returned value
00096         """
00097         res = MultiOrderedDict.keys(self)
00098         if self_only:
00099             return res
00100 
00101         for d in self._templates:
00102             for key in d.keys():
00103                 if key not in res:
00104                     res.append(key)
00105 
00106         for d in self._defaults:
00107             for key in d.keys():
00108                 if key not in res:
00109                     res.append(key)
00110         return res
00111 
00112     def add_defaults(self, defaults):
00113         """
00114         Add a list of defaults to the section. Defaults are
00115         sections such as 'general'
00116         """
00117         defaults.sort()
00118         for i in defaults:
00119             self._defaults.insert(0, i)
00120 
00121     def add_templates(self, templates):
00122         """
00123         Add a list of templates to the section.
00124         """
00125         templates.sort()
00126         for i in templates:
00127             self._templates.insert(0, i)
00128 
00129     def get_merged(self, key):
00130         """Return a list of values for a given key merged from default(s)"""
00131         # first merge key/values from defaults together
00132         merged = []
00133         for i in reversed(self._defaults):
00134             if not merged:
00135                 merged = i
00136                 continue
00137             merged = merge_values(merged, i, key)
00138 
00139         for i in reversed(self._templates):
00140             if not merged:
00141                 merged = i
00142                 continue
00143             merged = merge_values(merged, i, key)
00144 
00145         # then merge self in
00146         return merge_values(merged, self, key)
00147 
00148 ###############################################################################
00149 
00150 COMMENT = ';'
00151 COMMENT_START = ';--'
00152 COMMENT_END = '--;'
00153 
00154 DEFAULTSECT = 'general'
00155 
00156 
00157 def remove_comment(line, is_comment):
00158     """Remove any commented elements from the line."""
00159     if not line:
00160         return line, is_comment
00161 
00162     if is_comment:
00163         part = line.partition(COMMENT_END)
00164         if part[1]:
00165             # found multi-line comment end check string after it
00166             return remove_comment(part[2], False)
00167         return "", True
00168 
00169     part = line.partition(COMMENT_START)
00170     if part[1]:
00171         # found multi-line comment start check string before
00172         # it to make sure there wasn't an eol comment in it
00173         has_comment = part[0].partition(COMMENT)
00174         if has_comment[1]:
00175             # eol comment found return anything before it
00176             return has_comment[0], False
00177 
00178         # check string after it to see if the comment ends
00179         line, is_comment = remove_comment(part[2], True)
00180         if is_comment:
00181             # return possible string data before comment
00182             return part[0].strip(), True
00183 
00184         # otherwise it was an embedded comment so combine
00185         return ''.join([part[0].strip(), ' ', line]).rstrip(), False
00186 
00187     # check for eol comment
00188     return line.partition(COMMENT)[0].strip(), False
00189 
00190 
00191 def try_include(line):
00192     """
00193     Checks to see if the given line is an include.  If so return the
00194     included filename, otherwise None.
00195     """
00196 
00197     match = re.match('^#include\s*[<"]?(.*)[>"]?$', line)
00198     return match.group(1) if match else None
00199 
00200 
00201 def try_section(line):
00202     """
00203     Checks to see if the given line is a section. If so return the section
00204     name, otherwise return 'None'.
00205     """
00206     # leading spaces were stripped when checking for comments
00207     if not line.startswith('['):
00208         return None, False, []
00209 
00210     section, delim, templates = line.partition(']')
00211     if not templates:
00212         return section[1:], False, []
00213 
00214     # strip out the parens and parse into an array
00215     templates = templates.replace('(', "").replace(')', "").split(',')
00216     # go ahead and remove extra whitespace
00217     templates = [i.strip() for i in templates]
00218     try:
00219         templates.remove('!')
00220         return section[1:], True, templates
00221     except:
00222         return section[1:], False, templates
00223 
00224 
00225 def try_option(line):
00226     """Parses the line as an option, returning the key/value pair."""
00227     data = re.split('=>?', line)
00228     # should split in two (key/val), but either way use first two elements
00229     return data[0].rstrip(), data[1].lstrip()
00230 
00231 ###############################################################################
00232 
00233 
00234 def find_dict(mdicts, key, val):
00235     """
00236     Given a list of mult-dicts, return the multi-dict that contains
00237     the given key/value pair.
00238     """
00239 
00240     def found(d):
00241         return key in d and val in d[key]
00242 
00243     try:
00244         return [d for d in mdicts if found(d)][0]
00245     except IndexError:
00246         raise LookupError("Dictionary not located for key = %s, value = %s"
00247                           % (key, val))
00248 
00249 
00250 def write_dicts(config_file, mdicts):
00251     """Write the contents of the mdicts to the specified config file"""
00252     for section, sect_list in mdicts.iteritems():
00253         # every section contains a list of dictionaries
00254         for sect in sect_list:
00255             config_file.write("[%s]\n" % section)
00256             for key, val_list in sect.iteritems():
00257                 # every value is also a list
00258                 for v in val_list:
00259                     key_val = key
00260                     if v is not None:
00261                         key_val += " = " + str(v)
00262                         config_file.write("%s\n" % (key_val))
00263             config_file.write("\n")
00264 
00265 ###############################################################################
00266 
00267 
00268 class MultiOrderedConfigParser:
00269     def __init__(self, parent=None):
00270         self._parent = parent
00271         self._defaults = MultiOrderedDict()
00272         self._sections = MultiOrderedDict()
00273         self._includes = OrderedDict()
00274 
00275     def find_value(self, sections, key):
00276         """Given a list of sections, try to find value(s) for the given key."""
00277         # always start looking in the last one added
00278         sections.sort(reverse=True)
00279         for s in sections:
00280             try:
00281                 # try to find in section and section's templates
00282                 return s.get(key, from_defaults=False)
00283             except KeyError:
00284                 pass
00285 
00286         # wasn't found in sections or a section's templates so check in
00287         # defaults
00288         for s in sections:
00289             try:
00290                 # try to find in section's defaultsects
00291                 return s.get(key, from_self=False, from_templates=False)
00292             except KeyError:
00293                 pass
00294 
00295         raise KeyError(key)
00296 
00297     def defaults(self):
00298         return self._defaults
00299 
00300     def default(self, key):
00301         """Retrieves a list of dictionaries for a default section."""
00302         return self.get_defaults(key)
00303 
00304     def add_default(self, key, template_keys=None):
00305         """
00306         Adds a default section to defaults, returning the
00307         default Section object.
00308         """
00309         if template_keys is None:
00310             template_keys = []
00311         return self.add_section(key, template_keys, self._defaults)
00312 
00313     def sections(self):
00314         return self._sections
00315 
00316     def section(self, key):
00317         """Retrieves a list of dictionaries for a section."""
00318         return self.get_sections(key)
00319 
00320     def get_sections(self, key, attr='_sections', searched=None):
00321         """
00322         Retrieve a list of sections that have values for the given key.
00323         The attr parameter can be used to control what part of the parser
00324         to retrieve values from.
00325         """
00326         if searched is None:
00327             searched = []
00328         if self in searched:
00329             return []
00330 
00331         sections = getattr(self, attr)
00332         res = sections[key] if key in sections else []
00333         searched.append(self)
00334         if self._includes:
00335             res.extend(list(itertools.chain(*[
00336                 incl.get_sections(key, attr, searched)
00337                 for incl in self._includes.itervalues()])))
00338         if self._parent:
00339             res += self._parent.get_sections(key, attr, searched)
00340         return res
00341 
00342     def get_defaults(self, key):
00343         """
00344         Retrieve a list of defaults that have values for the given key.
00345         """
00346         return self.get_sections(key, '_defaults')
00347 
00348     def add_section(self, key, template_keys=None, mdicts=None):
00349         """
00350         Create a new section in the configuration. The name of the
00351         new section is the 'key' parameter.
00352         """
00353         if template_keys is None:
00354             template_keys = []
00355         if mdicts is None:
00356             mdicts = self._sections
00357         res = Section()
00358         for t in template_keys:
00359             res.add_templates(self.get_defaults(t))
00360         res.add_defaults(self.get_defaults(DEFAULTSECT))
00361         mdicts.insert(0, key, res)
00362         return res
00363 
00364     def includes(self):
00365         return self._includes
00366 
00367     def add_include(self, filename, parser=None):
00368         """
00369         Add a new #include file to the configuration.
00370         """
00371         if filename in self._includes:
00372             return self._includes[filename]
00373 
00374         self._includes[filename] = res = \
00375             MultiOrderedConfigParser(self) if parser is None else parser
00376         return res
00377 
00378     def get(self, section, key):
00379         """Retrieves the list of values from a section for a key."""
00380         try:
00381             # search for the value in the list of sections
00382             return self.find_value(self.section(section), key)
00383         except KeyError:
00384             pass
00385 
00386         try:
00387             # section may be a default section so, search
00388             # for the value in the list of defaults
00389             return self.find_value(self.default(section), key)
00390         except KeyError:
00391             raise LookupError("key %r not found for section %r"
00392                               % (key, section))
00393 
00394     def multi_get(self, section, key_list):
00395         """
00396         Retrieves the list of values from a section for a list of keys.
00397         This method is intended to be used for equivalent keys. Thus, as soon
00398         as any match is found for any key in the key_list, the match is
00399         returned. This does not concatenate the lookups of all of the keys
00400         together.
00401         """
00402         for i in key_list:
00403             try:
00404                 return self.get(section, i)
00405             except LookupError:
00406                 pass
00407 
00408         # Making it here means all lookups failed.
00409         raise LookupError("keys %r not found for section %r" %
00410                           (key_list, section))
00411 
00412     def set(self, section, key, val):
00413         """Sets an option in the given section."""
00414         # TODO - set in multiple sections? (for now set in first)
00415         # TODO - set in both sections and defaults?
00416         if section in self._sections:
00417             self.section(section)[0][key] = val
00418         else:
00419             self.defaults(section)[0][key] = val
00420 
00421     def read(self, filename, sect=None):
00422         """Parse configuration information from a file"""
00423         try:
00424             with open(filename, 'rt') as config_file:
00425                 self._read(config_file, sect)
00426         except IOError:
00427             print "Could not open file ", filename, " for reading"
00428 
00429     def _read(self, config_file, sect):
00430         """Parse configuration information from the config_file"""
00431         is_comment = False  # used for multi-lined comments
00432         for line in config_file:
00433             line, is_comment = remove_comment(line, is_comment)
00434             if not line:
00435                 # line was empty or was a comment
00436                 continue
00437 
00438             include_name = try_include(line)
00439             if include_name:
00440                 parser = self.add_include(include_name)
00441                 parser.read(include_name, sect)
00442                 continue
00443 
00444             section, is_template, templates = try_section(line)
00445             if section:
00446                 if section == DEFAULTSECT or is_template:
00447                     sect = self.add_default(section, templates)
00448                 else:
00449                     sect = self.add_section(section, templates)
00450                 continue
00451 
00452             key, val = try_option(line)
00453             if sect is None:
00454                 raise Exception("Section not defined before assignment")
00455             sect[key] = val
00456 
00457     def write(self, config_file):
00458         """Write configuration information out to a file"""
00459         try:
00460             for key, val in self._includes.iteritems():
00461                 val.write(key)
00462                 config_file.write('#include "%s"\n' % key)
00463 
00464             config_file.write('\n')
00465             write_dicts(config_file, self._defaults)
00466             write_dicts(config_file, self._sections)
00467         except:
00468             try:
00469                 with open(config_file, 'wt') as fp:
00470                     self.write(fp)
00471             except IOError:
00472                 print "Could not open file ", config_file, " for writing"

Generated on Thu Apr 16 06:27:14 2015 for Asterisk - The Open Source Telephony Project by  doxygen 1.5.6