Package Gnumed :: Package pycommon :: Module gmCfg2 |
[frames] | no frames]

# Source Code for Module Gnumed.pycommon.gmCfg2

      1  """GNUmed configuration handling.
      2  """
      3  #==================================================================
      4  # $Source: /home/ncq/Projekte/cvs2git/vcs-mirror/gnumed/gnumed/client/pycommon/gmCfg2.py,v $
      5  __version__ = "$Revision: 1.20 $"
      6  __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
      7  __licence__ = "GPL"
      8
      9
     10  import logging, sys, codecs, re as regex, shutil, os, types
     11
     12
     13  if __name__ == "__main__":
     14          sys.path.insert(0, '../../')
     15  from Gnumed.pycommon import gmBorg
     16
     17
     18  _log = logging.getLogger('gm.cfg')
     19  _log.info(__version__)
     20  #==================================================================
     21  # helper functions
     22  #==================================================================
     23 -def __set_opt_in_INI_file(src=None, sink=None, group=None, option=None, value=None):
     24
     25          group_seen = False
     26          option_seen = False
     27          in_list = False
     28
     29          for line in src:
     30
     31                  # after option already ?
     32                  if option_seen:
     33                          sink.write(line)
     34                          continue
     35
     36                  # start of list ?
     37                  if regex.match('(?P<list_name>.+)(\s|\t)*=(\s|\t)*\$(?P=list_name)\$', line) is not None:
     38                          in_list = True
     39                          sink.write(line)
     40                          continue
     41
     42                  # end of list ?
     43                  if regex.match('\$.+\$.*', line) is not None:
     44                          in_list = False
     45                          sink.write(line)
     46                          continue
     47
     48                  # our group ?
     49                  if line.strip() == u'[%s]' % group:
     50                          group_seen = True
     51                          sink.write(line)
     52                          continue
     53
     54                  # another group ?
     55                  if regex.match('\[.+\].*', line) is not None:
     56                          # next group but option not seen yet ?
     57                          if group_seen and not option_seen:
     58                                  sink.write(u'%s = %s\n\n\n' % (option, value))
     59                                  option_seen = True
     60                          sink.write(line)
     61                          continue
     62
     63                  # our option ?
     64                  if regex.match('%s(\s|\t)*=' % option, line) is not None:
     65                          if group_seen:
     66                                  sink.write(u'%s = %s\n' % (option, value))
     67                                  option_seen = True
     68                                  continue
     69                          sink.write(line)
     70                          continue
     71
     72                  # something else (comment, empty line, or other option)
     73                  sink.write(line)
     74
     75          # all done ?
     76          if option_seen:
     77                  return
     78
     79          # need to add group ?
     80          if not group_seen:
     81                  sink.write('[%s]\n' % group)
     82
     83          # We either just added the group or it was the last group
     84          # but did not contain the option. It must have been the
     85          # last group then or else the following group would have
     86          # triggered the option writeout.
     87          sink.write(u'%s = %s\n' % (option, value))
     88  #==================================================================
     89 -def __set_list_in_INI_file(src=None, sink=None, group=None, option=None, value=None):
     90
     91          group_seen = False
     92          option_seen = False
     93          in_list = False
     94
     95          for line in src:
     96
     97                  # found option but still in (old) list ?
     98                  if option_seen and in_list:
     99                          # end of (old) list ?
    100                          if regex.match('\$.+\$.*', line) is not None:
    101                                  in_list = False
    102                                  sink.write(line)
    103                                  continue
    104                          continue
    105
    106                  # after option already and not in (old) list anymore ?
    107                  if option_seen and not in_list:
    108                          sink.write(line)
    109                          continue
    110
    111                  # start of list ?
    112                  match = regex.match('(?P<list_name>.+)(\s|\t)*=(\s|\t)*\$(?P=list_name)\$', line)
    113                  if match is not None:
    114                          in_list = True
    115                          # our list ?
    116                          if group_seen and (match.group('list_name') == option):
    117                                  option_seen = True
    118                                  sink.write(line)
    119                                  sink.write('\n'.join(value))
    120                                  sink.write('\n')
    121                                  continue
    122                          sink.write(line)
    123                          continue
    124
    125                  # end of list ?
    126                  if regex.match('\$.+\$.*', line) is not None:
    127                          in_list = False
    128                          sink.write(line)
    129                          continue
    130
    131                  # our group ?
    132                  if line.strip() == u'[%s]' % group:
    133                          group_seen = True
    134                          sink.write(line)
    135                          continue
    136
    137                  # another group ?
    138                  if regex.match('\[%s\].*' % group, line) is not None:
    139                          # next group but option not seen yet ?
    140                          if group_seen and not option_seen:
    141                                  option_seen = True
    142                                  sink.write('%s = $%s$\n' % (option, option))
    143                                  sink.write('\n'.join(value))
    144                                  sink.write('\n')
    145                                  continue
    146                          sink.write(line)
    147                          continue
    148
    149                  # something else (comment, empty line, or other option)
    150                  sink.write(line)
    151
    152          # all done ?
    153          if option_seen:
    154                  return
    155
    156          # need to add group ?
    157          if not group_seen:
    158                  sink.write('[%s]\n' % group)
    159
    160          # We either just added the group or it was the last group
    161          # but did not contain the option. It must have been the
    162          # last group then or else the following group would have
    163          # triggered the option writeout.
    164          sink.write('%s = $%s$\n' % (option, option))
    165          sink.write('\n'.join(value))
    166          sink.write('\n')
    167          sink.write('$%s$\n' % option)
    168  #==================================================================
    169 -def set_option_in_INI_file(filename=None, group=None, option=None, value=None, encoding='utf8'):
    170
    171          _log.debug('setting option "%s" to "%s" in group [%s]', option, value, group)
    172          _log.debug('file: %s (%s)', filename, encoding)
    173
    174          src = codecs.open(filename = filename, mode = 'rU', encoding = encoding)
    175          # FIXME: add "." right before the *name* part of filename - this
    176          # FIXME: requires proper parsing (think of /home/lala/ -> ./home/lala vs /home/lala/gnumed/.gnumed.conf)
    177          sink_name = '%s.gmCfg2.new.conf' % filename
    178          sink = codecs.open(filename = sink_name, mode = 'wb', encoding = encoding)
    179
    180          # is value a list ?
    181          if isinstance(value, type([])):
    182                  __set_list_in_INI_file(src, sink, group, option, value)
    183          else:
    184                  __set_opt_in_INI_file(src, sink, group, option, value)
    185
    186          sink.close()
    187          src.close()
    188
    189          shutil.copy2(sink_name, filename)
    190          os.remove(sink_name)
    191  #==================================================================
    192 -def parse_INI_stream(stream=None):
    193          """Parse an iterable for INI-style data.
    194
    195          Returns a dict by sections containing a dict of values per section.
    196          """
    197          _log.debug(u'parsing INI-style data stream [%s]' % stream)
    198
    199          data = {}
    200          current_group = None
    201          current_option = None
    202          current_option_path = None
    203          inside_list = False
    204          line_idx = 0
    205
    206          for line in stream:
    207                  line = line.replace(u'\015', u'').replace(u'\012', u'').strip()
    208                  line_idx += 1
    209
    210                  if inside_list:
    211                          if line == u'$%s$' % current_option:            # end of list
    212                                  inside_list = False
    213                                  continue
    214                          data[current_option_path].append(line)
    215                          continue
    216
    217                  # noise
    218                  if line == u'' or line.startswith(u'#') or line.startswith(u';'):
    219                          continue
    220
    221                  # group
    222                  if line.startswith(u'['):
    223                          if not line.endswith(u']'):
    224                                  _log.error(u'group line does not end in "]", aborting')
    225                                  _log.error(line)
    226                                  raise ValueError('INI-stream parsing error')
    227                          group = line.strip(u'[]').strip()
    228                          if group == u'':
    229                                  _log.error(u'group name is empty, aborting')
    230                                  _log.error(line)
    231                                  raise ValueError('INI-stream parsing error')
    232                          current_group = group
    233                          continue
    234
    235                  # option
    236                  if current_group is None:
    237                          _log.warning('option found before first group, ignoring')
    238                          _log.error(line)
    239                          continue
    240
    241                  name, remainder = regex.split('\s*[=:]\s*', line, maxsplit = 1)
    242                  if name == u'':
    243                          _log.error('option name empty, aborting')
    244                          _log.error(line)
    245                          raise ValueError('INI-stream parsing error')
    246
    247                  if remainder.strip() == u'':
    248                          if (u'=' not in line) and (u':' not in line):
    249                                  _log.error('missing name/value separator (= or :), aborting')
    250                                  _log.error(line)
    251                                  raise ValueError('INI-stream parsing error')
    252
    253                  current_option = name
    254                  current_option_path = '%s::%s' % (current_group, current_option)
    255                  if data.has_key(current_option_path):
    256                          _log.warning(u'duplicate option [%s]', current_option_path)
    257
    258                  value = remainder.split(u'#', 1)[0].strip()
    259
    260                  # start of list ?
    261                  if value == '$%s$' % current_option:
    262                          inside_list = True
    263                          data[current_option_path] = []
    264                          continue
    265
    266                  data[current_option_path] = value
    267
    268          if inside_list:
    269                  _log.critical('unclosed list $%s$ detected at end of config stream [%s]', current_option, stream)
    270                  raise SyntaxError('end of config stream but still in list')
    271
    272          return data
    273  #==================================================================
    274 -class gmCfgData(gmBorg.cBorg):
    275
    276 -        def __init__(self):
    277                  try:
    278                          self.__cfg_data
    279                  except AttributeError:
    280                          self.__cfg_data = {}
    281                          self.source_files = {}
    282          #--------------------------------------------------
    283 -        def get(self, group=None, option=None, source_order=None):
    284                  """Get the value of a configuration option in a config file.
    285
    286                  <source_order> the order in which config files are searched
    287                          a list of tuples (source, policy)
    288                          policy:
    289                                  return: return only this value immediately
    290                                  append: append to list of potential values to return
    291                                  extend: if the value per source happens to be a list
    292                                          extend (rather than append to) the result list
    293
    294                  returns NONE when there's no value for an option
    295                  """
    296                  if source_order is None:
    297                          source_order = [(u'internal', u'return')]
    298                  results = []
    299                  for source, policy in source_order:
    300                          if group is None:
    301                                  group = source
    302                          option_path = u'%s::%s' % (group, option)
    303                          try: source_data = self.__cfg_data[source]
    304                          except KeyError:
    305                                  _log.error('invalid config source [%s]', source)
    306                                  _log.debug('currently known sources: %s', self.__cfg_data.keys())
    307                                  #raise
    308                                  continue
    309
    310                          try: value = source_data[option_path]
    311                          except KeyError:
    312                                  _log.debug('option [%s] not in group [%s] in source [%s]', option, group, source)
    313                                  continue
    314                          _log.debug(u'option [%s] found in source [%s]', option_path, source)
    315
    316                          if policy == u'return':
    317                                  return value
    318
    319                          if policy == u'extend':
    320                                  if isinstance(value, types.ListType):
    321                                          results.extend(value)
    322                                  else:
    323                                          results.append(value)
    324                          else:
    325                                  results.append(value)
    326
    327                  if len(results) == 0:
    328                          return None
    329
    330                  return results
    331          #--------------------------------------------------
    332 -        def set_option(self, option=None, value=None, group=None, source=None):
    333                  """Set a particular option to a particular value.
    334
    335                  Note that this does NOT PERSIST the option anywhere !
    336                  """
    337                  if None in [option, value]:
    338                          raise ValueError('neither <option> nor <value> can be None')
    339                  if source is None:
    340                          source = u'internal'
    341                          try:
    342                                  self.__cfg_data[source]
    343                          except KeyError:
    344                                  self.__cfg_data[source] = {}
    345                  if group is None:
    346                          group = source
    347                  option_path = u'%s::%s' % (group, option)
    348                  self.__cfg_data[source][option_path] = value
    349          #--------------------------------------------------
    350          # API: source related
    351          #--------------------------------------------------
    352 -        def add_stream_source(self, source=None, stream=None):
    353
    354                  try:
    355                          data = parse_INI_stream(stream = stream)
    356                  except ValueError:
    357                          _log.exception('error parsing source <%s> from [%s]', source, stream)
    358                          raise
    359
    360                  if self.__cfg_data.has_key(source):
    361                          _log.warning('overriding source <%s> with [%s]', source, stream)
    362
    363                  self.__cfg_data[source] = data
    364          #--------------------------------------------------
    365 -        def add_file_source(self, source=None, file=None, encoding='utf8'):
    366                  """Add a source (a file) to the instance."""
    367
    368                  _log.info('file source "%s": %s (%s)', source, file, encoding)
    369
    370                  for existing_source, existing_file in self.source_files.iteritems():
    371                          if existing_file == file:
    372                                  if source != existing_source:
    373                                          _log.warning('file [%s] already known as source [%s]', file, existing_source)
    374                                          _log.warning('adding it as source [%s] may provoke trouble', source)
    375
    376                  cfg_file = None
    377                  if file is not None:
    378                          try:
    379                                  cfg_file = codecs.open(filename = file, mode = 'rU', encoding = encoding)
    380                          except IOError:
    381                                  _log.error('cannot open [%s], keeping as dummy source', file)
    382
    383                  if cfg_file is None:
    384                          file = None
    385                          if self.__cfg_data.has_key(source):
    386                                  _log.warning('overriding source <%s> with dummy', source)
    387                          self.__cfg_data[source] = {}
    388                  else:
    389                          self.add_stream_source(source = source, stream = cfg_file)
    390                          cfg_file.close()
    391
    392                  self.source_files[source] = file
    393          #--------------------------------------------------
    394 -        def remove_source(self, source):
    395                  """Remove a source from the instance."""
    396
    397                  _log.info('removing source <%s>', source)
    398
    399                  try:
    400                          del self.__cfg_data[source]
    401                  except KeyError:
    402                          _log.warning("source <%s> doesn't exist", source)
    403
    404                  try:
    405                          del self.source_files[source]
    406                  except KeyError:
    407                          pass
    408          #--------------------------------------------------
    409 -        def reload_file_source(self, file=None, encoding='utf8'):
    410                  if file not in self.source_files.values():
    411                          return
    412
    413                  for src, fname in self.source_files.iteritems():
    414                          if fname == file:
    415                                  self.add_file_source(source = src, file = fname, encoding = encoding)
    416                                  # don't break the loop because there could be other sources
    417                                  # with the same file (not very reasonable, I know)
    418                                  #break
    419          #--------------------------------------------------
    420 -        def add_cli(self, short_options=u'', long_options=None):
    421                  """Add command line parameters to config data.
    422
    423                  short:
    424                          string containing one-letter options such as u'h?' for -h -?
    425                  long:
    426                          list of strings
    427                          'conf-file=' -> --conf-file=<...>
    428                          'debug' -> --debug
    429                  """
    430                  _log.info('adding command line arguments')
    431                  _log.debug('raw command line is:')
    432                  _log.debug('%s', sys.argv)
    433
    434                  import getopt
    435
    436                  if long_options is None:
    437                          long_options = []
    438
    439                  opts, remainder = getopt.gnu_getopt (
    440                          sys.argv[1:],
    441                          short_options,
    442                          long_options
    443                  )
    444
    445                  data = {}
    446                  for opt, val in opts:
    447                          if val == u'':
    448                                  data[u'%s::%s' % (u'cli', opt)] = True
    449                          else:
    450                                  data[u'%s::%s' % (u'cli', opt)] = val
    451
    452                  self.__cfg_data[u'cli'] = data
    453  #==================================================================
    454  # main
    455  #==================================================================
    456  if __name__ == "__main__":
    457
    458          logging.basicConfig(level = logging.DEBUG)
    459          #-----------------------------------------
    460 -        def test_gmCfgData():
    461                  cfg = gmCfgData()
    462                  cfg.add_cli(short_options=u'h?', long_options=[u'help', u'conf-file='])
    463                  cfg.set_option('internal option', True)
    464                  print cfg.get(option = '--help', source_order = [('cli', 'return')])
    465                  print cfg.get(option = '-?', source_order = [('cli', 'return')])
    466                  fname = cfg.get(option = '--conf-file', source_order = [('cli', 'return')])
    467                  if fname is not None:
    468                          cfg.add_file_source(source = 'explicit', file = fname)
    469          #-----------------------------------------
    470 -        def test_set_list_opt():
    471                  src = [
    472                          '# a comment',
    473                          '',
    474                          '[empty group]',
    475                          '[second group]',
    476                          'some option = in second group',
    477                          '# another comment',
    478                          '[test group]',
    479                          '',
    480                          'test list      = $test list$',
    481                          'old 1',
    482                          'old 2',
    483                          '$test list$',
    484                          '# another group:',
    485                          '[dummy group]'
    486                  ]
    487
    488                  __set_list_in_INI_file (
    489                          src = src,
    490                          sink = sys.stdout,
    491                          group = u'test group',
    492                          option = u'test list',
    493                          value = list('123')
    494                  )
    495          #-----------------------------------------
    496 -        def test_set_opt():
    497                  src = [
    498                          '# a comment',
    499                          '[empty group]',
    500                          '# another comment',
    501                          '',
    502                          '[second group]',
    503                          'some option = in second group',
    504                          '',
    505                          '[trap group]',
    506                          'trap list      = $trap list$',
    507                          'dummy 1',
    508                          'test option = a trap',
    509                          'dummy 2',
    510                          '$trap list$',
    511                          '',
    512                          '[test group]',
    513                          'test option = for real (old)',
    514                          ''
    515                  ]
    516
    517                  __set_opt_in_INI_file (
    518                          src = src,
    519                          sink = sys.stdout,
    520                          group = u'test group',
    521                          option = u'test option',
    522                          value = u'for real (new)'
    523                  )
    524          #-----------------------------------------
    525          if len(sys.argv) > 1 and sys.argv[1] == 'test':
    526                  test_gmCfgData()
    527                  #test_set_list_opt()
    528                  #test_set_opt()
    529
    530  #==================================================================
    531  # $Log: gmCfg2.py,v $
    532  # Revision 1.20  2009-06-10 21:00:01  ncq
    533  # - add remove-source
    534  #
    535  # Revision 1.19  2009/05/08 07:59:05  ncq
    536  # - .panic -> .critical
    537  #
    538  # Revision 1.18  2008/09/09 20:15:42  ncq
    539  # - warn on same-file different-source
    540  #
    541  # Revision 1.17  2008/08/31 16:12:12  ncq
    542  # - when getting from multiple sources, if policy is "extend",
    543  #   flatten list options into a single result list
    544  #
    545  # Revision 1.16  2008/08/31 14:51:42  ncq
    546  # - properly handle explicit file=None for dummy sources
    547  #
    548  # Revision 1.15  2008/08/03 20:03:11  ncq
    549  # - do not simply add "." before the entire path of the dummy
    550  #   conf file when setting option - it should go right before the name part
    551  #
    552  # Revision 1.14  2008/07/17 21:30:01  ncq
    553  # - detect unterminated list option
    554  #
    555  # Revision 1.13  2008/07/16 10:36:25  ncq
    556  # - fix two bugs in INI parsing
    557  # - better logging, some cleanup
    558  # - .reload_file_source
    559  #
    560  # Revision 1.12  2008/07/07 11:33:57  ncq
    561  # - a bit of cleanup
    562  #
    563  # Revision 1.11  2008/05/21 13:58:50  ncq
    564  # - factor out add_stream_source from add_file_source
    565  #
    566  # Revision 1.10  2008/03/09 20:15:29  ncq
    567  # - don't fail on non-existing sources
    568  # - cleanup
    569  # - better docs
    570  #
    571  # Revision 1.9  2008/01/27 21:09:38  ncq
    572  # - set_option_in_INI_file() and tests
    573  #
    574  # Revision 1.8  2008/01/11 16:10:35  ncq
    575  # - better logging
    576  #
    577  # Revision 1.7  2008/01/07 14:12:33  ncq
    578  # - add some documentation to add_cli()
    579  #
    580  # Revision 1.6  2007/12/26 22:43:28  ncq
    581  # - source order needs policy
    582  #
    583  # Revision 1.5  2007/12/26 21:50:45  ncq
    584  # - missing continue
    585  # - better test suite
    586  #
    587  # Revision 1.4  2007/12/26 21:11:11  ncq
    588  # - need codecs
    589  #
    590  # Revision 1.3  2007/12/26 20:47:22  ncq
    591  # - need to create internal source if doesn't exist
    592  #
    593  # Revision 1.2  2007/12/26 20:18:03  ncq
    594  # - fix test suite
    595  #
    596  # Revision 1.1  2007/12/23 11:53:13  ncq
    597  # - a much improved cfg options interface
    598  #   - no database handling yet
    599  #
    600  #
    601