]> SALOME platform Git repositories - tools/sat.git/commitdiff
Salome HOME
add src/test/config_0_3_9
authorChristian Van Wambeke <christian.van-wambeke@cea.fr>
Thu, 8 Feb 2018 15:23:10 +0000 (16:23 +0100)
committerChristian Van Wambeke <christian.van-wambeke@cea.fr>
Thu, 8 Feb 2018 15:23:10 +0000 (16:23 +0100)
28 files changed:
commands/config.py
commands/config_old.py [new file with mode: 0644]
commands/log.py
sat
src/ElementTree.py
src/debug.py
src/options.py
src/pyconf.py
src/salomeTools.py
src/salomeTools_old.py [new file with mode: 0755]
src/test/APPLI_TEST/APPLI_TEST.pyconf [new file with mode: 0644]
src/test/APPLI_TEST_Test.py [new file with mode: 0755]
src/test/README_config_0_3_9.txt [new file with mode: 0644]
src/test/__init__.py [new file with mode: 0755]
src/test/config_0_3_9/LICENSE [new file with mode: 0644]
src/test/config_0_3_9/PKG-INFO [new file with mode: 0644]
src/test/config_0_3_9/README.txt [new file with mode: 0644]
src/test/config_0_3_9/__init__.py [new file with mode: 0644]
src/test/config_0_3_9/config.py [new file with mode: 0644]
src/test/config_0_3_9/logconfig.cfg [new file with mode: 0644]
src/test/config_0_3_9/logconfig.py [new file with mode: 0644]
src/test/config_0_3_9/setup.py [new file with mode: 0644]
src/test/config_0_3_9/styles.json [new file with mode: 0644]
src/test/config_0_3_9/test_config.py [new file with mode: 0644]
src/test/debugTest.py [new file with mode: 0755]
src/test/pyconfTest.py [new file with mode: 0755]
src/test/satHelpTest.py
src/test/test_pyconf.py [new file with mode: 0755]

index f1ec2b8045b9ce13ba7ae2a1d0ba933dd2436fe1..14cfc660de239551d888230dcb2dbfe0ff8da4f2 100644 (file)
@@ -23,29 +23,9 @@ import shutil
 import sys
 
 import src
+import src.debug as DBG
+from src.salomeTools import _BaseCommand
 
-# Define all possible option for config command :  sat config <options>
-parser = src.options.Options()
-parser.add_option('v', 'value', 'string', 'value',
-    _("Optional: print the value of CONFIG_VARIABLE."))
-parser.add_option('e', 'edit', 'boolean', 'edit',
-    _("Optional: edit the product configuration file."))
-parser.add_option('i', 'info', 'string', 'info',
-    _("Optional: get information on a product."))
-parser.add_option('l', 'list', 'boolean', 'list',
-    _("Optional: list all available applications."))
-parser.add_option('', 'show_patchs', 'boolean', 'show_patchs',
-    _("Optional: synthetic view of all patches used in the application"))
-parser.add_option('c', 'copy', 'boolean', 'copy',
-    _("""Optional: copy a config file (.pyconf) to the personal config files directory.
-\tWarning: the included files are not copied.
-\tIf a name is given the new config file takes the given name."""))
-parser.add_option('n', 'no_label', 'boolean', 'no_label',
-    _("Internal use: do not print labels, Works only with --value and --list."))
-parser.add_option('', 'completion', 'boolean', 'completion',
-    _("Internal use: print only keys, works only with --value."))
-parser.add_option('s', 'schema', 'boolean', 'schema',
-    _("Internal use."))
 
 class ConfigOpener:
     '''Class that helps to find an application pyconf 
@@ -773,6 +753,23 @@ def print_value(config, path, show_label, logger, level=0, show_full_path=False)
             index = index + 1
     else: # case where val is just a str
         logger.write("%s\n" % val)
+        
+def print_debug(config, aPath, show_label, logger, level=0, show_full_path=False):
+    path = str(aPath)
+    if path == "." :
+      val = config
+      path = ""
+    else:
+      if path.endswith('.'): # Make sure that the path does not ends with a point
+        path = path[:-1]
+      val = config.getByPath(path)
+      
+    outStream = DBG.OutStream()
+    DBG.saveConfigDbg(val, outStream, path=path)
+    res = outStream.value
+    logger.write(res)
+    return
+    
 
 def get_config_children(config, args):
     '''Gets the names of the children of the given parameter.
@@ -810,7 +807,40 @@ def get_config_children(config, args):
     for v in sorted(vals):
         sys.stdout.write("%s\n" % v)
 
-def description():
+
+########################################################################
+# Command class for command 'sat config etc.'
+########################################################################
+class Command(_BaseCommand):
+  
+  def getParser(self):
+    # Define all possible option for config command :  sat config <options>
+    parser = src.options.Options()
+    parser.add_option('v', 'value', 'string', 'value',
+        _("Optional: print the value of CONFIG_VARIABLE."))
+    parser.add_option('d', 'debug', 'string', 'debug',
+        _("Optional: print the debugging value of CONFIG_VARIABLE."))
+    parser.add_option('e', 'edit', 'boolean', 'edit',
+        _("Optional: edit the product configuration file."))
+    parser.add_option('i', 'info', 'string', 'info',
+        _("Optional: get information on a product."))
+    parser.add_option('l', 'list', 'boolean', 'list',
+        _("Optional: list all available applications."))
+    parser.add_option('', 'show_patchs', 'boolean', 'show_patchs',
+        _("Optional: synthetic view of all patches used in the application"))
+    parser.add_option('c', 'copy', 'boolean', 'copy',
+        _("""Optional: copy a config file (.pyconf) to the personal config files directory.
+    \tWarning: the included files are not copied.
+    \tIf a name is given the new config file takes the given name."""))
+    parser.add_option('n', 'no_label', 'boolean', 'no_label',
+        _("Internal use: do not print labels, Works only with --value and --list."))
+    parser.add_option('', 'completion', 'boolean', 'completion',
+        _("Internal use: print only keys, works only with --value."))
+    parser.add_option('s', 'schema', 'boolean', 'schema',
+        _("Internal use."))
+    return parser
+
+  def description(self):
     '''method that is called when salomeTools is called with --help option.
     
     :return: The text to display for the config command description.
@@ -821,141 +851,149 @@ The config command allows manipulation and operation on config files.
 
 example:
 >> sat config SALOME-master --info ParaView""")
-    
-
-def run(args, runner, logger):
-    '''method that is called when salomeTools is called with config parameter.
-    '''
-    # Parse the options
-    (options, args) = parser.parse_args(args)
-
-    # Only useful for completion mechanism : print the keys of the config
-    if options.schema:
-        get_config_children(runner.cfg, args)
-        return
-    
-    # case : print a value of the config
-    if options.value:
-        if options.value == ".":
-            # if argument is ".", print all the config
-            for val in sorted(runner.cfg.keys()):
-                print_value(runner.cfg, val, not options.no_label, logger)
-        else:
-            print_value(runner.cfg, options.value, not options.no_label, logger, 
-                        level=0, show_full_path=False)
-    
-    # case : edit user pyconf file or application file
-    elif options.edit:
-        editor = runner.cfg.USER.editor
-        if ('APPLICATION' not in runner.cfg and
-            'open_application' not in runner.cfg): # edit user pyconf
-            usercfg = os.path.join(runner.cfg.VARS.personalDir, 'SAT.pyconf')
-            logger.write(_("Opening %s\n") % usercfg, 3)
-            src.system.show_in_editor(editor, usercfg, logger)
-        else:
-            # search for file <application>.pyconf and open it
-            for path in runner.cfg.PATHS.APPLICATIONPATH:
-                pyconf_path = os.path.join(path, runner.cfg.VARS.application + ".pyconf")
-                if os.path.exists(pyconf_path):
-                    logger.write(_("Opening %s\n") % pyconf_path, 3)
-                    src.system.show_in_editor(editor, pyconf_path, logger)
-                    break
-    
-    # case : give information about the product in parameter
-    elif options.info:
-        src.check_config_has_application(runner.cfg)
-        if options.info in runner.cfg.APPLICATION.products:
-            show_product_info(runner.cfg, options.info, logger)
-            return
-        raise src.SatException(
-            _("%(product_name)s is not a product of %(application_name)s.") % \
-            {'product_name' : options.info, 'application_name' : runner.cfg.VARS.application} )
-    
-    # case : copy an existing <application>.pyconf 
-    # to ~/.salomeTools/Applications/LOCAL_<application>.pyconf
-    elif options.copy:
-        # product is required
-        src.check_config_has_application( runner.cfg )
-
-        # get application file path 
-        source = runner.cfg.VARS.application + '.pyconf'
-        source_full_path = ""
-        for path in runner.cfg.PATHS.APPLICATIONPATH:
-            # ignore personal directory
-            if path == runner.cfg.VARS.personalDir:
-                continue
-            # loop on all directories that can have pyconf applications
-            zz = os.path.join(path, source)
-            if os.path.exists(zz):
-                source_full_path = zz
-                break
+        
 
-        if len(source_full_path) == 0:
-            raise src.SatException(
-                _("Config file for product %s not found\n") % source )
-        else:
-            if len(args) > 0:
-                # a name is given as parameter, use it
-                dest = args[0]
-            elif 'copy_prefix' in runner.cfg.INTERNAL.config:
-                # use prefix
-                dest = (runner.cfg.INTERNAL.config.copy_prefix 
-                        + runner.cfg.VARS.application)
-            else:
-                # use same name as source
-                dest = runner.cfg.VARS.application
-                
-            # the full path
-            dest_file = os.path.join(
-                runner.cfg.VARS.personalDir, 'Applications', dest + '.pyconf' )
-            if os.path.exists(dest_file):
-                raise src.SatException(
-                    _("A personal application '%s' already exists") % dest )
+  def run(self, args):
+      '''method that is called when salomeTools is called with config parameter.
+      '''
+      runner = self.getRunner()
+      logger = self.getLogger()
+      parser = self.getParser()
+      config = self.getConfig()
+      # Parse the options
+      (options, args) = parser.parse_args(args)
+  
+      # Only useful for completion mechanism : print the keys of the config
+      if options.schema:
+          get_config_children(config, args)
+          return
+      
+      # case : print a value of the config
+      if options.value:
+          if options.value == ".":
+              # if argument is ".", print all the config
+              for val in sorted(config.keys()):
+                  print_value(config, val, not options.no_label, logger)
+          else:
+              print_value(config, options.value, not options.no_label, logger, 
+                          level=0, show_full_path=False)
+      
+      if options.debug:
+          print_debug(config, str(options.debug), not options.no_label, logger, 
+                      level=0, show_full_path=False)
+      
+      # case : edit user pyconf file or application file
+      elif options.edit:
+          editor = config.USER.editor
+          if ('APPLICATION' not in config and
+              'open_application' not in config): # edit user pyconf
+              usercfg = os.path.join(config.VARS.personalDir, 'SAT.pyconf')
+              logger.write(_("Opening %s\n") % usercfg, 3)
+              src.system.show_in_editor(editor, usercfg, logger)
+          else:
+              # search for file <application>.pyconf and open it
+              for path in config.PATHS.APPLICATIONPATH:
+                  pyconf_path = os.path.join(path, config.VARS.application + ".pyconf")
+                  if os.path.exists(pyconf_path):
+                      logger.write(_("Opening %s\n") % pyconf_path, 3)
+                      src.system.show_in_editor(editor, pyconf_path, logger)
+                      break
+      
+      # case : give information about the product in parameter
+      elif options.info:
+          src.check_config_has_application(config)
+          if options.info in config.APPLICATION.products:
+              show_product_info(config, options.info, logger)
+              return
+          raise src.SatException(
+              _("%(product_name)s is not a product of %(application_name)s.") % \
+              {'product_name' : options.info, 'application_name' : config.VARS.application} )
+      
+      # case : copy an existing <application>.pyconf 
+      # to ~/.salomeTools/Applications/LOCAL_<application>.pyconf
+      elif options.copy:
+          # product is required
+          src.check_config_has_application( config )
+  
+          # get application file path 
+          source = config.VARS.application + '.pyconf'
+          source_full_path = ""
+          for path in config.PATHS.APPLICATIONPATH:
+              # ignore personal directory
+              if path == config.VARS.personalDir:
+                  continue
+              # loop on all directories that can have pyconf applications
+              zz = os.path.join(path, source)
+              if os.path.exists(zz):
+                  source_full_path = zz
+                  break
+  
+          if len(source_full_path) == 0:
+              raise src.SatException(
+                  _("Config file for product %s not found\n") % source )
+          else:
+              if len(args) > 0:
+                  # a name is given as parameter, use it
+                  dest = args[0]
+              elif 'copy_prefix' in config.INTERNAL.config:
+                  # use prefix
+                  dest = (config.INTERNAL.config.copy_prefix 
+                          + config.VARS.application)
+              else:
+                  # use same name as source
+                  dest = config.VARS.application
+                  
+              # the full path
+              dest_file = os.path.join(
+                  config.VARS.personalDir, 'Applications', dest + '.pyconf' )
+              if os.path.exists(dest_file):
+                  raise src.SatException(
+                      _("A personal application '%s' already exists") % dest )
+              
+              # perform the copy
+              shutil.copyfile(source_full_path, dest_file)
+              logger.write(_("%s has been created.\n") % dest_file)
+      
+      # case : display all the available pyconf applications
+      elif options.list:
+          lproduct = list()
+          # search in all directories that can have pyconf applications
+          for path in config.PATHS.APPLICATIONPATH:
+              # print a header
+              if not options.no_label:
+                  logger.write("------ %s\n" % src.printcolors.printcHeader(path))
+  
+              if not os.path.exists(path):
+                  logger.write(src.printcolors.printcError(
+                      _("Directory not found")) + "\n" )
+              else:
+                  for f in sorted(os.listdir(path)):
+                      # ignore file that does not ends with .pyconf
+                      if not f.endswith('.pyconf'):
+                          continue
+  
+                      appliname = f[:-len('.pyconf')]
+                      if appliname not in lproduct:
+                          lproduct.append(appliname)
+                          if path.startswith(config.VARS.personalDir) \
+                                      and not options.no_label:
+                              logger.write("%s*\n" % appliname)
+                          else:
+                              logger.write("%s\n" % appliname)
+                              
+              logger.write("\n")
+      # case : give a synthetic view of all patches used in the application
+      elif options.show_patchs:
+          src.check_config_has_application(config)
+          # Print some informations
+          logger.write(_('Show the patchs of application %s\n') % \
+                       src.printcolors.printcLabel(config.VARS.application), 3)
+          logger.write("\n", 2, False)
+          show_patchs(config, logger)
+      
+      # case: print all the products name of the application (internal use for completion)
+      elif options.completion:
+          for product_name in config.APPLICATION.products.keys():
+              logger.write("%s\n" % product_name)
             
-            # perform the copy
-            shutil.copyfile(source_full_path, dest_file)
-            logger.write(_("%s has been created.\n") % dest_file)
-    
-    # case : display all the available pyconf applications
-    elif options.list:
-        lproduct = list()
-        # search in all directories that can have pyconf applications
-        for path in runner.cfg.PATHS.APPLICATIONPATH:
-            # print a header
-            if not options.no_label:
-                logger.write("------ %s\n" % src.printcolors.printcHeader(path))
-
-            if not os.path.exists(path):
-                logger.write(src.printcolors.printcError(
-                    _("Directory not found")) + "\n" )
-            else:
-                for f in sorted(os.listdir(path)):
-                    # ignore file that does not ends with .pyconf
-                    if not f.endswith('.pyconf'):
-                        continue
-
-                    appliname = f[:-len('.pyconf')]
-                    if appliname not in lproduct:
-                        lproduct.append(appliname)
-                        if path.startswith(runner.cfg.VARS.personalDir) \
-                                    and not options.no_label:
-                            logger.write("%s*\n" % appliname)
-                        else:
-                            logger.write("%s\n" % appliname)
-                            
-            logger.write("\n")
-    # case : give a synthetic view of all patches used in the application
-    elif options.show_patchs:
-        src.check_config_has_application(runner.cfg)
-        # Print some informations
-        logger.write(_('Show the patchs of application %s\n') % \
-                     src.printcolors.printcLabel(runner.cfg.VARS.application), 3)
-        logger.write("\n", 2, False)
-        show_patchs(runner.cfg, logger)
-    
-    # case: print all the products name of the application (internal use for completion)
-    elif options.completion:
-        for product_name in runner.cfg.APPLICATION.products.keys():
-            logger.write("%s\n" % product_name)
         
-    
diff --git a/commands/config_old.py b/commands/config_old.py
new file mode 100644 (file)
index 0000000..cb9f033
--- /dev/null
@@ -0,0 +1,985 @@
+#!/usr/bin/env python
+#-*- coding:utf-8 -*-
+#  Copyright (C) 2010-2012  CEA/DEN
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 2.1 of the License.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library; if not, write to the Free Software
+#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+
+import os
+import platform
+import datetime
+import shutil
+import sys
+
+import src
+import src.debug as DBG
+
+# Define all possible option for config command :  sat config <options>
+parser = src.options.Options()
+parser.add_option('v', 'value', 'string', 'value',
+    _("Optional: print the value of CONFIG_VARIABLE."))
+parser.add_option('d', 'debug', 'string', 'debug',
+    _("Optional: print the debugging value of CONFIG_VARIABLE."))
+parser.add_option('e', 'edit', 'boolean', 'edit',
+    _("Optional: edit the product configuration file."))
+parser.add_option('i', 'info', 'string', 'info',
+    _("Optional: get information on a product."))
+parser.add_option('l', 'list', 'boolean', 'list',
+    _("Optional: list all available applications."))
+parser.add_option('', 'show_patchs', 'boolean', 'show_patchs',
+    _("Optional: synthetic view of all patches used in the application"))
+parser.add_option('c', 'copy', 'boolean', 'copy',
+    _("""Optional: copy a config file (.pyconf) to the personal config files directory.
+\tWarning: the included files are not copied.
+\tIf a name is given the new config file takes the given name."""))
+parser.add_option('n', 'no_label', 'boolean', 'no_label',
+    _("Internal use: do not print labels, Works only with --value and --list."))
+parser.add_option('', 'completion', 'boolean', 'completion',
+    _("Internal use: print only keys, works only with --value."))
+parser.add_option('s', 'schema', 'boolean', 'schema',
+    _("Internal use."))
+
+class ConfigOpener:
+    '''Class that helps to find an application pyconf 
+       in all the possible directories (pathList)
+    '''
+    def __init__(self, pathList):
+        '''Initialization
+        
+        :param pathList list: The list of paths where to search a pyconf.
+        '''
+        self.pathList = pathList
+
+    def __call__(self, name):
+        if os.path.isabs(name):
+            return src.pyconf.ConfigInputStream(open(name, 'rb'))
+        else:
+            return src.pyconf.ConfigInputStream( 
+                        open(os.path.join( self.get_path(name), name ), 'rb') )
+        raise IOError(_("Configuration file '%s' not found") % name)
+
+    def get_path( self, name ):
+        '''The method that returns the entire path of the pyconf searched
+        :param name str: The name of the searched pyconf.
+        '''
+        for path in self.pathList:
+            if os.path.exists(os.path.join(path, name)):
+                return path
+        raise IOError(_("Configuration file '%s' not found") % name)
+
+class ConfigManager:
+    '''Class that manages the read of all the configuration files of salomeTools
+    '''
+    def __init__(self, datadir=None):
+        pass
+
+    def _create_vars(self, application=None, command=None, datadir=None):
+        '''Create a dictionary that stores all information about machine,
+           user, date, repositories, etc...
+        
+        :param application str: The application for which salomeTools is called.
+        :param command str: The command that is called.
+        :param datadir str: The repository that contain external data 
+                            for salomeTools.
+        :return: The dictionary that stores all information.
+        :rtype: dict
+        '''
+        var = {}      
+        var['user'] = src.architecture.get_user()
+        var['salometoolsway'] = os.path.dirname(
+                                    os.path.dirname(os.path.abspath(__file__)))
+        var['srcDir'] = os.path.join(var['salometoolsway'], 'src')
+        var['internal_dir'] = os.path.join(var['srcDir'], 'internal_config')
+        var['sep']= os.path.sep
+        
+        # datadir has a default location
+        var['datadir'] = os.path.join(var['salometoolsway'], 'data')
+        if datadir is not None:
+            var['datadir'] = datadir
+
+        var['personalDir'] = os.path.join(os.path.expanduser('~'),
+                                           '.salomeTools')
+        src.ensure_path_exists(var['personalDir'])
+
+        var['personal_applications_dir'] = os.path.join(var['personalDir'],
+                                                        "Applications")
+        src.ensure_path_exists(var['personal_applications_dir'])
+        
+        var['personal_products_dir'] = os.path.join(var['personalDir'],
+                                                    "products")
+        src.ensure_path_exists(var['personal_products_dir'])
+        
+        var['personal_archives_dir'] = os.path.join(var['personalDir'],
+                                                    "Archives")
+        src.ensure_path_exists(var['personal_archives_dir'])
+
+        var['personal_jobs_dir'] = os.path.join(var['personalDir'],
+                                                "Jobs")
+        src.ensure_path_exists(var['personal_jobs_dir'])
+
+        var['personal_machines_dir'] = os.path.join(var['personalDir'],
+                                                    "Machines")
+        src.ensure_path_exists(var['personal_machines_dir'])
+
+        # read linux distributions dictionary
+        distrib_cfg = src.pyconf.Config(os.path.join(var['srcDir'],
+                                                      'internal_config',
+                                                      'distrib.pyconf'))
+        
+        # set platform parameters
+        dist_name = src.architecture.get_distribution(
+                                            codes=distrib_cfg.DISTRIBUTIONS)
+        dist_version = src.architecture.get_distrib_version(dist_name, 
+                                                    codes=distrib_cfg.VERSIONS)
+        dist = dist_name + dist_version
+        
+        var['dist_name'] = dist_name
+        var['dist_version'] = dist_version
+        var['dist'] = dist
+        var['python'] = src.architecture.get_python_version()
+
+        var['nb_proc'] = src.architecture.get_nb_proc()
+        node_name = platform.node()
+        var['node'] = node_name
+        var['hostname'] = node_name
+
+        # set date parameters
+        dt = datetime.datetime.now()
+        var['date'] = dt.strftime('%Y%m%d')
+        var['datehour'] = dt.strftime('%Y%m%d_%H%M%S')
+        var['hour'] = dt.strftime('%H%M%S')
+
+        var['command'] = str(command)
+        var['application'] = str(application)
+
+        # Root dir for temporary files 
+        var['tmp_root'] = os.sep + 'tmp' + os.sep + var['user']
+        # particular win case 
+        if src.architecture.is_windows() : 
+            var['tmp_root'] =  os.path.expanduser('~') + os.sep + 'tmp'
+        
+        return var
+
+    def get_command_line_overrides(self, options, sections):
+        '''get all the overwrites that are in the command line
+        
+        :param options: the options from salomeTools class 
+                        initialization (like -l5 or --overwrite)
+        :param sections str: The config section to overwrite.
+        :return: The list of all the overwrites to apply.
+        :rtype: list
+        '''
+        # when there are no options or not the overwrite option, 
+        # return an empty list
+        if options is None or options.overwrite is None:
+            return []
+        
+        over = []
+        for section in sections:
+            # only overwrite the sections that correspond to the option 
+            over.extend(filter(lambda l: l.startswith(section + "."), 
+                               options.overwrite))
+        return over
+
+    def get_config(self, application=None, options=None, command=None,
+                    datadir=None):
+        '''get the config from all the configuration files.
+        
+        :param application str: The application for which salomeTools is called.
+        :param options class Options: The general salomeToos
+                                      options (--overwrite or -l5, for example)
+        :param command str: The command that is called.
+        :param datadir str: The repository that contain 
+                            external data for salomeTools.
+        :return: The final config.
+        :rtype: class 'src.pyconf.Config'
+        '''        
+        
+        # create a ConfigMerger to handle merge
+        merger = src.pyconf.ConfigMerger()#MergeHandler())
+        
+        # create the configuration instance
+        cfg = src.pyconf.Config()
+        
+        # =====================================================================
+        # create VARS section
+        var = self._create_vars(application=application, command=command, 
+                                datadir=datadir)
+        # add VARS to config
+        cfg.VARS = src.pyconf.Mapping(cfg)
+        for variable in var:
+            cfg.VARS[variable] = var[variable]
+        
+        # apply overwrite from command line if needed
+        for rule in self.get_command_line_overrides(options, ["VARS"]):
+            exec('cfg.' + rule) # this cannot be factorized because of the exec
+        
+        # =====================================================================
+        # Load INTERNAL config
+        # read src/internal_config/salomeTools.pyconf
+        src.pyconf.streamOpener = ConfigOpener([
+                            os.path.join(cfg.VARS.srcDir, 'internal_config')])
+        try:
+            internal_cfg = src.pyconf.Config(open(os.path.join(cfg.VARS.srcDir, 
+                                    'internal_config', 'salomeTools.pyconf')))
+        except src.pyconf.ConfigError as e:
+            raise src.SatException(_("Error in configuration file:"
+                                     " salomeTools.pyconf\n  %(error)s") % \
+                                   {'error': str(e) })
+        
+        merger.merge(cfg, internal_cfg)
+
+        # apply overwrite from command line if needed
+        for rule in self.get_command_line_overrides(options, ["INTERNAL"]):
+            exec('cfg.' + rule) # this cannot be factorized because of the exec        
+               
+        # =====================================================================
+        # Load LOCAL config file
+        # search only in the data directory
+        src.pyconf.streamOpener = ConfigOpener([cfg.VARS.datadir])
+        try:
+            local_cfg = src.pyconf.Config(open(os.path.join(cfg.VARS.datadir, 
+                                                           'local.pyconf')),
+                                         PWD = ('LOCAL', cfg.VARS.datadir) )
+        except src.pyconf.ConfigError as e:
+            raise src.SatException(_("Error in configuration file: "
+                                     "local.pyconf\n  %(error)s") % \
+                {'error': str(e) })
+        except IOError as error:
+            e = str(error)
+            raise src.SatException( e );
+        merger.merge(cfg, local_cfg)
+
+        # When the key is "default", put the default value
+        if cfg.LOCAL.base == "default":
+            cfg.LOCAL.base = os.path.abspath(
+                                        os.path.join(cfg.VARS.salometoolsway,
+                                                     "..",
+                                                     "BASE"))
+        if cfg.LOCAL.workdir == "default":
+            cfg.LOCAL.workdir = os.path.abspath(
+                                        os.path.join(cfg.VARS.salometoolsway,
+                                                     ".."))
+        if cfg.LOCAL.log_dir == "default":
+            cfg.LOCAL.log_dir = os.path.abspath(
+                                        os.path.join(cfg.VARS.salometoolsway,
+                                                     "..",
+                                                     "LOGS"))
+
+        if cfg.LOCAL.archive_dir == "default":
+            cfg.LOCAL.archive_dir = os.path.abspath(
+                                        os.path.join(cfg.VARS.salometoolsway,
+                                                     "..",
+                                                     "ARCHIVES"))
+
+        # apply overwrite from command line if needed
+        for rule in self.get_command_line_overrides(options, ["LOCAL"]):
+            exec('cfg.' + rule) # this cannot be factorized because of the exec
+        
+        # =====================================================================
+        # Load the PROJECTS
+        projects_cfg = src.pyconf.Config()
+        projects_cfg.addMapping("PROJECTS",
+                                src.pyconf.Mapping(projects_cfg),
+                                "The projects\n")
+        projects_cfg.PROJECTS.addMapping("projects",
+                                src.pyconf.Mapping(cfg.PROJECTS),
+                                "The projects definition\n")
+        
+        for project_pyconf_path in cfg.PROJECTS.project_file_paths:
+            if not os.path.exists(project_pyconf_path):
+                msg = _("WARNING: The project file %s cannot be found. "
+                        "It will be ignored\n") % project_pyconf_path
+                sys.stdout.write(msg)
+                continue
+            project_name = os.path.basename(project_pyconf_path)[:-len(".pyconf")]
+            try:
+                project_pyconf_dir = os.path.dirname(project_pyconf_path)
+                project_cfg = src.pyconf.Config(open(project_pyconf_path),
+                                                PWD=("", project_pyconf_dir))
+            except Exception as e:
+                msg = _("ERROR: Error in configuration file: %(file_path)s\n  %(error)s\n") % \
+                       {'file_path' : project_pyconf_path, 'error': str(e) }
+                sys.stdout.write(msg)
+                continue
+            projects_cfg.PROJECTS.projects.addMapping(project_name,
+                             src.pyconf.Mapping(projects_cfg.PROJECTS.projects),
+                             "The %s project\n" % project_name)
+            projects_cfg.PROJECTS.projects[project_name]=project_cfg
+            projects_cfg.PROJECTS.projects[project_name]["file_path"] = \
+                                                        project_pyconf_path
+                   
+        merger.merge(cfg, projects_cfg)
+
+        # apply overwrite from command line if needed
+        for rule in self.get_command_line_overrides(options, ["PROJECTS"]):
+            exec('cfg.' + rule) # this cannot be factorized because of the exec
+        
+        # =====================================================================
+        # Create the paths where to search the application configurations, 
+        # the product configurations, the products archives, 
+        # the jobs configurations and the machines configurations
+        cfg.addMapping("PATHS", src.pyconf.Mapping(cfg), "The paths\n")
+        cfg.PATHS["APPLICATIONPATH"] = src.pyconf.Sequence(cfg.PATHS)
+        cfg.PATHS.APPLICATIONPATH.append(cfg.VARS.personal_applications_dir, "")
+        
+        cfg.PATHS["PRODUCTPATH"] = src.pyconf.Sequence(cfg.PATHS)
+        cfg.PATHS.PRODUCTPATH.append(cfg.VARS.personal_products_dir, "")
+        cfg.PATHS["ARCHIVEPATH"] = src.pyconf.Sequence(cfg.PATHS)
+        cfg.PATHS.ARCHIVEPATH.append(cfg.VARS.personal_archives_dir, "")
+        cfg.PATHS["JOBPATH"] = src.pyconf.Sequence(cfg.PATHS)
+        cfg.PATHS.JOBPATH.append(cfg.VARS.personal_jobs_dir, "")
+        cfg.PATHS["MACHINEPATH"] = src.pyconf.Sequence(cfg.PATHS)
+        cfg.PATHS.MACHINEPATH.append(cfg.VARS.personal_machines_dir, "")
+
+        # initialise the path with local directory
+        cfg.PATHS["ARCHIVEPATH"].append(cfg.LOCAL.archive_dir, "")
+
+        # Loop over the projects in order to complete the PATHS variables
+        for project in cfg.PROJECTS.projects:
+            for PATH in ["APPLICATIONPATH",
+                         "PRODUCTPATH",
+                         "ARCHIVEPATH",
+                         "JOBPATH",
+                         "MACHINEPATH"]:
+                if PATH not in cfg.PROJECTS.projects[project]:
+                    continue
+                cfg.PATHS[PATH].append(cfg.PROJECTS.projects[project][PATH], "")
+        
+        # apply overwrite from command line if needed
+        for rule in self.get_command_line_overrides(options, ["PATHS"]):
+            exec('cfg.' + rule) # this cannot be factorized because of the exec
+
+        # =====================================================================
+        # Load APPLICATION config file
+        if application is not None:
+            # search APPLICATION file in all directories in configPath
+            cp = cfg.PATHS.APPLICATIONPATH
+            src.pyconf.streamOpener = ConfigOpener(cp)
+            do_merge = True
+            try:
+                application_cfg = src.pyconf.Config(application + '.pyconf')
+            except IOError as e:
+                raise src.SatException(_("%s, use 'config --list' to get the"
+                                         " list of available applications.") % e)
+            except src.pyconf.ConfigError as e:
+                if (not ('-e' in parser.parse_args()[1]) 
+                                         or ('--edit' in parser.parse_args()[1]) 
+                                         and command == 'config'):
+                    raise src.SatException(
+                        _("Error in configuration file: (1)s.pyconf\n  %(2)s") % \
+                        { 'application': application, 'error': str(e) } )
+                else:
+                    sys.stdout.write(src.printcolors.printcWarning(
+                        "There is an error in the file %s.pyconf.\n" % \
+                        cfg.VARS.application))
+                    do_merge = False
+            except Exception as e:
+                if ( not('-e' in parser.parse_args()[1]) or
+                     ('--edit' in parser.parse_args()[1]) and
+                     command == 'config' ):
+                    sys.stdout.write(src.printcolors.printcWarning("%s\n" % str(e)))
+                    raise src.SatException(
+                        _("Error in configuration file: %s.pyconf\n") % application )
+                else:
+                    sys.stdout.write(src.printcolors.printcWarning(
+                        "ERROR: in file %s.pyconf. Opening the file with the default viewer\n" % \
+                        cfg.VARS.application))
+                    sys.stdout.write("\n%s\n" % src.printcolors.printcWarning(str(e)))
+                    do_merge = False
+        
+            else:
+                cfg['open_application'] = 'yes'
+
+        # =====================================================================
+        # Load product config files in PRODUCTS section
+        products_cfg = src.pyconf.Config()
+        products_cfg.addMapping("PRODUCTS",
+                                src.pyconf.Mapping(products_cfg),
+                                "The products\n")
+        if application is not None:
+            src.pyconf.streamOpener = ConfigOpener(cfg.PATHS.PRODUCTPATH)
+            for product_name in application_cfg.APPLICATION.products.keys():
+                # Loop on all files that are in softsDir directory
+                # and read their config
+                product_file_name = product_name + ".pyconf"
+                product_file_path = src.find_file_in_lpath(product_file_name, cfg.PATHS.PRODUCTPATH)
+                if product_file_path:
+                    products_dir = os.path.dirname(product_file_path)
+                    try:
+                        prod_cfg = src.pyconf.Config(open(product_file_path),
+                                                     PWD=("", products_dir))
+                        prod_cfg.from_file = product_file_path
+                        products_cfg.PRODUCTS[product_name] = prod_cfg
+                    except Exception as e:
+                        msg = _(
+                            "WARNING: Error in configuration file: %(prod)s\n  %(error)s" % \
+                            {'prod' :  product_name, 'error': str(e) })
+                        sys.stdout.write(msg)
+            
+            merger.merge(cfg, products_cfg)
+            
+            # apply overwrite from command line if needed
+            for rule in self.get_command_line_overrides(options, ["PRODUCTS"]):
+                exec('cfg.' + rule) # this cannot be factorized because of the exec
+            
+            if do_merge:
+                merger.merge(cfg, application_cfg)
+
+                # default launcher name ('salome')
+                if ('profile' in cfg.APPLICATION and 
+                    'launcher_name' not in cfg.APPLICATION.profile):
+                    cfg.APPLICATION.profile.launcher_name = 'salome'
+
+                # apply overwrite from command line if needed
+                for rule in self.get_command_line_overrides(options,
+                                                             ["APPLICATION"]):
+                    # this cannot be factorized because of the exec
+                    exec('cfg.' + rule)
+            
+        # =====================================================================
+        # load USER config
+        self.set_user_config_file(cfg)
+        user_cfg_file = self.get_user_config_file()
+        user_cfg = src.pyconf.Config(open(user_cfg_file))
+        merger.merge(cfg, user_cfg)
+
+        # apply overwrite from command line if needed
+        for rule in self.get_command_line_overrides(options, ["USER"]):
+            exec('cfg.' + rule) # this cannot be factorize because of the exec
+        
+        return cfg
+
+    def set_user_config_file(self, config):
+        '''Set the user config file name and path.
+        If necessary, build it from another one or create it from scratch.
+        
+        :param config class 'src.pyconf.Config': The global config 
+                                                 (containing all pyconf).
+        '''
+        # get the expected name and path of the file
+        self.config_file_name = 'SAT.pyconf'
+        self.user_config_file_path = os.path.join(config.VARS.personalDir,
+                                                   self.config_file_name)
+        
+        # if pyconf does not exist, create it from scratch
+        if not os.path.isfile(self.user_config_file_path): 
+            self.create_config_file(config)
+    
+    def create_config_file(self, config):
+        '''This method is called when there are no user config file. 
+           It build it from scratch.
+        
+        :param config class 'src.pyconf.Config': The global config.
+        :return: the config corresponding to the file created.
+        :rtype: config class 'src.pyconf.Config'
+        '''
+        
+        cfg_name = self.get_user_config_file()
+
+        user_cfg = src.pyconf.Config()
+        #
+        user_cfg.addMapping('USER', src.pyconf.Mapping(user_cfg), "")
+
+        user_cfg.USER.addMapping('cvs_user', config.VARS.user,
+            "This is the user name used to access salome cvs base.\n")
+        user_cfg.USER.addMapping('svn_user', config.VARS.user,
+            "This is the user name used to access salome svn base.\n")
+        user_cfg.USER.addMapping('output_verbose_level', 3,
+            "This is the default output_verbose_level you want."
+            " 0=>no output, 5=>debug.\n")
+        user_cfg.USER.addMapping('publish_dir', 
+                                 os.path.join(os.path.expanduser('~'),
+                                 'websupport', 
+                                 'satreport'), 
+                                 "")
+        user_cfg.USER.addMapping('editor',
+                                 'vi', 
+                                 "This is the editor used to "
+                                 "modify configuration files\n")
+        user_cfg.USER.addMapping('browser', 
+                                 'firefox', 
+                                 "This is the browser used to "
+                                 "read html documentation\n")
+        user_cfg.USER.addMapping('pdf_viewer', 
+                                 'evince', 
+                                 "This is the pdf_viewer used "
+                                 "to read pdf documentation\n")
+# CNC 25/10/17 : plus nécessaire a priori
+#        user_cfg.USER.addMapping("base",
+#                                 src.pyconf.Reference(
+#                                            user_cfg,
+#                                            src.pyconf.DOLLAR,
+#                                            'workdir  + $VARS.sep + "BASE"'),
+#                                 "The products installation base (could be "
+#                                 "ignored if this key exists in the local.pyconf"
+#                                 " file of salomTools).\n")
+               
+        # 
+        src.ensure_path_exists(config.VARS.personalDir)
+        src.ensure_path_exists(os.path.join(config.VARS.personalDir, 
+                                            'Applications'))
+
+        f = open(cfg_name, 'w')
+        user_cfg.__save__(f)
+        f.close()
+
+        return user_cfg   
+
+    def get_user_config_file(self):
+        '''Get the user config file
+        :return: path to the user config file.
+        :rtype: str
+        '''
+        if not self.user_config_file_path:
+            raise src.SatException(
+                _("Error in get_user_config_file: missing user config file path") )
+        return self.user_config_file_path     
+
+def check_path(path, ext=[]):
+    '''Construct a text with the input path and "not found" if it does not
+       exist.
+    
+    :param path Str: the path to check.
+    :param ext List: An extension. Verify that the path extension 
+                     is in the list
+    :return: The string of the path with information
+    :rtype: Str
+    '''
+    # check if file exists
+    if not os.path.exists(path):
+        return "'%s' %s" % (path, src.printcolors.printcError(_("** not found")))
+
+    # check extension
+    if len(ext) > 0:
+        fe = os.path.splitext(path)[1].lower()
+        if fe not in ext:
+            return "'%s' %s" % (path, src.printcolors.printcError(_("** bad extension")))
+
+    return path
+
+def show_product_info(config, name, logger):
+    '''Display on the terminal and logger information about a product.
+    
+    :param config Config: the global configuration.
+    :param name Str: The name of the product
+    :param logger Logger: The logger instance to use for the display
+    '''
+    
+    logger.write(_("%s is a product\n") % src.printcolors.printcLabel(name), 2)
+    pinfo = src.product.get_product_config(config, name)
+    
+    if "depend" in pinfo:
+        src.printcolors.print_value(logger, 
+                                    "depends on", 
+                                    ', '.join(pinfo.depend), 2)
+
+    if "opt_depend" in pinfo:
+        src.printcolors.print_value(logger, 
+                                    "optional", 
+                                    ', '.join(pinfo.opt_depend), 2)
+
+    # information on pyconf
+    logger.write("\n", 2)
+    logger.write(src.printcolors.printcLabel("configuration:") + "\n", 2)
+    if "from_file" in pinfo:
+        src.printcolors.print_value(logger, 
+                                    "pyconf file path", 
+                                    pinfo.from_file, 
+                                    2)
+    if "section" in pinfo:
+        src.printcolors.print_value(logger, 
+                                    "section", 
+                                    pinfo.section, 
+                                    2)
+
+    # information on prepare
+    logger.write("\n", 2)
+    logger.write(src.printcolors.printcLabel("prepare:") + "\n", 2)
+
+    is_dev = src.product.product_is_dev(pinfo)
+    method = pinfo.get_source
+    if is_dev:
+        method += " (dev)"
+    src.printcolors.print_value(logger, "get method", method, 2)
+
+    if method == 'cvs':
+        src.printcolors.print_value(logger, "server", pinfo.cvs_info.server, 2)
+        src.printcolors.print_value(logger, "base module",
+                                    pinfo.cvs_info.module_base, 2)
+        src.printcolors.print_value(logger, "source", pinfo.cvs_info.source, 2)
+        src.printcolors.print_value(logger, "tag", pinfo.cvs_info.tag, 2)
+
+    elif method == 'svn':
+        src.printcolors.print_value(logger, "repo", pinfo.svn_info.repo, 2)
+
+    elif method == 'git':
+        src.printcolors.print_value(logger, "repo", pinfo.git_info.repo, 2)
+        src.printcolors.print_value(logger, "tag", pinfo.git_info.tag, 2)
+
+    elif method == 'archive':
+        src.printcolors.print_value(logger, 
+                                    "get from", 
+                                    check_path(pinfo.archive_info.archive_name), 
+                                    2)
+
+    if 'patches' in pinfo:
+        for patch in pinfo.patches:
+            src.printcolors.print_value(logger, "patch", check_path(patch), 2)
+
+    if src.product.product_is_fixed(pinfo):
+        src.printcolors.print_value(logger, "install_dir", 
+                                    check_path(pinfo.install_dir), 2)
+
+    if src.product.product_is_native(pinfo) or src.product.product_is_fixed(pinfo):
+        return
+    
+    # information on compilation
+    if src.product.product_compiles(pinfo):
+        logger.write("\n", 2)
+        logger.write(src.printcolors.printcLabel("compile:") + "\n", 2)
+        src.printcolors.print_value(logger, 
+                                    "compilation method", 
+                                    pinfo.build_source, 
+                                    2)
+        
+        if pinfo.build_source == "script" and "compil_script" in pinfo:
+            src.printcolors.print_value(logger, 
+                                        "Compilation script", 
+                                        pinfo.compil_script, 
+                                        2)
+        
+        if 'nb_proc' in pinfo:
+            src.printcolors.print_value(logger, "make -j", pinfo.nb_proc, 2)
+    
+        src.printcolors.print_value(logger, 
+                                    "source dir", 
+                                    check_path(pinfo.source_dir), 
+                                    2)
+        if 'install_dir' in pinfo:
+            src.printcolors.print_value(logger, 
+                                        "build dir", 
+                                        check_path(pinfo.build_dir), 
+                                        2)
+            src.printcolors.print_value(logger, 
+                                        "install dir", 
+                                        check_path(pinfo.install_dir), 
+                                        2)
+        else:
+            logger.write("  %s\n" % src.printcolors.printcWarning(_("no install dir")) , 2)
+    else:
+        logger.write("\n", 2)
+        msg = _("This product does not compile")
+        logger.write("%s\n" % msg, 2)
+
+    # information on environment
+    logger.write("\n", 2)
+    logger.write(src.printcolors.printcLabel("environ :") + "\n", 2)
+    if "environ" in pinfo and "env_script" in pinfo.environ:
+        src.printcolors.print_value(logger, 
+                                    "script", 
+                                    check_path(pinfo.environ.env_script), 
+                                    2)
+
+    zz = src.environment.SalomeEnviron(config, 
+                                       src.fileEnviron.ScreenEnviron(logger), 
+                                       False)
+    zz.set_python_libdirs()
+    zz.set_a_product(name, logger)
+        
+def show_patchs(config, logger):
+    '''Prints all the used patchs in the application.
+    
+    :param config Config: the global configuration.
+    :param logger Logger: The logger instance to use for the display
+    '''
+    len_max = max([len(p) for p in config.APPLICATION.products]) + 2
+    for product in config.APPLICATION.products:
+        product_info = src.product.get_product_config(config, product)
+        if src.product.product_has_patches(product_info):
+            logger.write("%s: " % product, 1)
+            logger.write(src.printcolors.printcInfo(
+                                            " " * (len_max - len(product) -2) +
+                                            "%s\n" % product_info.patches[0]),
+                         1)
+            if len(product_info.patches) > 1:
+                for patch in product_info.patches[1:]:
+                    logger.write(src.printcolors.printcInfo(len_max*" " +
+                                                            "%s\n" % patch), 1)
+            logger.write("\n", 1)
+
+def print_value(config, path, show_label, logger, level=0, show_full_path=False):
+    '''Prints a value from the configuration. Prints recursively the values 
+       under the initial path.
+    
+    :param config class 'src.pyconf.Config': The configuration 
+                                             from which the value is displayed.
+    :param path str : the path in the configuration of the value to print.
+    :param show_label boolean: if True, do a basic display. 
+                               (useful for bash completion)
+    :param logger Logger: the logger instance
+    :param level int: The number of spaces to add before display.
+    :param show_full_path :
+    '''            
+    
+    # Make sure that the path does not ends with a point
+    if path.endswith('.'):
+        path = path[:-1]
+    
+    # display all the path or not
+    if show_full_path:
+        vname = path
+    else:
+        vname = path.split('.')[-1]
+
+    # number of spaces before the display
+    tab_level = "  " * level
+    
+    # call to the function that gets the value of the path.
+    try:
+        val = config.getByPath(path)
+    except Exception as e:
+        logger.write(tab_level)
+        logger.write("%s: ERROR %s\n" % (src.printcolors.printcLabel(vname), 
+                                         src.printcolors.printcError(str(e))))
+        return
+
+    # in this case, display only the value
+    if show_label:
+        logger.write(tab_level)
+        logger.write("%s: " % src.printcolors.printcLabel(vname))
+
+    # The case where the value has under values, 
+    # do a recursive call to the function
+    if dir(val).__contains__('keys'):
+        if show_label: logger.write("\n")
+        for v in sorted(val.keys()):
+            print_value(config, path + '.' + v, show_label, logger, level + 1)
+    elif val.__class__ == src.pyconf.Sequence or isinstance(val, list): 
+        # in this case, value is a list (or a Sequence)
+        if show_label: logger.write("\n")
+        index = 0
+        for v in val:
+            print_value(config, path + "[" + str(index) + "]", 
+                        show_label, logger, level + 1)
+            index = index + 1
+    else: # case where val is just a str
+        logger.write("%s\n" % val)
+        
+def print_debug(config, aPath, show_label, logger, level=0, show_full_path=False):
+    path = str(aPath)
+    if path == "." :
+      val = config
+      path = ""
+    else:
+      if path.endswith('.'): # Make sure that the path does not ends with a point
+        path = path[:-1]
+      val = config.getByPath(path)
+      
+    outStream = DBG.OutStream()
+    DBG.saveConfigDbg(val, outStream, path=path)
+    res = outStream.value
+    logger.write(res)
+    return
+    
+
+def get_config_children(config, args):
+    '''Gets the names of the children of the given parameter.
+       Useful only for completion mechanism
+    
+    :param config Config: The configuration where to read the values
+    :param args: The path in the config from which get the keys
+    '''
+    vals = []
+    rootkeys = config.keys()
+    
+    if len(args) == 0:
+        # no parameter returns list of root keys
+        vals = rootkeys
+    else:
+        parent = args[0]
+        pos = parent.rfind('.')
+        if pos < 0:
+            # Case where there is only on key as parameter.
+            # For example VARS
+            vals = [m for m in rootkeys if m.startswith(parent)]
+        else:
+            # Case where there is a part from a key
+            # for example VARS.us  (for VARS.user)
+            head = parent[0:pos]
+            tail = parent[pos+1:]
+            try:
+                a = config.getByPath(head)
+                if dir(a).__contains__('keys'):
+                    vals = map(lambda x: head + '.' + x,
+                               [m for m in a.keys() if m.startswith(tail)])
+            except:
+                pass
+
+    for v in sorted(vals):
+        sys.stdout.write("%s\n" % v)
+
+def description():
+    '''method that is called when salomeTools is called with --help option.
+    
+    :return: The text to display for the config command description.
+    :rtype: str
+    '''
+    return _("""\
+The config command allows manipulation and operation on config files.
+
+example:
+>> sat config SALOME-master --info ParaView""")
+    
+
+def run(args, runner, logger):
+    '''method that is called when salomeTools is called with config parameter.
+    '''
+    # Parse the options
+    (options, args) = parser.parse_args(args)
+
+    # Only useful for completion mechanism : print the keys of the config
+    if options.schema:
+        get_config_children(runner.cfg, args)
+        return
+    
+    # case : print a value of the config
+    if options.value:
+        if options.value == ".":
+            # if argument is ".", print all the config
+            for val in sorted(runner.cfg.keys()):
+                print_value(runner.cfg, val, not options.no_label, logger)
+        else:
+            print_value(runner.cfg, options.value, not options.no_label, logger, 
+                        level=0, show_full_path=False)
+    
+    if options.debug:
+        print_debug(runner.cfg, str(options.debug), not options.no_label, logger, 
+                    level=0, show_full_path=False)
+    
+    # case : edit user pyconf file or application file
+    elif options.edit:
+        editor = runner.cfg.USER.editor
+        if ('APPLICATION' not in runner.cfg and
+            'open_application' not in runner.cfg): # edit user pyconf
+            usercfg = os.path.join(runner.cfg.VARS.personalDir, 'SAT.pyconf')
+            logger.write(_("Opening %s\n") % usercfg, 3)
+            src.system.show_in_editor(editor, usercfg, logger)
+        else:
+            # search for file <application>.pyconf and open it
+            for path in runner.cfg.PATHS.APPLICATIONPATH:
+                pyconf_path = os.path.join(path, runner.cfg.VARS.application + ".pyconf")
+                if os.path.exists(pyconf_path):
+                    logger.write(_("Opening %s\n") % pyconf_path, 3)
+                    src.system.show_in_editor(editor, pyconf_path, logger)
+                    break
+    
+    # case : give information about the product in parameter
+    elif options.info:
+        src.check_config_has_application(runner.cfg)
+        if options.info in runner.cfg.APPLICATION.products:
+            show_product_info(runner.cfg, options.info, logger)
+            return
+        raise src.SatException(
+            _("%(product_name)s is not a product of %(application_name)s.") % \
+            {'product_name' : options.info, 'application_name' : runner.cfg.VARS.application} )
+    
+    # case : copy an existing <application>.pyconf 
+    # to ~/.salomeTools/Applications/LOCAL_<application>.pyconf
+    elif options.copy:
+        # product is required
+        src.check_config_has_application( runner.cfg )
+
+        # get application file path 
+        source = runner.cfg.VARS.application + '.pyconf'
+        source_full_path = ""
+        for path in runner.cfg.PATHS.APPLICATIONPATH:
+            # ignore personal directory
+            if path == runner.cfg.VARS.personalDir:
+                continue
+            # loop on all directories that can have pyconf applications
+            zz = os.path.join(path, source)
+            if os.path.exists(zz):
+                source_full_path = zz
+                break
+
+        if len(source_full_path) == 0:
+            raise src.SatException(
+                _("Config file for product %s not found\n") % source )
+        else:
+            if len(args) > 0:
+                # a name is given as parameter, use it
+                dest = args[0]
+            elif 'copy_prefix' in runner.cfg.INTERNAL.config:
+                # use prefix
+                dest = (runner.cfg.INTERNAL.config.copy_prefix 
+                        + runner.cfg.VARS.application)
+            else:
+                # use same name as source
+                dest = runner.cfg.VARS.application
+                
+            # the full path
+            dest_file = os.path.join(
+                runner.cfg.VARS.personalDir, 'Applications', dest + '.pyconf' )
+            if os.path.exists(dest_file):
+                raise src.SatException(
+                    _("A personal application '%s' already exists") % dest )
+            
+            # perform the copy
+            shutil.copyfile(source_full_path, dest_file)
+            logger.write(_("%s has been created.\n") % dest_file)
+    
+    # case : display all the available pyconf applications
+    elif options.list:
+        lproduct = list()
+        # search in all directories that can have pyconf applications
+        for path in runner.cfg.PATHS.APPLICATIONPATH:
+            # print a header
+            if not options.no_label:
+                logger.write("------ %s\n" % src.printcolors.printcHeader(path))
+
+            if not os.path.exists(path):
+                logger.write(src.printcolors.printcError(
+                    _("Directory not found")) + "\n" )
+            else:
+                for f in sorted(os.listdir(path)):
+                    # ignore file that does not ends with .pyconf
+                    if not f.endswith('.pyconf'):
+                        continue
+
+                    appliname = f[:-len('.pyconf')]
+                    if appliname not in lproduct:
+                        lproduct.append(appliname)
+                        if path.startswith(runner.cfg.VARS.personalDir) \
+                                    and not options.no_label:
+                            logger.write("%s*\n" % appliname)
+                        else:
+                            logger.write("%s\n" % appliname)
+                            
+            logger.write("\n")
+    # case : give a synthetic view of all patches used in the application
+    elif options.show_patchs:
+        src.check_config_has_application(runner.cfg)
+        # Print some informations
+        logger.write(_('Show the patchs of application %s\n') % \
+                     src.printcolors.printcLabel(runner.cfg.VARS.application), 3)
+        logger.write("\n", 2, False)
+        show_patchs(runner.cfg, logger)
+    
+    # case: print all the products name of the application (internal use for completion)
+    elif options.completion:
+        for product_name in runner.cfg.APPLICATION.products.keys():
+            logger.write("%s\n" % product_name)
+        
+    
index 01164bf5033bebb5c5ff8e6c350ffa28afcc6083..d3eb1ca0bd7f375ec0ca95ce1043f7ffdf94fb78 100644 (file)
@@ -34,20 +34,24 @@ import src
 
 # Define all possible option for log command :  sat log <options>
 parser = src.options.Options()
-parser.add_option('t', 'terminal', 'boolean', 'terminal', "Optional: "
-                  "Terminal log.")
-parser.add_option('l', 'last', 'boolean', 'last', "Show the log of the last "
-                  "Optional: launched command.")
-parser.add_option('', 'last_terminal', 'boolean', 'last_terminal', "Show the "
-                  "log of the last compilations"
-                  "Optional: launched command.")
-parser.add_option('f', 'full', 'boolean', 'full', "Optional: Show the logs of "
-                  "ALL the launched commands.")
-parser.add_option('c', 'clean', 'int', 'clean', "Optional: Erase the n most "
-                  "ancient log files.")
-parser.add_option('n', 'no_browser', 'boolean', 'no_browser', "Optional: Do not"
-                  " launch the browser at the end of the command. Only update "
-                  "the hat file.")
+parser.add_option(
+  't', 'terminal', 'boolean', 'terminal', 
+  "Optional: Show sat instances logs, no browser.")
+parser.add_option(
+  'l', 'last', 'boolean', 'last', 
+  "Show the log of the last launched command.")
+parser.add_option(
+  'x', 'last_terminal', 'boolean', 'last_terminal', 
+  """Optional: Show compile log of products, no browser.""")
+parser.add_option(
+  'f', 'full', 'boolean', 'full', 
+  "Optional: Show the logs of ALL the launched commands.")
+parser.add_option(
+  'c', 'clean', 'int', 'clean', 
+  "Optional: Erase the n most ancient log files.")
+parser.add_option(
+  'n', 'no_browser', 'boolean', 'no_browser', 
+  "Optional: Do not launch the browser at the end of the command. Only update the hat file.")
 
 def get_last_log_file(logDir, notShownCommands):
     '''Used in case of last option. Get the last log command file path.
@@ -71,7 +75,11 @@ def get_last_log_file(logDir, notShownCommands):
                 continue
             if int(datehour) > last[1]:
                 last = (fileName, int(datehour))
-    return os.path.join(logDir, last[0])
+    if last[1] != 0:
+      res = os.path.join(logDir, last[0])
+    else:
+      res = None #no log file
+    return res
 
 def remove_log_file(filePath, logger):
     '''if it exists, print a warning and remove the input file
@@ -110,13 +118,26 @@ def print_log_command_in_terminal(filePath, logger):
                                     _("Here are the command traces :\n")), 1)
         logger.write(command_traces, 1)
         logger.write("\n", 1)
+        
+def getMaxFormat(aListOfStr, offset=1):
+    """returns format for columns width as '%-30s"' for example"""
+    maxLen = max([len(i) for i in aListOfStr]) + offset
+    fmt =  "%-" + str(maxLen) + "s" # "%-30s" for example  
+    return fmt, maxLen
 
 def show_last_logs(logger, config, log_dirs):
     """Show last compilation logs"""
     log_dir = os.path.join(config.APPLICATION.workdir, 'LOGS')
     # list the logs
     nb = len(log_dirs)
-    nb_cols = 4
+    fmt1, maxLen = getMaxFormat(log_dirs, offset=1)
+    fmt2 = "%s: " + fmt1 # "%s: %-30s" for example
+    nb_cols = 5
+    # line ~ no more 100 chars
+    if maxLen > 20: nb_cols = 4
+    if maxLen > 25: nb_cols = 3
+    if maxLen > 33: nb_cols = 2
+    if maxLen > 50: nb_cols = 1
     col_size = (nb / nb_cols) + 1
     for index in range(0, col_size):
         for i in range(0, nb_cols):
@@ -125,7 +146,7 @@ def show_last_logs(logger, config, log_dirs):
                 l = log_dirs[k]
                 str_indice = src.printcolors.printcLabel("%2d" % (k+1))
                 log_name = l
-                logger.write("%s: %-30s" % (str_indice, log_name), 1, False)
+                logger.write(fmt2 % (str_indice, log_name), 1, False)
         logger.write("\n", 1, False)
 
     # loop till exit
@@ -145,7 +166,7 @@ def show_product_last_logs(logger, config, product_log_dir):
         l_time_file.append(
               (datetime.datetime.fromtimestamp(my_stat[stat.ST_MTIME]), file_n))
     
-    # display the available logs
+    # display the available logs
     for i, (__, file_name) in enumerate(sorted(l_time_file)):
         str_indice = src.printcolors.printcLabel("%2d" % (i+1))
         opt = []
@@ -268,15 +289,21 @@ def run(args, runner, logger):
     # If the last option is invoked, just, show the last log file
     if options.last_terminal:
         src.check_config_has_application(runner.cfg)
-        log_dirs = os.listdir(os.path.join(runner.cfg.APPLICATION.workdir,
-                                           'LOGS'))
+        rootLogDir = os.path.join(runner.cfg.APPLICATION.workdir, 'LOGS')
+        src.ensure_path_exists(rootLogDir)
+        log_dirs = os.listdir(rootLogDir)
+        if log_dirs == []:
+          raise Exception("log directory empty")
+        log_dirs= sorted(log_dirs)
         show_last_logs(logger, runner.cfg, log_dirs)
         return 0
 
     # If the last option is invoked, just, show the last log file
     if options.last:
-        lastLogFilePath = get_last_log_file(logDir,
-                                            notShownCommands + ["config"])        
+        lastLogFilePath = get_last_log_file(
+            logDir, notShownCommands + ["config"])
+        if lastLogFilePath is None:
+            raise Exception("last log file not found in '%s'" % logDir)
         if options.terminal:
             # Show the log corresponding to the selected command call
             print_log_command_in_terminal(lastLogFilePath, logger)
diff --git a/sat b/sat
index ae3cb05530831dd543cc41d07808980817d9f2b0..7e6b337b95e1c2f58e3c6719373d6183135bdabd 100755 (executable)
--- a/sat
+++ b/sat
@@ -43,6 +43,7 @@ if __name__ == "__main__":
 
     # instantiate the salomeTools class with correct options
     sat = Sat(sys.argv[1:])
+    # or sat = Sat("config -l") for example
     exitCode = sat.execute_command()
     DBG.write("sat exit code", src.okToStr(exitCode))
 
index 34bd557d0f0df99cc019a005c5976d797aed847b..52b775585d7273078ae7ac7869218d19721d2de9 100644 (file)
@@ -587,9 +587,6 @@ class ElementTree:
 
     def parse(self, source, parser=None):
         if not hasattr(source, "read"):
-            # OP 14/11/2017 Ajout de traces pour essayer de decouvrir le pb
-            #               de remontee de log des tests
-            print "TRACES OP - ElementTree.py/ElementTree.parse() source = '#%s#'" %source
             source = open(source, "rb")
         if not parser:
             parser = XMLTreeBuilder()
@@ -881,9 +878,6 @@ def fixtag(tag, namespaces):
 # @return An ElementTree instance
 
 def parse(source, parser=None):
-    # OP 14/11/2017 Ajout de traces pour essayer de decouvrir le pb
-    #               de remontee de log des tests
-    print "TRACES OP - ElementTree.py/parse() source = '#%s#'" %source
     tree = ElementTree()
     tree.parse(source, parser)
     return tree
@@ -1280,9 +1274,6 @@ class XMLTreeBuilder:
     # @param data Encoded data.
 
     def feed(self, data):
-        # OP 14/11/2017 Ajout de traces pour essayer de decouvrir le pb
-        #               de remontee de log des tests
-        #print "TRACES OP - ElementTree.py/XMLTreeBuilder.feed() data = '#%s#'" %data
         self._parser.Parse(data, 0)
 
     ##
index 33dda56cf863be5c5f263481ad5cc44880cbf916..31b00d84a6c6747f6374d755d1f12ee0f003a2e2 100644 (file)
 #  License along with this library; if not, write to the Free Software
 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
 
-'''This file is to assume print debug messages sys.stderr for salomeTools
-warning: only for SAT development phase
-'''
+"""
+This file assume DEBUG functionalities use
+- get debug representation from instances of SAT classes
+- print debug messages in sys.stderr for salomeTools
 
+WARNING: supposed only use in SAT development phase
+"""
+
+import os
 import sys
+import StringIO as SIO
 import pprint as PP
 
-_debug = [False] #support push/pop for temporary active outputs
+_debug = [False] #support push/pop for temporary activate debug outputs
 
 def indent(text, amount=2, ch=' '):
     padding = amount * ch
@@ -34,10 +40,12 @@ def write(title, var="", force=None):
     """write sys.stderr a message if _debug[-1]==True or optionaly force=True"""
     fmt = "\n#### DEBUG: %s:\n%s\n"
     if _debug[-1] or force:
-        if type(var) is not str:
-          sys.stderr.write(fmt % (title, indent(PP.pformat(var))))
+        if 'src.pyconf.Config' in str(type(var)): 
+            sys.stderr.write(fmt % (title, indent(getStrConfigDbg(var))))
+        elif type(var) is not str:
+            sys.stderr.write(fmt % (title, indent(PP.pformat(var))))
         else:
-          sys.stderr.write(fmt % (title, indent(var)))
+            sys.stderr.write(fmt % (title, indent(var)))
     return
 
 def push_debug(aBool):
@@ -51,3 +59,107 @@ def pop_debug():
     else:
         sys.stderr.write("\nERROR: pop_debug: too much pop.")
         return None
+
+###############################################
+# utilitaires divers pour debug
+###############################################
+
+class OutStream(SIO.StringIO):
+    """utility class for pyconf.Config output iostream"""
+    def close(self):
+      """because Config.__save__ calls close() stream as file
+      keep value before lost as self.value
+      """
+      self.value = self.getvalue()
+      SIO.StringIO.close(self)
+    
+class InStream(SIO.StringIO):
+    """utility class for pyconf.Config input iostream"""
+    pass
+
+def getLocalEnv():
+    """get string for environment variables representation"""
+    res = ""
+    for i in sorted(os.environ):
+        res += "%s : %s\n" % (i, os.environ[i])
+    return res
+
+# save as initial Config.save() moved as Config.__save__() 
+def saveConfigStd(config, aStream):
+    """returns as file .pyconf"""
+    indent =  0
+    config.__save__(aStream, indent) 
+
+def getStrConfigStd(config):
+    """set string as saveConfigStd, 
+    as file .pyconf"""
+    outStream = OutStream()
+    saveConfigStd(config, outStream)
+    return outStream.value
+
+def getStrConfigDbg(config):
+    """set string as saveConfigDbg, 
+    as (path expression evaluation) for debug"""
+    outStream = OutStream()
+    saveConfigDbg(config, outStream)
+    return outStream.value
+
+def saveConfigDbg(config, aStream, indent=0, path=""):
+    """returns multilines (path expression evaluation) for debug"""
+    _saveConfigRecursiveDbg(config, aStream, indent, path)
+    aStream.close() # as config.__save__()
+
+def _saveConfigRecursiveDbg(config, aStream, indent, path):
+    """inspired from Mapping.__save__"""
+    debug = False
+    if indent <= 0: 
+      indentp = 0
+    else:
+      indentp = indentp + 2
+    indstr = indent * ' ' # '':no indent, ' ':indent
+    strType = str(type(config))
+    if "Sequence" in strType:
+      for i in range(len(config)):
+        _saveConfigRecursiveDbg(config[i], aStream, indentp, path+"[%i]" % i)
+      return
+    try: 
+      order = object.__getattribute__(config, 'order')
+      data = object.__getattribute__(config, 'data')
+    except:
+      aStream.write("%s%s : '%s'\n" % (indstr, path, str(config)))
+      return     
+    for key in sorted(order):
+      value = data[key]
+      strType = str(type(value))
+      if debug: print indstr + 'strType = %s' % strType, key
+      if "Config" in strType:
+        _saveConfigRecursiveDbg(value, aStream, indentp, path+"."+key)
+        continue
+      if "Mapping" in strType:
+        _saveConfigRecursiveDbg(value, aStream, indentp, path+"."+key)
+        continue
+      if "Sequence" in strType:
+        for i in range(len(value)):
+          _saveConfigRecursiveDbg(value[i], aStream, indentp, path+"."+key+"[%i]" % i)
+        continue
+      if "Expression" in strType:
+        try:
+          evaluate = value.evaluate(config)
+          aStream.write("%s%s.%s : %s --> '%s'\n" % (indstr, path, key, str(value), evaluate))
+        except Exception as e:      
+          aStream.write("%s%s.%s : !!! ERROR: %s !!!\n" % (indstr, path, key, e.message))     
+        continue
+      if "Reference" in strType:
+        try:
+          evaluate = value.resolve(config)
+          aStream.write("%s%s.%s : %s --> '%s'\n" % (indstr, path, key, str(value), evaluate))
+        except Exception as e:  
+          aStream.write("%s%s.%s : !!! ERROR: %s !!!\n" % (indstr, path, key, e.message))     
+        continue
+      if type(value) in [str, bool, int, type(None), unicode]:
+        aStream.write("%s%s.%s : '%s'\n" % (indstr, path, key, str(value)))
+        continue
+      try:
+        aStream.write("!!! TODO fix that %s %s%s.%s : %s\n" % (type(value), indstr, path, key, str(value)))
+      except Exception as e:      
+        aStream.write("%s%s.%s : !!! %s\n" % (indstr, path, key, e.message))
index cce23d34aa5597a5d3757d82257cb98afc649385..484454137c4417a81c62b13a19521baeeccd03bc 100644 (file)
@@ -66,7 +66,7 @@ class OptResult(object):
 
     def __repr__(self):
         aStr = PP.pformat(self.__dict__)
-        res = "%s(\n %s)" % (self.__class__.__name__, aStr[1:-1])
+        res = "%s(\n %s\n)" % (self.__class__.__name__, aStr[1:-1])
         return res
 
 class Options(object):
@@ -175,9 +175,10 @@ class Options(object):
         # passed in the command regarding the available options
         try:
           optlist, args = getopt.getopt(argList, shortNameOption, longNameOption)
-        except:
-          DBG.write("ERROR", (shortNameOption, longNameOption), True)
-        
+        except Exception as e:
+          msg = str(e) + ", expected:\n\n%s" % str(self)
+          raise Exception(msg)
+
         # instantiate and completing the optResult that will be returned
         optResult = OptResult()
         for option in self.options:
@@ -223,6 +224,18 @@ class Options(object):
         '''
         aDict = {'options': self.options, 'results': self.results}
         aStr = PP.pformat(aDict)
+        res = "%s(\n %s\n)" % (self.__class__.__name__, aStr[1:-1])
+        return res
+        
+    def __str__(self): 
+        '''str for only resume expected self.options
+        '''
+        #aDict = [(k["longName"], k["shortName", k["helpString"]) for k in self.options}
+        #aList = [(k, self.options[k]) for k in sorted(self.options.keys())]
+        aDict = {}
+        for o in self.options:
+          aDict[o["longName"]] = (o["shortName"], o["helpString"])
+        aStr = PP.pformat(aDict)
         res = "%s(\n %s)" % (self.__class__.__name__, aStr[1:-1])
         return res
         
index de9af0c89c0eeaa4249b73c94c40b4b2f862e3ae..ee01cc0b7e0051a31446321e4e93dbe3c45eddcb 100644 (file)
@@ -756,7 +756,7 @@ class Config(Mapping):
         try:
             return eval(s)
         except Exception as e:
-            raise ConfigError(str(e))
+            raise ConfigError("Config path not found: '%s'" % path)
 
 class Sequence(Container):
     """
@@ -1740,5 +1740,5 @@ class ConfigList(list):
             except ConfigError:
                 pass
         if not found:
-            raise ConfigError("unable to resolve %r" % path)
+            raise ConfigError("ConfigList path not found '%r'" % path)
         return rv
index 4036061e7f49a98e9a65c93854ca0727cc559324..ba6384114d5c5fa0f5e7f7dcfa17a0bf0609af16 100755 (executable)
@@ -25,14 +25,15 @@ import sys
 import re
 import tempfile
 import imp
-import types
 import gettext
 import traceback
 import subprocess as SP
+import pprint as PP
 
-#################################
-# NOT MAIN allowed
-#################################
+
+########################################################################
+# NOT MAIN entry allowed, use sat
+########################################################################
 if __name__ == "__main__":
     sys.stderr.write("\nERROR: 'salomeTools.py' is not main command entry for sat: use 'sat' instead.\n\n")
     KOSYS = 1 # avoid import src
@@ -40,6 +41,7 @@ if __name__ == "__main__":
 
 
 import src.debug as DBG # Easy print stderr (for DEBUG only)
+import src
 
 # get path to src
 rootdir = os.path.realpath( os.path.join(os.path.dirname(__file__), "..") )
@@ -50,10 +52,6 @@ cmdsdir = os.path.join(rootdir, "commands")
 # load resources for internationalization
 gettext.install('salomeTools', os.path.join(srcdir, 'i18n'))
 
-# salomeTools imports
-import src
-import commands.config
-
 # The possible hooks : 
 # pre is for hooks to be executed before commands
 # post is for hooks to be executed after commands
@@ -62,6 +60,9 @@ C_POST_HOOK = "post"
 
 _LANG = os.environ["LANG"] # original locale
 
+########################################################################
+# utility methods
+########################################################################
 def find_command_list(dirPath):
     '''Parse files in dirPath that end with .py : it gives commands list
     
@@ -77,14 +78,14 @@ def find_command_list(dirPath):
             cmd_list.append(item[:-len('.py')])
     return cmd_list
 
-# The list of valid salomeTools commands
-#lCommand = ['config', 'compile', 'prepare',...]
-lCommand = find_command_list(cmdsdir)
+# The list of valid salomeTools commands from cmdsdir
+#_COMMANDS_NAMES = ['config', 'compile', 'prepare',...]
+_COMMANDS_NAMES = find_command_list(cmdsdir)
 
 def getCommandsList():
-    """Gives commands list (as basename of files commands/*.py)
+    """Gives commands list (as basename of files cmdsdir/*.py)
     """ 
-    return lCommand
+    return _COMMANDS_NAMES
 
 def launchSat(command):
     """launch sat as subprocess popen
@@ -113,23 +114,191 @@ def setLocale():
     gettext.install('salomeTools', os.path.join(srcdir, 'i18n'))
     DBG.write("setLocale", os.environ["LANG"])
     
-# Define all possible option for salomeTools command :  sat <options> <args>
-parser = src.options.Options()
-parser.add_option('h', 'help', 'boolean', 'help', 
-                  _("shows global help or help on a specific command."))
-parser.add_option('o', 'overwrite', 'list', "overwrite", 
-                  _("overwrites a configuration parameters."))
-parser.add_option('g', 'debug', 'boolean', 'debug_mode', 
-                  _("run salomeTools in debug mode."))
-parser.add_option('v', 'verbose', 'int', "output_verbose_level", 
-                  _("change output verbose level (default is 3)."))
-parser.add_option('b', 'batch', 'boolean', "batch", 
-                  _("batch mode (no question)."))
-parser.add_option('t', 'all_in_terminal', 'boolean', "all_in_terminal", 
-                  _("all traces in the terminal (for example compilation logs)."))
-parser.add_option('l', 'logs_paths_in_file', 'string', "logs_paths_in_file", 
-                  _("put the command result and paths to log files."))
+
+########################################################################
+# _BaseCmd class
+########################################################################
+class _BaseCommand(object):
+    '''_BaseCmd is the base class for all inherited commands of salomeTools
+    instancied as classes 'Command' in files '.../commands/*.py'
+    '''
+    def __init__(self, name):
+        self.name = name
+        self.runner = None # runner (as caller) usually as Sat instance
+        self.logger = None # logger (from caller) usually as Sat instance logger
+        self.config = None # config usually loaded with _getConfig method
+        
+    def getClassName(self):
+        """
+        returns 'config.Command' or prepare.Command' for example 
+        as inherited classes 'Command' in files
+        '.../commands/config.py' or '.../commands/prepare.py'
+        """
+        return "%s.%s" % (self.name, self.__class__.__name__)
+        
+    def __repr__(self):
+        tmp = PP.pformat(self.__dict__)
+        res = "%s(\n %s)\n" % (self.getClassName(), tmp[1:-1])
+        return res
+        
+    def setRunner(self, runner):
+        """set who owns me, and use me whith method run()"""
+        self.runner  = runner
+        
+    def setLogger(self, logger):
+        """set logger for run command"""
+        self.logger  = logger
+        
+    def getLogger(self):
+        if self.logger is None: # could use owner Sat instance logger
+          return self.runner.getLogger()
+        else:                   # could use local logger
+          return self.logger
+        
+    def getRunner(self):
+        if self.runner is None:
+          raise Exception("have to set runner attribute, fix it.")
+        else:
+          return self.runner
+                  
+    def getParser(self):
+        raise Exception("_BaseCmd class have not to be instancied, only for inheritage")
+    
+    def description(self):
+        '''method that is called when salomeTools is called with --help option.
+        
+        :return: The text to display for the config command description.
+        :rtype: str
+        '''
+        raise Exception("_BaseCmd class have not to be instancied, only for inheritage")
     
+    def run(self, args, runner, logger):
+        '''method that is called when salomeTools is called with self.name parameter.
+        '''
+        raise Exception("_BaseCmd class have not to be instancied, only for inheritage")
+
+        
+    def getConfig(self):
+        if self.config is None:
+          self.config = self._getConfig()
+        return self.config
+
+
+    def _getConfig(self):
+        '''The function that will load the configuration (all pyconf)
+        and return the config from files .pyconf
+        '''
+        if self.config is not None:
+          raise Exception("config existing yet in 's' instance" % self.getClassName())
+          
+        # Get the arguments in a list and remove the empty elements
+        print "command.runner.arguments", self.runner.arguments
+
+        self.parser = self.getParser() 
+        try:
+            options, args = self.parser.parse_args(self.runner.arguments[1:])
+            DBG.write("%s args", args)
+            DBG.write("%s options", options)    
+        except Exception as exc:
+            write_exception(exc)
+            sys.exit(src.KOSYS)
+
+        self.arguments = args # args are postfixes options: args[0] is the 'commands' command
+        self.options = options # the options passed to salomeTools
+        
+        print "command.arguments", self.arguments
+        print "command.options", self.options
+        
+        if type(args) == type(''):
+            # split by spaces without considering spaces in quotes
+            argv_0 = re.findall(r'(?:"[^"]*"|[^\s"])+', args)
+        else:
+            argv_0 = args
+        
+        if argv_0 != ['']:
+            while "" in argv_0: argv_0.remove("")
+        
+        # Format the argv list in order to prevent strings 
+        # that contain a blank to be separated
+        argv = []
+        elem_old = ""
+        for elem in argv_0:
+            if argv == [] or elem_old.startswith("-") or elem.startswith("-"):
+                argv.append(elem)
+            else:
+                argv[-1] += " " + elem
+            elem_old = elem
+                   
+        # if it is provided by the command line, get the application
+        appliToLoad = None
+        if argv not in [[''], []] and argv[0][0] != "-":
+            appliToLoad = argv[0].rstrip('*')
+            argv = argv[1:]
+        
+        # Check if the global options of salomeTools have to be changed
+        if options:
+            options_save = self.options
+            self.options = options  
+
+        # read the configuration from all the pyconf files    
+        cfgManager = commands.config.ConfigManager()
+        config = cfgManager.get_config(datadir=self.datadir, 
+                                         application=appliToLoad, 
+                                         options=self.options, 
+                                         command=__nameCmd__)
+                       
+        # Set the verbose mode if called
+        if verbose > -1:
+            verbose_save = self.options.output_verbose_level
+            self.options.__setattr__("output_verbose_level", verbose)    
+
+        # Set batch mode if called
+        if batch:
+            batch_save = self.options.batch
+            self.options.__setattr__("batch", True)
+
+        # set output level
+        if self.options.output_verbose_level is not None:
+            config.USER.output_verbose_level = self.options.output_verbose_level
+        if config.USER.output_verbose_level < 1:
+            config.USER.output_verbose_level = 0
+        silent = (config.USER.output_verbose_level == 0)
+
+        # create log file
+        micro_command = False
+        if logger_add_link:
+            micro_command = True
+        logger_command = src.logger.Logger(config, 
+                           silent_sysstd=silent,
+                           all_in_terminal=self.options.all_in_terminal,
+                           micro_command=micro_command)
+        
+        # Check that the path given by the logs_paths_in_file option
+        # is a file path that can be written
+        if self.options.logs_paths_in_file and not micro_command:
+            try:
+                self.options.logs_paths_in_file = os.path.abspath(
+                                        self.options.logs_paths_in_file)
+                dir_file = os.path.dirname(self.options.logs_paths_in_file)
+                if not os.path.exists(dir_file):
+                    os.makedirs(dir_file)
+                if os.path.exists(self.options.logs_paths_in_file):
+                    os.remove(self.options.logs_paths_in_file)
+                file_test = open(self.options.logs_paths_in_file, "w")
+                file_test.close()
+            except Exception as e:
+                msg = _("WARNING: the logs_paths_in_file option will "
+                        "not be taken into account.\nHere is the error:")
+                logger_command.write("%s\n%s\n\n" % (
+                                     src.printcolors.printcWarning(msg),
+                                     str(e)))
+                self.options.logs_paths_in_file = None
+                
+        return config
+
+########################################################################
+# Sat class
+########################################################################
 class Sat(object):
     '''The main class that stores all the commands of salomeTools
     '''
@@ -142,52 +311,99 @@ class Sat(object):
         '''
         # Read the salomeTools prefixes options before the 'commands' tag
         # sat <options> <args>
-        # (the list of possible options is  at the beginning of this file)                
+        # (the list of possible options is  at the beginning of this file)
+        DBG.push_debug(True)
+        self.parser = self._getParser() 
         try:
             if type(opt) is not list: # as string 'sat --help' for example'
                 opts = opt.split()
             else:
                 opts = opt
-            options, args = parser.parse_args(opts)
-            DBG.write("Sat args", args)
-            DBG.write("Sat options", options)    
+            options, args = self.parser.parse_args(opts)
+            DBG.write("Sat args", args)
+            DBG.write("Sat options", options)    
         except Exception as exc:
             write_exception(exc)
             sys.exit(src.KOSYS)
 
-        # initialization of class attributes       
-        self.__dict__ = dict()
-        self.cfg = None # the config that will be read using pyconf module
+        self.logger = None # the logger that will be use
+        self.config = None # the config that will be read using pyconf module
         self.arguments = args # args are postfixes options: args[0] is the 'commands' command
         self.options = options # the options passed to salomeTools
         self.datadir = datadir # default value will be <salomeTools root>/data
-        # set the commands by calling the dedicated function
-        self._setCommands(cmdsdir)
-        
-        '''
-        # done with execute_command, to avoid sys.exit
-        # if the help option has been called, print help and exit
-        if options.help:
-            try:
-                self.print_help(args)
-                sys.exit(src.OKSYS)
-            except Exception as exc:
-                write_exception(exc)
-                DBG.write("args", args, True) 
-                sys.exit(src.KOSYS)
-        '''
+        # contains commands classes needed (think micro commands)
+        # if useful 'a la demande'
+        self.commands = {}
 
-    def __getattr__(self, name):
-        '''overwrite of __getattr__ function in order to display 
-           a customized message in case of a wrong call
         
-        :param name str: The name of the attribute 
-        '''
-        if name in self.__dict__:
-            return self.__dict__[name]
-        else:
-            raise AttributeError("'%s'" % name + _(" is not a valid command"))
+    def __repr__(self):
+        aDict = {
+          "arguments": self.arguments, 
+          "options": self.options,
+          "datadir": self.datadir,
+          "commands": sorted(self.commands.keys()),
+        }
+        tmp = PP.pformat(aDict)
+        res = "Sat(\n %s\n)\n" % tmp[1:-1]
+        return res
 
+    
+    def getLogger(self):
+        return self.logger
+
+
+    def _getParser(self):
+        """
+        Define all possible <options> for salomeTools/sat command: 'sat <options> <args>'
+        (internal use only)
+        """
+        import src.options
+        parser = src.options.Options()
+        parser.add_option('h', 'help', 'boolean', 'help', 
+                          _("shows global help or help on a specific command."))
+        parser.add_option('o', 'overwrite', 'list', "overwrite", 
+                          _("overwrites a configuration parameters."))
+        parser.add_option('g', 'debug', 'boolean', 'debug_mode', 
+                          _("run salomeTools in debug mode."))
+        parser.add_option('v', 'verbose', 'int', "output_verbose_level", 
+                          _("change output verbose level (default is 3)."))
+        parser.add_option('b', 'batch', 'boolean', "batch", 
+                          _("batch mode (no question)."))
+        parser.add_option('t', 'all_in_terminal', 'boolean', "all_in_terminal", 
+                          _("all traces in the terminal (for example compilation logs)."))
+        parser.add_option('l', 'logs_paths_in_file', 'string', "logs_paths_in_file", 
+                          _("put the command result and paths to log files."))
+        return parser
+
+        
+    def _getCommand(self, name):
+        """
+        create and add Command 'name' as instance of class in dict self.commands
+        create only one instance
+        """
+        if name not in _COMMANDS_NAMES:
+            raise AttributeError(_("command not valid: '%s'") % name)
+        if name in self.commands.keys():
+            raise AttributeError(_("command existing yet: '%s', use getCommand") % name)
+        file_, pathname, description = imp.find_module(name, [cmdsdir])
+        module = imp.load_module(name, file_, pathname, description)
+        cmdInstance = module.Command(name)
+        cmdInstance.setRunner(self) # self is runner, owns cmdInstance
+        DBG.write("new command", cmdInstance)
+        return cmdInstance       
+                    
+    def getCommand(self, name):
+        """
+        returns inherited instance of _BaseCmd for command 'name'
+        if not existing as self.commands[name], create it.
+        
+        example:
+        returns Commamd() from commamd.config 
+        """
+        if name not in self.commands.keys():
+            self.commands[name] = self._getCommand(name)
+        return self.commands[name]    
+       
     def execute_command(self, opt=None):
         """select first argument as a command in directory 'commands', and launch on arguments
         
@@ -209,293 +425,12 @@ class Sat(object):
             return src.OKSYS
        
         # the command called
-        command = args[0]
-        # get dynamically the command function to call
-        fun_command = self.__getattr__(command)
+        cmdName = args[0]
+        # create/get dynamically the command instance to call its 'run' method
+        cmdInstance = self.getCommand(cmdName)
         # Run the command using the arguments
-        exitCode = fun_command(args[1:])
-        return exitCode 
-    
-    def _setCommands(self, dirPath):
-        '''set class attributes corresponding to all commands that are 
-           in the dirPath directory
-        
-        :param dirPath str: The directory path containing the commands 
-        '''
-        # loop on the commands name
-        for nameCmd in lCommand:
-            
-            # Exception for the jobs command that requires the paramiko module
-            if nameCmd == "jobs":
-                try:
-                    saveout = sys.stderr
-                    ff = tempfile.TemporaryFile()
-                    sys.stderr = ff
-                    import paramiko
-                    sys.stderr = saveout
-                except:
-                    sys.stderr = saveout
-                    continue
-
-            # load the module that has name nameCmd in dirPath
-            (file_, pathname, description) = imp.find_module(nameCmd, [dirPath])
-            module = imp.load_module(nameCmd, file_, pathname, description)
-            
-            def run_command(args='',
-                            options=None,
-                            batch = False,
-                            verbose = -1,
-                            logger_add_link = None):
-                '''The function that will load the configuration (all pyconf)
-                and return the function run of the command corresponding to module
-                
-                :param args str: The arguments of the command 
-                '''
-                # Make sure the internationalization is available
-                # gettext.install('salomeTools', os.path.join(srcdir, 'i18n'))
-
-                # Get the arguments in a list and remove the empty elements
-                if type(args) == type(''):
-                    # split by spaces without considering spaces in quotes
-                    argv_0 = re.findall(r'(?:"[^"]*"|[^\s"])+', args)
-                else:
-                    argv_0 = args
-                
-                if argv_0 != ['']:
-                    while "" in argv_0: argv_0.remove("")
-                
-                # Format the argv list in order to prevent strings 
-                # that contain a blank to be separated
-                argv = []
-                elem_old = ""
-                for elem in argv_0:
-                    if argv == [] or elem_old.startswith("-") or elem.startswith("-"):
-                        argv.append(elem)
-                    else:
-                        argv[-1] += " " + elem
-                    elem_old = elem
-                           
-                # if it is provided by the command line, get the application
-                appliToLoad = None
-                if argv not in [[''], []] and argv[0][0] != "-":
-                    appliToLoad = argv[0].rstrip('*')
-                    argv = argv[1:]
-                
-                # Check if the global options of salomeTools have to be changed
-                if options:
-                    options_save = self.options
-                    self.options = options  
-
-                # read the configuration from all the pyconf files    
-                cfgManager = commands.config.ConfigManager()
-                self.cfg = cfgManager.get_config(datadir=self.datadir, 
-                                                 application=appliToLoad, 
-                                                 options=self.options, 
-                                                 command=__nameCmd__)
-                               
-                # Set the verbose mode if called
-                if verbose > -1:
-                    verbose_save = self.options.output_verbose_level
-                    self.options.__setattr__("output_verbose_level", verbose)    
-
-                # Set batch mode if called
-                if batch:
-                    batch_save = self.options.batch
-                    self.options.__setattr__("batch", True)
-
-                # set output level
-                if self.options.output_verbose_level is not None:
-                    self.cfg.USER.output_verbose_level = self.options.output_verbose_level
-                if self.cfg.USER.output_verbose_level < 1:
-                    self.cfg.USER.output_verbose_level = 0
-                silent = (self.cfg.USER.output_verbose_level == 0)
-
-                # create log file
-                micro_command = False
-                if logger_add_link:
-                    micro_command = True
-                logger_command = src.logger.Logger(self.cfg, 
-                                   silent_sysstd=silent,
-                                   all_in_terminal=self.options.all_in_terminal,
-                                   micro_command=micro_command)
-                
-                # Check that the path given by the logs_paths_in_file option
-                # is a file path that can be written
-                if self.options.logs_paths_in_file and not micro_command:
-                    try:
-                        self.options.logs_paths_in_file = os.path.abspath(
-                                                self.options.logs_paths_in_file)
-                        dir_file = os.path.dirname(self.options.logs_paths_in_file)
-                        if not os.path.exists(dir_file):
-                            os.makedirs(dir_file)
-                        if os.path.exists(self.options.logs_paths_in_file):
-                            os.remove(self.options.logs_paths_in_file)
-                        file_test = open(self.options.logs_paths_in_file, "w")
-                        file_test.close()
-                    except Exception as e:
-                        msg = _("WARNING: the logs_paths_in_file option will "
-                                "not be taken into account.\nHere is the error:")
-                        logger_command.write("%s\n%s\n\n" % (
-                                             src.printcolors.printcWarning(msg),
-                                             str(e)))
-                        self.options.logs_paths_in_file = None
-                
-                options_launched = ""
-                res = None
-                try:
-                    # Execute the hooks (if there is any) 
-                    # and run method of the command
-                    self.run_hook(__nameCmd__, C_PRE_HOOK, logger_command)
-                    res = __module__.run(argv, self, logger_command)
-                    self.run_hook(__nameCmd__, C_POST_HOOK, logger_command)
-                    if res is None:
-                        res = 0
-                        
-                except Exception as e:
-                    # Get error
-                    logger_command.write("\n***** ", 1)
-                    logger_command.write(
-                        src.printcolors.printcError("salomeTools ERROR:"), 1)
-                    logger_command.write("\n" + str(e) + "\n\n", 1)
-                    # get stack
-                    __, __, exc_traceback = sys.exc_info()
-                    fp = tempfile.TemporaryFile()
-                    traceback.print_tb(exc_traceback, file=fp)
-                    fp.seek(0)
-                    stack = fp.read()
-                    verbosity = 5
-                    if self.options.debug_mode:
-                        verbosity = 1
-                    logger_command.write("TRACEBACK:\n%s" % stack.replace('"',"'"),
-                                         verbosity)
-                finally:
-                    # set res if it is not set in the command
-                    if res is None:
-                        res = 1
-                                            
-                    # come back to the original global options
-                    if options:
-                        options_launched = get_text_from_options(self.options)
-                        self.options = options_save
-                    
-                    # come back in the original batch mode if 
-                    # batch argument was called
-                    if batch:
-                        self.options.__setattr__("batch", batch_save)
-
-                    # come back in the original verbose mode if 
-                    # verbose argument was called                        
-                    if verbose > -1:
-                        self.options.__setattr__("output_verbose_level", 
-                                                 verbose_save)
-                    # put final attributes in xml log file 
-                    # (end time, total time, ...) and write it
-                    launchedCommand = ' '.join([self.cfg.VARS.salometoolsway +
-                                                os.path.sep +
-                                                'sat',
-                                                options_launched,
-                                                __nameCmd__, 
-                                                ' '.join(argv_0)])
-                    launchedCommand = launchedCommand.replace('"', "'")
-                    
-                    # Add a link to the parent command      
-                    if logger_add_link is not None:
-                        logger_add_link.add_link(logger_command.logFileName,
-                                                 __nameCmd__,
-                                                 res,
-                                                 launchedCommand)
-                        logger_add_link.l_logFiles += logger_command.l_logFiles
-                                            
-                    # Put the final attributes corresponding to end time and
-                    # Write the file to the hard drive
-                    logger_command.end_write(
-                                        {"launchedCommand" : launchedCommand})
-                    
-                    if res != 0:
-                        res = 1
-                        
-                    # print the log file path if 
-                    # the maximum verbose mode is invoked
-                    if not micro_command:
-                        logger_command.write("\nPath to the xml log file:\n", 5)
-                        logger_command.write("%s\n\n" % \
-                            src.printcolors.printcInfo(logger_command.logFilePath), 5)
-
-                    # If the logs_paths_in_file was called, write the result
-                    # and log files in the given file path
-                    if self.options.logs_paths_in_file and not micro_command:
-                        file_res = open(self.options.logs_paths_in_file, "w")
-                        file_res.write(str(res) + "\n")
-                        for i, filepath in enumerate(logger_command.l_logFiles):
-                            file_res.write(filepath)
-                            if i < len(logger_command.l_logFiles):
-                                file_res.write("\n")
-                                file_res.flush()
-                
-                return res
-
-            # Make sure that run_command will be redefined 
-            # at each iteration of the loop
-            globals_up = {}
-            globals_up.update(run_command.__globals__)
-            globals_up.update({'__nameCmd__': nameCmd, '__module__' : module})
-            func = types.FunctionType(run_command.__code__,
-                                      globals_up,
-                                      run_command.__name__,
-                                      run_command.__defaults__,
-                                      run_command.__closure__)
-
-            # set the attribute corresponding to the command
-            self.__setattr__(nameCmd, func)
-
-    def run_hook(self, cmd_name, hook_type, logger):
-        '''Execute a hook file for a given command regarding the fact 
-           it is pre or post
-        
-        :param cmd_name str: The the command on which execute the hook
-        :param hook_type str: pre or post
-        :param logger Logger: the logging instance to use for the prints
-        '''
-        # The hooks must be defined in the application pyconf
-        # So, if there is no application, do not do anything
-        if not src.config_has_application(self.cfg):
-            return
-
-        # The hooks must be defined in the application pyconf in the
-        # APPLICATION section, hook : { command : 'script_path.py'}
-        if "hook" not in self.cfg.APPLICATION \
-                    or cmd_name not in self.cfg.APPLICATION.hook:
-            return
-
-        # Get the hook_script path and verify that it exists
-        hook_script_path = self.cfg.APPLICATION.hook[cmd_name]
-        if not os.path.exists(hook_script_path):
-            raise src.SatException(_("Hook script not found: %s") % 
-                                   hook_script_path)
-        
-        # Try to execute the script, catch the exception if it fails
-        try:
-            # import the module (in the sense of python)
-            pymodule = imp.load_source(cmd_name, hook_script_path)
-            
-            # format a message to be printed at hook execution
-            msg = src.printcolors.printcWarning(_("Run hook script"))
-            msg = "%s: %s\n" % (msg, 
-                                src.printcolors.printcInfo(hook_script_path))
-            
-            # run the function run_pre_hook if this function is called 
-            # before the command, run_post_hook if it is called after
-            if hook_type == C_PRE_HOOK and "run_pre_hook" in dir(pymodule):
-                logger.write(msg, 1)
-                pymodule.run_pre_hook(self.cfg, logger)
-            elif hook_type == C_POST_HOOK and "run_post_hook" in dir(pymodule):
-                logger.write(msg, 1)
-                pymodule.run_post_hook(self.cfg, logger)
-
-        except Exception as exc:
-            msg = _("Unable to run hook script: %s") % hook_script_path
-            msg += "\n" + str(exc)
-            raise src.SatException(msg)
+        exitCode = cmdInstance.run(args[1:])
+        return exitCode
 
     def print_help(self, opt):
         '''Prints help for a command. Function called when "sat -h <command>"
@@ -516,11 +451,8 @@ class Sat(object):
         # Check if this command exists
         if not hasattr(self, command):
             raise src.SatException(_("Command '%s' does not exist") % command)
-        
-        # load the module
-        module = self.get_module(command)
-        
-        msg = self.get_module_help(module)
+                
+        msg = self.get_command_help(module)
             
         if isStdoutPipe():
             # clean color if the terminal is redirected by user
@@ -529,7 +461,7 @@ class Sat(object):
         print(msg)
         return 
             
-    def get_module_help(self, module):
+    def get_command_help(self, module):
         """get help for a command
         as 'sat --help config' for example
         """
@@ -545,23 +477,9 @@ class Sat(object):
         if hasattr( module, "parser" ) :
             msg += module.parser.get_help() + "\n"
         return msg
+      
         
-
-    def get_module(self, module):
-        '''Loads a command. Function called only by print_help
-        
-        :param module str: the command to load
-        '''
-        # Check if this command exists
-        if not hasattr(self, module):
-            raise src.SatException(_("Command '%s' does not exist") % module)
-
-        # load the module
-        (file_, pathname, description) = imp.find_module(module, [cmdsdir])
-        module = imp.load_module(module, file_, pathname, description)
-        return module
-        
-
+###################################################################     
 def get_text_from_options(options):
     text_options = ""
     for attr in dir(options):
diff --git a/src/salomeTools_old.py b/src/salomeTools_old.py
new file mode 100755 (executable)
index 0000000..4036061
--- /dev/null
@@ -0,0 +1,637 @@
+#!/usr/bin/env python
+#-*- coding:utf-8 -*-
+
+#  Copyright (C) 2010-2018  CEA/DEN
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 2.1 of the License.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library; if not, write to the Free Software
+#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+
+'''This file is the main entry file to salomeTools
+'''
+
+import os
+import sys
+import re
+import tempfile
+import imp
+import types
+import gettext
+import traceback
+import subprocess as SP
+
+#################################
+# NOT MAIN allowed
+#################################
+if __name__ == "__main__":
+    sys.stderr.write("\nERROR: 'salomeTools.py' is not main command entry for sat: use 'sat' instead.\n\n")
+    KOSYS = 1 # avoid import src
+    sys.exit(KOSYS)
+
+
+import src.debug as DBG # Easy print stderr (for DEBUG only)
+
+# get path to src
+rootdir = os.path.realpath( os.path.join(os.path.dirname(__file__), "..") )
+DBG.write("sat rootdir", rootdir)  
+srcdir = os.path.join(rootdir, "src")
+cmdsdir = os.path.join(rootdir, "commands")
+
+# load resources for internationalization
+gettext.install('salomeTools', os.path.join(srcdir, 'i18n'))
+
+# salomeTools imports
+import src
+import commands.config
+
+# The possible hooks : 
+# pre is for hooks to be executed before commands
+# post is for hooks to be executed after commands
+C_PRE_HOOK = "pre"
+C_POST_HOOK = "post"
+
+_LANG = os.environ["LANG"] # original locale
+
+def find_command_list(dirPath):
+    '''Parse files in dirPath that end with .py : it gives commands list
+    
+    :param dirPath str: The directory path where to search the commands
+    :return: cmd_list : the list containing the commands name 
+    :rtype: list
+    '''
+    cmd_list = []
+    for item in os.listdir(dirPath):
+        if item in ["__init__.py"]: #avoid theses files
+            continue
+        if item.endswith('.py'):
+            cmd_list.append(item[:-len('.py')])
+    return cmd_list
+
+# The list of valid salomeTools commands
+#lCommand = ['config', 'compile', 'prepare',...]
+lCommand = find_command_list(cmdsdir)
+
+def getCommandsList():
+    """Gives commands list (as basename of files commands/*.py)
+    """ 
+    return lCommand
+
+def launchSat(command):
+    """launch sat as subprocess popen
+    command as string ('sat --help' for example)
+    used for unittest, or else...
+    returns tuple (stdout, stderr)
+    """
+    if "sat" not in command.split()[0]:
+      raise Exception(_("Not a valid command for launchSat: '%s'") % command)
+    env = dict(os.environ)
+    env["PATH"] = rootdir + ":" + env["PATH"]
+    res =SP.Popen(command, shell=True, env=env, stdout=SP.PIPE, stderr=SP.PIPE).communicate()
+    return res
+
+def setNotLocale():
+    """force english at any moment"""
+    os.environ["LANG"] = ''
+    gettext.install('salomeTools', os.path.join(srcdir, 'i18n'))
+    DBG.write("setNotLocale", os.environ["LANG"])
+    
+def setLocale():
+    """reset initial locale at any moment 
+    'fr' or else 'TODO' from initial environment var '$LANG'
+    """
+    os.environ["LANG"] = _LANG
+    gettext.install('salomeTools', os.path.join(srcdir, 'i18n'))
+    DBG.write("setLocale", os.environ["LANG"])
+    
+# Define all possible option for salomeTools command :  sat <options> <args>
+parser = src.options.Options()
+parser.add_option('h', 'help', 'boolean', 'help', 
+                  _("shows global help or help on a specific command."))
+parser.add_option('o', 'overwrite', 'list', "overwrite", 
+                  _("overwrites a configuration parameters."))
+parser.add_option('g', 'debug', 'boolean', 'debug_mode', 
+                  _("run salomeTools in debug mode."))
+parser.add_option('v', 'verbose', 'int', "output_verbose_level", 
+                  _("change output verbose level (default is 3)."))
+parser.add_option('b', 'batch', 'boolean', "batch", 
+                  _("batch mode (no question)."))
+parser.add_option('t', 'all_in_terminal', 'boolean', "all_in_terminal", 
+                  _("all traces in the terminal (for example compilation logs)."))
+parser.add_option('l', 'logs_paths_in_file', 'string', "logs_paths_in_file", 
+                  _("put the command result and paths to log files."))
+    
+class Sat(object):
+    '''The main class that stores all the commands of salomeTools
+    '''
+    def __init__(self, opt='', datadir=None):
+        '''Initialization
+        
+        :param opt str or list: The sat options 
+        :param: datadir str : the directory that contain all the external 
+                              data (like software pyconf and software scripts)
+        '''
+        # Read the salomeTools prefixes options before the 'commands' tag
+        # sat <options> <args>
+        # (the list of possible options is  at the beginning of this file)                
+        try:
+            if type(opt) is not list: # as string 'sat --help' for example'
+                opts = opt.split()
+            else:
+                opts = opt
+            options, args = parser.parse_args(opts)
+            # DBG.write("Sat args", args)
+            # DBG.write("Sat options", options)    
+        except Exception as exc:
+            write_exception(exc)
+            sys.exit(src.KOSYS)
+
+        # initialization of class attributes       
+        self.__dict__ = dict()
+        self.cfg = None # the config that will be read using pyconf module
+        self.arguments = args # args are postfixes options: args[0] is the 'commands' command
+        self.options = options # the options passed to salomeTools
+        self.datadir = datadir # default value will be <salomeTools root>/data
+        # set the commands by calling the dedicated function
+        self._setCommands(cmdsdir)
+        
+        '''
+        # done with execute_command, to avoid sys.exit
+        # if the help option has been called, print help and exit
+        if options.help:
+            try:
+                self.print_help(args)
+                sys.exit(src.OKSYS)
+            except Exception as exc:
+                write_exception(exc)
+                DBG.write("args", args, True) 
+                sys.exit(src.KOSYS)
+        '''
+
+    def __getattr__(self, name):
+        '''overwrite of __getattr__ function in order to display 
+           a customized message in case of a wrong call
+        
+        :param name str: The name of the attribute 
+        '''
+        if name in self.__dict__:
+            return self.__dict__[name]
+        else:
+            raise AttributeError("'%s'" % name + _(" is not a valid command"))
+
+    def execute_command(self, opt=None):
+        """select first argument as a command in directory 'commands', and launch on arguments
+        
+        :param opt str, optionnal: The sat options (as sys.argv)
+        """
+        if opt is not None:
+            args = opt
+        else:
+            args = self.arguments 
+
+        # print general help and returns
+        if len(args) == 0:
+            print_help()
+            return src.OKSYS
+            
+        # if the help option has been called, print command help and returns
+        if self.options.help:
+            self.print_help(self.arguments)
+            return src.OKSYS
+       
+        # the command called
+        command = args[0]
+        # get dynamically the command function to call
+        fun_command = self.__getattr__(command)
+        # Run the command using the arguments
+        exitCode = fun_command(args[1:])
+        return exitCode 
+    
+    def _setCommands(self, dirPath):
+        '''set class attributes corresponding to all commands that are 
+           in the dirPath directory
+        
+        :param dirPath str: The directory path containing the commands 
+        '''
+        # loop on the commands name
+        for nameCmd in lCommand:
+            
+            # Exception for the jobs command that requires the paramiko module
+            if nameCmd == "jobs":
+                try:
+                    saveout = sys.stderr
+                    ff = tempfile.TemporaryFile()
+                    sys.stderr = ff
+                    import paramiko
+                    sys.stderr = saveout
+                except:
+                    sys.stderr = saveout
+                    continue
+
+            # load the module that has name nameCmd in dirPath
+            (file_, pathname, description) = imp.find_module(nameCmd, [dirPath])
+            module = imp.load_module(nameCmd, file_, pathname, description)
+            
+            def run_command(args='',
+                            options=None,
+                            batch = False,
+                            verbose = -1,
+                            logger_add_link = None):
+                '''The function that will load the configuration (all pyconf)
+                and return the function run of the command corresponding to module
+                
+                :param args str: The arguments of the command 
+                '''
+                # Make sure the internationalization is available
+                # gettext.install('salomeTools', os.path.join(srcdir, 'i18n'))
+
+                # Get the arguments in a list and remove the empty elements
+                if type(args) == type(''):
+                    # split by spaces without considering spaces in quotes
+                    argv_0 = re.findall(r'(?:"[^"]*"|[^\s"])+', args)
+                else:
+                    argv_0 = args
+                
+                if argv_0 != ['']:
+                    while "" in argv_0: argv_0.remove("")
+                
+                # Format the argv list in order to prevent strings 
+                # that contain a blank to be separated
+                argv = []
+                elem_old = ""
+                for elem in argv_0:
+                    if argv == [] or elem_old.startswith("-") or elem.startswith("-"):
+                        argv.append(elem)
+                    else:
+                        argv[-1] += " " + elem
+                    elem_old = elem
+                           
+                # if it is provided by the command line, get the application
+                appliToLoad = None
+                if argv not in [[''], []] and argv[0][0] != "-":
+                    appliToLoad = argv[0].rstrip('*')
+                    argv = argv[1:]
+                
+                # Check if the global options of salomeTools have to be changed
+                if options:
+                    options_save = self.options
+                    self.options = options  
+
+                # read the configuration from all the pyconf files    
+                cfgManager = commands.config.ConfigManager()
+                self.cfg = cfgManager.get_config(datadir=self.datadir, 
+                                                 application=appliToLoad, 
+                                                 options=self.options, 
+                                                 command=__nameCmd__)
+                               
+                # Set the verbose mode if called
+                if verbose > -1:
+                    verbose_save = self.options.output_verbose_level
+                    self.options.__setattr__("output_verbose_level", verbose)    
+
+                # Set batch mode if called
+                if batch:
+                    batch_save = self.options.batch
+                    self.options.__setattr__("batch", True)
+
+                # set output level
+                if self.options.output_verbose_level is not None:
+                    self.cfg.USER.output_verbose_level = self.options.output_verbose_level
+                if self.cfg.USER.output_verbose_level < 1:
+                    self.cfg.USER.output_verbose_level = 0
+                silent = (self.cfg.USER.output_verbose_level == 0)
+
+                # create log file
+                micro_command = False
+                if logger_add_link:
+                    micro_command = True
+                logger_command = src.logger.Logger(self.cfg, 
+                                   silent_sysstd=silent,
+                                   all_in_terminal=self.options.all_in_terminal,
+                                   micro_command=micro_command)
+                
+                # Check that the path given by the logs_paths_in_file option
+                # is a file path that can be written
+                if self.options.logs_paths_in_file and not micro_command:
+                    try:
+                        self.options.logs_paths_in_file = os.path.abspath(
+                                                self.options.logs_paths_in_file)
+                        dir_file = os.path.dirname(self.options.logs_paths_in_file)
+                        if not os.path.exists(dir_file):
+                            os.makedirs(dir_file)
+                        if os.path.exists(self.options.logs_paths_in_file):
+                            os.remove(self.options.logs_paths_in_file)
+                        file_test = open(self.options.logs_paths_in_file, "w")
+                        file_test.close()
+                    except Exception as e:
+                        msg = _("WARNING: the logs_paths_in_file option will "
+                                "not be taken into account.\nHere is the error:")
+                        logger_command.write("%s\n%s\n\n" % (
+                                             src.printcolors.printcWarning(msg),
+                                             str(e)))
+                        self.options.logs_paths_in_file = None
+                
+                options_launched = ""
+                res = None
+                try:
+                    # Execute the hooks (if there is any) 
+                    # and run method of the command
+                    self.run_hook(__nameCmd__, C_PRE_HOOK, logger_command)
+                    res = __module__.run(argv, self, logger_command)
+                    self.run_hook(__nameCmd__, C_POST_HOOK, logger_command)
+                    if res is None:
+                        res = 0
+                        
+                except Exception as e:
+                    # Get error
+                    logger_command.write("\n***** ", 1)
+                    logger_command.write(
+                        src.printcolors.printcError("salomeTools ERROR:"), 1)
+                    logger_command.write("\n" + str(e) + "\n\n", 1)
+                    # get stack
+                    __, __, exc_traceback = sys.exc_info()
+                    fp = tempfile.TemporaryFile()
+                    traceback.print_tb(exc_traceback, file=fp)
+                    fp.seek(0)
+                    stack = fp.read()
+                    verbosity = 5
+                    if self.options.debug_mode:
+                        verbosity = 1
+                    logger_command.write("TRACEBACK:\n%s" % stack.replace('"',"'"),
+                                         verbosity)
+                finally:
+                    # set res if it is not set in the command
+                    if res is None:
+                        res = 1
+                                            
+                    # come back to the original global options
+                    if options:
+                        options_launched = get_text_from_options(self.options)
+                        self.options = options_save
+                    
+                    # come back in the original batch mode if 
+                    # batch argument was called
+                    if batch:
+                        self.options.__setattr__("batch", batch_save)
+
+                    # come back in the original verbose mode if 
+                    # verbose argument was called                        
+                    if verbose > -1:
+                        self.options.__setattr__("output_verbose_level", 
+                                                 verbose_save)
+                    # put final attributes in xml log file 
+                    # (end time, total time, ...) and write it
+                    launchedCommand = ' '.join([self.cfg.VARS.salometoolsway +
+                                                os.path.sep +
+                                                'sat',
+                                                options_launched,
+                                                __nameCmd__, 
+                                                ' '.join(argv_0)])
+                    launchedCommand = launchedCommand.replace('"', "'")
+                    
+                    # Add a link to the parent command      
+                    if logger_add_link is not None:
+                        logger_add_link.add_link(logger_command.logFileName,
+                                                 __nameCmd__,
+                                                 res,
+                                                 launchedCommand)
+                        logger_add_link.l_logFiles += logger_command.l_logFiles
+                                            
+                    # Put the final attributes corresponding to end time and
+                    # Write the file to the hard drive
+                    logger_command.end_write(
+                                        {"launchedCommand" : launchedCommand})
+                    
+                    if res != 0:
+                        res = 1
+                        
+                    # print the log file path if 
+                    # the maximum verbose mode is invoked
+                    if not micro_command:
+                        logger_command.write("\nPath to the xml log file:\n", 5)
+                        logger_command.write("%s\n\n" % \
+                            src.printcolors.printcInfo(logger_command.logFilePath), 5)
+
+                    # If the logs_paths_in_file was called, write the result
+                    # and log files in the given file path
+                    if self.options.logs_paths_in_file and not micro_command:
+                        file_res = open(self.options.logs_paths_in_file, "w")
+                        file_res.write(str(res) + "\n")
+                        for i, filepath in enumerate(logger_command.l_logFiles):
+                            file_res.write(filepath)
+                            if i < len(logger_command.l_logFiles):
+                                file_res.write("\n")
+                                file_res.flush()
+                
+                return res
+
+            # Make sure that run_command will be redefined 
+            # at each iteration of the loop
+            globals_up = {}
+            globals_up.update(run_command.__globals__)
+            globals_up.update({'__nameCmd__': nameCmd, '__module__' : module})
+            func = types.FunctionType(run_command.__code__,
+                                      globals_up,
+                                      run_command.__name__,
+                                      run_command.__defaults__,
+                                      run_command.__closure__)
+
+            # set the attribute corresponding to the command
+            self.__setattr__(nameCmd, func)
+
+    def run_hook(self, cmd_name, hook_type, logger):
+        '''Execute a hook file for a given command regarding the fact 
+           it is pre or post
+        
+        :param cmd_name str: The the command on which execute the hook
+        :param hook_type str: pre or post
+        :param logger Logger: the logging instance to use for the prints
+        '''
+        # The hooks must be defined in the application pyconf
+        # So, if there is no application, do not do anything
+        if not src.config_has_application(self.cfg):
+            return
+
+        # The hooks must be defined in the application pyconf in the
+        # APPLICATION section, hook : { command : 'script_path.py'}
+        if "hook" not in self.cfg.APPLICATION \
+                    or cmd_name not in self.cfg.APPLICATION.hook:
+            return
+
+        # Get the hook_script path and verify that it exists
+        hook_script_path = self.cfg.APPLICATION.hook[cmd_name]
+        if not os.path.exists(hook_script_path):
+            raise src.SatException(_("Hook script not found: %s") % 
+                                   hook_script_path)
+        
+        # Try to execute the script, catch the exception if it fails
+        try:
+            # import the module (in the sense of python)
+            pymodule = imp.load_source(cmd_name, hook_script_path)
+            
+            # format a message to be printed at hook execution
+            msg = src.printcolors.printcWarning(_("Run hook script"))
+            msg = "%s: %s\n" % (msg, 
+                                src.printcolors.printcInfo(hook_script_path))
+            
+            # run the function run_pre_hook if this function is called 
+            # before the command, run_post_hook if it is called after
+            if hook_type == C_PRE_HOOK and "run_pre_hook" in dir(pymodule):
+                logger.write(msg, 1)
+                pymodule.run_pre_hook(self.cfg, logger)
+            elif hook_type == C_POST_HOOK and "run_post_hook" in dir(pymodule):
+                logger.write(msg, 1)
+                pymodule.run_post_hook(self.cfg, logger)
+
+        except Exception as exc:
+            msg = _("Unable to run hook script: %s") % hook_script_path
+            msg += "\n" + str(exc)
+            raise src.SatException(msg)
+
+    def print_help(self, opt):
+        '''Prints help for a command. Function called when "sat -h <command>"
+        
+        :param argv str: the options passed (to get the command name)
+        '''
+        # if no command as argument (sat -h)
+        if len(opt)==0:
+            print_help()
+            return
+            
+        # get command name
+        command = opt[0]
+        # read the configuration from all the pyconf files
+        cfgManager = commands.config.ConfigManager()
+        self.cfg = cfgManager.get_config(datadir=self.datadir)
+
+        # Check if this command exists
+        if not hasattr(self, command):
+            raise src.SatException(_("Command '%s' does not exist") % command)
+        
+        # load the module
+        module = self.get_module(command)
+        
+        msg = self.get_module_help(module)
+            
+        if isStdoutPipe():
+            # clean color if the terminal is redirected by user
+            # ex: sat compile appli > log.txt
+            msg = src.printcolors.cleancolor(msg)  
+        print(msg)
+        return 
+            
+    def get_module_help(self, module):
+        """get help for a command
+        as 'sat --help config' for example
+        """
+        # get salomeTools version
+        msg = get_version() + "\n\n"
+        
+        # print the description of the command that is done in the command file
+        if hasattr( module, "description" ):
+            msg += src.printcolors.printcHeader( _("Description:") ) + "\n"
+            msg += module.description() + "\n\n"
+
+        # print the description of the command options
+        if hasattr( module, "parser" ) :
+            msg += module.parser.get_help() + "\n"
+        return msg
+        
+
+    def get_module(self, module):
+        '''Loads a command. Function called only by print_help
+        
+        :param module str: the command to load
+        '''
+        # Check if this command exists
+        if not hasattr(self, module):
+            raise src.SatException(_("Command '%s' does not exist") % module)
+
+        # load the module
+        (file_, pathname, description) = imp.find_module(module, [cmdsdir])
+        module = imp.load_module(module, file_, pathname, description)
+        return module
+        
+
+def get_text_from_options(options):
+    text_options = ""
+    for attr in dir(options):
+        if attr.startswith("__"):
+            continue
+        if options.__getattr__(attr) != None:
+            option_contain = options.__getattr__(attr)
+            if type(option_contain)==type([]):
+                option_contain = ",".join(option_contain)
+            if type(option_contain)==type(True):
+                option_contain = ""
+            text_options+= "--%s %s " % (attr, option_contain)
+    return text_options
+         
+
+def isStdoutPipe():
+    """check if the terminal is redirected by user (elsewhere a tty) 
+    example: 
+    >> sat compile appli > log.txt
+    """
+    return not ('isatty' in dir(sys.stdout) and sys.stdout.isatty())
+     
+def get_version():
+    """get version colored string
+    """
+    cfgManager = commands.config.ConfigManager()
+    cfg = cfgManager.get_config()
+    # print the key corresponding to salomeTools version
+    msg = src.printcolors.printcHeader( _("Version: ") ) + \
+          cfg.INTERNAL.sat_version
+    return msg
+  
+def get_help():
+    """get general help colored string
+    """
+    # read the config 
+    msg = get_version() + "\n\n"
+    msg += src.printcolors.printcHeader(_("Usage: ")) + \
+          "sat [sat_options] <command> [product] [command_options]\n\n"
+    msg += parser.get_help() + "\n"
+    msg += src.printcolors.printcHeader(_("Available commands are:")) + "\n\n"
+    for command in lCommand:
+        msg += " - %s\n" % (command)
+    msg += "\n"
+    # Explain how to get the help for a specific command
+    msg += src.printcolors.printcHeader(
+           _("Getting the help for a specific command: ")) + \
+           "sat --help <command>\n"
+    return msg
+
+def print_help():
+    """prints salomeTools general help
+    """
+    msg = get_help() 
+    if isStdoutPipe():
+        # clean color if the terminal is redirected by user
+        # ex: sat compile appli > log.txt
+        msg = src.printcolors.cleancolor(msg)  
+    print(msg)
+    return
+
+def write_exception(exc):
+    '''write in stderr exception in case of error in a command
+    
+    :param exc exception: the exception to print
+    '''
+    sys.stderr.write("\n***** ")
+    sys.stderr.write(src.printcolors.printcError("salomeTools ERROR:"))
+    sys.stderr.write("\n" + str(exc) + "\n")
+
+
+
+
diff --git a/src/test/APPLI_TEST/APPLI_TEST.pyconf b/src/test/APPLI_TEST/APPLI_TEST.pyconf
new file mode 100644 (file)
index 0000000..5fc08b3
--- /dev/null
@@ -0,0 +1,45 @@
+
+APPLICATION :
+{
+    name : 'APPLI_TEST'
+    workdir : $LOCAL.workdir + $VARS.sep + $APPLICATION.name + '-' + $VARS.dist
+    base : 'base'
+    tag : 'master'
+    get_method : 'git'
+    environ :
+    {
+        ACCEPT_SALOME_WARNINGS : '1'
+        LC_NUMERIC : 'C'
+        TESTS_ROOT_DIR : "/tmp/" + $VARS.user+ "/TESTS/APPLI_TEST"
+    }
+    products :
+    {
+        # PREREQUISITES :
+        'Python' : 'native'
+
+        # SALOME MODULES :
+        'CONFIGURATION'
+        'MEDCOUPLING'
+        'KERNEL'
+        'GUI'
+        'GEOM'
+        'SMESH'
+
+    }
+    grid_to_test : 'SALOME_V8'
+    profile :
+    {
+        launcher_name : "appli_test"
+        product : "SALOME"
+    }
+    virtual_app:
+    {
+        name : "appli_test"
+        application_name : "APPLI"
+    }
+    test_base : 
+    {
+        name : "SALOME"
+        tag : "SalomeV8"
+    }
+}
diff --git a/src/test/APPLI_TEST_Test.py b/src/test/APPLI_TEST_Test.py
new file mode 100755 (executable)
index 0000000..e29e0a7
--- /dev/null
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+#-*- coding:utf-8 -*-
+
+#  Copyright (C) 2010-2018  CEA/DEN
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 2.1 of the License.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library; if not, write to the Free Software
+#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+
+import os
+import sys
+import unittest
+
+import src.salomeTools as SAT
+import src
+import src.debug as DBG # Easy print stderr (for DEBUG only)
+
+class TestCase(unittest.TestCase):
+  "Test the sat commands on APPLI_TEST configuration pyconf etc. files"""
+  
+  def test_000(self):
+    # one shot setUp() for this TestCase
+    # DBG.push_debug(True)
+    SAT.setNotLocale() # test english
+    return
+
+  def test_010(self):
+    cmd = "-v 5 config -l"
+    s = SAT.Sat(cmd)
+    # DBG.push_debug(True)
+    DBG.write("s.cfg", s.cfg) #none
+    DBG.write("s.__dict__", s.__dict__) # have 
+    exitCode = s.execute_command()
+    # DBG.write("s.cfg", s.cfg)
+    self.assertEqual(src.okToStr(exitCode), "OK")
+    DBG.pop_debug()
+      
+  def test_999(self):
+    # one shot tearDown() for this TestCase
+    SAT.setLocale() # end test english
+    # DBG.pop_debug()
+    return
+      
+if __name__ == '__main__':
+    unittest.main(exit=False)
+    pass
diff --git a/src/test/README_config_0_3_9.txt b/src/test/README_config_0_3_9.txt
new file mode 100644 (file)
index 0000000..4605ae9
--- /dev/null
@@ -0,0 +1,2 @@
+#TODO
+switch pyconf.py 0.3.7.1 -> 0.3.9, here for test
diff --git a/src/test/__init__.py b/src/test/__init__.py
new file mode 100755 (executable)
index 0000000..e69de29
diff --git a/src/test/config_0_3_9/LICENSE b/src/test/config_0_3_9/LICENSE
new file mode 100644 (file)
index 0000000..98773c8
--- /dev/null
@@ -0,0 +1,16 @@
+Copyright (C) 2004-2010 by Vinay Sajip. All Rights Reserved.\r
+\r
+Permission to use, copy, modify, and distribute this software and its\r
+documentation for any purpose and without fee is hereby granted,\r
+provided that the above copyright notice appear in all copies and that\r
+both that copyright notice and this permission notice appear in\r
+supporting documentation, and that the name of Vinay Sajip\r
+not be used in advertising or publicity pertaining to distribution\r
+of the software without specific, written prior permission.\r
+\r
+VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING\r
+ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL\r
+VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR\r
+ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN\r
+AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR\r
+IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/src/test/config_0_3_9/PKG-INFO b/src/test/config_0_3_9/PKG-INFO
new file mode 100644 (file)
index 0000000..69ba705
--- /dev/null
@@ -0,0 +1,16 @@
+Metadata-Version: 1.0
+Name: config
+Version: 0.3.9
+Summary: A hierarchical, easy-to-use, powerful configuration module for Python
+Home-page: http://www.red-dove.com/python_config.html
+Author: Vinay Sajip
+Author-email: vinay_sajip@red-dove.com
+License: Copyright (C) 2004-2010 by Vinay Sajip. All Rights Reserved. See LICENSE for license.
+Description: This module allows a hierarchical configuration scheme with support for mappings
+        and sequences, cross-references between one part of the configuration and
+        another, the ability to flexibly access real Python objects without full-blown
+        eval(), an include facility, simple expression evaluation and the ability to
+        change, save, cascade and merge configurations. Interfaces easily with
+        environment variables and command-line options. It has been developed on python
+        2.3 but should work on version 2.2 or greater.
+Platform: UNKNOWN
diff --git a/src/test/config_0_3_9/README.txt b/src/test/config_0_3_9/README.txt
new file mode 100644 (file)
index 0000000..0c20eab
--- /dev/null
@@ -0,0 +1,84 @@
+This module is intended to provide configuration functionality for Python\r
+programs.\r
+\r
+Change History\r
+--------------\r
+\r
+Version   Date        Description\r
+=============================================================================\r
+0.3.9     11 May 2010 Fixed parsing bug which caused failure for numbers with\r
+                      exponents.\r
+-----------------------------------------------------------------------------\r
+0.3.8     03 Mar 2010 Fixed parsing bug which caused failure for negative\r
+                      numbers in sequences. Improved resolution logic.\r
+-----------------------------------------------------------------------------\r
+0.3.7     05 Oct 2007 Added Mapping.__delitem__ (patch by John Drummond).\r
+                      Mapping.__getattribute__ no longer returns "" when\r
+                      asked for "__class__" - doing so causes pickle to\r
+                      crash (reported by Jamila Gunawardena).\r
+                      Allow negative numbers (reported by Gary Schoep; had\r
+                      already been fixed but not yet released).\r
+-----------------------------------------------------------------------------\r
+0.3.6     09 Mar 2006 Made classes derive from object (previously they were\r
+                      old-style classes).\r
+                      Changed ConfigMerger to use a more flexible merge\r
+                      strategy.\r
+                      Multiline strings (using """ or ''') are now supported.\r
+                      A typo involving raising a ConfigError was fixed.\r
+                      Patches received with thanks from David Janes & Tim\r
+                      Desjardins (BlogMatrix) and Erick Tryzelaar.\r
+-----------------------------------------------------------------------------\r
+0.3.5     27 Dec 2004 Added ConfigOutputStream to provide better Unicode\r
+                      output support. Altered save code to put platform-\r
+                      dependent newlines for Unicode.\r
+-----------------------------------------------------------------------------\r
+0.3.4     11 Nov 2004 Added ConfigInputStream to provide better Unicode\r
+                      support.\r
+                      Added ConfigReader.setStream().\r
+-----------------------------------------------------------------------------\r
+0.3.3     09 Nov 2004 Renamed config.get() to getByPath(), and likewise for\r
+                      ConfigList.\r
+                      Added Mapping.get() to work like dict.get().\r
+                      Added logconfig.py and logconfig.cfg to distribution.\r
+-----------------------------------------------------------------------------\r
+0.3.2     04 Nov 2004 Simplified parseMapping().\r
+                      Allowed Config.__init__ to accept a string as well as a\r
+                      stream. If a string is passed in, streamOpener is used\r
+                      to obtain the stream to be used.\r
+-----------------------------------------------------------------------------\r
+0.3.1     04 Nov 2004 Changed addNamespace/removeNamespace to make name\r
+                      specification easier.\r
+                      Refactored save(), added Container.writeToStream and\r
+                      Container.writeValue() to help with this.\r
+-----------------------------------------------------------------------------\r
+0.3       03 Nov 2004 Added test harness (test_config.py)\r
+                      Fixed bugs in bracket parsing.\r
+                      Refactored internal classes.\r
+                      Added merging functionality.\r
+-----------------------------------------------------------------------------\r
+0.2       01 Nov 2004 Added support for None.\r
+                      Stream closed in load() and save().\r
+                      Added support for changing configuration.\r
+                      Fixed bugs in identifier parsing and isword().\r
+-----------------------------------------------------------------------------\r
+0.1       31 Oct 2004 Initial implementation (for community feedback)\r
+-----------------------------------------------------------------------------\r
+\r
+-----------------------------------------------------------------------------\r
+COPYRIGHT\r
+-----------------------------------------------------------------------------\r
+Copyright 2004-2010 by Vinay Sajip. All Rights Reserved.\r
+\r
+Permission to use, copy, modify, and distribute this software and its\r
+documentation for any purpose and without fee is hereby granted,\r
+provided that the above copyright notice appear in all copies and that\r
+both that copyright notice and this permission notice appear in\r
+supporting documentation, and that the name of Vinay Sajip\r
+not be used in advertising or publicity pertaining to distribution\r
+of the software without specific, written prior permission.\r
+VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING\r
+ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL\r
+VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR\r
+ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN\r
+AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR\r
+IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\r
diff --git a/src/test/config_0_3_9/__init__.py b/src/test/config_0_3_9/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/test/config_0_3_9/config.py b/src/test/config_0_3_9/config.py
new file mode 100644 (file)
index 0000000..94d04c0
--- /dev/null
@@ -0,0 +1,1678 @@
+# Copyright 2004-2010 by Vinay Sajip. All Rights Reserved.\r
+#\r
+# Permission to use, copy, modify, and distribute this software and its\r
+# documentation for any purpose and without fee is hereby granted,\r
+# provided that the above copyright notice appear in all copies and that\r
+# both that copyright notice and this permission notice appear in\r
+# supporting documentation, and that the name of Vinay Sajip\r
+# not be used in advertising or publicity pertaining to distribution\r
+# of the software without specific, written prior permission.\r
+# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING\r
+# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL\r
+# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR\r
+# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER\r
+# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\r
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\r
+\r
+"""\r
+This is a configuration module for Python.\r
+\r
+This module should work under Python versions >= 2.2, and cannot be used with\r
+earlier versions since it uses new-style classes.\r
+\r
+Development and testing has only been carried out (so far) on Python 2.3.4 and\r
+Python 2.4.2. See the test module (test_config.py) included in the\r
+U{distribution<http://www.red-dove.com/python_config.html|_blank>} (follow the\r
+download link).\r
+\r
+A simple example - with the example configuration file::\r
+\r
+    messages:\r
+    [\r
+      {\r
+        stream : `sys.stderr`\r
+        message: 'Welcome'\r
+        name: 'Harry'\r
+      }\r
+      {\r
+        stream : `sys.stdout`\r
+        message: 'Welkom'\r
+        name: 'Ruud'\r
+      }\r
+      {\r
+        stream : $messages[0].stream\r
+        message: 'Bienvenue'\r
+        name: Yves\r
+      }\r
+    ]\r
+\r
+a program to read the configuration would be::\r
+\r
+    from config import Config\r
+\r
+    f = file('simple.cfg')\r
+    cfg = Config(f)\r
+    for m in cfg.messages:\r
+        s = '%s, %s' % (m.message, m.name)\r
+        try:\r
+            print >> m.stream, s\r
+        except IOError, e:\r
+            print e\r
+\r
+which, when run, would yield the console output::\r
+\r
+    Welcome, Harry\r
+    Welkom, Ruud\r
+    Bienvenue, Yves\r
+\r
+See U{this tutorial<http://www.red-dove.com/python_config.html|_blank>} for more\r
+information.\r
+\r
+@version: 0.3.9\r
+\r
+@author: Vinay Sajip\r
+\r
+@copyright: Copyright (C) 2004-2010 Vinay Sajip. All Rights Reserved.\r
+\r
+\r
+@var streamOpener: The default stream opener. This is a factory function which\r
+takes a string (e.g. filename) and returns a stream suitable for reading. If\r
+unable to open the stream, an IOError exception should be thrown.\r
+\r
+The default value of this variable is L{defaultStreamOpener}. For an example\r
+of how it's used, see test_config.py (search for streamOpener).\r
+"""\r
+\r
+__author__  = "Vinay Sajip <vinay_sajip@red-dove.com>"\r
+__status__  = "alpha"\r
+__version__ = "0.3.9"\r
+__date__    = "11 May 2010"\r
+\r
+from types import StringType, UnicodeType\r
+\r
+import codecs\r
+import logging\r
+import os\r
+import sys\r
+\r
+WORD = 'a'\r
+NUMBER = '9'\r
+STRING = '"'\r
+EOF = ''\r
+LCURLY = '{'\r
+RCURLY = '}'\r
+LBRACK = '['\r
+LBRACK2 = 'a['\r
+RBRACK = ']'\r
+LPAREN = '('\r
+LPAREN2 = '(('\r
+RPAREN = ')'\r
+DOT = '.'\r
+COMMA = ','\r
+COLON = ':'\r
+AT = '@'\r
+PLUS = '+'\r
+MINUS = '-'\r
+STAR = '*'\r
+SLASH = '/'\r
+MOD = '%'\r
+BACKTICK = '`'\r
+DOLLAR = '$'\r
+TRUE = 'True'\r
+FALSE = 'False'\r
+NONE = 'None'\r
+\r
+WORDCHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_"\r
+\r
+if sys.platform == 'win32':\r
+    NEWLINE = '\r\n'\r
+elif os.name == 'mac':\r
+    NEWLINE = '\r'\r
+else:\r
+    NEWLINE = '\n'\r
+\r
+try:\r
+    import encodings.utf_32\r
+    has_utf32 = True\r
+except:\r
+    has_utf32 = False\r
+\r
+try:\r
+    from logging.handlers import NullHandler\r
+except ImportError:\r
+    class NullHandler(logging.Handler):\r
+        def emit(self, record):\r
+            pass\r
+\r
+logger = logging.getLogger(__name__)\r
+if not logger.handlers:\r
+    logger.addHandler(NullHandler())\r
+\r
+class ConfigInputStream(object):\r
+    """\r
+    An input stream which can read either ANSI files with default encoding\r
+    or Unicode files with BOMs.\r
+\r
+    Handles UTF-8, UTF-16LE, UTF-16BE. Could handle UTF-32 if Python had\r
+    built-in support.\r
+    """\r
+    def __init__(self, stream):\r
+        """\r
+        Initialize an instance.\r
+\r
+        @param stream: The underlying stream to be read. Should be seekable.\r
+        @type stream: A stream (file-like object).\r
+        """\r
+        encoding = None\r
+        signature = stream.read(4)\r
+        used = -1\r
+        if has_utf32:\r
+            if signature == codecs.BOM_UTF32_LE:\r
+                encoding = 'utf-32le'\r
+            elif signature == codecs.BOM_UTF32_BE:\r
+                encoding = 'utf-32be'\r
+        if encoding is None:\r
+            if signature[:3] == codecs.BOM_UTF8:\r
+                used = 3\r
+                encoding = 'utf-8'\r
+            elif signature[:2] == codecs.BOM_UTF16_LE:\r
+                used = 2\r
+                encoding = 'utf-16le'\r
+            elif signature[:2] == codecs.BOM_UTF16_BE:\r
+                used = 2\r
+                encoding = 'utf-16be'\r
+            else:\r
+                used = 0\r
+        if used >= 0:\r
+            stream.seek(used)\r
+        if encoding:\r
+            reader = codecs.getreader(encoding)\r
+            stream = reader(stream)\r
+        self.stream = stream\r
+        self.encoding = encoding\r
+\r
+    def read(self, size):\r
+        if (size == 0) or (self.encoding is None):\r
+            rv = self.stream.read(size)\r
+        else:\r
+            rv = u''\r
+            while size > 0:\r
+                rv += self.stream.read(1)\r
+                size -= 1\r
+        return rv\r
+\r
+    def close(self):\r
+        self.stream.close()\r
+\r
+    def readline(self):\r
+        if self.encoding is None:\r
+            line = ''\r
+        else:\r
+            line = u''\r
+        while True:\r
+            c = self.stream.read(1)\r
+            if c:\r
+                line += c\r
+            if c == '\n':\r
+                break\r
+        return line\r
+\r
+class ConfigOutputStream(object):\r
+    """\r
+    An output stream which can write either ANSI files with default encoding\r
+    or Unicode files with BOMs.\r
+\r
+    Handles UTF-8, UTF-16LE, UTF-16BE. Could handle UTF-32 if Python had\r
+    built-in support.\r
+    """\r
+\r
+    def __init__(self, stream, encoding=None):\r
+        """\r
+        Initialize an instance.\r
+\r
+        @param stream: The underlying stream to be written.\r
+        @type stream: A stream (file-like object).\r
+        @param encoding: The desired encoding.\r
+        @type encoding: str\r
+        """\r
+        if encoding is not None:\r
+            encoding = str(encoding).lower()\r
+        self.encoding = encoding\r
+        if encoding == "utf-8":\r
+            stream.write(codecs.BOM_UTF8)\r
+        elif encoding == "utf-16be":\r
+            stream.write(codecs.BOM_UTF16_BE)\r
+        elif encoding == "utf-16le":\r
+            stream.write(codecs.BOM_UTF16_LE)\r
+        elif encoding == "utf-32be":\r
+            stream.write(codecs.BOM_UTF32_BE)\r
+        elif encoding == "utf-32le":\r
+            stream.write(codecs.BOM_UTF32_LE)\r
+\r
+        if encoding is not None:\r
+            writer = codecs.getwriter(encoding)\r
+            stream = writer(stream)\r
+        self.stream = stream\r
+\r
+    def write(self, data):\r
+        self.stream.write(data)\r
+\r
+    def flush(self):\r
+        self.stream.flush()\r
+\r
+    def close(self):\r
+        self.stream.close()\r
+\r
+def defaultStreamOpener(name):\r
+    """\r
+    This function returns a read-only stream, given its name. The name passed\r
+    in should correspond to an existing stream, otherwise an exception will be\r
+    raised.\r
+\r
+    This is the default value of L{streamOpener}; assign your own callable to\r
+    streamOpener to return streams based on names. For example, you could use\r
+    urllib2.urlopen().\r
+\r
+    @param name: The name of a stream, most commonly a file name.\r
+    @type name: str\r
+    @return: A stream with the specified name.\r
+    @rtype: A read-only stream (file-like object)\r
+    """\r
+    return ConfigInputStream(file(name, 'rb'))\r
+\r
+streamOpener = None\r
+\r
+class ConfigError(Exception):\r
+    """\r
+    This is the base class of exceptions raised by this module.\r
+    """\r
+    pass\r
+\r
+class ConfigFormatError(ConfigError):\r
+    """\r
+    This is the base class of exceptions raised due to syntax errors in\r
+    configurations.\r
+    """\r
+    pass\r
+\r
+class ConfigResolutionError(ConfigError):\r
+    """\r
+    This is the base class of exceptions raised due to semantic errors in\r
+    configurations.\r
+    """\r
+    pass\r
+\r
+def isWord(s):\r
+    """\r
+    See if a passed-in value is an identifier. If the value passed in is not a\r
+    string, False is returned. An identifier consists of alphanumerics or\r
+    underscore characters.\r
+\r
+    Examples::\r
+\r
+        isWord('a word') ->False\r
+        isWord('award') -> True\r
+        isWord(9) -> False\r
+        isWord('a_b_c_') ->True\r
+\r
+    @note: isWord('9abc') will return True - not exactly correct, but adequate\r
+    for the way it's used here.\r
+\r
+    @param s: The name to be tested\r
+    @type s: any\r
+    @return: True if a word, else False\r
+    @rtype: bool\r
+    """\r
+    if type(s) != type(''):\r
+        return False\r
+    s = s.replace('_', '')\r
+    return s.isalnum()\r
+\r
+def makePath(prefix, suffix):\r
+    """\r
+    Make a path from a prefix and suffix.\r
+\r
+    Examples::\r
+\r
+        makePath('', 'suffix') -> 'suffix'\r
+        makePath('prefix', 'suffix') -> 'prefix.suffix'\r
+        makePath('prefix', '[1]') -> 'prefix[1]'\r
+\r
+    @param prefix:  The prefix to use. If it evaluates as false, the suffix\r
+                    is returned.\r
+    @type prefix:   str\r
+    @param suffix:  The suffix to use. It is either an identifier or an\r
+                    index in brackets.\r
+    @type suffix:   str\r
+    @return:        The path concatenation of prefix and suffix, with a\r
+                    dot if the suffix is not a bracketed index.\r
+    @rtype:         str\r
+\r
+    """\r
+    if not prefix:\r
+        rv = suffix\r
+    elif suffix[0] == '[':\r
+        rv = prefix + suffix\r
+    else:\r
+        rv = prefix + '.' + suffix\r
+    return rv\r
+\r
+\r
+class Container(object):\r
+    """\r
+    This internal class is the base class for mappings and sequences.\r
+\r
+    @ivar path: A string which describes how to get\r
+    to this instance from the root of the hierarchy.\r
+\r
+    Example::\r
+\r
+        a.list.of[1].or['more'].elements\r
+    """\r
+    def __init__(self, parent):\r
+        """\r
+        Initialize an instance.\r
+\r
+        @param parent: The parent of this instance in the hierarchy.\r
+        @type parent: A L{Container} instance.\r
+        """\r
+        object.__setattr__(self, 'parent', parent)\r
+\r
+    def setPath(self, path):\r
+        """\r
+        Set the path for this instance.\r
+        @param path: The path - a string which describes how to get\r
+        to this instance from the root of the hierarchy.\r
+        @type path: str\r
+        """\r
+        object.__setattr__(self, 'path', path)\r
+\r
+    def evaluate(self, item):\r
+        """\r
+        Evaluate items which are instances of L{Reference} or L{Expression}.\r
+\r
+        L{Reference} instances are evaluated using L{Reference.resolve},\r
+        and L{Expression} instances are evaluated using\r
+        L{Expression.evaluate}.\r
+\r
+        @param item: The item to be evaluated.\r
+        @type item: any\r
+        @return: If the item is an instance of L{Reference} or L{Expression},\r
+        the evaluated value is returned, otherwise the item is returned\r
+        unchanged.\r
+        """\r
+        if isinstance(item, Reference):\r
+            item = item.resolve(self)\r
+        elif isinstance(item, Expression):\r
+            item = item.evaluate(self)\r
+        return item\r
+\r
+    def writeToStream(self, stream, indent, container):\r
+        """\r
+        Write this instance to a stream at the specified indentation level.\r
+\r
+        Should be redefined in subclasses.\r
+\r
+        @param stream: The stream to write to\r
+        @type stream: A writable stream (file-like object)\r
+        @param indent: The indentation level\r
+        @type indent: int\r
+        @param container: The container of this instance\r
+        @type container: L{Container}\r
+        @raise NotImplementedError: If a subclass does not override this\r
+        """\r
+        raise NotImplementedError\r
+\r
+    def writeValue(self, value, stream, indent):\r
+        if isinstance(self, Mapping):\r
+            indstr = ' '\r
+        else:\r
+            indstr = indent * '  '\r
+        if isinstance(value, Reference) or isinstance(value, Expression):\r
+            stream.write('%s%r%s' % (indstr, value, NEWLINE))\r
+        else:\r
+            if (type(value) is StringType): # and not isWord(value):\r
+                value = repr(value)\r
+            stream.write('%s%s%s' % (indstr, value, NEWLINE))\r
+\r
+class Mapping(Container):\r
+    """\r
+    This internal class implements key-value mappings in configurations.\r
+    """\r
+\r
+    def __init__(self, parent=None):\r
+        """\r
+        Initialize an instance.\r
+\r
+        @param parent: The parent of this instance in the hierarchy.\r
+        @type parent: A L{Container} instance.\r
+        """\r
+        Container.__init__(self, parent)\r
+        object.__setattr__(self, 'path', '')\r
+        object.__setattr__(self, 'data', {})\r
+        object.__setattr__(self, 'order', [])   # to preserve ordering\r
+        object.__setattr__(self, 'comments', {})\r
+\r
+    def __delitem__(self, key):\r
+        """\r
+        Remove an item\r
+        """\r
+        data = object.__getattribute__(self, 'data')\r
+        if key not in data:\r
+            raise AttributeError(key)\r
+        order = object.__getattribute__(self, 'order')\r
+        comments = object.__getattribute__(self, 'comments')\r
+        del data[key]\r
+        order.remove(key)\r
+        del comments[key]\r
+\r
+    def __getitem__(self, key):\r
+        data = object.__getattribute__(self, 'data')\r
+        if key not in data:\r
+            raise AttributeError(key)\r
+        rv = data[key]\r
+        return self.evaluate(rv)\r
+\r
+    __getattr__ = __getitem__\r
+\r
+    def __getattribute__(self, name):\r
+        if name == "__dict__":\r
+            return {}\r
+        if name in ["__methods__", "__members__"]:\r
+            return []\r
+        #if name == "__class__":\r
+        #    return ''\r
+        data = object.__getattribute__(self, "data")\r
+        useData = data.has_key(name)\r
+        if useData:\r
+            rv = getattr(data, name)\r
+        else:\r
+            rv = object.__getattribute__(self, name)\r
+            if rv is None:\r
+                raise AttributeError(name)\r
+        return rv\r
+\r
+    def iteritems(self):\r
+        for key in self.keys():\r
+            yield(key, self[key])\r
+        raise StopIteration\r
+\r
+    def __contains__(self, item):\r
+        order = object.__getattribute__(self, 'order')\r
+        return item in order\r
+\r
+    def addMapping(self, key, value, comment, setting=False):\r
+        """\r
+        Add a key-value mapping with a comment.\r
+\r
+        @param key: The key for the mapping.\r
+        @type key: str\r
+        @param value: The value for the mapping.\r
+        @type value: any\r
+        @param comment: The comment for the key (can be None).\r
+        @type comment: str\r
+        @param setting: If True, ignore clashes. This is set\r
+        to true when called from L{__setattr__}.\r
+        @raise ConfigFormatError: If an existing key is seen\r
+        again and setting is False.\r
+        """\r
+        data = object.__getattribute__(self, 'data')\r
+        order = object.__getattribute__(self, 'order')\r
+        comments = object.__getattribute__(self, 'comments')\r
+\r
+        data[key] = value\r
+        if key not in order:\r
+            order.append(key)\r
+        elif not setting:\r
+            raise ConfigFormatError("repeated key: %s" % key)\r
+        comments[key] = comment\r
+\r
+    def __setattr__(self, name, value):\r
+        self.addMapping(name, value, None, True)\r
+\r
+    __setitem__ = __setattr__\r
+\r
+    def keys(self):\r
+        """\r
+        Return the keys in a similar way to a dictionary.\r
+        """\r
+        return object.__getattribute__(self, 'order')\r
+\r
+    def get(self, key, default=None):\r
+        """\r
+        Allows a dictionary-style get operation.\r
+        """\r
+        if key in self:\r
+            return self[key]\r
+        return default\r
+\r
+    def __str__(self):\r
+        return str(object.__getattribute__(self, 'data'))\r
+\r
+    def __repr__(self):\r
+        return repr(object.__getattribute__(self, 'data'))\r
+\r
+    def __len__(self):\r
+        return len(object.__getattribute__(self, 'order'))\r
+\r
+    def __iter__(self):\r
+        return self.iterkeys()\r
+\r
+    def iterkeys(self):\r
+        order = object.__getattribute__(self, 'order')\r
+        return order.__iter__()\r
+\r
+    def writeToStream(self, stream, indent, container):\r
+        """\r
+        Write this instance to a stream at the specified indentation level.\r
+\r
+        Should be redefined in subclasses.\r
+\r
+        @param stream: The stream to write to\r
+        @type stream: A writable stream (file-like object)\r
+        @param indent: The indentation level\r
+        @type indent: int\r
+        @param container: The container of this instance\r
+        @type container: L{Container}\r
+        """\r
+        indstr = indent * '  '\r
+        if len(self) == 0:\r
+            stream.write(' { }%s' % NEWLINE)\r
+        else:\r
+            if isinstance(container, Mapping):\r
+                stream.write(NEWLINE)\r
+            stream.write('%s{%s' % (indstr, NEWLINE))\r
+            self.save(stream, indent + 1)\r
+            stream.write('%s}%s' % (indstr, NEWLINE))\r
+\r
+    def save(self, stream, indent=0):\r
+        """\r
+        Save this configuration to the specified stream.\r
+        @param stream: A stream to which the configuration is written.\r
+        @type stream: A write-only stream (file-like object).\r
+        @param indent: The indentation level for the output.\r
+        @type indent: int\r
+        """\r
+        indstr = indent * '  '\r
+        order = object.__getattribute__(self, 'order')\r
+        data = object.__getattribute__(self, 'data')\r
+        maxlen = 0 # max(map(lambda x: len(x), order))\r
+        for key in order:\r
+            comment = self.comments[key]\r
+            if isWord(key):\r
+                skey = key\r
+            else:\r
+                skey = repr(key)\r
+            if comment:\r
+                stream.write('%s#%s' % (indstr, comment))\r
+            stream.write('%s%-*s :' % (indstr, maxlen, skey))\r
+            value = data[key]\r
+            if isinstance(value, Container):\r
+                value.writeToStream(stream, indent, self)\r
+            else:\r
+                self.writeValue(value, stream, indent)\r
+\r
+class Config(Mapping):\r
+    """\r
+    This class represents a configuration, and is the only one which clients\r
+    need to interface to, under normal circumstances.\r
+    """\r
+\r
+    class Namespace(object):\r
+        """\r
+        This internal class is used for implementing default namespaces.\r
+\r
+        An instance acts as a namespace.\r
+        """\r
+        def __init__(self):\r
+            self.sys = sys\r
+            self.os = os\r
+\r
+        def __repr__(self):\r
+            return "<Namespace('%s')>" % ','.join(self.__dict__.keys())\r
+\r
+    def __init__(self, streamOrFile=None, parent=None):\r
+        """\r
+        Initializes an instance.\r
+\r
+        @param streamOrFile: If specified, causes this instance to be loaded\r
+        from the stream (by calling L{load}). If a string is provided, it is\r
+        passed to L{streamOpener} to open a stream. Otherwise, the passed\r
+        value is assumed to be a stream and used as is.\r
+        @type streamOrFile: A readable stream (file-like object) or a name.\r
+        @param parent: If specified, this becomes the parent of this instance\r
+        in the configuration hierarchy.\r
+        @type parent: a L{Container} instance.\r
+        """\r
+        Mapping.__init__(self, parent)\r
+        object.__setattr__(self, 'reader', ConfigReader(self))\r
+        object.__setattr__(self, 'namespaces', [Config.Namespace()])\r
+        object.__setattr__(self, 'resolving', set())\r
+        if streamOrFile is not None:\r
+            if isinstance(streamOrFile, StringType) or isinstance(streamOrFile, UnicodeType):\r
+                global streamOpener\r
+                if streamOpener is None:\r
+                    streamOpener = defaultStreamOpener\r
+                streamOrFile = streamOpener(streamOrFile)\r
+            load = object.__getattribute__(self, "load")\r
+            load(streamOrFile)\r
+\r
+    def load(self, stream):\r
+        """\r
+        Load the configuration from the specified stream. Multiple streams can\r
+        be used to populate the same instance, as long as there are no\r
+        clashing keys. The stream is closed.\r
+        @param stream: A stream from which the configuration is read.\r
+        @type stream: A read-only stream (file-like object).\r
+        @raise ConfigError: if keys in the loaded configuration clash with\r
+        existing keys.\r
+        @raise ConfigFormatError: if there is a syntax error in the stream.\r
+        """\r
+        reader = object.__getattribute__(self, 'reader')\r
+        #object.__setattr__(self, 'root', reader.load(stream))\r
+        reader.load(stream)\r
+        stream.close()\r
+\r
+    def addNamespace(self, ns, name=None):\r
+        """\r
+        Add a namespace to this configuration which can be used to evaluate\r
+        (resolve) dotted-identifier expressions.\r
+        @param ns: The namespace to be added.\r
+        @type ns: A module or other namespace suitable for passing as an\r
+        argument to vars().\r
+        @param name: A name for the namespace, which, if specified, provides\r
+        an additional level of indirection.\r
+        @type name: str\r
+        """\r
+        namespaces = object.__getattribute__(self, 'namespaces')\r
+        if name is None:\r
+            namespaces.append(ns)\r
+        else:\r
+            setattr(namespaces[0], name, ns)\r
+\r
+    def removeNamespace(self, ns, name=None):\r
+        """\r
+        Remove a namespace added with L{addNamespace}.\r
+        @param ns: The namespace to be removed.\r
+        @param name: The name which was specified when L{addNamespace} was\r
+        called.\r
+        @type name: str\r
+        """\r
+        namespaces = object.__getattribute__(self, 'namespaces')\r
+        if name is None:\r
+            namespaces.remove(ns)\r
+        else:\r
+            delattr(namespaces[0], name)\r
+\r
+    def save(self, stream, indent=0):\r
+        """\r
+        Save this configuration to the specified stream. The stream is\r
+        closed if this is the top-level configuration in the hierarchy.\r
+        L{Mapping.save} is called to do all the work.\r
+        @param stream: A stream to which the configuration is written.\r
+        @type stream: A write-only stream (file-like object).\r
+        @param indent: The indentation level for the output.\r
+        @type indent: int\r
+        """\r
+        Mapping.save(self, stream, indent)\r
+        if indent == 0:\r
+            stream.close()\r
+\r
+    def getByPath(self, path):\r
+        """\r
+        Obtain a value in the configuration via its path.\r
+        @param path: The path of the required value\r
+        @type path: str\r
+        @return the value at the specified path.\r
+        @rtype: any\r
+        @raise ConfigError: If the path is invalid\r
+        """\r
+        s = 'self.' + path\r
+        try:\r
+            return eval(s)\r
+        except Exception, e:\r
+            raise ConfigError(str(e))\r
+\r
+class Sequence(Container):\r
+    """\r
+    This internal class implements a value which is a sequence of other values.\r
+    """\r
+    class SeqIter(object):\r
+        """\r
+        This internal class implements an iterator for a L{Sequence} instance.\r
+        """\r
+        def __init__(self, seq):\r
+            self.seq = seq\r
+            self.limit = len(object.__getattribute__(seq, 'data'))\r
+            self.index = 0\r
+\r
+        def __iter__(self):\r
+            return self\r
+\r
+        def next(self):\r
+            if self.index >= self.limit:\r
+                raise StopIteration\r
+            rv = self.seq[self.index]\r
+            self.index += 1\r
+            return rv\r
+\r
+    def __init__(self, parent=None):\r
+        """\r
+        Initialize an instance.\r
+\r
+        @param parent: The parent of this instance in the hierarchy.\r
+        @type parent: A L{Container} instance.\r
+        """\r
+        Container.__init__(self, parent)\r
+        object.__setattr__(self, 'data', [])\r
+        object.__setattr__(self, 'comments', [])\r
+\r
+    def append(self, item, comment):\r
+        """\r
+        Add an item to the sequence.\r
+\r
+        @param item: The item to add.\r
+        @type item: any\r
+        @param comment: A comment for the item.\r
+        @type comment: str\r
+        """\r
+        data = object.__getattribute__(self, 'data')\r
+        comments = object.__getattribute__(self, 'comments')\r
+        data.append(item)\r
+        comments.append(comment)\r
+\r
+    def __getitem__(self, index):\r
+        data = object.__getattribute__(self, 'data')\r
+        try:\r
+            rv = data[index]\r
+        except (IndexError, KeyError, TypeError):\r
+            raise ConfigResolutionError('%r is not a valid index for %r' % (index, object.__getattribute__(self, 'path')))\r
+        if not isinstance(rv, list):\r
+            rv = self.evaluate(rv)\r
+        else:\r
+            # deal with a slice\r
+            result = []\r
+            for a in rv:\r
+                result.append(self.evaluate(a))\r
+            rv = result\r
+        return rv\r
+\r
+    def __iter__(self):\r
+        return Sequence.SeqIter(self)\r
+\r
+    def __repr__(self):\r
+        return repr(object.__getattribute__(self, 'data'))\r
+\r
+    def __str__(self):\r
+        return str(self[:]) # using the slice evaluates the contents\r
+\r
+    def __len__(self):\r
+        return len(object.__getattribute__(self, 'data'))\r
+\r
+    def writeToStream(self, stream, indent, container):\r
+        """\r
+        Write this instance to a stream at the specified indentation level.\r
+\r
+        Should be redefined in subclasses.\r
+\r
+        @param stream: The stream to write to\r
+        @type stream: A writable stream (file-like object)\r
+        @param indent: The indentation level\r
+        @type indent: int\r
+        @param container: The container of this instance\r
+        @type container: L{Container}\r
+        """\r
+        indstr = indent * '  '\r
+        if len(self) == 0:\r
+            stream.write(' [ ]%s' % NEWLINE)\r
+        else:\r
+            if isinstance(container, Mapping):\r
+                stream.write(NEWLINE)\r
+            stream.write('%s[%s' % (indstr, NEWLINE))\r
+            self.save(stream, indent + 1)\r
+            stream.write('%s]%s' % (indstr, NEWLINE))\r
+\r
+    def save(self, stream, indent):\r
+        """\r
+        Save this instance to the specified stream.\r
+        @param stream: A stream to which the configuration is written.\r
+        @type stream: A write-only stream (file-like object).\r
+        @param indent: The indentation level for the output, > 0\r
+        @type indent: int\r
+        """\r
+        if indent == 0:\r
+            raise ConfigError("sequence cannot be saved as a top-level item")\r
+        data = object.__getattribute__(self, 'data')\r
+        comments = object.__getattribute__(self, 'comments')\r
+        indstr = indent * '  '\r
+        for i in xrange(0, len(data)):\r
+            value = data[i]\r
+            comment = comments[i]\r
+            if comment:\r
+                stream.write('%s#%s' % (indstr, comment))\r
+            if isinstance(value, Container):\r
+                value.writeToStream(stream, indent, self)\r
+            else:\r
+                self.writeValue(value, stream, indent)\r
+\r
+class Reference(object):\r
+    """\r
+    This internal class implements a value which is a reference to another value.\r
+    """\r
+    def __init__(self, config, type, ident):\r
+        """\r
+        Initialize an instance.\r
+\r
+        @param config: The configuration which contains this reference.\r
+        @type config: A L{Config} instance.\r
+        @param type: The type of reference.\r
+        @type type: BACKTICK or DOLLAR\r
+        @param ident: The identifier which starts the reference.\r
+        @type ident: str\r
+        """\r
+        self.config = config\r
+        self.type = type\r
+        self.elements = [ident]\r
+\r
+    def addElement(self, type, ident):\r
+        """\r
+        Add an element to the reference.\r
+\r
+        @param type: The type of reference.\r
+        @type type: BACKTICK or DOLLAR\r
+        @param ident: The identifier which continues the reference.\r
+        @type ident: str\r
+        """\r
+        self.elements.append((type, ident))\r
+\r
+    def findConfig(self, container):\r
+        """\r
+        Find the closest enclosing configuration to the specified container.\r
+\r
+        @param container: The container to start from.\r
+        @type container: L{Container}\r
+        @return: The closest enclosing configuration, or None.\r
+        @rtype: L{Config}\r
+        """\r
+        while (container is not None) and not isinstance(container, Config):\r
+            container = object.__getattribute__(container, 'parent')\r
+        return container\r
+\r
+    def resolve(self, container):\r
+        """\r
+        Resolve this instance in the context of a container.\r
+\r
+        @param container: The container to resolve from.\r
+        @type container: L{Container}\r
+        @return: The resolved value.\r
+        @rtype: any\r
+        @raise ConfigResolutionError: If resolution fails.\r
+        """\r
+        rv = None\r
+        path = object.__getattribute__(container, 'path')\r
+        current = self.findConfig(container)\r
+        while current is not None:\r
+            if self.type == BACKTICK:\r
+                namespaces = object.__getattribute__(current, 'namespaces')\r
+                found = False\r
+                s = str(self)[1:-1]\r
+                for ns in namespaces:\r
+                    try:\r
+                        try:\r
+                            rv = eval(s, vars(ns))\r
+                        except TypeError: #Python 2.7 - vars is a dictproxy\r
+                            rv = eval(s, {}, vars(ns))\r
+                        found = True\r
+                        break\r
+                    except:\r
+                        logger.debug("unable to resolve %r in %r", s, ns)\r
+                        pass\r
+                if found:\r
+                    break\r
+            else:\r
+                firstkey = self.elements[0]\r
+                if firstkey in current.resolving:\r
+                    current.resolving.remove(firstkey)\r
+                    raise ConfigResolutionError("Circular reference: %r" % firstkey)\r
+                current.resolving.add(firstkey)\r
+                key = firstkey\r
+                try:\r
+                    rv = current[key]\r
+                    for item in self.elements[1:]:\r
+                        key = item[1]\r
+                        rv = rv[key]\r
+                    current.resolving.remove(firstkey)\r
+                    break\r
+                except ConfigResolutionError:\r
+                    raise\r
+                except:\r
+                    logger.debug("Unable to resolve %r: %s", key, sys.exc_info()[1])\r
+                    rv = None\r
+                    pass\r
+                current.resolving.discard(firstkey)\r
+            current = self.findConfig(object.__getattribute__(current, 'parent'))\r
+        if current is None:\r
+            raise ConfigResolutionError("unable to evaluate %r in the configuration %s" % (self, path))\r
+        return rv\r
+\r
+    def __str__(self):\r
+        s = self.elements[0]\r
+        for tt, tv in self.elements[1:]:\r
+            if tt == DOT:\r
+                s += '.%s' % tv\r
+            else:\r
+                s += '[%r]' % tv\r
+        if self.type == BACKTICK:\r
+            return BACKTICK + s + BACKTICK\r
+        else:\r
+            return DOLLAR + s\r
+\r
+    def __repr__(self):\r
+        return self.__str__()\r
+\r
+class Expression(object):\r
+    """\r
+    This internal class implements a value which is obtained by evaluating an expression.\r
+    """\r
+    def __init__(self, op, lhs, rhs):\r
+        """\r
+        Initialize an instance.\r
+\r
+        @param op: the operation expressed in the expression.\r
+        @type op: PLUS, MINUS, STAR, SLASH, MOD\r
+        @param lhs: the left-hand-side operand of the expression.\r
+        @type lhs: any Expression or primary value.\r
+        @param rhs: the right-hand-side operand of the expression.\r
+        @type rhs: any Expression or primary value.\r
+        """\r
+        self.op = op\r
+        self.lhs = lhs\r
+        self.rhs = rhs\r
+\r
+    def __str__(self):\r
+        return '%r %s %r' % (self.lhs, self.op, self.rhs)\r
+\r
+    def __repr__(self):\r
+        return self.__str__()\r
+\r
+    def evaluate(self, container):\r
+        """\r
+        Evaluate this instance in the context of a container.\r
+\r
+        @param container: The container to evaluate in from.\r
+        @type container: L{Container}\r
+        @return: The evaluated value.\r
+        @rtype: any\r
+        @raise ConfigResolutionError: If evaluation fails.\r
+        @raise ZeroDivideError: If division by zero occurs.\r
+        @raise TypeError: If the operation is invalid, e.g.\r
+        subtracting one string from another.\r
+        """\r
+        lhs = self.lhs\r
+        if isinstance(lhs, Reference):\r
+            lhs = lhs.resolve(container)\r
+        elif isinstance(lhs, Expression):\r
+            lhs = lhs.evaluate(container)\r
+        rhs = self.rhs\r
+        if isinstance(rhs, Reference):\r
+            rhs = rhs.resolve(container)\r
+        elif isinstance(rhs, Expression):\r
+            rhs = rhs.evaluate(container)\r
+        op = self.op\r
+        if op == PLUS:\r
+            rv = lhs + rhs\r
+        elif op == MINUS:\r
+            rv = lhs - rhs\r
+        elif op == STAR:\r
+            rv = lhs * rhs\r
+        elif op == SLASH:\r
+            rv = lhs / rhs\r
+        else:\r
+            rv = lhs % rhs\r
+        return rv\r
+\r
+class ConfigReader(object):\r
+    """\r
+    This internal class implements a parser for configurations.\r
+    """\r
+\r
+    def __init__(self, config):\r
+        self.filename = None\r
+        self.config = config\r
+        self.lineno = 0\r
+        self.colno = 0\r
+        self.lastc = None\r
+        self.last_token = None\r
+        self.commentchars = '#'\r
+        self.whitespace = ' \t\r\n'\r
+        self.quotes = '\'"'\r
+        self.punct = ':-+*/%,.{}[]()@`$'\r
+        self.digits = '0123456789'\r
+        self.wordchars = '%s' % WORDCHARS # make a copy\r
+        self.identchars = self.wordchars + self.digits\r
+        self.pbchars = []\r
+        self.pbtokens = []\r
+        self.comment = None\r
+\r
+    def location(self):\r
+        """\r
+        Return the current location (filename, line, column) in the stream\r
+        as a string.\r
+\r
+        Used when printing error messages,\r
+\r
+        @return: A string representing a location in the stream being read.\r
+        @rtype: str\r
+        """\r
+        return "%s(%d,%d)" % (self.filename, self.lineno, self.colno)\r
+\r
+    def getChar(self):\r
+        """\r
+        Get the next char from the stream. Update line and column numbers\r
+        appropriately.\r
+\r
+        @return: The next character from the stream.\r
+        @rtype: str\r
+        """\r
+        if self.pbchars:\r
+            c = self.pbchars.pop()\r
+        else:\r
+            c = self.stream.read(1)\r
+            self.colno += 1\r
+            if c == '\n':\r
+                self.lineno += 1\r
+                self.colno = 1\r
+        return c\r
+\r
+    def __repr__(self):\r
+        return "<ConfigReader at 0x%08x>" % id(self)\r
+\r
+    __str__ = __repr__\r
+\r
+    def getToken(self):\r
+        """\r
+        Get a token from the stream. String values are returned in a form\r
+        where you need to eval() the returned value to get the actual\r
+        string. The return value is (token_type, token_value).\r
+\r
+        Multiline string tokenizing is thanks to David Janes (BlogMatrix)\r
+\r
+        @return: The next token.\r
+        @rtype: A token tuple.\r
+        """\r
+        if self.pbtokens:\r
+            return self.pbtokens.pop()\r
+        stream = self.stream\r
+        self.comment = None\r
+        token = ''\r
+        tt = EOF\r
+        while True:\r
+            c = self.getChar()\r
+            if not c:\r
+                break\r
+            elif c == '#':\r
+                self.comment = stream.readline()\r
+                self.lineno += 1\r
+                continue\r
+            if c in self.quotes:\r
+                token = c\r
+                quote = c\r
+                tt = STRING\r
+                escaped = False\r
+                multiline = False\r
+                c1 = self.getChar()\r
+                if c1 == quote:\r
+                    c2 = self.getChar()\r
+                    if c2 == quote:\r
+                        multiline = True\r
+                        token += quote\r
+                        token += quote\r
+                    else:\r
+                        self.pbchars.append(c2)\r
+                        self.pbchars.append(c1)\r
+                else:\r
+                    self.pbchars.append(c1)\r
+                while True:\r
+                    c = self.getChar()\r
+                    if not c:\r
+                        break\r
+                    token += c\r
+                    if (c == quote) and not escaped:\r
+                        if not multiline or (len(token) >= 6 and token.endswith(token[:3]) and token[-4] != '\\'):\r
+                            break\r
+                    if c == '\\':\r
+                        escaped = not escaped\r
+                    else:\r
+                        escaped = False\r
+                if not c:\r
+                    raise ConfigFormatError('%s: Unterminated quoted string: %r, %r' % (self.location(), token, c))\r
+                break\r
+            if c in self.whitespace:\r
+                self.lastc = c\r
+                continue\r
+            elif c in self.punct:\r
+                token = c\r
+                tt = c\r
+                if (self.lastc == ']') or (self.lastc in self.identchars):\r
+                    if c == '[':\r
+                        tt = LBRACK2\r
+                    elif c == '(':\r
+                        tt = LPAREN2\r
+                break\r
+            elif c in self.digits:\r
+                token = c\r
+                tt = NUMBER\r
+                in_exponent=False\r
+                while True:\r
+                    c = self.getChar()\r
+                    if not c:\r
+                        break\r
+                    if c in self.digits:\r
+                        token += c\r
+                    elif (c == '.') and token.find('.') < 0 and not in_exponent:\r
+                        token += c\r
+                    elif (c == '-') and token.find('-') < 0 and in_exponent:\r
+                        token += c\r
+                    elif (c in 'eE') and token.find('e') < 0 and\\r
+                         token.find('E') < 0:\r
+                        token += c\r
+                        in_exponent = True\r
+                    else:\r
+                        if c and (c not in self.whitespace):\r
+                            self.pbchars.append(c)\r
+                        break\r
+                break\r
+            elif c in self.wordchars:\r
+                token = c\r
+                tt = WORD\r
+                c = self.getChar()\r
+                while c and (c in self.identchars):\r
+                    token += c\r
+                    c = self.getChar()\r
+                if c: # and c not in self.whitespace:\r
+                    self.pbchars.append(c)\r
+                if token == "True":\r
+                    tt = TRUE\r
+                elif token == "False":\r
+                    tt = FALSE\r
+                elif token == "None":\r
+                    tt = NONE\r
+                break\r
+            else:\r
+                raise ConfigFormatError('%s: Unexpected character: %r' % (self.location(), c))\r
+        if token:\r
+            self.lastc = token[-1]\r
+        else:\r
+            self.lastc = None\r
+        self.last_token = tt\r
+        return (tt, token)\r
+\r
+    def load(self, stream, parent=None, suffix=None):\r
+        """\r
+        Load the configuration from the specified stream.\r
+\r
+        @param stream: A stream from which to load the configuration.\r
+        @type stream: A stream (file-like object).\r
+        @param parent: The parent of the configuration (to which this reader\r
+        belongs) in the hierarchy. Specified when the configuration is\r
+        included in another one.\r
+        @type parent: A L{Container} instance.\r
+        @param suffix: The suffix of this configuration in the parent\r
+        configuration. Should be specified whenever the parent is not None.\r
+        @raise ConfigError: If parent is specified but suffix is not.\r
+        @raise ConfigFormatError: If there are syntax errors in the stream.\r
+        """\r
+        if parent is not None:\r
+            if suffix is None:\r
+                raise ConfigError("internal error: load called with parent but no suffix")\r
+            self.config.setPath(makePath(object.__getattribute__(parent, 'path'), suffix))\r
+        self.setStream(stream)\r
+        self.token = self.getToken()\r
+        self.parseMappingBody(self.config)\r
+        if self.token[0] != EOF:\r
+            raise ConfigFormatError('%s: expecting EOF, found %r' % (self.location(), self.token[1]))\r
+\r
+    def setStream(self, stream):\r
+        """\r
+        Set the stream to the specified value, and prepare to read from it.\r
+\r
+        @param stream: A stream from which to load the configuration.\r
+        @type stream: A stream (file-like object).\r
+        """\r
+        self.stream = stream\r
+        if hasattr(stream, 'name'):\r
+            filename = stream.name\r
+        else:\r
+            filename = '?'\r
+        self.filename = filename\r
+        self.lineno = 1\r
+        self.colno = 1\r
+\r
+    def match(self, t):\r
+        """\r
+        Ensure that the current token type matches the specified value, and\r
+        advance to the next token.\r
+\r
+        @param t: The token type to match.\r
+        @type t: A valid token type.\r
+        @return: The token which was last read from the stream before this\r
+        function is called.\r
+        @rtype: a token tuple - see L{getToken}.\r
+        @raise ConfigFormatError: If the token does not match what's expected.\r
+        """\r
+        if self.token[0] != t:\r
+            raise ConfigFormatError("%s: expecting %s, found %r" % (self.location(), t, self.token[1]))\r
+        rv = self.token\r
+        self.token = self.getToken()\r
+        return rv\r
+\r
+    def parseMappingBody(self, parent):\r
+        """\r
+        Parse the internals of a mapping, and add entries to the provided\r
+        L{Mapping}.\r
+\r
+        @param parent: The mapping to add entries to.\r
+        @type parent: A L{Mapping} instance.\r
+        """\r
+        while self.token[0] in [WORD, STRING]:\r
+            self.parseKeyValuePair(parent)\r
+\r
+    def parseKeyValuePair(self, parent):\r
+        """\r
+        Parse a key-value pair, and add it to the provided L{Mapping}.\r
+\r
+        @param parent: The mapping to add entries to.\r
+        @type parent: A L{Mapping} instance.\r
+        @raise ConfigFormatError: if a syntax error is found.\r
+        """\r
+        comment = self.comment\r
+        tt, tv = self.token\r
+        if tt == WORD:\r
+            key = tv\r
+            suffix = tv\r
+        elif tt == STRING:\r
+            key = eval(tv)\r
+            suffix = '[%s]' % tv\r
+        else:\r
+            msg = "%s: expecting word or string, found %r"\r
+            raise ConfigFormatError(msg % (self.location(), tv))\r
+        self.token = self.getToken()\r
+        # for now, we allow key on its own as a short form of key : True\r
+        if self.token[0] == COLON:\r
+            self.token = self.getToken()\r
+            value = self.parseValue(parent, suffix)\r
+        else:\r
+            value = True\r
+        try:\r
+            parent.addMapping(key, value, comment)\r
+        except Exception, e:\r
+            raise ConfigFormatError("%s: %s, %r" % (self.location(), e,\r
+                                    self.token[1]))\r
+        tt = self.token[0]\r
+        if tt not in [EOF, WORD, STRING, RCURLY, COMMA]:\r
+            msg = "%s: expecting one of EOF, WORD, STRING,\\r
+RCURLY, COMMA, found %r"\r
+            raise ConfigFormatError(msg  % (self.location(), self.token[1]))\r
+        if tt == COMMA:\r
+            self.token = self.getToken()\r
+\r
+    def parseValue(self, parent, suffix):\r
+        """\r
+        Parse a value.\r
+\r
+        @param parent: The container to which the value will be added.\r
+        @type parent: A L{Container} instance.\r
+        @param suffix: The suffix for the value.\r
+        @type suffix: str\r
+        @return: The value\r
+        @rtype: any\r
+        @raise ConfigFormatError: if a syntax error is found.\r
+        """\r
+        tt = self.token[0]\r
+        if tt in [STRING, WORD, NUMBER, LPAREN, DOLLAR,\r
+                  TRUE, FALSE, NONE, BACKTICK, MINUS]:\r
+            rv = self.parseScalar()\r
+        elif tt == LBRACK:\r
+            rv = self.parseSequence(parent, suffix)\r
+        elif tt in [LCURLY, AT]:\r
+            rv = self.parseMapping(parent, suffix)\r
+        else:\r
+            raise ConfigFormatError("%s: unexpected input: %r" %\r
+               (self.location(), self.token[1]))\r
+        return rv\r
+\r
+    def parseSequence(self, parent, suffix):\r
+        """\r
+        Parse a sequence.\r
+\r
+        @param parent: The container to which the sequence will be added.\r
+        @type parent: A L{Container} instance.\r
+        @param suffix: The suffix for the value.\r
+        @type suffix: str\r
+        @return: a L{Sequence} instance representing the sequence.\r
+        @rtype: L{Sequence}\r
+        @raise ConfigFormatError: if a syntax error is found.\r
+        """\r
+        rv = Sequence(parent)\r
+        rv.setPath(makePath(object.__getattribute__(parent, 'path'), suffix))\r
+        self.match(LBRACK)\r
+        comment = self.comment\r
+        tt = self.token[0]\r
+        while tt in [STRING, WORD, NUMBER, LCURLY, LBRACK, LPAREN, DOLLAR,\r
+                     TRUE, FALSE, NONE, BACKTICK, MINUS]:\r
+            suffix = '[%d]' % len(rv)\r
+            value = self.parseValue(parent, suffix)\r
+            rv.append(value, comment)\r
+            tt = self.token[0]\r
+            comment = self.comment\r
+            if tt == COMMA:\r
+                self.match(COMMA)\r
+                tt = self.token[0]\r
+                comment = self.comment\r
+                continue\r
+        self.match(RBRACK)\r
+        return rv\r
+\r
+    def parseMapping(self, parent, suffix):\r
+        """\r
+        Parse a mapping.\r
+\r
+        @param parent: The container to which the mapping will be added.\r
+        @type parent: A L{Container} instance.\r
+        @param suffix: The suffix for the value.\r
+        @type suffix: str\r
+        @return: a L{Mapping} instance representing the mapping.\r
+        @rtype: L{Mapping}\r
+        @raise ConfigFormatError: if a syntax error is found.\r
+        """\r
+        if self.token[0] == LCURLY:\r
+            self.match(LCURLY)\r
+            rv = Mapping(parent)\r
+            rv.setPath(\r
+               makePath(object.__getattribute__(parent, 'path'), suffix))\r
+            self.parseMappingBody(rv)\r
+            self.match(RCURLY)\r
+        else:\r
+            self.match(AT)\r
+            tt, fn = self.match(STRING)\r
+            rv = Config(eval(fn), parent)\r
+        return rv\r
+\r
+    def parseScalar(self):\r
+        """\r
+        Parse a scalar - a terminal value such as a string or number, or\r
+        an L{Expression} or L{Reference}.\r
+\r
+        @return: the parsed scalar\r
+        @rtype: any scalar\r
+        @raise ConfigFormatError: if a syntax error is found.\r
+        """\r
+        lhs = self.parseTerm()\r
+        tt = self.token[0]\r
+        while tt in [PLUS, MINUS]:\r
+            self.match(tt)\r
+            rhs = self.parseTerm()\r
+            lhs = Expression(tt, lhs, rhs)\r
+            tt = self.token[0]\r
+        return lhs\r
+\r
+    def parseTerm(self):\r
+        """\r
+        Parse a term in an additive expression (a + b, a - b)\r
+\r
+        @return: the parsed term\r
+        @rtype: any scalar\r
+        @raise ConfigFormatError: if a syntax error is found.\r
+        """\r
+        lhs = self.parseFactor()\r
+        tt = self.token[0]\r
+        while tt in [STAR, SLASH, MOD]:\r
+            self.match(tt)\r
+            rhs = self.parseFactor()\r
+            lhs = Expression(tt, lhs, rhs)\r
+            tt = self.token[0]\r
+        return lhs\r
+\r
+    def parseFactor(self):\r
+        """\r
+        Parse a factor in an multiplicative expression (a * b, a / b, a % b)\r
+\r
+        @return: the parsed factor\r
+        @rtype: any scalar\r
+        @raise ConfigFormatError: if a syntax error is found.\r
+        """\r
+        tt = self.token[0]\r
+        if tt in [NUMBER, WORD, STRING, TRUE, FALSE, NONE]:\r
+            rv = self.token[1]\r
+            if tt != WORD:\r
+                rv = eval(rv)\r
+            self.match(tt)\r
+        elif tt == LPAREN:\r
+            self.match(LPAREN)\r
+            rv = self.parseScalar()\r
+            self.match(RPAREN)\r
+        elif tt == DOLLAR:\r
+            self.match(DOLLAR)\r
+            rv = self.parseReference(DOLLAR)\r
+        elif tt == BACKTICK:\r
+            self.match(BACKTICK)\r
+            rv = self.parseReference(BACKTICK)\r
+            self.match(BACKTICK)\r
+        elif tt == MINUS:\r
+            self.match(MINUS)\r
+            rv = -self.parseScalar()\r
+        else:\r
+            raise ConfigFormatError("%s: unexpected input: %r" %\r
+               (self.location(), self.token[1]))\r
+        return rv\r
+\r
+    def parseReference(self, type):\r
+        """\r
+        Parse a reference.\r
+\r
+        @return: the parsed reference\r
+        @rtype: L{Reference}\r
+        @raise ConfigFormatError: if a syntax error is found.\r
+        """\r
+        word = self.match(WORD)\r
+        rv = Reference(self.config, type, word[1])\r
+        while self.token[0] in [DOT, LBRACK2]:\r
+            self.parseSuffix(rv)\r
+        return rv\r
+\r
+    def parseSuffix(self, ref):\r
+        """\r
+        Parse a reference suffix.\r
+\r
+        @param ref: The reference of which this suffix is a part.\r
+        @type ref: L{Reference}.\r
+        @raise ConfigFormatError: if a syntax error is found.\r
+        """\r
+        tt = self.token[0]\r
+        if tt == DOT:\r
+            self.match(DOT)\r
+            word = self.match(WORD)\r
+            ref.addElement(DOT, word[1])\r
+        else:\r
+            self.match(LBRACK2)\r
+            tt, tv = self.token\r
+            if tt not in [NUMBER, STRING]:\r
+                raise ConfigFormatError("%s: expected number or string, found %r" % (self.location(), tv))\r
+            self.token = self.getToken()\r
+            tv = eval(tv)\r
+            self.match(RBRACK)\r
+            ref.addElement(LBRACK, tv)\r
+\r
+def defaultMergeResolve(map1, map2, key):\r
+    """\r
+    A default resolver for merge conflicts. Returns a string\r
+    indicating what action to take to resolve the conflict.\r
+\r
+    @param map1: The map being merged into.\r
+    @type map1: L{Mapping}.\r
+    @param map2: The map being used as the merge operand.\r
+    @type map2: L{Mapping}.\r
+    @param key: The key in map2 (which also exists in map1).\r
+    @type key: str\r
+    @return: One of "merge", "append", "mismatch" or "overwrite"\r
+             indicating what action should be taken. This should\r
+             be appropriate to the objects being merged - e.g.\r
+             there is no point returning "merge" if the two objects\r
+             are instances of L{Sequence}.\r
+    @rtype: str\r
+    """\r
+    obj1 = map1[key]\r
+    obj2 = map2[key]\r
+    if isinstance(obj1, Mapping) and isinstance(obj2, Mapping):\r
+        rv = "merge"\r
+    elif isinstance(obj1, Sequence) and isinstance(obj2, Sequence):\r
+        rv = "append"\r
+    else:\r
+        rv = "mismatch"\r
+    return rv\r
+\r
+def overwriteMergeResolve(map1, map2, key):\r
+    """\r
+    An overwriting resolver for merge conflicts. Calls L{defaultMergeResolve},\r
+    but where a "mismatch" is detected, returns "overwrite" instead.\r
+\r
+    @param map1: The map being merged into.\r
+    @type map1: L{Mapping}.\r
+    @param map2: The map being used as the merge operand.\r
+    @type map2: L{Mapping}.\r
+    @param key: The key in map2 (which also exists in map1).\r
+    @type key: str\r
+    """\r
+    rv = defaultMergeResolve(map1, map2, key)\r
+    if rv == "mismatch":\r
+        rv = "overwrite"\r
+    return rv\r
+\r
+class ConfigMerger(object):\r
+    """\r
+    This class is used for merging two configurations. If a key exists in the\r
+    merge operand but not the merge target, then the entry is copied from the\r
+    merge operand to the merge target. If a key exists in both configurations,\r
+    then a resolver (a callable) is called to decide how to handle the\r
+    conflict.\r
+    """\r
+\r
+    def __init__(self, resolver=defaultMergeResolve):\r
+        """\r
+        Initialise an instance.\r
+\r
+        @param resolver:\r
+        @type resolver: A callable which takes the argument list\r
+        (map1, map2, key) where map1 is the mapping being merged into,\r
+        map2 is the merge operand and key is the clashing key. The callable\r
+        should return a string indicating how the conflict should be resolved.\r
+        For possible return values, see L{defaultMergeResolve}. The default\r
+        value preserves the old behaviour\r
+        """\r
+        self.resolver = resolver\r
+\r
+    def merge(self, merged, mergee):\r
+        """\r
+        Merge two configurations. The second configuration is unchanged,\r
+        and the first is changed to reflect the results of the merge.\r
+\r
+        @param merged: The configuration to merge into.\r
+        @type merged: L{Config}.\r
+        @param mergee: The configuration to merge.\r
+        @type mergee: L{Config}.\r
+        """\r
+        self.mergeMapping(merged, mergee)\r
+\r
+    def mergeMapping(self, map1, map2):\r
+        """\r
+        Merge two mappings recursively. The second mapping is unchanged,\r
+        and the first is changed to reflect the results of the merge.\r
+\r
+        @param map1: The mapping to merge into.\r
+        @type map1: L{Mapping}.\r
+        @param map2: The mapping to merge.\r
+        @type map2: L{Mapping}.\r
+        """\r
+        keys = map1.keys()\r
+        for key in map2.keys():\r
+            if key not in keys:\r
+                map1[key] = map2[key]\r
+            else:\r
+                obj1 = map1[key]\r
+                obj2 = map2[key]\r
+                decision = self.resolver(map1, map2, key)\r
+                if decision == "merge":\r
+                    self.mergeMapping(obj1, obj2)\r
+                elif decision == "append":\r
+                    self.mergeSequence(obj1, obj2)\r
+                elif decision == "overwrite":\r
+                    map1[key] = obj2\r
+                elif decision == "mismatch":\r
+                    self.handleMismatch(obj1, obj2)\r
+                else:\r
+                    msg = "unable to merge: don't know how to implement %r"\r
+                    raise ValueError(msg % decision)\r
+\r
+    def mergeSequence(self, seq1, seq2):\r
+        """\r
+        Merge two sequences. The second sequence is unchanged,\r
+        and the first is changed to have the elements of the second\r
+        appended to it.\r
+\r
+        @param seq1: The sequence to merge into.\r
+        @type seq1: L{Sequence}.\r
+        @param seq2: The sequence to merge.\r
+        @type seq2: L{Sequence}.\r
+        """\r
+        data1 = object.__getattribute__(seq1, 'data')\r
+        data2 = object.__getattribute__(seq2, 'data')\r
+        for obj in data2:\r
+            data1.append(obj)\r
+        comment1 = object.__getattribute__(seq1, 'comments')\r
+        comment2 = object.__getattribute__(seq2, 'comments')\r
+        for obj in comment2:\r
+            comment1.append(obj)\r
+\r
+    def handleMismatch(self, obj1, obj2):\r
+        """\r
+        Handle a mismatch between two objects.\r
+\r
+        @param obj1: The object to merge into.\r
+        @type obj1: any\r
+        @param obj2: The object to merge.\r
+        @type obj2: any\r
+        """\r
+        raise ConfigError("unable to merge %r with %r" % (obj1, obj2))\r
+\r
+class ConfigList(list):\r
+    """\r
+    This class implements an ordered list of configurations and allows you\r
+    to try getting the configuration from each entry in turn, returning\r
+    the first successfully obtained value.\r
+    """\r
+\r
+    def getByPath(self, path):\r
+        """\r
+        Obtain a value from the first configuration in the list which defines\r
+        it.\r
+\r
+        @param path: The path of the value to retrieve.\r
+        @type path: str\r
+        @return: The value from the earliest configuration in the list which\r
+        defines it.\r
+        @rtype: any\r
+        @raise ConfigError: If no configuration in the list has an entry with\r
+        the specified path.\r
+        """\r
+        found = False\r
+        rv = None\r
+        for entry in self:\r
+            try:\r
+                rv = entry.getByPath(path)\r
+                found = True\r
+                break\r
+            except ConfigError:\r
+                pass\r
+        if not found:\r
+            raise ConfigError("unable to resolve %r" % path)\r
+        return rv\r
diff --git a/src/test/config_0_3_9/logconfig.cfg b/src/test/config_0_3_9/logconfig.cfg
new file mode 100644 (file)
index 0000000..2845e3c
--- /dev/null
@@ -0,0 +1,65 @@
+# Configuration file for logconfig.py\r
+\r
+# root logger configuration\r
+root:\r
+{\r
+  level     : `DEBUG`\r
+  handlers  : [$handlers.console, $handlers.file]\r
+}\r
+formatters: {\r
+  brief:\r
+  {\r
+    format: '%(levelname)-8s: %(name)s: %(message)s'\r
+  }\r
+  precise:\r
+  {\r
+    format: '%(asctime)s %(name)-15s %(levelname)-8s %(message)s'\r
+  }\r
+}\r
+handlers:\r
+{\r
+  console:\r
+  {\r
+    class : `logconfig.StreamHandler`\r
+    config:\r
+    {\r
+      level   : `INFO`\r
+      stream  : `sys.stdout`\r
+      formatter: $formatters.brief\r
+    }\r
+  }\r
+  file:\r
+  {\r
+    class : `logconfig.RotatingFileHandler`\r
+    config:\r
+    {\r
+      name: 'logconfig.log'\r
+      maxBytes: 1024\r
+      backupCount: 3\r
+      formatter: $formatters.precise\r
+    }\r
+  }\r
+  debugfile:\r
+  {\r
+    class : `logconfig.FileHandler`\r
+    config:\r
+    {\r
+      name: 'logconfig-detail.log'\r
+      mode: 'a'\r
+      formatter: $formatters.precise\r
+    }\r
+  }\r
+}\r
+loggers:\r
+{\r
+  area1:\r
+  {\r
+    level : `ERROR`\r
+    handlers: [$handlers.debugfile]\r
+  }\r
+  area2:\r
+  {\r
+    level : `CRITICAL`\r
+    handlers: [$handlers.debugfile]\r
+  }\r
+}
\ No newline at end of file
diff --git a/src/test/config_0_3_9/logconfig.py b/src/test/config_0_3_9/logconfig.py
new file mode 100644 (file)
index 0000000..44ae1ed
--- /dev/null
@@ -0,0 +1,138 @@
+#!/usr/bin/env python\r
+#\r
+# Copyright 2001-2004 by Vinay Sajip. All Rights Reserved.\r
+#\r
+# Permission to use, copy, modify, and distribute this software and its\r
+# documentation for any purpose and without fee is hereby granted,\r
+# provided that the above copyright notice appear in all copies and that\r
+# both that copyright notice and this permission notice appear in\r
+# supporting documentation, and that the name of Vinay Sajip\r
+# not be used in advertising or publicity pertaining to distribution\r
+# of the software without specific, written prior permission.\r
+# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING\r
+# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL\r
+# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR\r
+# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER\r
+# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\r
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\r
+#\r
+# This file is part of the Python config distribution. See\r
+# http://www.red-dove.com/python_config.html\r
+#\r
+"""\r
+A test for the config module through seeing how to use it to configure logging.\r
+\r
+Copyright (C) 2004 Vinay Sajip. All Rights Reserved.\r
+"""\r
+\r
+from config import Config\r
+from optparse import OptionParser, get_prog_name\r
+from random import choice\r
+import logging\r
+import logging.handlers\r
+import sys\r
+\r
+class Usage(Exception):\r
+    pass\r
+\r
+class BaseHandler:\r
+    def __init__(self, config):\r
+        if 'level' in config:\r
+            self.setLevel(config.level)\r
+        if 'formatter' in config:\r
+            self.setFormatter(config.formatter)\r
+\r
+class StreamHandler(logging.StreamHandler, BaseHandler):\r
+    def __init__(self, config):\r
+        stream = config.get('stream')\r
+        logging.StreamHandler.__init__(self, stream)\r
+        BaseHandler.__init__(self, config)\r
+\r
+class RotatingFileHandler(logging.handlers.RotatingFileHandler, BaseHandler):\r
+    def __init__(self, config):\r
+        name = config.get('name')\r
+        if name is None:\r
+            raise ValueError('RotatingFileHandler: name not specified')\r
+        mode = config.get('mode', 'a')\r
+        maxBytes = config.get('maxBytes', 0)\r
+        backupCount = config.get('backupCount', 0)\r
+        logging.handlers.RotatingFileHandler.__init__(self, name, mode, maxBytes, backupCount)\r
+        BaseHandler.__init__(self, config)\r
+\r
+class FileHandler(logging.FileHandler, BaseHandler):\r
+    def __init__(self, config):\r
+        name = config.get('name')\r
+        if name is None:\r
+            raise ValueError('FileHandler: name not specified')\r
+        mode = config.get('mode', 'a')\r
+        logging.FileHandler.__init__(self, name, mode)\r
+        BaseHandler.__init__(self, config)\r
+\r
+def configLogger(logger, config):\r
+    for handler in logger.handlers:\r
+        logger.removeHandler(handler)\r
+    if 'level' in config:\r
+        logger.setLevel(config.level)\r
+    if 'handlers' in config:\r
+        for handler in config.handlers:\r
+            logger.addHandler(handler)\r
+\r
+def fileConfig(fname, *args, **kwargs):\r
+    cfg = Config(fname)\r
+    cfg.addNamespace(logging)\r
+    cfg.addNamespace(sys.modules[StreamHandler.__module__], 'logconfig')\r
+\r
+    for name in cfg.formatters.keys():\r
+        formatterConfig = cfg.formatters[name]\r
+        fmt = formatterConfig.get('format')\r
+        datefmt = formatterConfig.get('datefmt')\r
+        formatter = logging.Formatter(fmt, datefmt)\r
+        cfg.formatters[name] = formatter\r
+\r
+    for name in cfg.handlers.keys():\r
+        klass = cfg.handlers[name].get('class')\r
+        config = cfg.handlers[name].get('config')\r
+        cfg.handlers[name] = klass(config)\r
+\r
+    for name in cfg.loggers.keys():\r
+        loggerConfig = cfg.loggers[name]\r
+        logger = logging.getLogger(name)\r
+        configLogger(logger, loggerConfig)\r
+\r
+    if 'root' in cfg:\r
+        configLogger(logging.getLogger(''), cfg.root)\r
+\r
+def testConfig():\r
+    levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL]\r
+    loggers = ['', 'area1', 'area2']\r
+    for i in xrange(1000):\r
+        logger = logging.getLogger(choice(loggers))\r
+        level = choice(levels)\r
+        logger.log(level, "Message number %d", i)\r
+\r
+def main(args=None):\r
+    rv = 0\r
+    if args is None:\r
+        args = sys.argv[1:]\r
+    parser = OptionParser(usage="usage: %prog [options] CONFIG-FILE")\r
+\r
+    (options, args) = parser.parse_args(args)\r
+    try:\r
+        if len(args) == 0:\r
+            raise Usage("No configuration file specified")\r
+        fileConfig(args[0])\r
+        testConfig()\r
+    except Usage, e:\r
+        parser.print_help()\r
+        print "\n%s: error: %s" % (get_prog_name(), e)\r
+        rv = 1\r
+    except Exception, e:\r
+        print "\n%s: error: %s" % (get_prog_name(), e)\r
+        typ, val, tb = sys.exc_info()\r
+        import traceback\r
+        traceback.print_tb(tb)\r
+        rv = 2\r
+    return rv\r
+\r
+if __name__ == "__main__":\r
+    sys.exit(main())\r
diff --git a/src/test/config_0_3_9/setup.py b/src/test/config_0_3_9/setup.py
new file mode 100644 (file)
index 0000000..9c7dfab
--- /dev/null
@@ -0,0 +1,20 @@
+from distutils.core import setup
+
+setup(name = "config",
+   description="A hierarchical, easy-to-use, powerful configuration module for Python",
+   long_description = """This module allows a hierarchical configuration scheme with support for mappings
+and sequences, cross-references between one part of the configuration and
+another, the ability to flexibly access real Python objects without full-blown
+eval(), an include facility, simple expression evaluation and the ability to
+change, save, cascade and merge configurations. Interfaces easily with
+environment variables and command-line options. It has been developed on python
+2.3 but should work on version 2.2 or greater.""",
+   license="""Copyright (C) 2004-2010 by Vinay Sajip. All Rights Reserved. See LICENSE for license.""",
+            version = "0.3.9",
+            author = "Vinay Sajip",
+            author_email = "vinay_sajip@red-dove.com",
+            maintainer = "Vinay Sajip",
+            maintainer_email = "vinay_sajip@red-dove.com",
+            url = "http://www.red-dove.com/python_config.html",
+            py_modules = ["config"],
+            )
diff --git a/src/test/config_0_3_9/styles.json b/src/test/config_0_3_9/styles.json
new file mode 100644 (file)
index 0000000..259697e
--- /dev/null
@@ -0,0 +1,554 @@
+{
+  "embeddedFonts" : [ ],
+  "pageSetup" : {
+    "size": "A4",
+    "width": null,
+    "height": null,
+    "margin-top": "2cm",
+    "margin-bottom": "2cm",
+    "margin-left": "2cm",
+    "margin-right": "2cm",
+    "margin-gutter": "0cm",
+    "spacing-header": "5mm",
+    "spacing-footer": "5mm",
+    "firstTemplate": "oneColumn"
+  },
+  "pageTemplates" : {
+    "coverPage": {
+        "frames": [
+            ["0cm", "0cm", "100%", "100%"]
+        ],
+        "showHeader" : false,
+        "showFooter" : false
+    },
+    "oneColumn": {
+        "frames": [
+            ["0cm", "0cm", "100%", "100%"]
+        ],
+        "showHeader" : true,
+        "showFooter" : true
+    },
+    "twoColumn": {
+        "frames": [
+            ["0cm", "0cm", "49%", "100%"],
+            ["51%", "0cm", "49%", "100%"]
+        ],
+        "showHeader" : true,
+        "showFooter" : true
+    },
+    "threeColumn": {
+        "frames": [
+            ["2%", "0cm", "29.333%", "100%"],
+            ["35.333%", "0cm", "29.333%", "100%"],
+            ["68.666%", "0cm", "29.333%", "100%"]
+        ],
+        "showHeader" : true,
+        "showFooter" : true
+    },
+    "cutePage": {
+        "frames": [
+            ["0%", "0%", "100%", "100%"]
+        ],
+        "showHeader" : true,
+        "showFooter" : true,
+        "defaultFooter" : "###Page###",
+        "defaultHeader" : "###Section###"
+    }
+  },
+  "fontsAlias" : {
+    "stdFont": "Helvetica",     
+    "stdBold": "Helvetica-Bold",
+    "stdItalic": "Helvetica-Oblique",
+    "stdBoldItalic": "Helvetica-BoldOblique",
+    "stdSans": "Helvetica",
+    "stdSansBold": "Helvetica-Bold",
+    "stdSansItalic": "Helvetica-Oblique",
+    "stdSansBoldItalic": "Helvetica-BoldOblique",
+    "stdMono": "Courier",
+    "stdMonoItalic": "Courier-Oblique",
+    "stdMonoBold": "Courier-Bold",
+    "stdMonoBoldItalic": "Courier-BoldOblique",
+    "stdSerif": "Times-Roman"
+  },
+  "linkColor" : "navy",
+  "styles" : [
+    [ "base" , {
+      "parent": null,
+      "fontName": "stdFont",
+      "fontSize":10,
+      "leading":12,
+      "leftIndent":0,
+      "rightIndent":0,
+      "firstLineIndent":0,
+      "alignment":"TA_LEFT",
+      "spaceBefore":0,
+      "spaceAfter":0,
+      "bulletFontName":"stdFont",
+      "bulletFontSize":10,
+      "bulletIndent":0,
+      "textColor": "black",
+      "backColor": null,
+      "wordWrap": null,
+      "borderWidth": 0,
+      "borderPadding": 0,
+      "borderColor": null,
+      "borderRadius": null,
+      "allowWidows": false,
+      "allowOrphans": false,
+      "hyphenation": false,
+      "kerning": false
+    }] ,
+    ["normal" , {
+      "parent": "base"
+    }],
+    ["title_reference" , {
+      "parent": "normal",
+      "fontName": "stdItalic"
+    }],
+    ["bodytext" , {
+      "parent": "normal",
+      "spaceBefore": 6,
+      "alignment": "TA_JUSTIFY",
+      "hyphenation": true
+    }],
+    ["toc" , {
+      "parent": "normal"
+    }],
+    ["blockquote" , {
+      "parent": "bodytext",
+      "leftIndent": 20
+    }],
+    ["lineblock" , {
+      "parent": "bodytext"
+    }],
+    ["line" , {
+      "parent": "lineblock",
+      "spaceBefore": 0
+    }],
+    ["toc1" , {
+      "parent": "toc",
+      "fontName": "stdBold"
+    }],
+    ["toc2" , {
+      "parent": "toc",
+      "leftIndent": 20
+    }],
+    ["toc3" , {
+      "parent": "toc",
+      "leftIndent": 40
+    }],
+    ["toc4" , {
+      "parent": "toc",
+      "leftIndent": 60
+    }],
+    ["toc5" , {
+      "parent": "toc",
+      "leftIndent": 80
+    }],
+    ["toc6" , {
+      "parent": "toc",
+      "leftIndent": 100
+    }],
+    ["toc7" , {
+      "parent": "toc",
+      "leftIndent": 100
+    }],
+    ["toc8" , {
+      "parent": "toc",
+      "leftIndent": 100
+    }],
+    ["toc9" , {
+      "parent": "toc",
+      "leftIndent": 100
+    }],
+    ["toc10" , {
+      "parent": "toc",
+      "leftIndent": 100
+    }],
+    ["toc11" , {
+      "parent": "toc",
+      "leftIndent": 100
+    }],
+    ["toc12" , {
+      "parent": "toc",
+      "leftIndent": 100
+    }],
+    ["toc13" , {
+      "parent": "toc",
+      "leftIndent": 100
+    }],
+    ["toc14" , {
+      "parent": "toc",
+      "leftIndent": 100
+    }],
+    ["toc15" , {
+      "parent": "toc",
+      "leftIndent": 100
+    }],
+    ["footer" , {
+      "parent": "normal",
+      "alignment": "TA_CENTER"
+    }],
+    ["header" , {
+      "parent": "normal",
+      "alignment": "TA_CENTER"
+    }],
+    ["attribution" , {
+      "parent": "bodytext",
+      "alignment": "TA_RIGHT"
+    }],
+    ["figure" , {
+      "parent": "bodytext",
+      "alignment": "TA_CENTER"
+    }],
+    ["figure-caption" , {
+      "parent": "bodytext",
+      "fontName": "stdItalic",
+      "alignment": "TA_CENTER"
+    }],
+    ["figure-legend" , {
+      "parent": "bodytext",
+      "alignment": "TA_CENTER"
+    }],
+    ["bullet_list", {
+      "parent": "bodytext",
+      "commands": [
+            [ "VALIGN", [ 0, 0 ], [ -1, -1 ], "TOP" ],
+            [ "RIGHTPADDING", [ 0, 0 ], [ 1, -1 ], 0 ] 
+        ], 
+        "colWidths": ["20",null]
+    }],
+    ["bullet_list_item" , {
+      "parent": "bodytext"
+    }],
+    ["item_list", { 
+      "parent": "bodytext",
+      "commands": [
+            [ "VALIGN", [ 0, 0 ], [ -1, -1 ], "TOP" ], 
+            [ "RIGHTPADDING", [ 0, 0 ], [ 1, -1 ], 0 ] 
+        ],
+        "colWidths": ["20pt",null]
+    }],
+    ["item_list_item" , {
+      "parent": "bodytext"
+    }],
+    ["definition_list_term" , {
+      "parent": "normal",
+      "fontName": "stdBold",
+      "spaceBefore": 4,
+      "spaceAfter": 0,
+      "keepWithNext": true
+    }],
+    ["definition_list_classifier" , {
+      "parent": "normal",
+      "fontName": "stdItalic"
+    }],
+    ["definition" , {
+      "parent": "bodytext",
+      "firstLineIndent": 0,
+      "bulletIndent": 0,
+      "spaceBefore": 0
+    }],
+    ["fieldname" , {
+      "parent": "bodytext",
+      "alignment": "TA_RIGHT",
+      "fontName": "stdBold"
+    }],
+    ["fieldvalue" , {
+      "parent": "bodytext"
+    }],
+    ["rubric" , {
+      "parent": "bodytext",
+      "textColor": "darkred",
+      "alignment": "TA_CENTER"
+    }],
+    ["italic" , {
+      "parent": "bodytext",
+      "fontName": "stdItalic"
+    }],
+    ["heading" , {
+      "parent": "normal",
+      "keepWithNext": true,
+      "spaceBefore": 12,
+      "spaceAfter": 6
+    }],
+    ["title" , {
+      "parent": "heading",
+      "fontName": "stdBold",
+      "fontSize": "200%",
+      "alignment": "TA_CENTER",
+      "keepWithNext": false,
+      "spaceAfter": 10
+    }],
+    ["subtitle" , {
+      "parent": "title",
+      "spaceBefore": 12,
+      "fontSize": "75%"
+    }],
+    ["heading1" , {
+      "parent": "heading",
+      "fontName": "stdBold",
+      "fontSize": "175%"
+    }],
+    ["heading2" , {
+      "parent": "heading",
+      "fontName": "stdBold",
+      "fontSize": "150%"
+    }],
+    ["heading3" , {
+      "parent": "heading",
+      "fontName": "stdBoldItalic",
+      "fontSize": "125%"
+    }],
+    ["heading4" , {
+      "parent": "heading",
+      "fontName": "stdBoldItalic"
+    }],
+    ["heading5" , {
+      "parent": "heading",
+      "fontName": "stdBoldItalic"
+    }],
+    ["heading6" , {
+      "parent": "heading",
+      "fontName": "stdBoldItalic"
+    }],
+    ["topic-title" , {
+      "parent": "heading3"
+    }],
+    ["sidebar-title" , {
+      "parent": "heading3"
+    }],
+    ["sidebar-subtitle" , {
+      "parent": "heading4"
+    }],
+    ["sidebar" , {
+      "float": "none",
+      "width": "100%",
+      "parent": "normal",
+      "backColor": "beige",
+      "borderColor": "darkgray",
+      "borderPadding": 8,
+      "borderWidth": 0.5
+    }],
+    ["admonition" , {
+      "parent": "normal",
+      "spaceBefore": 12,
+      "spaceAfter": 6,
+      "borderPadding": [16,16,16,16],
+      "backColor": "beige",
+      "borderColor": "darkgray",
+      "borderWidth": 0.5,
+      "commands":[
+            [ "VALIGN", [ 0, 0 ], [ -1, -1 ], "TOP" ]
+      ]
+    }],
+    ["attention" , {
+      "parent": "admonition"
+    }],
+    ["caution" , {
+      "parent": "admonition"
+    }],
+    ["danger" , {
+      "parent": "admonition"
+    }],
+    ["error" , {
+      "parent": "admonition"
+    }],
+    ["hint" , {
+      "parent": "admonition"
+    }],
+    ["important" , {
+      "parent": "admonition"
+    }],
+    ["note" , {
+      "parent": "admonition"
+    }],
+    ["tip" , {
+      "parent": "admonition"
+    }],
+    ["warning" , {
+      "parent": "admonition"
+    }],
+    ["admonition-title" , {
+      "parent": "heading3"
+    }],
+    ["admonition-heading" , {
+      "parent": "heading3"
+    }],
+    ["attention-heading" , {
+      "parent": "admonition-heading"
+    }],
+    ["caution-heading" , {
+      "parent": "admonition-heading"
+    }],
+    ["danger-heading" , {
+      "parent": "admonition-heading"
+    }],
+    ["error-heading" , {
+      "parent": "admonition-heading"
+    }],
+    ["hint-heading" , {
+      "parent": "admonition-heading"
+    }],
+    ["important-heading" , {
+      "parent": "admonition-heading"
+    }],
+    ["note-heading" , {
+      "parent": "admonition-heading"
+    }],
+    ["tip-heading" , {
+      "parent": "admonition-heading"
+    }],
+    ["warning-heading" , {
+      "parent": "admonition-heading"
+    }],
+    ["literal" , {
+      "parent": "normal",
+      "fontName": "stdMono",
+      "firstLineIndent": 0,
+      "hyphenation": false
+    }],
+    ["aafigure" , {
+      "parent": "literal"
+    }],
+    ["table" , {
+      "spaceBefore":6,
+      "spaceAfter":0,
+      "alignment": "TA_CENTER",
+      "commands": [
+            [ "VALIGN", [ 0, 0 ], [ -1, -1 ], "TOP" ], 
+            [ "INNERGRID", [ 0, 0 ], [ -1, -1 ], 0.25, "black" ], 
+            [ "ROWBACKGROUNDS", [0, 0], [-1, -1], ["white","#E0E0E0"]],
+            [ "BOX", [ 0, 0 ], [ -1, -1 ], 0.25, "black" ]
+      ]
+    }],
+    ["table-title" , {
+      "parent" : "heading4",
+      "keepWithNext": false,
+      "alignment" : "TA_CENTER"
+    }],
+    ["table-heading" , {
+      "parent" : "heading",
+      "backColor" : "beige",
+      "alignment" : "TA_CENTER",
+      "valign" : "BOTTOM",
+      "borderPadding" : 0
+    }],
+    ["table-body", {
+      "parent" : "normal"
+    }],
+    ["dedication" , {
+      "parent" : "normal"
+    }],
+    ["abstract" , {
+      "parent" : "normal"
+    }],
+    ["contents" , {
+      "parent" : "normal"
+    }],
+    ["tableofcontents" , {
+      "parent" : "normal"
+    }],
+    ["code" , {
+      "parent": "literal",
+      "leftIndent": 0,
+      "spaceBefore": 8,
+      "spaceAfter": 8,
+      "backColor": "beige",
+      "borderColor": "darkgray",
+      "borderWidth": 0.5,
+      "borderPadding": 6
+    }],
+    ["pygments-n" , {"parent": "code"}],
+    ["pygments-nx" , {"parent": "code"}],
+    ["pygments-p" , {"parent": "code"}],
+    ["pygments-hll", {"parent": "code", "backColor": "#ffffcc"}],
+    ["pygments-c", {"textColor": "#008800", "parent": "code"}],
+    ["pygments-err", {"parent": "code"}],
+    ["pygments-k", {"textColor": "#AA22FF", "parent": "code"}],
+    ["pygments-o", {"textColor": "#666666", "parent": "code"}],
+    ["pygments-cm", {"textColor": "#008800", "parent": "code"}],
+    ["pygments-cp", {"textColor": "#008800", "parent": "code"}],
+    ["pygments-c1", {"textColor": "#008800", "parent": "code"}],
+    ["pygments-cs", {"textColor": "#008800", "parent": "code"}],
+    ["pygments-gd", {"textColor": "#A00000", "parent": "code"}],
+    ["pygments-ge", {"parent": "code"}],
+    ["pygments-gr", {"textColor": "#FF0000", "parent": "code"}],
+    ["pygments-gh", {"textColor": "#000080", "parent": "code"}],
+    ["pygments-gi", {"textColor": "#00A000", "parent": "code"}],
+    ["pygments-go", {"textColor": "#808080", "parent": "code"}],
+    ["pygments-gp", {"textColor": "#000080", "parent": "code"}],
+    ["pygments-gs", {"parent": "code"}],
+    ["pygments-gu", {"textColor": "#800080", "parent": "code"}],
+    ["pygments-gt", {"textColor": "#0040D0", "parent": "code"}],
+    ["pygments-kc", {"textColor": "#AA22FF", "parent": "code"}],
+    ["pygments-kd", {"textColor": "#AA22FF", "parent": "code"}],
+    ["pygments-kn", {"textColor": "#AA22FF", "parent": "code"}],
+    ["pygments-kp", {"textColor": "#AA22FF", "parent": "code"}],
+    ["pygments-kr", {"textColor": "#AA22FF", "parent": "code"}],
+    ["pygments-kt", {"textColor": "#00BB00", "parent": "code"}],
+    ["pygments-m", {"textColor": "#666666", "parent": "code"}],
+    ["pygments-s", {"textColor": "#BB4444", "parent": "code"}],
+    ["pygments-na", {"textColor": "#BB4444", "parent": "code"}],
+    ["pygments-nb", {"textColor": "#AA22FF", "parent": "code"}],
+    ["pygments-nc", {"textColor": "#0000FF", "parent": "code"}],
+    ["pygments-no", {"textColor": "#880000", "parent": "code"}],
+    ["pygments-nd", {"textColor": "#AA22FF", "parent": "code"}],
+    ["pygments-ni", {"textColor": "#999999", "parent": "code"}],
+    ["pygments-ne", {"textColor": "#D2413A", "parent": "code"}],
+    ["pygments-nf", {"textColor": "#00A000", "parent": "code"}],
+    ["pygments-nl", {"textColor": "#A0A000", "parent": "code"}],
+    ["pygments-nn", {"textColor": "#0000FF", "parent": "code"}],
+    ["pygments-nt", {"textColor": "#008000", "parent": "code"}],
+    ["pygments-nv", {"textColor": "#B8860B", "parent": "code"}],
+    ["pygments-ow", {"textColor": "#AA22FF", "parent": "code"}],
+    ["pygments-w", {"textColor": "#bbbbbb", "parent": "code"}],
+    ["pygments-mf", {"textColor": "#666666", "parent": "code"}],
+    ["pygments-mh", {"textColor": "#666666", "parent": "code"}],
+    ["pygments-mi", {"textColor": "#666666", "parent": "code"}],
+    ["pygments-mo", {"textColor": "#666666", "parent": "code"}],
+    ["pygments-sb", {"textColor": "#BB4444", "parent": "code"}],
+    ["pygments-sc", {"textColor": "#BB4444", "parent": "code"}],
+    ["pygments-sd", {"textColor": "#BB4444", "parent": "code"}],
+    ["pygments-s2", {"textColor": "#BB4444", "parent": "code"}],
+    ["pygments-se", {"textColor": "#BB6622", "parent": "code"}],
+    ["pygments-sh", {"textColor": "#BB4444", "parent": "code"}],
+    ["pygments-si", {"textColor": "#BB6688", "parent": "code"}],
+    ["pygments-sx", {"textColor": "#008000", "parent": "code"}],
+    ["pygments-sr", {"textColor": "#BB6688", "parent": "code"}],
+    ["pygments-s1", {"textColor": "#BB4444", "parent": "code"}],
+    ["pygments-ss", {"textColor": "#B8860B", "parent": "code"}],
+    ["pygments-bp", {"textColor": "#AA22FF", "parent": "code"}],
+    ["pygments-vc", {"textColor": "#B8860B", "parent": "code"}],
+    ["pygments-vg", {"textColor": "#B8860B", "parent": "code"}],
+    ["pygments-vi", {"textColor": "#B8860B", "parent": "code"}],
+    ["pygments-il", {"textColor": "#666666", "parent": "code"}],
+    
+     [ "endnote", {
+         "parent": "bodytext",
+         "commands": [
+            [ "VALIGN", [ 0, 0 ], [ -1, -1 ], "TOP" ],
+            [ "TOPPADDING", [ 0, 0 ], [ -1, -1 ], 0 ], 
+            [ "BOTTOMPADDING", [ 0, 0 ], [ -1, -1 ], 0 ], 
+            [ "RIGHTPADDING", [ 0, 0 ], [ 1, -1 ], 0 ] 
+        ], 
+        "colWidths": ["3cm",null]
+    }],
+    [ "field_list", {
+         "parent": "bodytext",
+         "commands": [
+            [ "VALIGN", [ 0, 0 ], [ -1, -1 ], "TOP" ], 
+            [ "TOPPADDING", [ 0, 0 ], [ -1, -1 ], 0 ]
+        ], 
+        "colWidths": ["3cm",null],
+        "spaceBefore": 6
+    }],
+    [ "option_list", {
+         "commands": [
+            [ "VALIGN", [ 0, 0 ], [ -1, -1 ], "TOP" ], 
+            [ "TOPPADDING", [ 0, 0 ], [ -1, -1 ], 0 ]
+        ],
+        "colWidths": [null,null]
+    }]
+  ]
+}
diff --git a/src/test/config_0_3_9/test_config.py b/src/test/config_0_3_9/test_config.py
new file mode 100644 (file)
index 0000000..393be86
--- /dev/null
@@ -0,0 +1,439 @@
+# Copyright 2004-2010 by Vinay Sajip. All Rights Reserved.\r
+#\r
+# Permission to use, copy, modify, and distribute this software and its\r
+# documentation for any purpose and without fee is hereby granted,\r
+# provided that the above copyright notice appear in all copies and that\r
+# both that copyright notice and this permission notice appear in\r
+# supporting documentation, and that the name of Vinay Sajip\r
+# not be used in advertising or publicity pertaining to distribution\r
+# of the software without specific, written prior permission.\r
+# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING\r
+# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL\r
+# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR\r
+# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER\r
+# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\r
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\r
+\r
+"""\r
+Test harness for the configuration module 'config' for Python.\r
+"""\r
+\r
+import unittest\r
+# import test_support\r
+import config\r
+from config import Config, ConfigMerger, ConfigList\r
+from config import ConfigError, ConfigFormatError, ConfigResolutionError\r
+import logging\r
+from StringIO import StringIO\r
+\r
+STREAMS = {\r
+    "simple_1" :\r
+"""\r
+message: 'Hello, world!'\r
+""",\r
+    "malformed_1" :\r
+"""\r
+123\r
+""",\r
+    "malformed_2" :\r
+"""\r
+[ 123, 'abc' ]\r
+""",\r
+    "malformed_3" :\r
+"""\r
+{ a : 7, b : 1.3, c : 'test' }\r
+""",\r
+    "malformed_4" :\r
+"""\r
+test: $a [7] # note space before bracket\r
+""",\r
+    "malformed_5" :\r
+"""\r
+test: 'abc'\r
+test: 'def'\r
+""",\r
+    "wellformed_1" :\r
+"""\r
+test: $a[7] # note no space before bracket\r
+""",\r
+    "boolean_1":\r
+"""\r
+test : False\r
+another_test: True\r
+""",\r
+    "boolean_2":\r
+"""\r
+test : false\r
+another_test: true\r
+""",\r
+    "none_1":\r
+"""\r
+test : None\r
+""",\r
+    "none_2":\r
+"""\r
+test : none\r
+""",\r
+    "number_1":\r
+"""\r
+root: 1\r
+stream: 1.7\r
+neg: -1\r
+negfloat: -2.0\r
+posexponent: 2.0999999e-08\r
+negexponent: -2.0999999e-08\r
+exponent: 2.0999999e08\r
+""",\r
+    "sequence_1":\r
+"""\r
+mixed: [ "VALIGN", [ 0, 0 ], [ -1, -1 ], "TOP" ]\r
+simple: [1, 2]\r
+nested: [1, [2, 3], [4, [5, 6]]]\r
+""",\r
+    "include_1":\r
+"""\r
+included: @'include_2'\r
+""",\r
+    "include_2":\r
+"""\r
+test: 123\r
+another_test: 'abc'\r
+""",\r
+    "expr_1":\r
+"""\r
+value1 : 10\r
+value2 : 5\r
+value3 : 'abc'\r
+value4 : 'ghi'\r
+value5 : 0\r
+value6 : { 'a' : $value1, 'b': $value2 }\r
+derived1 : $value1 + $value2\r
+derived2 : $value1 - $value2\r
+derived3 : $value1 * $value2\r
+derived4 : $value1 / $value2\r
+derived5 : $value1 % $value2\r
+derived6 : $value3 + $value4\r
+derived7 : $value3 + 'def' + $value4\r
+derived8 : $value3 - $value4 # meaningless\r
+derived9 : $value1 / $value5    # div by zero\r
+derived10 : $value1 % $value5   # div by zero\r
+derived11 : $value17    # doesn't exist\r
+derived12 : $value6.a + $value6.b\r
+""",\r
+    "eval_1":\r
+"""\r
+stderr : `sys.stderr`\r
+stdout : `sys.stdout`\r
+stdin : `sys.stdin`\r
+debug : `debug`\r
+DEBUG : `DEBUG`\r
+derived: $DEBUG * 10\r
+""",\r
+    "merge_1":\r
+"""\r
+value1: True\r
+value3: [1, 2, 3]\r
+value5: [ 7 ]\r
+value6: { 'a' : 1, 'c' : 3 }\r
+""",\r
+    "merge_2":\r
+"""\r
+value2: False\r
+value4: [4, 5, 6]\r
+value5: ['abc']\r
+value6: { 'b' : 2, 'd' : 4 }\r
+""",\r
+    "merge_3":\r
+"""\r
+value1: True\r
+value2: 3\r
+value3: [1, 3, 5]\r
+value4: [1, 3, 5]\r
+""",\r
+    "merge_4":\r
+"""\r
+value1: False\r
+value2: 4\r
+value3: [2, 4, 6]\r
+value4: [2, 4, 6]\r
+""",\r
+    "list_1":\r
+"""\r
+verbosity : 1\r
+""",\r
+    "list_2":\r
+"""\r
+verbosity : 2\r
+program_value: 4\r
+""",\r
+    "list_3":\r
+"""\r
+verbosity : 3\r
+suite_value: 5\r
+""",\r
+    "get_1":\r
+"""\r
+value1 : 123\r
+value2 : 'abcd'\r
+value3 : True\r
+value4 : None\r
+value5:\r
+{\r
+    value1 : 123\r
+    value2 : 'abcd'\r
+    value3 : True\r
+    value4 : None\r
+}\r
+""",\r
+    "multiline_1":\r
+"""\r
+value1: '''Value One\r
+Value Two\r
+'''\r
+value2: \"\"\"Value Three\r
+Value Four\"\"\"\r
+"""\r
+}\r
+\r
+def makeStream(name):\r
+    s = StringIO(STREAMS[name])\r
+    s.name = name\r
+    return s\r
+\r
+class OutStream(StringIO):\r
+    def close(self):\r
+        self.value = self.getvalue()\r
+        StringIO.close(self)\r
+\r
+class TestConfig(unittest.TestCase):\r
+\r
+    def setUp(self):\r
+        self.cfg = Config(None)\r
+\r
+    def tearDown(self):\r
+        del self.cfg\r
+\r
+    def testCreation(self):\r
+        self.assertEqual(0, len(self.cfg))  # should be empty\r
+\r
+    def testSimple(self):\r
+        self.cfg.load(makeStream("simple_1"))\r
+        self.failUnless('message' in self.cfg)\r
+        self.failIf('root' in self.cfg)\r
+        self.failIf('stream' in self.cfg)\r
+        self.failIf('load' in self.cfg)\r
+        self.failIf('save' in self.cfg)\r
+\r
+    def testValueOnly(self):\r
+        self.assertRaises(ConfigError, self.cfg.load,\r
+           makeStream("malformed_1"))\r
+        self.assertRaises(ConfigError, self.cfg.load,\r
+           makeStream("malformed_2"))\r
+        self.assertRaises(ConfigError, self.cfg.load,\r
+           makeStream("malformed_3"))\r
+\r
+    def testBadBracket(self):\r
+        self.assertRaises(ConfigError, self.cfg.load,\r
+           makeStream("malformed_4"))\r
+\r
+    def testDuplicate(self):\r
+        self.assertRaises(ConfigError, self.cfg.load,\r
+           makeStream("malformed_5"))\r
+\r
+    def testGoodBracket(self):\r
+        self.cfg.load(makeStream("wellformed_1"))\r
+\r
+    def testBoolean(self):\r
+        self.cfg.load(makeStream("boolean_1"))\r
+        self.assertEqual(True, self.cfg.another_test)\r
+        self.assertEqual(False, self.cfg.test)\r
+\r
+    def testNotBoolean(self):\r
+        self.cfg.load(makeStream("boolean_2"))\r
+        self.assertEqual('true', self.cfg.another_test)\r
+        self.assertEqual('false', self.cfg.test)\r
+\r
+    def testNone(self):\r
+        self.cfg.load(makeStream("none_1"))\r
+        self.assertEqual(None, self.cfg.test)\r
+\r
+    def testNotNone(self):\r
+        self.cfg.load(makeStream("none_2"))\r
+        self.assertEqual('none', self.cfg.test)\r
+\r
+    def testNumber(self):\r
+        self.cfg.load(makeStream("number_1"))\r
+        self.assertEqual(1, self.cfg.root)\r
+        self.assertEqual(1.7, self.cfg.stream)\r
+        self.assertEqual(-1, self.cfg.neg)\r
+        self.assertEqual(-2.0, self.cfg.negfloat)\r
+        self.assertAlmostEqual(-2.0999999e-08, self.cfg.negexponent)\r
+        self.assertAlmostEqual(2.0999999e-08, self.cfg.posexponent)\r
+        self.assertAlmostEqual(2.0999999e08, self.cfg.exponent)\r
+\r
+    def testChange(self):\r
+        self.cfg.load(makeStream("simple_1"))\r
+        self.cfg.message = 'Goodbye, cruel world!'\r
+        self.assertEqual('Goodbye, cruel world!', self.cfg.message)\r
+\r
+    def testSave(self):\r
+        self.cfg.load(makeStream("simple_1"))\r
+        self.cfg.message = 'Goodbye, cruel world!'\r
+        out = OutStream()\r
+        self.cfg.save(out)\r
+        self.assertEqual("message : 'Goodbye, cruel world!'" + config.NEWLINE,\r
+           out.value)\r
+\r
+    def testInclude(self):\r
+        config.streamOpener = makeStream\r
+        self.cfg = Config("include_1")\r
+        config.streamOpener = config.defaultStreamOpener\r
+        out = OutStream()\r
+        self.cfg.save(out)\r
+        s = "included :%s{%s  test : 123%s  another_test : 'abc'%s}%s" % (5 *\r
+           (config.NEWLINE,))\r
+        self.assertEqual(s, out.value)\r
+\r
+    def testExpression(self):\r
+        self.cfg.load(makeStream("expr_1"))\r
+        self.assertEqual(15, self.cfg.derived1)\r
+        self.assertEqual(5, self.cfg.derived2)\r
+        self.assertEqual(50, self.cfg.derived3)\r
+        self.assertEqual(2, self.cfg.derived4)\r
+        self.assertEqual(0, self.cfg.derived5)\r
+        self.assertEqual('abcghi', self.cfg.derived6)\r
+        self.assertEqual('abcdefghi', self.cfg.derived7)\r
+        self.assertRaises(TypeError, lambda x: x.derived8, self.cfg)\r
+        self.assertRaises(ZeroDivisionError, lambda x: x.derived9, self.cfg)\r
+        self.assertRaises(ZeroDivisionError, lambda x: x.derived10, self.cfg)\r
+        self.assertRaises(ConfigResolutionError,\r
+           lambda x: x.derived11, self.cfg)\r
+        self.assertEqual(15, self.cfg.derived12)\r
+\r
+    def testEval(self):\r
+        import sys, logging\r
+        self.cfg.load(makeStream("eval_1"))\r
+        self.assertEqual(sys.stderr, self.cfg.stderr)\r
+        self.assertEqual(sys.stdout, self.cfg.stdout)\r
+        self.assertEqual(sys.stdin, self.cfg.stdin)\r
+        self.assertRaises(ConfigResolutionError, lambda x: x.debug, self.cfg)\r
+        self.cfg.addNamespace(logging.Logger)\r
+        self.assertEqual(logging.Logger.debug.im_func, self.cfg.debug)\r
+        self.assertRaises(ConfigResolutionError, lambda x: x.DEBUG, self.cfg)\r
+        self.cfg.addNamespace(logging)\r
+        self.assertEqual(logging.DEBUG, self.cfg.DEBUG)\r
+        self.cfg.removeNamespace(logging.Logger)\r
+        self.assertEqual(logging.debug, self.cfg.debug)\r
+        self.assertEqual(logging.DEBUG * 10, self.cfg.derived)\r
+\r
+    def testFunctions(self):\r
+        makePath = config.makePath\r
+        isWord = config.isWord\r
+        self.assertEqual('suffix', makePath('', 'suffix'))\r
+        self.assertEqual('suffix', makePath(None, 'suffix'))\r
+        self.assertEqual('prefix.suffix', makePath('prefix', 'suffix'))\r
+        self.assertEqual('prefix[1]', makePath('prefix', '[1]'))\r
+        self.failUnless(isWord('a9'))\r
+        self.failUnless(isWord('9a'))    #perverse, but there you go\r
+        self.failIf(isWord(9))\r
+        self.failIf(isWord(None))\r
+        self.failIf(isWord(self))\r
+        self.failIf(isWord(''))\r
+\r
+    def testMerge(self):\r
+        cfg1 = Config()\r
+        cfg1.load(makeStream("merge_1"))\r
+        cfg2 = Config(makeStream("merge_2"))\r
+        ConfigMerger().merge(cfg1, cfg2)\r
+        merged = cfg1\r
+        cfg1 = Config()\r
+        cfg1.load(makeStream("merge_1"))\r
+        for i in xrange(0, 5):\r
+            key = 'value%d' % (i + 1,)\r
+            self.failUnless(key in merged)\r
+        self.assertEqual(len(cfg1.value5) + len(cfg2.value5),\r
+           len(merged.value5))\r
+        cfg3 = Config()\r
+        cfg3.load(makeStream("merge_3"))\r
+        cfg4 = Config(makeStream("merge_4"))\r
+        merger = ConfigMerger()\r
+        self.assertRaises(ConfigError, merger.merge, cfg3, cfg4)\r
+\r
+        cfg3 = Config(makeStream("merge_3"))\r
+        cfg4 = Config(makeStream("merge_4"))\r
+        merger = ConfigMerger(config.overwriteMergeResolve)\r
+        merger.merge(cfg3, cfg4)\r
+        self.assertEqual(False, cfg3['value1'])\r
+        self.assertEqual(4, cfg3['value2'])\r
+\r
+        def customMergeResolve(map1, map2, key):\r
+            if key == "value3":\r
+                rv = "overwrite"\r
+            else:\r
+                rv = config.overwriteMergeResolve(map1, map2, key)\r
+            return rv\r
+\r
+        cfg3 = Config(makeStream("merge_3"))\r
+        cfg4 = Config(makeStream("merge_4"))\r
+        merger = ConfigMerger(customMergeResolve)\r
+        merger.merge(cfg3, cfg4)\r
+        self.assertEqual("[2, 4, 6]", str(cfg3.value3))\r
+        self.assertEqual("[1, 3, 5, 2, 4, 6]", str(cfg3.value4))\r
+\r
+    def testList(self):\r
+        list = ConfigList()\r
+        list.append(Config(makeStream("list_1")))\r
+        list.append(Config(makeStream("list_2")))\r
+        list.append(Config(makeStream("list_3")))\r
+        self.assertEqual(1, list.getByPath('verbosity'))\r
+        self.assertEqual(4, list.getByPath('program_value'))\r
+        self.assertEqual(5, list.getByPath('suite_value'))\r
+        self.assertRaises(ConfigError, list.getByPath, 'nonexistent_value')\r
+\r
+    def testGet(self):\r
+        cfg = self.cfg\r
+        cfg.load(makeStream("get_1"))\r
+        self.assertEqual(123, cfg.get('value1'))\r
+        self.assertEqual(123, cfg.get('value1', -123))\r
+        self.assertEqual(-123, cfg.get('value11', -123))\r
+        self.assertEqual('abcd', cfg.get('value2'))\r
+        self.failUnless(cfg.get('value3'))\r
+        self.failIf(cfg.get('value4') is not None)\r
+        self.assertEqual(123, cfg.value5.get('value1'))\r
+        self.assertEqual(123, cfg.value5.get('value1', -123))\r
+        self.assertEqual(-123, cfg.value5.get('value11', -123))\r
+        self.assertEqual('abcd', cfg.value5.get('value2'))\r
+        self.failUnless(cfg.value5.get('value3'))\r
+        self.failIf(cfg.value5.get('value4') is not None)\r
+\r
+    def testMultiline(self):\r
+        cfg = self.cfg\r
+        cfg.load(makeStream("multiline_1"))\r
+        self.assertEqual("Value One\nValue Two\n", cfg.get('value1'))\r
+        self.assertEqual("Value Three\nValue Four", cfg.get('value2'))\r
+\r
+    def testSequence(self):\r
+        cfg = self.cfg\r
+        strm = makeStream("sequence_1")\r
+        cfg.load(strm)\r
+        self.assertEqual(str(cfg.simple), "[1, 2]")\r
+        self.assertEqual(str(cfg.nested), "[1, [2, 3], [4, [5, 6]]]")\r
+        self.assertEqual(str(cfg.mixed), "['VALIGN', [0, 0], [-1, -1], 'TOP']")\r
+\r
+    def testJSON(self):\r
+        data = StringIO('dummy: ' + open('styles.json', 'r').read())\r
+        self.cfg.load(data)\r
+\r
+def init_logging():\r
+    logging.basicConfig(level=logging.DEBUG, filename="test_config.log",\r
+                        filemode="w", format="%(asctime)s %(levelname)-5s %(name)-10s %(message)s")\r
+"""\r
+def test_main():\r
+    init_logging()\r
+    test_support.run_unittest(TestConfig)\r
+"""\r
+\r
+if __name__ == "__main__":\r
+    unittest.main(exit=False)\r
+    pass\r
+    # test_main()\r
diff --git a/src/test/debugTest.py b/src/test/debugTest.py
new file mode 100755 (executable)
index 0000000..bc84f53
--- /dev/null
@@ -0,0 +1,127 @@
+#!/usr/bin/env python
+#-*- coding:utf-8 -*-
+
+#  Copyright (C) 2010-2018  CEA/DEN
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 2.1 of the License.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library; if not, write to the Free Software
+#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+
+import os
+import sys
+import unittest
+
+import src
+import src.debug as DBG # Easy print stderr (for DEBUG only)
+import src.pyconf as PYF # 0.3.7
+import src.test.config_0_3_9.config as PYF9 # TODO 0.3.9
+
+_EXAMPLES = {
+1 : """\
+  messages:
+  [
+    {
+      stream : "sys.stderr" # modified
+      message: 111 # modified
+      name: 'Harry'
+    }
+    {
+      stream : $messages[0].stream
+      message: 1.23e4 # modified do not work 0.3.7
+      name: 'Ruud'
+    }
+    {
+      stream : "HELLO " + $messages[0].stream
+      message: 'Bienvenue'
+      name: "Yves"
+    }
+  ]
+""",
+
+2 : """\
+  aa: 111
+  bb: $aa + 222
+""",
+
+3 : """\
+  aa: Yves
+  bb: "Herve" # avoid Hervé -> 'utf8' codec can't decode byte
+""",
+
+4 : """\
+  aa: Yves
+  bb: "Hervé" # avoid Hervé -> 'utf8' codec can't decode byte
+""",
+
+
+}
+
+
+class TestCase(unittest.TestCase):
+  "Test the debug.py"""
+  
+  def test_000(self):
+    # one shot setUp() for this TestCase
+    # DBG.push_debug(True)
+    # SAT.setNotLocale() # test english
+    return
+    
+  def test_005(self):
+    res = DBG.getLocalEnv()
+    self.assertTrue(len(res.split()) > 0)
+    self.assertTrue("USER :" in res)
+    self.assertTrue("LANG :" in res)
+       
+  def test_010(self):
+    inStream = DBG.InStream(_EXAMPLES[1])
+    self.assertEqual(inStream.getvalue(), _EXAMPLES[1])
+    cfg = PYF.Config(inStream)
+    self.assertEqual(len(cfg.messages), 3)
+    outStream = DBG.OutStream()
+    DBG.saveConfigStd(cfg, outStream)
+    res = outStream.value
+    DBG.write("test_010 cfg std", res)
+    self.assertTrue("messages :" in res)
+    self.assertTrue("'sys.stderr'" in res)
+    
+  def test_020(self):
+    inStream = DBG.InStream(_EXAMPLES[2])
+    cfg = PYF.Config(inStream)
+    res = DBG.getStrConfigDbg(cfg)
+    DBG.write("test_020 cfg dbg", res)
+    ress = res.split("\n")
+    self.assertTrue(".aa : '111'" in ress[0])
+    self.assertTrue(".bb : $aa + 222 --> '333'" in ress[1])
+    
+  def test_025(self):
+    inStream = DBG.InStream(_EXAMPLES[1])
+    cfg = PYF.Config(inStream)
+    outStream = DBG.OutStream()
+    DBG.saveConfigDbg(cfg, outStream)
+    res = outStream.value
+    DBG.write("test_025 cfg dbg", res)
+    for i in range(len(cfg.messages)):
+      self.assertTrue("messages[%i].name" % i in res)
+    self.assertTrue("--> 'HELLO sys.stderr'" in res)
+
+      
+  def test_999(self):
+    # one shot tearDown() for this TestCase
+    # SAT.setLocale() # end test english
+    # DBG.pop_debug()
+    return
+    
+if __name__ == '__main__':
+    unittest.main(exit=False)
+    pass
+
diff --git a/src/test/pyconfTest.py b/src/test/pyconfTest.py
new file mode 100755 (executable)
index 0000000..2adaeb8
--- /dev/null
@@ -0,0 +1,152 @@
+#!/usr/bin/env python
+#-*- coding:utf-8 -*-
+
+#  Copyright (C) 2010-2018  CEA/DEN
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 2.1 of the License.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library; if not, write to the Free Software
+#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+
+import os
+import sys
+import unittest
+
+import src
+import src.debug as DBG # Easy print stderr (for DEBUG only)
+import src.pyconf as PYF # 0.3.7
+import src.test.config_0_3_9.config as PYF9 # TODO 0.3.9
+
+_EXAMPLES = {
+1 : """\
+  messages:
+  [
+    {
+      stream : "sys.stderr" # modified
+      message: 'Welcome'
+      name: 'Harry'
+    }
+    {
+      stream : "sys.stdout" # modified
+      message: 'Welkom'
+      name: 'Ruud'
+    }
+    {
+      stream : $messages[0].stream
+      message: 'Bienvenue'
+      name: "Yves"
+    }
+  ]
+""",
+
+2 : """\
+  aa: 111
+  bb: $aa + 222
+""",
+
+3 : """\
+  aa: Yves
+  bb: "Herve" # avoid Hervé -> 'utf8' codec can't decode byte
+""",
+
+4 : """\
+  aa: Yves
+  bb: "Hervé" # avoid Hervé -> 'utf8' codec can't decode byte
+""",
+
+
+}
+
+
+class TestCase(unittest.TestCase):
+  "Test the pyconf.py"""
+  
+  def test_000(self):
+    # one shot setUp() for this TestCase
+    # DBG.push_debug(True)
+    # SAT.setNotLocale() # test english
+    return
+
+  def test_010(self):
+    # pyconf.py doc example 0.3.7
+    # https://www.red-dove.com/config-doc/ is 0.3.9 !
+    # which, when run, would yield the console output:
+
+    expected = """\
+Welcome, Harry
+Welkom, Ruud
+Bienvenue, Yves
+"""
+    inStream = DBG.InStream(_EXAMPLES[1])
+    cfg = PYF.Config(inStream)
+    res = ''
+    for m in cfg.messages:
+        res += '%s, %s\n' % (m.message, m.name)
+    self.assertEqual(res, expected)
+    outStream = DBG.OutStream()
+    cfg.__save__(outStream) # sat renamed save() in __save__()
+    res = outStream.value
+    DBG.write("test_010 cfg", res)
+    self.assertTrue("name : 'Harry'" in res)
+    self.assertTrue("name : 'Ruud'" in res)
+    self.assertTrue("name : 'Yves'" in res)
+        
+  def test_020(self):
+    cfg = PYF.Config()
+    self.assertEqual(str(cfg), '{}')
+    self.assertEqual(cfg.__repr__(), '{}')
+    cfg.aa = "1111"
+    self.assertEqual(str(cfg), "{'aa': '1111'}")
+    cfg.bb = 2222
+    self.assertTrue("'bb': 2222" in str(cfg))
+    self.assertTrue("'aa': '1111'" in str(cfg))
+    cfg.cc = 3333.
+    self.assertTrue("'cc': 3333." in str(cfg))
+    
+  def test_030(self):
+    inStream = DBG.InStream(_EXAMPLES[2])
+    cfg = PYF.Config(inStream)
+    self.assertEqual(str(cfg),  "{'aa': 111, 'bb': $aa + 222}")
+    self.assertEqual(cfg.aa, 111)
+    self.assertEqual(cfg.bb, 333)
+      
+  def test_040(self):
+    inStream = DBG.InStream(_EXAMPLES[3])
+    cfg = PYF.Config(inStream)
+    self.assertEqual(cfg.aa, "Yves")
+    self.assertEqual(cfg.bb, "Herve")
+    self.assertEqual(type(cfg.bb), str)
+    cfg.bb = "Hervé" # try this
+    self.assertEqual(type(cfg.bb), str)
+    self.assertEqual(cfg.bb, "Hervé")
+    
+  def test_045(self):
+    """TODO: make Hervé valid with pyconf.py as 0.3.9"""
+    inStream = DBG.InStream(_EXAMPLES[4])
+    cfg = PYF9.Config(inStream)
+    outStream = DBG.OutStream()
+    cfg.save(outStream) # sat renamed save() in __save__()
+    res = outStream.value
+    DBG.write("test_045 cfg", res)
+    self.assertTrue("aa : 'Yves'" in res)
+    self.assertTrue(r"bb : 'Herv\xc3\xa9'" in res)
+    self.assertEqual(cfg.bb, "Hervé")
+      
+  def test_999(self):
+    # one shot tearDown() for this TestCase
+    # SAT.setLocale() # end test english
+    # DBG.pop_debug()
+    return
+    
+if __name__ == '__main__':
+    unittest.main(exit=False)
+    pass
index 6eea8bdecf283440fe4cb7017720352d84db00b8..b62de2a44ab1452e10ba369a1e620bc641e3c43e 100755 (executable)
@@ -32,6 +32,7 @@ class TestCase(unittest.TestCase):
     # one shot setUp() for this TestCase
     # DBG.push_debug(True)
     SAT.setNotLocale() # test english
+    return
 
   def test_010(self):
     cmd = "sat --help"
@@ -76,6 +77,7 @@ class TestCase(unittest.TestCase):
     # one shot tearDown() for this TestCase
     SAT.setLocale() # end test english
     # DBG.pop_debug()
+    return
       
 if __name__ == '__main__':
     unittest.main(exit=False)
diff --git a/src/test/test_pyconf.py b/src/test/test_pyconf.py
new file mode 100755 (executable)
index 0000000..7f884de
--- /dev/null
@@ -0,0 +1,450 @@
+#!/usr/bin/env python
+#-*- coding:utf-8 -*-
+
+# Copyright 2004-2010 by Vinay Sajip. All Rights Reserved.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose and without fee is hereby granted,
+# provided that the above copyright notice appear in all copies and that
+# both that copyright notice and this permission notice appear in
+# supporting documentation, and that the name of Vinay Sajip
+# not be used in advertising or publicity pertaining to distribution
+# of the software without specific, written prior permission.
+# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
+# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
+# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
+# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""
+Test harness for the configuration module 'config' for Python.
+
+from test_config 0.3.9 modified to test 0.3.7.1
+this test obviously have FAILED (errors=6), TODO, fix upgrading 0.3.9, or not.
+"""
+
+import unittest
+# import test_support
+import src.pyconf as config
+from src.pyconf import Config, ConfigMerger, ConfigList
+from src.pyconf import ConfigError, ConfigFormatError, ConfigResolutionError
+import logging
+from StringIO import StringIO
+
+STREAMS = {
+    "simple_1" :
+"""
+message: 'Hello, world!'
+""",
+    "malformed_1" :
+"""
+123
+""",
+    "malformed_2" :
+"""
+[ 123, 'abc' ]
+""",
+    "malformed_3" :
+"""
+{ a : 7, b : 1.3, c : 'test' }
+""",
+    "malformed_4" :
+"""
+test: $a [7] # note space before bracket
+""",
+    "malformed_5" :
+"""
+test: 'abc'
+test: 'def'
+""",
+    "wellformed_1" :
+"""
+test: $a[7] # note no space before bracket
+""",
+    "boolean_1":
+"""
+test : False
+another_test: True
+""",
+    "boolean_2":
+"""
+test : false
+another_test: true
+""",
+    "none_1":
+"""
+test : None
+""",
+    "none_2":
+"""
+test : none
+""",
+    "number_1":
+"""
+root: 1
+stream: 1.7
+neg: -1
+negfloat: -2.0
+posexponent: 2.0999999e-08
+negexponent: -2.0999999e-08
+exponent: 2.0999999e08
+""",
+    "sequence_1":
+"""
+mixed: [ "VALIGN", [ 0, 0 ], [ -1, -1 ], "TOP" ]
+simple: [1, 2]
+nested: [1, [2, 3], [4, [5, 6]]]
+""",
+    "include_1":
+"""
+included: @'include_2'
+""",
+    "include_2":
+"""
+test: 123
+another_test: 'abc'
+""",
+    "expr_1":
+"""
+value1 : 10
+value2 : 5
+value3 : 'abc'
+value4 : 'ghi'
+value5 : 0
+value6 : { 'a' : $value1, 'b': $value2 }
+derived1 : $value1 + $value2
+derived2 : $value1 - $value2
+derived3 : $value1 * $value2
+derived4 : $value1 / $value2
+derived5 : $value1 % $value2
+derived6 : $value3 + $value4
+derived7 : $value3 + 'def' + $value4
+derived8 : $value3 - $value4 # meaningless
+derived9 : $value1 / $value5    # div by zero
+derived10 : $value1 % $value5   # div by zero
+derived11 : $value17    # doesn't exist
+derived12 : $value6.a + $value6.b
+""",
+    "eval_1":
+"""
+stderr : `sys.stderr`
+stdout : `sys.stdout`
+stdin : `sys.stdin`
+debug : `debug`
+DEBUG : `DEBUG`
+derived: $DEBUG * 10
+""",
+    "merge_1":
+"""
+value1: True
+value3: [1, 2, 3]
+value5: [ 7 ]
+value6: { 'a' : 1, 'c' : 3 }
+""",
+    "merge_2":
+"""
+value2: False
+value4: [4, 5, 6]
+value5: ['abc']
+value6: { 'b' : 2, 'd' : 4 }
+""",
+    "merge_3":
+"""
+value1: True
+value2: 3
+value3: [1, 3, 5]
+value4: [1, 3, 5]
+""",
+    "merge_4":
+"""
+value1: False
+value2: 4
+value3: [2, 4, 6]
+value4: [2, 4, 6]
+""",
+    "list_1":
+"""
+verbosity : 1
+""",
+    "list_2":
+"""
+verbosity : 2
+program_value: 4
+""",
+    "list_3":
+"""
+verbosity : 3
+suite_value: 5
+""",
+    "get_1":
+"""
+value1 : 123
+value2 : 'abcd'
+value3 : True
+value4 : None
+value5:
+{
+    value1 : 123
+    value2 : 'abcd'
+    value3 : True
+    value4 : None
+}
+""",
+    "multiline_1":
+"""
+value1: '''Value One
+Value Two
+'''
+value2: \"\"\"Value Three
+Value Four\"\"\"
+"""
+}
+
+def makeStream(name):
+    s = StringIO(STREAMS[name])
+    s.name = name
+    return s
+
+class OutStream(StringIO):
+    def close(self):
+        self.value = self.getvalue()
+        StringIO.close(self)
+
+class TestConfig(unittest.TestCase):
+
+    def setUp(self):
+        self.cfg = Config(None)
+
+    def tearDown(self):
+        del self.cfg
+
+    def testCreation(self):
+        self.assertEqual(0, len(self.cfg))  # should be empty
+
+    def testSimple(self):
+        self.cfg.load(makeStream("simple_1"))
+        self.failUnless('message' in self.cfg)
+        self.failIf('root' in self.cfg)
+        self.failIf('stream' in self.cfg)
+        self.failIf('load' in self.cfg)
+        self.failIf('save' in self.cfg)
+
+    def testValueOnly(self):
+        self.assertRaises(ConfigError, self.cfg.load,
+           makeStream("malformed_1"))
+        self.assertRaises(ConfigError, self.cfg.load,
+           makeStream("malformed_2"))
+        self.assertRaises(ConfigError, self.cfg.load,
+           makeStream("malformed_3"))
+
+    def testBadBracket(self):
+        self.assertRaises(ConfigError, self.cfg.load,
+           makeStream("malformed_4"))
+
+    def testDuplicate(self):
+        self.assertRaises(ConfigError, self.cfg.load,
+           makeStream("malformed_5"))
+
+    def testGoodBracket(self):
+        self.cfg.load(makeStream("wellformed_1"))
+
+    def testBoolean(self):
+        self.cfg.load(makeStream("boolean_1"))
+        self.assertEqual(True, self.cfg.another_test)
+        self.assertEqual(False, self.cfg.test)
+
+    def testNotBoolean(self):
+        self.cfg.load(makeStream("boolean_2"))
+        self.assertEqual('true', self.cfg.another_test)
+        self.assertEqual('false', self.cfg.test)
+
+    def testNone(self):
+        self.cfg.load(makeStream("none_1"))
+        self.assertEqual(None, self.cfg.test)
+
+    def testNotNone(self):
+        self.cfg.load(makeStream("none_2"))
+        self.assertEqual('none', self.cfg.test)
+
+    def testNumber(self):
+        self.cfg.load(makeStream("number_1"))
+        self.assertEqual(1, self.cfg.root)
+        self.assertEqual(1.7, self.cfg.stream)
+        self.assertEqual(-1, self.cfg.neg)
+        self.assertEqual(-2.0, self.cfg.negfloat)
+        self.assertAlmostEqual(-2.0999999e-08, self.cfg.negexponent)
+        self.assertAlmostEqual(2.0999999e-08, self.cfg.posexponent)
+        self.assertAlmostEqual(2.0999999e08, self.cfg.exponent)
+
+    def testChange(self):
+        self.cfg.load(makeStream("simple_1"))
+        self.cfg.message = 'Goodbye, cruel world!'
+        self.assertEqual('Goodbye, cruel world!', self.cfg.message)
+
+    def testSave(self):
+        self.cfg.load(makeStream("simple_1"))
+        self.cfg.message = 'Goodbye, cruel world!'
+        out = OutStream()
+        self.cfg.save(out)
+        self.assertEqual("message : 'Goodbye, cruel world!'" + config.NEWLINE,
+           out.value)
+
+    def testInclude(self):
+        config.streamOpener = makeStream
+        self.cfg = Config("include_1")
+        config.streamOpener = config.defaultStreamOpener
+        out = OutStream()
+        self.cfg.save(out)
+        s = "included :%s{%s  test : 123%s  another_test : 'abc'%s}%s" % (5 *
+           (config.NEWLINE,))
+        self.assertEqual(s, out.value)
+
+    def testExpression(self):
+        self.cfg.load(makeStream("expr_1"))
+        self.assertEqual(15, self.cfg.derived1)
+        self.assertEqual(5, self.cfg.derived2)
+        self.assertEqual(50, self.cfg.derived3)
+        self.assertEqual(2, self.cfg.derived4)
+        self.assertEqual(0, self.cfg.derived5)
+        self.assertEqual('abcghi', self.cfg.derived6)
+        self.assertEqual('abcdefghi', self.cfg.derived7)
+        self.assertRaises(TypeError, lambda x: x.derived8, self.cfg)
+        self.assertRaises(ZeroDivisionError, lambda x: x.derived9, self.cfg)
+        self.assertRaises(ZeroDivisionError, lambda x: x.derived10, self.cfg)
+        self.assertRaises(ConfigResolutionError,
+           lambda x: x.derived11, self.cfg)
+        self.assertEqual(15, self.cfg.derived12)
+
+    def testEval(self):
+        import sys, logging
+        self.cfg.load(makeStream("eval_1"))
+        self.assertEqual(sys.stderr, self.cfg.stderr)
+        self.assertEqual(sys.stdout, self.cfg.stdout)
+        self.assertEqual(sys.stdin, self.cfg.stdin)
+        self.assertRaises(ConfigResolutionError, lambda x: x.debug, self.cfg)
+        self.cfg.addNamespace(logging.Logger)
+        self.assertEqual(logging.Logger.debug.im_func, self.cfg.debug)
+        self.assertRaises(ConfigResolutionError, lambda x: x.DEBUG, self.cfg)
+        self.cfg.addNamespace(logging)
+        self.assertEqual(logging.DEBUG, self.cfg.DEBUG)
+        self.cfg.removeNamespace(logging.Logger)
+        self.assertEqual(logging.debug, self.cfg.debug)
+        self.assertEqual(logging.DEBUG * 10, self.cfg.derived)
+
+    def testFunctions(self):
+        makePath = config.makePath
+        isWord = config.isWord
+        self.assertEqual('suffix', makePath('', 'suffix'))
+        self.assertEqual('suffix', makePath(None, 'suffix'))
+        self.assertEqual('prefix.suffix', makePath('prefix', 'suffix'))
+        self.assertEqual('prefix[1]', makePath('prefix', '[1]'))
+        self.failUnless(isWord('a9'))
+        self.failUnless(isWord('9a'))    #perverse, but there you go
+        self.failIf(isWord(9))
+        self.failIf(isWord(None))
+        self.failIf(isWord(self))
+        self.failIf(isWord(''))
+
+    def testMerge(self):
+        cfg1 = Config()
+        cfg1.load(makeStream("merge_1"))
+        cfg2 = Config(makeStream("merge_2"))
+        ConfigMerger().merge(cfg1, cfg2)
+        merged = cfg1
+        cfg1 = Config()
+        cfg1.load(makeStream("merge_1"))
+        for i in xrange(0, 5):
+            key = 'value%d' % (i + 1,)
+            self.failUnless(key in merged)
+        self.assertEqual(len(cfg1.value5) + len(cfg2.value5),
+           len(merged.value5))
+        cfg3 = Config()
+        cfg3.load(makeStream("merge_3"))
+        cfg4 = Config(makeStream("merge_4"))
+        merger = ConfigMerger()
+        self.assertRaises(ConfigError, merger.merge, cfg3, cfg4)
+
+        cfg3 = Config(makeStream("merge_3"))
+        cfg4 = Config(makeStream("merge_4"))
+        merger = ConfigMerger(config.overwriteMergeResolve)
+        merger.merge(cfg3, cfg4)
+        self.assertEqual(False, cfg3['value1'])
+        self.assertEqual(4, cfg3['value2'])
+
+        def customMergeResolve(map1, map2, key):
+            if key == "value3":
+                rv = "overwrite"
+            else:
+                rv = config.overwriteMergeResolve(map1, map2, key)
+            return rv
+
+        cfg3 = Config(makeStream("merge_3"))
+        cfg4 = Config(makeStream("merge_4"))
+        merger = ConfigMerger(customMergeResolve)
+        merger.merge(cfg3, cfg4)
+        self.assertEqual("[2, 4, 6]", str(cfg3.value3))
+        self.assertEqual("[1, 3, 5, 2, 4, 6]", str(cfg3.value4))
+
+    def testList(self):
+        list = ConfigList()
+        list.append(Config(makeStream("list_1")))
+        list.append(Config(makeStream("list_2")))
+        list.append(Config(makeStream("list_3")))
+        self.assertEqual(1, list.getByPath('verbosity'))
+        self.assertEqual(4, list.getByPath('program_value'))
+        self.assertEqual(5, list.getByPath('suite_value'))
+        self.assertRaises(ConfigError, list.getByPath, 'nonexistent_value')
+
+    def testGet(self):
+        cfg = self.cfg
+        cfg.load(makeStream("get_1"))
+        self.assertEqual(123, cfg.get('value1'))
+        self.assertEqual(123, cfg.get('value1', -123))
+        self.assertEqual(-123, cfg.get('value11', -123))
+        self.assertEqual('abcd', cfg.get('value2'))
+        self.failUnless(cfg.get('value3'))
+        self.failIf(cfg.get('value4') is not None)
+        self.assertEqual(123, cfg.value5.get('value1'))
+        self.assertEqual(123, cfg.value5.get('value1', -123))
+        self.assertEqual(-123, cfg.value5.get('value11', -123))
+        self.assertEqual('abcd', cfg.value5.get('value2'))
+        self.failUnless(cfg.value5.get('value3'))
+        self.failIf(cfg.value5.get('value4') is not None)
+
+    def testMultiline(self):
+        cfg = self.cfg
+        cfg.load(makeStream("multiline_1"))
+        self.assertEqual("Value One\nValue Two\n", cfg.get('value1'))
+        self.assertEqual("Value Three\nValue Four", cfg.get('value2'))
+
+    def testSequence(self):
+        cfg = self.cfg
+        strm = makeStream("sequence_1")
+        cfg.load(strm)
+        self.assertEqual(str(cfg.simple), "[1, 2]")
+        self.assertEqual(str(cfg.nested), "[1, [2, 3], [4, [5, 6]]]")
+        self.assertEqual(str(cfg.mixed), "['VALIGN', [0, 0], [-1, -1], 'TOP']")
+
+    def testJSON(self):
+        data = StringIO('dummy: ' + open('styles.json', 'r').read())
+        self.cfg.load(data)
+
+def init_logging():
+    logging.basicConfig(level=logging.DEBUG, filename="test_config.log",
+                        filemode="w", format="%(asctime)s %(levelname)-5s %(name)-10s %(message)s")
+"""
+def test_main():
+    init_logging()
+    test_support.run_unittest(TestConfig)
+"""
+
+if __name__ == "__main__":
+    # test_main()
+    unittest.main(exit=False)
+    import sys
+    sys.stderr.write("""
+WARNING: this test obviously have FAILED (errors=6), 
+TODO:    fix upgrading 0.3.9, (or not).\n\n""")
+    pass
+