github.com/hwaf/hwaf@v0.0.0-20140814122253-5465f73b20f1/py-hwaftools/orch/deconf.py (about)

     1  #!/usr/bin/env python
     2  '''A descending configuration parser.
     3  
     4  This parser provides for composing a hierarchical data structure from
     5  a Python (ConfigParser) configuration file.  It also allows for a more
     6  flexible string interpolation than the base parser.
     7  
     8  The file is parsed (see parse() function) by the usual ConfigParser
     9  module and the resulting data is interpreted (see interpret()
    10  function) into a dictionary.  Two special sections are checked.
    11  
    12   - [keytype] :: a mapping of a key name to a "section type"
    13  
    14   - [<name>] :: the starting point of interpretation, default <name> is "start"
    15  
    16  The [keytype] section maps a key name to a section type.  When a key
    17  is found in a section which matches a keytype key the key is
    18  interpreted as a comma-separated list of section names and its value
    19  in the keytype dictionary as a section type.  A section expresses its
    20  type and name by starting with a line that matches "[<type> <name>]".
    21  When a matching key is encountered the keytype mapping and the list of
    22  names are used to set a list on that key in the returned data
    23  structure which contains the result of interpreting the referred to
    24  sections.
    25  
    26  This interpretation continues until all potential sections have been
    27  loaded.  Not all sections in the file may be represented in the
    28  resulting data structure if they have not been referenced through the
    29  keytype mechanism described above.
    30  
    31  After interpretation the data structure is inflated (see inflate()
    32  function).  The result is a new dictionary in which any daughter
    33  dictionaries now contain all scalar, string (non-list, non-dictionary)
    34  entries from all its mothers.
    35  
    36  Finally, each dictionary in the hierarchy has all of its scalar,
    37  string entries interpolated (see format_any() function).  This portion
    38  of the dictionary is itself used as a source of "{variable}"
    39  interpolation.  Infinite loops must be avoided.
    40  
    41  By default a scalar, string value is formatted via its string format()
    42  function.  A "formatter(string, **kwds)" function can be provided to
    43  provide customized formatting.
    44  
    45  This formatting continues until all possible interpolations have been
    46  resolved.  Any unresolved ones will be retained.  This interpolation
    47  is done on intermediate dictionaries but it is only the ones at leafs
    48  of the hierarchy which are likely useful to the application.
    49  
    50  Either a single filename or a list of filenames can be given for
    51  parsing.  The [start] section may include a special key "includes" to
    52  specify a list of other configuration files to also parse.  The files
    53  are searched first in the current working directory, next in the
    54  directories holding the files already specified and next from any
    55  colon-separated list of directories specified by a DECONF_INCLUDE_PATH
    56  environment variable.
    57  
    58  The section "defaults" may provide additional configuration items to
    59  place in the "start" section.  This allows a single file to specify
    60  defaults used by multiple main configuration files
    61  '''
    62  
    63  import os
    64  
    65  def merge_defaults(cfg, start = 'start', sections = 'defaults'):
    66      if isinstance(sections, type('')):
    67          sections = [x.strip() for x in sections.split(',')]
    68      #print 'CFG sections:', cfg.sections()
    69      for sec in sections:
    70          if not cfg.has_section(sec):
    71              continue
    72          for k,v in cfg.items(sec):
    73              if cfg.has_option(start, k):
    74                  continue
    75              #print 'DECONF: merging',k,v
    76              cfg.set(start,k,v)
    77  
    78  def parse(filename):
    79      'Parse the filename, return an uninterpreted object'
    80      try:                from ConfigParser import SafeConfigParser
    81      except ImportError: from configparser import SafeConfigParser
    82      cfg = SafeConfigParser()
    83      cfg.optionxform = str       # want case sensitive
    84      cfg.read(filename)
    85      if isinstance(filename, type("")):
    86          filename = [filename]
    87      cfg.files = filename
    88  
    89      return cfg
    90  
    91  def to_list(lst):
    92      return [x.strip() for x in lst.split(',')]
    93  
    94  def get_first_typed_section(cfg, typ, name):
    95      target = '%s %s' % (typ, name)
    96      for sec in cfg.sections():
    97          if sec == target:
    98              return sec
    99      raise ValueError('No section: <%s> %s' % (typ,name))
   100  
   101  
   102  def find_file(fname, others = None):
   103      '''Find file <fname> in current working directory, in the "others"
   104      directories and finally in a environment path variable
   105      DECONF_INCLUDE_PATH.'''
   106  
   107      dirs = ['.'] 
   108      if others:
   109          dirs += others
   110      dirs += os.environ.get('DECONF_INCLUDE_PATH','').split(':')
   111  
   112      for check in dirs:
   113          maybe = os.path.join(check, fname)
   114          if os.path.exists(maybe):
   115              return maybe
   116      return
   117      
   118  
   119  def add_includes(cfg, sec):
   120      '''
   121      Resolves any "includes" item in section <sec> by reading the
   122      specified files into the cfg object.
   123      '''
   124      if not cfg.has_option(sec,'includes'):
   125          return
   126  
   127      inc_val = cfg.get(sec,'includes')
   128      inc_list = to_list(inc_val)
   129      if not inc_list:
   130          raise ValueError('Not includes: "%s"' % inc_val)
   131          return
   132  
   133      to_check = ['.']
   134      if hasattr(cfg, 'files') and cfg.files:
   135          already_read = cfg.files
   136          if isinstance(already_read, type('')):
   137              already_read = [already_read]
   138          other_dirs = map(os.path.realpath, map(os.path.dirname, already_read))
   139          to_check += other_dirs
   140      to_check += os.environ.get('DECONF_INCLUDE_PATH','').split(':')
   141  
   142      for fname in inc_list:
   143          fpath = find_file(fname, to_check)
   144          if not fpath:
   145              raise ValueError(
   146                  'Failed to locate file: %s (%s)' % 
   147                  (fname, ':'.join(to_check))
   148                  )
   149          cfg.read(fpath)
   150          if hasattr(cfg, 'files'):
   151              cfg.files.append(fpath)
   152      return
   153  
   154  def resolve(cfg, sec, **kwds):
   155      'Recursively load the configuration starting at the given section'
   156  
   157      add_includes(cfg, sec)
   158  
   159      secitems = dict(cfg.items(sec))
   160  
   161      # get special keytype section governing the hierarchy schema
   162      keytype = dict(cfg.items('keytype'))
   163  
   164      ret = {}
   165      for k,v in secitems.items():
   166          typ = keytype.get(k)
   167          if not typ:
   168              ret[k] = v
   169              continue
   170          lst = []
   171          for name in to_list(v):
   172              other_sec = get_first_typed_section(cfg, typ, name)
   173              other = resolve(cfg, other_sec, **kwds)
   174              other.setdefault(typ,name)
   175              lst.append(other)
   176          ret[k] = lst
   177      return ret
   178  
   179  def interpret(cfg, start = 'start', **kwds):
   180      '''
   181      Interpret a parsed file by following any keytypes, return raw data
   182      structure.
   183  
   184      The <start> keyword can select a different section to start the
   185      interpretation.  Any additional keywords are override or otherwise
   186      added to the initial section.
   187      '''
   188      return resolve(cfg, start, **kwds)
   189  
   190  def format_flat_dict(dat, formatter = str.format, **kwds):
   191      kwds = dict(kwds)
   192      unformatted = dict(dat)
   193      formatted = dict()
   194  
   195      while unformatted:
   196          changed = False
   197          for k,v in list(unformatted.items()):
   198              try:
   199                  new_v = formatter(v, **kwds)
   200              except KeyError:
   201                  continue        # maybe next time
   202              except TypeError:   # can't be formatted
   203                  new_v = v       # pretend we just did
   204  
   205              changed = True
   206              formatted[k] = new_v
   207              kwds[k] = new_v
   208              unformatted.pop(k)
   209              continue
   210          if not changed:
   211              break
   212          continue
   213      if unformatted:
   214          formatted.update(unformatted)
   215      return formatted
   216  
   217  def format_any(dat, formatter = str.format, **kwds):
   218      if dat is None:
   219          return dat
   220      if isinstance(dat, type(b"")):
   221          dat = "%s" % dat  # py3: bytes -> string
   222      if isinstance(dat, type("")):
   223          try:
   224              return formatter(dat, **kwds)
   225          except KeyError:
   226              return dat
   227      if isinstance(dat, list):
   228          return [format_any(x, formatter=formatter, **kwds) for x in dat]
   229      flat = dict()
   230      other = dict()
   231      for k,v in dat.items():
   232          if isinstance(v, type("")):
   233              flat[k] = v
   234          else:
   235              other[k] = v
   236      ret = format_flat_dict(flat, formatter=formatter, **kwds)
   237      for k,v in other.items():
   238          v = format_any(v, formatter=formatter, **kwds)
   239          ret[k] = v
   240      return ret
   241  
   242  
   243  def inflate(src, defaults = None):
   244      '''
   245      Copy scalar dictionary entries down the hierarchy to other
   246      dictionaries.
   247      '''
   248      if not defaults: defaults = dict()
   249      ret = dict()
   250      ret.update(defaults)
   251      ret.update(src)
   252  
   253      flat = dict()
   254      other = dict()
   255      for k,v in ret.items():
   256          if isinstance(v, type("")):
   257              flat[k] = v
   258          else:
   259              other[k] = v
   260      for key,lst in other.items():
   261          ret[key] = [inflate(x, flat) for x in lst]
   262  
   263      return ret
   264  
   265  def load(filename, start = 'start', formatter = str.format, **kwds):
   266      '''
   267      Return the fully parsed, interpreted, inflated and formatted suite.
   268      '''
   269      if not filename:
   270          raise ValueError('deconf.load not given any files to load')
   271  
   272      cfg = parse(filename)
   273      add_includes(cfg,start)
   274      merge_defaults(cfg, start)
   275      data = interpret(cfg, start, **kwds)
   276      data2 = inflate(data)
   277      if not formatter:
   278          return data2
   279      data3 = format_any(data2, formatter=formatter, **kwds)
   280      return data3
   281  
   282  
   283  
   284  ### testing
   285  
   286  def extra_formatter(string, **kwds):
   287      tags = kwds.get('tags')
   288      if tags:
   289          tags = [x.strip() for x in tags.split(',')]
   290          kwds.setdefault('tagsdashed',  '-'.join(tags))
   291          kwds.setdefault('tagsunderscore', '_'.join(tags))
   292      
   293      version = kwds.get('version')
   294      if version:
   295          kwds.setdefault('version_2digit', '.'.join(version.split('.')[:2]))
   296          kwds.setdefault('version_underscore', version.replace('.','_'))
   297          kwds.setdefault('version_nodots', version.replace('.',''))
   298  
   299      return string.format(**kwds)
   300      
   301  
   302  def example_formatter(string, **kwds):
   303      '''
   304      Example to add extra formatting
   305      '''
   306      kwds.setdefault('prefix','/tmp/simple')
   307      kwds.setdefault('PREFIX','/tmp/simple')
   308      ret = extra_formatter(string, **kwds)
   309  
   310      return ret
   311  
   312  
   313  def test():
   314      from pprint import PrettyPrinter
   315      pp = PrettyPrinter(indent=2)
   316  
   317      cfg = parse('deconf.cfg')
   318      data = interpret(cfg)
   319      data2 = inflate(data)
   320      data3 = format_any(data2)
   321  
   322      data4 = load('deconf.cfg')
   323      assert data3 == data4
   324  
   325      print ('INTERPRETED:')
   326      pp.pprint(data)
   327      print ('INFLATED:')
   328      pp.pprint(data2)
   329      print ('FORMATTED:')
   330      pp.pprint(data3)
   331  
   332  def dump(filename, start='start', formatter=str.format):
   333      from pprint import PrettyPrinter
   334      pp = PrettyPrinter(indent=2)
   335      data = load(filename, start=start, formatter=example_formatter)
   336      print ('Starting from "%s"' % start)
   337      pp.pprint(data)
   338  
   339  if '__main__' == __name__:
   340      import sys
   341      dump(sys.argv[1:])