Gnumed.pycommon.gmCfg2

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

# Source Code for Module Gnumed.pycommon.gmCfg2

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