From 106136433e7030d116213037fee5e1811b4cc801 Mon Sep 17 00:00:00 2001 From: Christian Van Wambeke Date: Thu, 8 Feb 2018 16:23:10 +0100 Subject: [PATCH] add src/test/config_0_3_9 --- commands/config.py | 354 +++--- commands/config_old.py | 985 +++++++++++++++ commands/log.py | 71 +- sat | 1 + src/ElementTree.py | 9 - src/debug.py | 126 +- src/options.py | 21 +- src/pyconf.py | 4 +- src/salomeTools.py | 654 +++++----- src/salomeTools_old.py | 637 ++++++++++ src/test/APPLI_TEST/APPLI_TEST.pyconf | 45 + src/test/APPLI_TEST_Test.py | 56 + src/test/README_config_0_3_9.txt | 2 + src/test/__init__.py | 0 src/test/config_0_3_9/LICENSE | 16 + src/test/config_0_3_9/PKG-INFO | 16 + src/test/config_0_3_9/README.txt | 84 ++ src/test/config_0_3_9/__init__.py | 0 src/test/config_0_3_9/config.py | 1678 +++++++++++++++++++++++++ src/test/config_0_3_9/logconfig.cfg | 65 + src/test/config_0_3_9/logconfig.py | 138 ++ src/test/config_0_3_9/setup.py | 20 + src/test/config_0_3_9/styles.json | 554 ++++++++ src/test/config_0_3_9/test_config.py | 439 +++++++ src/test/debugTest.py | 127 ++ src/test/pyconfTest.py | 152 +++ src/test/satHelpTest.py | 2 + src/test/test_pyconf.py | 450 +++++++ 28 files changed, 6136 insertions(+), 570 deletions(-) create mode 100644 commands/config_old.py create mode 100755 src/salomeTools_old.py create mode 100644 src/test/APPLI_TEST/APPLI_TEST.pyconf create mode 100755 src/test/APPLI_TEST_Test.py create mode 100644 src/test/README_config_0_3_9.txt create mode 100755 src/test/__init__.py create mode 100644 src/test/config_0_3_9/LICENSE create mode 100644 src/test/config_0_3_9/PKG-INFO create mode 100644 src/test/config_0_3_9/README.txt create mode 100644 src/test/config_0_3_9/__init__.py create mode 100644 src/test/config_0_3_9/config.py create mode 100644 src/test/config_0_3_9/logconfig.cfg create mode 100644 src/test/config_0_3_9/logconfig.py create mode 100644 src/test/config_0_3_9/setup.py create mode 100644 src/test/config_0_3_9/styles.json create mode 100644 src/test/config_0_3_9/test_config.py create mode 100755 src/test/debugTest.py create mode 100755 src/test/pyconfTest.py create mode 100755 src/test/test_pyconf.py diff --git a/commands/config.py b/commands/config.py index f1ec2b8..14cfc66 100644 --- a/commands/config.py +++ b/commands/config.py @@ -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 -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 + 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 .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 .pyconf - # to ~/.salomeTools/Applications/LOCAL_.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 .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 .pyconf + # to ~/.salomeTools/Applications/LOCAL_.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 index 0000000..cb9f033 --- /dev/null +++ b/commands/config_old.py @@ -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 +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 .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 .pyconf + # to ~/.salomeTools/Applications/LOCAL_.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) + + diff --git a/commands/log.py b/commands/log.py index 01164bf..d3eb1ca 100644 --- a/commands/log.py +++ b/commands/log.py @@ -34,20 +34,24 @@ import src # Define all possible option for log command : sat log 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 ae3cb05..7e6b337 100755 --- 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)) diff --git a/src/ElementTree.py b/src/ElementTree.py index 34bd557..52b7755 100644 --- a/src/ElementTree.py +++ b/src/ElementTree.py @@ -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) ## diff --git a/src/debug.py b/src/debug.py index 33dda56..31b00d8 100644 --- a/src/debug.py +++ b/src/debug.py @@ -17,14 +17,20 @@ # 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)) diff --git a/src/options.py b/src/options.py index cce23d3..4844541 100644 --- a/src/options.py +++ b/src/options.py @@ -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 diff --git a/src/pyconf.py b/src/pyconf.py index de9af0c..ee01cc0 100644 --- a/src/pyconf.py +++ b/src/pyconf.py @@ -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 diff --git a/src/salomeTools.py b/src/salomeTools.py index 4036061..ba63841 100755 --- a/src/salomeTools.py +++ b/src/salomeTools.py @@ -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 -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 - # (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 /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 for salomeTools/sat command: 'sat ' + (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 " @@ -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 index 0000000..4036061 --- /dev/null +++ b/src/salomeTools_old.py @@ -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 +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 + # (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 /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 " + + :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] [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 \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 index 0000000..5fc08b3 --- /dev/null +++ b/src/test/APPLI_TEST/APPLI_TEST.pyconf @@ -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 index 0000000..e29e0a7 --- /dev/null +++ b/src/test/APPLI_TEST_Test.py @@ -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 index 0000000..4605ae9 --- /dev/null +++ b/src/test/README_config_0_3_9.txt @@ -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 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 index 0000000..98773c8 --- /dev/null +++ b/src/test/config_0_3_9/LICENSE @@ -0,0 +1,16 @@ +Copyright (C) 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. diff --git a/src/test/config_0_3_9/PKG-INFO b/src/test/config_0_3_9/PKG-INFO new file mode 100644 index 0000000..69ba705 --- /dev/null +++ b/src/test/config_0_3_9/PKG-INFO @@ -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 index 0000000..0c20eab --- /dev/null +++ b/src/test/config_0_3_9/README.txt @@ -0,0 +1,84 @@ +This module is intended to provide configuration functionality for Python +programs. + +Change History +-------------- + +Version Date Description +============================================================================= +0.3.9 11 May 2010 Fixed parsing bug which caused failure for numbers with + exponents. +----------------------------------------------------------------------------- +0.3.8 03 Mar 2010 Fixed parsing bug which caused failure for negative + numbers in sequences. Improved resolution logic. +----------------------------------------------------------------------------- +0.3.7 05 Oct 2007 Added Mapping.__delitem__ (patch by John Drummond). + Mapping.__getattribute__ no longer returns "" when + asked for "__class__" - doing so causes pickle to + crash (reported by Jamila Gunawardena). + Allow negative numbers (reported by Gary Schoep; had + already been fixed but not yet released). +----------------------------------------------------------------------------- +0.3.6 09 Mar 2006 Made classes derive from object (previously they were + old-style classes). + Changed ConfigMerger to use a more flexible merge + strategy. + Multiline strings (using """ or ''') are now supported. + A typo involving raising a ConfigError was fixed. + Patches received with thanks from David Janes & Tim + Desjardins (BlogMatrix) and Erick Tryzelaar. +----------------------------------------------------------------------------- +0.3.5 27 Dec 2004 Added ConfigOutputStream to provide better Unicode + output support. Altered save code to put platform- + dependent newlines for Unicode. +----------------------------------------------------------------------------- +0.3.4 11 Nov 2004 Added ConfigInputStream to provide better Unicode + support. + Added ConfigReader.setStream(). +----------------------------------------------------------------------------- +0.3.3 09 Nov 2004 Renamed config.get() to getByPath(), and likewise for + ConfigList. + Added Mapping.get() to work like dict.get(). + Added logconfig.py and logconfig.cfg to distribution. +----------------------------------------------------------------------------- +0.3.2 04 Nov 2004 Simplified parseMapping(). + Allowed Config.__init__ to accept a string as well as a + stream. If a string is passed in, streamOpener is used + to obtain the stream to be used. +----------------------------------------------------------------------------- +0.3.1 04 Nov 2004 Changed addNamespace/removeNamespace to make name + specification easier. + Refactored save(), added Container.writeToStream and + Container.writeValue() to help with this. +----------------------------------------------------------------------------- +0.3 03 Nov 2004 Added test harness (test_config.py) + Fixed bugs in bracket parsing. + Refactored internal classes. + Added merging functionality. +----------------------------------------------------------------------------- +0.2 01 Nov 2004 Added support for None. + Stream closed in load() and save(). + Added support for changing configuration. + Fixed bugs in identifier parsing and isword(). +----------------------------------------------------------------------------- +0.1 31 Oct 2004 Initial implementation (for community feedback) +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +COPYRIGHT +----------------------------------------------------------------------------- +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. diff --git a/src/test/config_0_3_9/__init__.py b/src/test/config_0_3_9/__init__.py new file mode 100644 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 index 0000000..94d04c0 --- /dev/null +++ b/src/test/config_0_3_9/config.py @@ -0,0 +1,1678 @@ +# 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. + +""" +This is a configuration module for Python. + +This module should work under Python versions >= 2.2, and cannot be used with +earlier versions since it uses new-style classes. + +Development and testing has only been carried out (so far) on Python 2.3.4 and +Python 2.4.2. See the test module (test_config.py) included in the +U{distribution} (follow the +download link). + +A simple example - with the example configuration file:: + + messages: + [ + { + stream : `sys.stderr` + message: 'Welcome' + name: 'Harry' + } + { + stream : `sys.stdout` + message: 'Welkom' + name: 'Ruud' + } + { + stream : $messages[0].stream + message: 'Bienvenue' + name: Yves + } + ] + +a program to read the configuration would be:: + + from config import Config + + f = file('simple.cfg') + cfg = Config(f) + for m in cfg.messages: + s = '%s, %s' % (m.message, m.name) + try: + print >> m.stream, s + except IOError, e: + print e + +which, when run, would yield the console output:: + + Welcome, Harry + Welkom, Ruud + Bienvenue, Yves + +See U{this tutorial} for more +information. + +@version: 0.3.9 + +@author: Vinay Sajip + +@copyright: Copyright (C) 2004-2010 Vinay Sajip. All Rights Reserved. + + +@var streamOpener: The default stream opener. This is a factory function which +takes a string (e.g. filename) and returns a stream suitable for reading. If +unable to open the stream, an IOError exception should be thrown. + +The default value of this variable is L{defaultStreamOpener}. For an example +of how it's used, see test_config.py (search for streamOpener). +""" + +__author__ = "Vinay Sajip " +__status__ = "alpha" +__version__ = "0.3.9" +__date__ = "11 May 2010" + +from types import StringType, UnicodeType + +import codecs +import logging +import os +import sys + +WORD = 'a' +NUMBER = '9' +STRING = '"' +EOF = '' +LCURLY = '{' +RCURLY = '}' +LBRACK = '[' +LBRACK2 = 'a[' +RBRACK = ']' +LPAREN = '(' +LPAREN2 = '((' +RPAREN = ')' +DOT = '.' +COMMA = ',' +COLON = ':' +AT = '@' +PLUS = '+' +MINUS = '-' +STAR = '*' +SLASH = '/' +MOD = '%' +BACKTICK = '`' +DOLLAR = '$' +TRUE = 'True' +FALSE = 'False' +NONE = 'None' + +WORDCHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_" + +if sys.platform == 'win32': + NEWLINE = '\r\n' +elif os.name == 'mac': + NEWLINE = '\r' +else: + NEWLINE = '\n' + +try: + import encodings.utf_32 + has_utf32 = True +except: + has_utf32 = False + +try: + from logging.handlers import NullHandler +except ImportError: + class NullHandler(logging.Handler): + def emit(self, record): + pass + +logger = logging.getLogger(__name__) +if not logger.handlers: + logger.addHandler(NullHandler()) + +class ConfigInputStream(object): + """ + An input stream which can read either ANSI files with default encoding + or Unicode files with BOMs. + + Handles UTF-8, UTF-16LE, UTF-16BE. Could handle UTF-32 if Python had + built-in support. + """ + def __init__(self, stream): + """ + Initialize an instance. + + @param stream: The underlying stream to be read. Should be seekable. + @type stream: A stream (file-like object). + """ + encoding = None + signature = stream.read(4) + used = -1 + if has_utf32: + if signature == codecs.BOM_UTF32_LE: + encoding = 'utf-32le' + elif signature == codecs.BOM_UTF32_BE: + encoding = 'utf-32be' + if encoding is None: + if signature[:3] == codecs.BOM_UTF8: + used = 3 + encoding = 'utf-8' + elif signature[:2] == codecs.BOM_UTF16_LE: + used = 2 + encoding = 'utf-16le' + elif signature[:2] == codecs.BOM_UTF16_BE: + used = 2 + encoding = 'utf-16be' + else: + used = 0 + if used >= 0: + stream.seek(used) + if encoding: + reader = codecs.getreader(encoding) + stream = reader(stream) + self.stream = stream + self.encoding = encoding + + def read(self, size): + if (size == 0) or (self.encoding is None): + rv = self.stream.read(size) + else: + rv = u'' + while size > 0: + rv += self.stream.read(1) + size -= 1 + return rv + + def close(self): + self.stream.close() + + def readline(self): + if self.encoding is None: + line = '' + else: + line = u'' + while True: + c = self.stream.read(1) + if c: + line += c + if c == '\n': + break + return line + +class ConfigOutputStream(object): + """ + An output stream which can write either ANSI files with default encoding + or Unicode files with BOMs. + + Handles UTF-8, UTF-16LE, UTF-16BE. Could handle UTF-32 if Python had + built-in support. + """ + + def __init__(self, stream, encoding=None): + """ + Initialize an instance. + + @param stream: The underlying stream to be written. + @type stream: A stream (file-like object). + @param encoding: The desired encoding. + @type encoding: str + """ + if encoding is not None: + encoding = str(encoding).lower() + self.encoding = encoding + if encoding == "utf-8": + stream.write(codecs.BOM_UTF8) + elif encoding == "utf-16be": + stream.write(codecs.BOM_UTF16_BE) + elif encoding == "utf-16le": + stream.write(codecs.BOM_UTF16_LE) + elif encoding == "utf-32be": + stream.write(codecs.BOM_UTF32_BE) + elif encoding == "utf-32le": + stream.write(codecs.BOM_UTF32_LE) + + if encoding is not None: + writer = codecs.getwriter(encoding) + stream = writer(stream) + self.stream = stream + + def write(self, data): + self.stream.write(data) + + def flush(self): + self.stream.flush() + + def close(self): + self.stream.close() + +def defaultStreamOpener(name): + """ + This function returns a read-only stream, given its name. The name passed + in should correspond to an existing stream, otherwise an exception will be + raised. + + This is the default value of L{streamOpener}; assign your own callable to + streamOpener to return streams based on names. For example, you could use + urllib2.urlopen(). + + @param name: The name of a stream, most commonly a file name. + @type name: str + @return: A stream with the specified name. + @rtype: A read-only stream (file-like object) + """ + return ConfigInputStream(file(name, 'rb')) + +streamOpener = None + +class ConfigError(Exception): + """ + This is the base class of exceptions raised by this module. + """ + pass + +class ConfigFormatError(ConfigError): + """ + This is the base class of exceptions raised due to syntax errors in + configurations. + """ + pass + +class ConfigResolutionError(ConfigError): + """ + This is the base class of exceptions raised due to semantic errors in + configurations. + """ + pass + +def isWord(s): + """ + See if a passed-in value is an identifier. If the value passed in is not a + string, False is returned. An identifier consists of alphanumerics or + underscore characters. + + Examples:: + + isWord('a word') ->False + isWord('award') -> True + isWord(9) -> False + isWord('a_b_c_') ->True + + @note: isWord('9abc') will return True - not exactly correct, but adequate + for the way it's used here. + + @param s: The name to be tested + @type s: any + @return: True if a word, else False + @rtype: bool + """ + if type(s) != type(''): + return False + s = s.replace('_', '') + return s.isalnum() + +def makePath(prefix, suffix): + """ + Make a path from a prefix and suffix. + + Examples:: + + makePath('', 'suffix') -> 'suffix' + makePath('prefix', 'suffix') -> 'prefix.suffix' + makePath('prefix', '[1]') -> 'prefix[1]' + + @param prefix: The prefix to use. If it evaluates as false, the suffix + is returned. + @type prefix: str + @param suffix: The suffix to use. It is either an identifier or an + index in brackets. + @type suffix: str + @return: The path concatenation of prefix and suffix, with a + dot if the suffix is not a bracketed index. + @rtype: str + + """ + if not prefix: + rv = suffix + elif suffix[0] == '[': + rv = prefix + suffix + else: + rv = prefix + '.' + suffix + return rv + + +class Container(object): + """ + This internal class is the base class for mappings and sequences. + + @ivar path: A string which describes how to get + to this instance from the root of the hierarchy. + + Example:: + + a.list.of[1].or['more'].elements + """ + def __init__(self, parent): + """ + Initialize an instance. + + @param parent: The parent of this instance in the hierarchy. + @type parent: A L{Container} instance. + """ + object.__setattr__(self, 'parent', parent) + + def setPath(self, path): + """ + Set the path for this instance. + @param path: The path - a string which describes how to get + to this instance from the root of the hierarchy. + @type path: str + """ + object.__setattr__(self, 'path', path) + + def evaluate(self, item): + """ + Evaluate items which are instances of L{Reference} or L{Expression}. + + L{Reference} instances are evaluated using L{Reference.resolve}, + and L{Expression} instances are evaluated using + L{Expression.evaluate}. + + @param item: The item to be evaluated. + @type item: any + @return: If the item is an instance of L{Reference} or L{Expression}, + the evaluated value is returned, otherwise the item is returned + unchanged. + """ + if isinstance(item, Reference): + item = item.resolve(self) + elif isinstance(item, Expression): + item = item.evaluate(self) + return item + + def writeToStream(self, stream, indent, container): + """ + Write this instance to a stream at the specified indentation level. + + Should be redefined in subclasses. + + @param stream: The stream to write to + @type stream: A writable stream (file-like object) + @param indent: The indentation level + @type indent: int + @param container: The container of this instance + @type container: L{Container} + @raise NotImplementedError: If a subclass does not override this + """ + raise NotImplementedError + + def writeValue(self, value, stream, indent): + if isinstance(self, Mapping): + indstr = ' ' + else: + indstr = indent * ' ' + if isinstance(value, Reference) or isinstance(value, Expression): + stream.write('%s%r%s' % (indstr, value, NEWLINE)) + else: + if (type(value) is StringType): # and not isWord(value): + value = repr(value) + stream.write('%s%s%s' % (indstr, value, NEWLINE)) + +class Mapping(Container): + """ + This internal class implements key-value mappings in configurations. + """ + + def __init__(self, parent=None): + """ + Initialize an instance. + + @param parent: The parent of this instance in the hierarchy. + @type parent: A L{Container} instance. + """ + Container.__init__(self, parent) + object.__setattr__(self, 'path', '') + object.__setattr__(self, 'data', {}) + object.__setattr__(self, 'order', []) # to preserve ordering + object.__setattr__(self, 'comments', {}) + + def __delitem__(self, key): + """ + Remove an item + """ + data = object.__getattribute__(self, 'data') + if key not in data: + raise AttributeError(key) + order = object.__getattribute__(self, 'order') + comments = object.__getattribute__(self, 'comments') + del data[key] + order.remove(key) + del comments[key] + + def __getitem__(self, key): + data = object.__getattribute__(self, 'data') + if key not in data: + raise AttributeError(key) + rv = data[key] + return self.evaluate(rv) + + __getattr__ = __getitem__ + + def __getattribute__(self, name): + if name == "__dict__": + return {} + if name in ["__methods__", "__members__"]: + return [] + #if name == "__class__": + # return '' + data = object.__getattribute__(self, "data") + useData = data.has_key(name) + if useData: + rv = getattr(data, name) + else: + rv = object.__getattribute__(self, name) + if rv is None: + raise AttributeError(name) + return rv + + def iteritems(self): + for key in self.keys(): + yield(key, self[key]) + raise StopIteration + + def __contains__(self, item): + order = object.__getattribute__(self, 'order') + return item in order + + def addMapping(self, key, value, comment, setting=False): + """ + Add a key-value mapping with a comment. + + @param key: The key for the mapping. + @type key: str + @param value: The value for the mapping. + @type value: any + @param comment: The comment for the key (can be None). + @type comment: str + @param setting: If True, ignore clashes. This is set + to true when called from L{__setattr__}. + @raise ConfigFormatError: If an existing key is seen + again and setting is False. + """ + data = object.__getattribute__(self, 'data') + order = object.__getattribute__(self, 'order') + comments = object.__getattribute__(self, 'comments') + + data[key] = value + if key not in order: + order.append(key) + elif not setting: + raise ConfigFormatError("repeated key: %s" % key) + comments[key] = comment + + def __setattr__(self, name, value): + self.addMapping(name, value, None, True) + + __setitem__ = __setattr__ + + def keys(self): + """ + Return the keys in a similar way to a dictionary. + """ + return object.__getattribute__(self, 'order') + + def get(self, key, default=None): + """ + Allows a dictionary-style get operation. + """ + if key in self: + return self[key] + return default + + def __str__(self): + return str(object.__getattribute__(self, 'data')) + + def __repr__(self): + return repr(object.__getattribute__(self, 'data')) + + def __len__(self): + return len(object.__getattribute__(self, 'order')) + + def __iter__(self): + return self.iterkeys() + + def iterkeys(self): + order = object.__getattribute__(self, 'order') + return order.__iter__() + + def writeToStream(self, stream, indent, container): + """ + Write this instance to a stream at the specified indentation level. + + Should be redefined in subclasses. + + @param stream: The stream to write to + @type stream: A writable stream (file-like object) + @param indent: The indentation level + @type indent: int + @param container: The container of this instance + @type container: L{Container} + """ + indstr = indent * ' ' + if len(self) == 0: + stream.write(' { }%s' % NEWLINE) + else: + if isinstance(container, Mapping): + stream.write(NEWLINE) + stream.write('%s{%s' % (indstr, NEWLINE)) + self.save(stream, indent + 1) + stream.write('%s}%s' % (indstr, NEWLINE)) + + def save(self, stream, indent=0): + """ + Save this configuration to the specified stream. + @param stream: A stream to which the configuration is written. + @type stream: A write-only stream (file-like object). + @param indent: The indentation level for the output. + @type indent: int + """ + indstr = indent * ' ' + order = object.__getattribute__(self, 'order') + data = object.__getattribute__(self, 'data') + maxlen = 0 # max(map(lambda x: len(x), order)) + for key in order: + comment = self.comments[key] + if isWord(key): + skey = key + else: + skey = repr(key) + if comment: + stream.write('%s#%s' % (indstr, comment)) + stream.write('%s%-*s :' % (indstr, maxlen, skey)) + value = data[key] + if isinstance(value, Container): + value.writeToStream(stream, indent, self) + else: + self.writeValue(value, stream, indent) + +class Config(Mapping): + """ + This class represents a configuration, and is the only one which clients + need to interface to, under normal circumstances. + """ + + class Namespace(object): + """ + This internal class is used for implementing default namespaces. + + An instance acts as a namespace. + """ + def __init__(self): + self.sys = sys + self.os = os + + def __repr__(self): + return "" % ','.join(self.__dict__.keys()) + + def __init__(self, streamOrFile=None, parent=None): + """ + Initializes an instance. + + @param streamOrFile: If specified, causes this instance to be loaded + from the stream (by calling L{load}). If a string is provided, it is + passed to L{streamOpener} to open a stream. Otherwise, the passed + value is assumed to be a stream and used as is. + @type streamOrFile: A readable stream (file-like object) or a name. + @param parent: If specified, this becomes the parent of this instance + in the configuration hierarchy. + @type parent: a L{Container} instance. + """ + Mapping.__init__(self, parent) + object.__setattr__(self, 'reader', ConfigReader(self)) + object.__setattr__(self, 'namespaces', [Config.Namespace()]) + object.__setattr__(self, 'resolving', set()) + if streamOrFile is not None: + if isinstance(streamOrFile, StringType) or isinstance(streamOrFile, UnicodeType): + global streamOpener + if streamOpener is None: + streamOpener = defaultStreamOpener + streamOrFile = streamOpener(streamOrFile) + load = object.__getattribute__(self, "load") + load(streamOrFile) + + def load(self, stream): + """ + Load the configuration from the specified stream. Multiple streams can + be used to populate the same instance, as long as there are no + clashing keys. The stream is closed. + @param stream: A stream from which the configuration is read. + @type stream: A read-only stream (file-like object). + @raise ConfigError: if keys in the loaded configuration clash with + existing keys. + @raise ConfigFormatError: if there is a syntax error in the stream. + """ + reader = object.__getattribute__(self, 'reader') + #object.__setattr__(self, 'root', reader.load(stream)) + reader.load(stream) + stream.close() + + def addNamespace(self, ns, name=None): + """ + Add a namespace to this configuration which can be used to evaluate + (resolve) dotted-identifier expressions. + @param ns: The namespace to be added. + @type ns: A module or other namespace suitable for passing as an + argument to vars(). + @param name: A name for the namespace, which, if specified, provides + an additional level of indirection. + @type name: str + """ + namespaces = object.__getattribute__(self, 'namespaces') + if name is None: + namespaces.append(ns) + else: + setattr(namespaces[0], name, ns) + + def removeNamespace(self, ns, name=None): + """ + Remove a namespace added with L{addNamespace}. + @param ns: The namespace to be removed. + @param name: The name which was specified when L{addNamespace} was + called. + @type name: str + """ + namespaces = object.__getattribute__(self, 'namespaces') + if name is None: + namespaces.remove(ns) + else: + delattr(namespaces[0], name) + + def save(self, stream, indent=0): + """ + Save this configuration to the specified stream. The stream is + closed if this is the top-level configuration in the hierarchy. + L{Mapping.save} is called to do all the work. + @param stream: A stream to which the configuration is written. + @type stream: A write-only stream (file-like object). + @param indent: The indentation level for the output. + @type indent: int + """ + Mapping.save(self, stream, indent) + if indent == 0: + stream.close() + + def getByPath(self, path): + """ + Obtain a value in the configuration via its path. + @param path: The path of the required value + @type path: str + @return the value at the specified path. + @rtype: any + @raise ConfigError: If the path is invalid + """ + s = 'self.' + path + try: + return eval(s) + except Exception, e: + raise ConfigError(str(e)) + +class Sequence(Container): + """ + This internal class implements a value which is a sequence of other values. + """ + class SeqIter(object): + """ + This internal class implements an iterator for a L{Sequence} instance. + """ + def __init__(self, seq): + self.seq = seq + self.limit = len(object.__getattribute__(seq, 'data')) + self.index = 0 + + def __iter__(self): + return self + + def next(self): + if self.index >= self.limit: + raise StopIteration + rv = self.seq[self.index] + self.index += 1 + return rv + + def __init__(self, parent=None): + """ + Initialize an instance. + + @param parent: The parent of this instance in the hierarchy. + @type parent: A L{Container} instance. + """ + Container.__init__(self, parent) + object.__setattr__(self, 'data', []) + object.__setattr__(self, 'comments', []) + + def append(self, item, comment): + """ + Add an item to the sequence. + + @param item: The item to add. + @type item: any + @param comment: A comment for the item. + @type comment: str + """ + data = object.__getattribute__(self, 'data') + comments = object.__getattribute__(self, 'comments') + data.append(item) + comments.append(comment) + + def __getitem__(self, index): + data = object.__getattribute__(self, 'data') + try: + rv = data[index] + except (IndexError, KeyError, TypeError): + raise ConfigResolutionError('%r is not a valid index for %r' % (index, object.__getattribute__(self, 'path'))) + if not isinstance(rv, list): + rv = self.evaluate(rv) + else: + # deal with a slice + result = [] + for a in rv: + result.append(self.evaluate(a)) + rv = result + return rv + + def __iter__(self): + return Sequence.SeqIter(self) + + def __repr__(self): + return repr(object.__getattribute__(self, 'data')) + + def __str__(self): + return str(self[:]) # using the slice evaluates the contents + + def __len__(self): + return len(object.__getattribute__(self, 'data')) + + def writeToStream(self, stream, indent, container): + """ + Write this instance to a stream at the specified indentation level. + + Should be redefined in subclasses. + + @param stream: The stream to write to + @type stream: A writable stream (file-like object) + @param indent: The indentation level + @type indent: int + @param container: The container of this instance + @type container: L{Container} + """ + indstr = indent * ' ' + if len(self) == 0: + stream.write(' [ ]%s' % NEWLINE) + else: + if isinstance(container, Mapping): + stream.write(NEWLINE) + stream.write('%s[%s' % (indstr, NEWLINE)) + self.save(stream, indent + 1) + stream.write('%s]%s' % (indstr, NEWLINE)) + + def save(self, stream, indent): + """ + Save this instance to the specified stream. + @param stream: A stream to which the configuration is written. + @type stream: A write-only stream (file-like object). + @param indent: The indentation level for the output, > 0 + @type indent: int + """ + if indent == 0: + raise ConfigError("sequence cannot be saved as a top-level item") + data = object.__getattribute__(self, 'data') + comments = object.__getattribute__(self, 'comments') + indstr = indent * ' ' + for i in xrange(0, len(data)): + value = data[i] + comment = comments[i] + if comment: + stream.write('%s#%s' % (indstr, comment)) + if isinstance(value, Container): + value.writeToStream(stream, indent, self) + else: + self.writeValue(value, stream, indent) + +class Reference(object): + """ + This internal class implements a value which is a reference to another value. + """ + def __init__(self, config, type, ident): + """ + Initialize an instance. + + @param config: The configuration which contains this reference. + @type config: A L{Config} instance. + @param type: The type of reference. + @type type: BACKTICK or DOLLAR + @param ident: The identifier which starts the reference. + @type ident: str + """ + self.config = config + self.type = type + self.elements = [ident] + + def addElement(self, type, ident): + """ + Add an element to the reference. + + @param type: The type of reference. + @type type: BACKTICK or DOLLAR + @param ident: The identifier which continues the reference. + @type ident: str + """ + self.elements.append((type, ident)) + + def findConfig(self, container): + """ + Find the closest enclosing configuration to the specified container. + + @param container: The container to start from. + @type container: L{Container} + @return: The closest enclosing configuration, or None. + @rtype: L{Config} + """ + while (container is not None) and not isinstance(container, Config): + container = object.__getattribute__(container, 'parent') + return container + + def resolve(self, container): + """ + Resolve this instance in the context of a container. + + @param container: The container to resolve from. + @type container: L{Container} + @return: The resolved value. + @rtype: any + @raise ConfigResolutionError: If resolution fails. + """ + rv = None + path = object.__getattribute__(container, 'path') + current = self.findConfig(container) + while current is not None: + if self.type == BACKTICK: + namespaces = object.__getattribute__(current, 'namespaces') + found = False + s = str(self)[1:-1] + for ns in namespaces: + try: + try: + rv = eval(s, vars(ns)) + except TypeError: #Python 2.7 - vars is a dictproxy + rv = eval(s, {}, vars(ns)) + found = True + break + except: + logger.debug("unable to resolve %r in %r", s, ns) + pass + if found: + break + else: + firstkey = self.elements[0] + if firstkey in current.resolving: + current.resolving.remove(firstkey) + raise ConfigResolutionError("Circular reference: %r" % firstkey) + current.resolving.add(firstkey) + key = firstkey + try: + rv = current[key] + for item in self.elements[1:]: + key = item[1] + rv = rv[key] + current.resolving.remove(firstkey) + break + except ConfigResolutionError: + raise + except: + logger.debug("Unable to resolve %r: %s", key, sys.exc_info()[1]) + rv = None + pass + current.resolving.discard(firstkey) + current = self.findConfig(object.__getattribute__(current, 'parent')) + if current is None: + raise ConfigResolutionError("unable to evaluate %r in the configuration %s" % (self, path)) + return rv + + def __str__(self): + s = self.elements[0] + for tt, tv in self.elements[1:]: + if tt == DOT: + s += '.%s' % tv + else: + s += '[%r]' % tv + if self.type == BACKTICK: + return BACKTICK + s + BACKTICK + else: + return DOLLAR + s + + def __repr__(self): + return self.__str__() + +class Expression(object): + """ + This internal class implements a value which is obtained by evaluating an expression. + """ + def __init__(self, op, lhs, rhs): + """ + Initialize an instance. + + @param op: the operation expressed in the expression. + @type op: PLUS, MINUS, STAR, SLASH, MOD + @param lhs: the left-hand-side operand of the expression. + @type lhs: any Expression or primary value. + @param rhs: the right-hand-side operand of the expression. + @type rhs: any Expression or primary value. + """ + self.op = op + self.lhs = lhs + self.rhs = rhs + + def __str__(self): + return '%r %s %r' % (self.lhs, self.op, self.rhs) + + def __repr__(self): + return self.__str__() + + def evaluate(self, container): + """ + Evaluate this instance in the context of a container. + + @param container: The container to evaluate in from. + @type container: L{Container} + @return: The evaluated value. + @rtype: any + @raise ConfigResolutionError: If evaluation fails. + @raise ZeroDivideError: If division by zero occurs. + @raise TypeError: If the operation is invalid, e.g. + subtracting one string from another. + """ + lhs = self.lhs + if isinstance(lhs, Reference): + lhs = lhs.resolve(container) + elif isinstance(lhs, Expression): + lhs = lhs.evaluate(container) + rhs = self.rhs + if isinstance(rhs, Reference): + rhs = rhs.resolve(container) + elif isinstance(rhs, Expression): + rhs = rhs.evaluate(container) + op = self.op + if op == PLUS: + rv = lhs + rhs + elif op == MINUS: + rv = lhs - rhs + elif op == STAR: + rv = lhs * rhs + elif op == SLASH: + rv = lhs / rhs + else: + rv = lhs % rhs + return rv + +class ConfigReader(object): + """ + This internal class implements a parser for configurations. + """ + + def __init__(self, config): + self.filename = None + self.config = config + self.lineno = 0 + self.colno = 0 + self.lastc = None + self.last_token = None + self.commentchars = '#' + self.whitespace = ' \t\r\n' + self.quotes = '\'"' + self.punct = ':-+*/%,.{}[]()@`$' + self.digits = '0123456789' + self.wordchars = '%s' % WORDCHARS # make a copy + self.identchars = self.wordchars + self.digits + self.pbchars = [] + self.pbtokens = [] + self.comment = None + + def location(self): + """ + Return the current location (filename, line, column) in the stream + as a string. + + Used when printing error messages, + + @return: A string representing a location in the stream being read. + @rtype: str + """ + return "%s(%d,%d)" % (self.filename, self.lineno, self.colno) + + def getChar(self): + """ + Get the next char from the stream. Update line and column numbers + appropriately. + + @return: The next character from the stream. + @rtype: str + """ + if self.pbchars: + c = self.pbchars.pop() + else: + c = self.stream.read(1) + self.colno += 1 + if c == '\n': + self.lineno += 1 + self.colno = 1 + return c + + def __repr__(self): + return "" % id(self) + + __str__ = __repr__ + + def getToken(self): + """ + Get a token from the stream. String values are returned in a form + where you need to eval() the returned value to get the actual + string. The return value is (token_type, token_value). + + Multiline string tokenizing is thanks to David Janes (BlogMatrix) + + @return: The next token. + @rtype: A token tuple. + """ + if self.pbtokens: + return self.pbtokens.pop() + stream = self.stream + self.comment = None + token = '' + tt = EOF + while True: + c = self.getChar() + if not c: + break + elif c == '#': + self.comment = stream.readline() + self.lineno += 1 + continue + if c in self.quotes: + token = c + quote = c + tt = STRING + escaped = False + multiline = False + c1 = self.getChar() + if c1 == quote: + c2 = self.getChar() + if c2 == quote: + multiline = True + token += quote + token += quote + else: + self.pbchars.append(c2) + self.pbchars.append(c1) + else: + self.pbchars.append(c1) + while True: + c = self.getChar() + if not c: + break + token += c + if (c == quote) and not escaped: + if not multiline or (len(token) >= 6 and token.endswith(token[:3]) and token[-4] != '\\'): + break + if c == '\\': + escaped = not escaped + else: + escaped = False + if not c: + raise ConfigFormatError('%s: Unterminated quoted string: %r, %r' % (self.location(), token, c)) + break + if c in self.whitespace: + self.lastc = c + continue + elif c in self.punct: + token = c + tt = c + if (self.lastc == ']') or (self.lastc in self.identchars): + if c == '[': + tt = LBRACK2 + elif c == '(': + tt = LPAREN2 + break + elif c in self.digits: + token = c + tt = NUMBER + in_exponent=False + while True: + c = self.getChar() + if not c: + break + if c in self.digits: + token += c + elif (c == '.') and token.find('.') < 0 and not in_exponent: + token += c + elif (c == '-') and token.find('-') < 0 and in_exponent: + token += c + elif (c in 'eE') and token.find('e') < 0 and\ + token.find('E') < 0: + token += c + in_exponent = True + else: + if c and (c not in self.whitespace): + self.pbchars.append(c) + break + break + elif c in self.wordchars: + token = c + tt = WORD + c = self.getChar() + while c and (c in self.identchars): + token += c + c = self.getChar() + if c: # and c not in self.whitespace: + self.pbchars.append(c) + if token == "True": + tt = TRUE + elif token == "False": + tt = FALSE + elif token == "None": + tt = NONE + break + else: + raise ConfigFormatError('%s: Unexpected character: %r' % (self.location(), c)) + if token: + self.lastc = token[-1] + else: + self.lastc = None + self.last_token = tt + return (tt, token) + + def load(self, stream, parent=None, suffix=None): + """ + Load the configuration from the specified stream. + + @param stream: A stream from which to load the configuration. + @type stream: A stream (file-like object). + @param parent: The parent of the configuration (to which this reader + belongs) in the hierarchy. Specified when the configuration is + included in another one. + @type parent: A L{Container} instance. + @param suffix: The suffix of this configuration in the parent + configuration. Should be specified whenever the parent is not None. + @raise ConfigError: If parent is specified but suffix is not. + @raise ConfigFormatError: If there are syntax errors in the stream. + """ + if parent is not None: + if suffix is None: + raise ConfigError("internal error: load called with parent but no suffix") + self.config.setPath(makePath(object.__getattribute__(parent, 'path'), suffix)) + self.setStream(stream) + self.token = self.getToken() + self.parseMappingBody(self.config) + if self.token[0] != EOF: + raise ConfigFormatError('%s: expecting EOF, found %r' % (self.location(), self.token[1])) + + def setStream(self, stream): + """ + Set the stream to the specified value, and prepare to read from it. + + @param stream: A stream from which to load the configuration. + @type stream: A stream (file-like object). + """ + self.stream = stream + if hasattr(stream, 'name'): + filename = stream.name + else: + filename = '?' + self.filename = filename + self.lineno = 1 + self.colno = 1 + + def match(self, t): + """ + Ensure that the current token type matches the specified value, and + advance to the next token. + + @param t: The token type to match. + @type t: A valid token type. + @return: The token which was last read from the stream before this + function is called. + @rtype: a token tuple - see L{getToken}. + @raise ConfigFormatError: If the token does not match what's expected. + """ + if self.token[0] != t: + raise ConfigFormatError("%s: expecting %s, found %r" % (self.location(), t, self.token[1])) + rv = self.token + self.token = self.getToken() + return rv + + def parseMappingBody(self, parent): + """ + Parse the internals of a mapping, and add entries to the provided + L{Mapping}. + + @param parent: The mapping to add entries to. + @type parent: A L{Mapping} instance. + """ + while self.token[0] in [WORD, STRING]: + self.parseKeyValuePair(parent) + + def parseKeyValuePair(self, parent): + """ + Parse a key-value pair, and add it to the provided L{Mapping}. + + @param parent: The mapping to add entries to. + @type parent: A L{Mapping} instance. + @raise ConfigFormatError: if a syntax error is found. + """ + comment = self.comment + tt, tv = self.token + if tt == WORD: + key = tv + suffix = tv + elif tt == STRING: + key = eval(tv) + suffix = '[%s]' % tv + else: + msg = "%s: expecting word or string, found %r" + raise ConfigFormatError(msg % (self.location(), tv)) + self.token = self.getToken() + # for now, we allow key on its own as a short form of key : True + if self.token[0] == COLON: + self.token = self.getToken() + value = self.parseValue(parent, suffix) + else: + value = True + try: + parent.addMapping(key, value, comment) + except Exception, e: + raise ConfigFormatError("%s: %s, %r" % (self.location(), e, + self.token[1])) + tt = self.token[0] + if tt not in [EOF, WORD, STRING, RCURLY, COMMA]: + msg = "%s: expecting one of EOF, WORD, STRING,\ +RCURLY, COMMA, found %r" + raise ConfigFormatError(msg % (self.location(), self.token[1])) + if tt == COMMA: + self.token = self.getToken() + + def parseValue(self, parent, suffix): + """ + Parse a value. + + @param parent: The container to which the value will be added. + @type parent: A L{Container} instance. + @param suffix: The suffix for the value. + @type suffix: str + @return: The value + @rtype: any + @raise ConfigFormatError: if a syntax error is found. + """ + tt = self.token[0] + if tt in [STRING, WORD, NUMBER, LPAREN, DOLLAR, + TRUE, FALSE, NONE, BACKTICK, MINUS]: + rv = self.parseScalar() + elif tt == LBRACK: + rv = self.parseSequence(parent, suffix) + elif tt in [LCURLY, AT]: + rv = self.parseMapping(parent, suffix) + else: + raise ConfigFormatError("%s: unexpected input: %r" % + (self.location(), self.token[1])) + return rv + + def parseSequence(self, parent, suffix): + """ + Parse a sequence. + + @param parent: The container to which the sequence will be added. + @type parent: A L{Container} instance. + @param suffix: The suffix for the value. + @type suffix: str + @return: a L{Sequence} instance representing the sequence. + @rtype: L{Sequence} + @raise ConfigFormatError: if a syntax error is found. + """ + rv = Sequence(parent) + rv.setPath(makePath(object.__getattribute__(parent, 'path'), suffix)) + self.match(LBRACK) + comment = self.comment + tt = self.token[0] + while tt in [STRING, WORD, NUMBER, LCURLY, LBRACK, LPAREN, DOLLAR, + TRUE, FALSE, NONE, BACKTICK, MINUS]: + suffix = '[%d]' % len(rv) + value = self.parseValue(parent, suffix) + rv.append(value, comment) + tt = self.token[0] + comment = self.comment + if tt == COMMA: + self.match(COMMA) + tt = self.token[0] + comment = self.comment + continue + self.match(RBRACK) + return rv + + def parseMapping(self, parent, suffix): + """ + Parse a mapping. + + @param parent: The container to which the mapping will be added. + @type parent: A L{Container} instance. + @param suffix: The suffix for the value. + @type suffix: str + @return: a L{Mapping} instance representing the mapping. + @rtype: L{Mapping} + @raise ConfigFormatError: if a syntax error is found. + """ + if self.token[0] == LCURLY: + self.match(LCURLY) + rv = Mapping(parent) + rv.setPath( + makePath(object.__getattribute__(parent, 'path'), suffix)) + self.parseMappingBody(rv) + self.match(RCURLY) + else: + self.match(AT) + tt, fn = self.match(STRING) + rv = Config(eval(fn), parent) + return rv + + def parseScalar(self): + """ + Parse a scalar - a terminal value such as a string or number, or + an L{Expression} or L{Reference}. + + @return: the parsed scalar + @rtype: any scalar + @raise ConfigFormatError: if a syntax error is found. + """ + lhs = self.parseTerm() + tt = self.token[0] + while tt in [PLUS, MINUS]: + self.match(tt) + rhs = self.parseTerm() + lhs = Expression(tt, lhs, rhs) + tt = self.token[0] + return lhs + + def parseTerm(self): + """ + Parse a term in an additive expression (a + b, a - b) + + @return: the parsed term + @rtype: any scalar + @raise ConfigFormatError: if a syntax error is found. + """ + lhs = self.parseFactor() + tt = self.token[0] + while tt in [STAR, SLASH, MOD]: + self.match(tt) + rhs = self.parseFactor() + lhs = Expression(tt, lhs, rhs) + tt = self.token[0] + return lhs + + def parseFactor(self): + """ + Parse a factor in an multiplicative expression (a * b, a / b, a % b) + + @return: the parsed factor + @rtype: any scalar + @raise ConfigFormatError: if a syntax error is found. + """ + tt = self.token[0] + if tt in [NUMBER, WORD, STRING, TRUE, FALSE, NONE]: + rv = self.token[1] + if tt != WORD: + rv = eval(rv) + self.match(tt) + elif tt == LPAREN: + self.match(LPAREN) + rv = self.parseScalar() + self.match(RPAREN) + elif tt == DOLLAR: + self.match(DOLLAR) + rv = self.parseReference(DOLLAR) + elif tt == BACKTICK: + self.match(BACKTICK) + rv = self.parseReference(BACKTICK) + self.match(BACKTICK) + elif tt == MINUS: + self.match(MINUS) + rv = -self.parseScalar() + else: + raise ConfigFormatError("%s: unexpected input: %r" % + (self.location(), self.token[1])) + return rv + + def parseReference(self, type): + """ + Parse a reference. + + @return: the parsed reference + @rtype: L{Reference} + @raise ConfigFormatError: if a syntax error is found. + """ + word = self.match(WORD) + rv = Reference(self.config, type, word[1]) + while self.token[0] in [DOT, LBRACK2]: + self.parseSuffix(rv) + return rv + + def parseSuffix(self, ref): + """ + Parse a reference suffix. + + @param ref: The reference of which this suffix is a part. + @type ref: L{Reference}. + @raise ConfigFormatError: if a syntax error is found. + """ + tt = self.token[0] + if tt == DOT: + self.match(DOT) + word = self.match(WORD) + ref.addElement(DOT, word[1]) + else: + self.match(LBRACK2) + tt, tv = self.token + if tt not in [NUMBER, STRING]: + raise ConfigFormatError("%s: expected number or string, found %r" % (self.location(), tv)) + self.token = self.getToken() + tv = eval(tv) + self.match(RBRACK) + ref.addElement(LBRACK, tv) + +def defaultMergeResolve(map1, map2, key): + """ + A default resolver for merge conflicts. Returns a string + indicating what action to take to resolve the conflict. + + @param map1: The map being merged into. + @type map1: L{Mapping}. + @param map2: The map being used as the merge operand. + @type map2: L{Mapping}. + @param key: The key in map2 (which also exists in map1). + @type key: str + @return: One of "merge", "append", "mismatch" or "overwrite" + indicating what action should be taken. This should + be appropriate to the objects being merged - e.g. + there is no point returning "merge" if the two objects + are instances of L{Sequence}. + @rtype: str + """ + obj1 = map1[key] + obj2 = map2[key] + if isinstance(obj1, Mapping) and isinstance(obj2, Mapping): + rv = "merge" + elif isinstance(obj1, Sequence) and isinstance(obj2, Sequence): + rv = "append" + else: + rv = "mismatch" + return rv + +def overwriteMergeResolve(map1, map2, key): + """ + An overwriting resolver for merge conflicts. Calls L{defaultMergeResolve}, + but where a "mismatch" is detected, returns "overwrite" instead. + + @param map1: The map being merged into. + @type map1: L{Mapping}. + @param map2: The map being used as the merge operand. + @type map2: L{Mapping}. + @param key: The key in map2 (which also exists in map1). + @type key: str + """ + rv = defaultMergeResolve(map1, map2, key) + if rv == "mismatch": + rv = "overwrite" + return rv + +class ConfigMerger(object): + """ + This class is used for merging two configurations. If a key exists in the + merge operand but not the merge target, then the entry is copied from the + merge operand to the merge target. If a key exists in both configurations, + then a resolver (a callable) is called to decide how to handle the + conflict. + """ + + def __init__(self, resolver=defaultMergeResolve): + """ + Initialise an instance. + + @param resolver: + @type resolver: A callable which takes the argument list + (map1, map2, key) where map1 is the mapping being merged into, + map2 is the merge operand and key is the clashing key. The callable + should return a string indicating how the conflict should be resolved. + For possible return values, see L{defaultMergeResolve}. The default + value preserves the old behaviour + """ + self.resolver = resolver + + def merge(self, merged, mergee): + """ + Merge two configurations. The second configuration is unchanged, + and the first is changed to reflect the results of the merge. + + @param merged: The configuration to merge into. + @type merged: L{Config}. + @param mergee: The configuration to merge. + @type mergee: L{Config}. + """ + self.mergeMapping(merged, mergee) + + def mergeMapping(self, map1, map2): + """ + Merge two mappings recursively. The second mapping is unchanged, + and the first is changed to reflect the results of the merge. + + @param map1: The mapping to merge into. + @type map1: L{Mapping}. + @param map2: The mapping to merge. + @type map2: L{Mapping}. + """ + keys = map1.keys() + for key in map2.keys(): + if key not in keys: + map1[key] = map2[key] + else: + obj1 = map1[key] + obj2 = map2[key] + decision = self.resolver(map1, map2, key) + if decision == "merge": + self.mergeMapping(obj1, obj2) + elif decision == "append": + self.mergeSequence(obj1, obj2) + elif decision == "overwrite": + map1[key] = obj2 + elif decision == "mismatch": + self.handleMismatch(obj1, obj2) + else: + msg = "unable to merge: don't know how to implement %r" + raise ValueError(msg % decision) + + def mergeSequence(self, seq1, seq2): + """ + Merge two sequences. The second sequence is unchanged, + and the first is changed to have the elements of the second + appended to it. + + @param seq1: The sequence to merge into. + @type seq1: L{Sequence}. + @param seq2: The sequence to merge. + @type seq2: L{Sequence}. + """ + data1 = object.__getattribute__(seq1, 'data') + data2 = object.__getattribute__(seq2, 'data') + for obj in data2: + data1.append(obj) + comment1 = object.__getattribute__(seq1, 'comments') + comment2 = object.__getattribute__(seq2, 'comments') + for obj in comment2: + comment1.append(obj) + + def handleMismatch(self, obj1, obj2): + """ + Handle a mismatch between two objects. + + @param obj1: The object to merge into. + @type obj1: any + @param obj2: The object to merge. + @type obj2: any + """ + raise ConfigError("unable to merge %r with %r" % (obj1, obj2)) + +class ConfigList(list): + """ + This class implements an ordered list of configurations and allows you + to try getting the configuration from each entry in turn, returning + the first successfully obtained value. + """ + + def getByPath(self, path): + """ + Obtain a value from the first configuration in the list which defines + it. + + @param path: The path of the value to retrieve. + @type path: str + @return: The value from the earliest configuration in the list which + defines it. + @rtype: any + @raise ConfigError: If no configuration in the list has an entry with + the specified path. + """ + found = False + rv = None + for entry in self: + try: + rv = entry.getByPath(path) + found = True + break + except ConfigError: + pass + if not found: + raise ConfigError("unable to resolve %r" % path) + return rv diff --git a/src/test/config_0_3_9/logconfig.cfg b/src/test/config_0_3_9/logconfig.cfg new file mode 100644 index 0000000..2845e3c --- /dev/null +++ b/src/test/config_0_3_9/logconfig.cfg @@ -0,0 +1,65 @@ +# Configuration file for logconfig.py + +# root logger configuration +root: +{ + level : `DEBUG` + handlers : [$handlers.console, $handlers.file] +} +formatters: { + brief: + { + format: '%(levelname)-8s: %(name)s: %(message)s' + } + precise: + { + format: '%(asctime)s %(name)-15s %(levelname)-8s %(message)s' + } +} +handlers: +{ + console: + { + class : `logconfig.StreamHandler` + config: + { + level : `INFO` + stream : `sys.stdout` + formatter: $formatters.brief + } + } + file: + { + class : `logconfig.RotatingFileHandler` + config: + { + name: 'logconfig.log' + maxBytes: 1024 + backupCount: 3 + formatter: $formatters.precise + } + } + debugfile: + { + class : `logconfig.FileHandler` + config: + { + name: 'logconfig-detail.log' + mode: 'a' + formatter: $formatters.precise + } + } +} +loggers: +{ + area1: + { + level : `ERROR` + handlers: [$handlers.debugfile] + } + area2: + { + level : `CRITICAL` + handlers: [$handlers.debugfile] + } +} \ 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 index 0000000..44ae1ed --- /dev/null +++ b/src/test/config_0_3_9/logconfig.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python +# +# Copyright 2001-2004 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. +# +# This file is part of the Python config distribution. See +# http://www.red-dove.com/python_config.html +# +""" +A test for the config module through seeing how to use it to configure logging. + +Copyright (C) 2004 Vinay Sajip. All Rights Reserved. +""" + +from config import Config +from optparse import OptionParser, get_prog_name +from random import choice +import logging +import logging.handlers +import sys + +class Usage(Exception): + pass + +class BaseHandler: + def __init__(self, config): + if 'level' in config: + self.setLevel(config.level) + if 'formatter' in config: + self.setFormatter(config.formatter) + +class StreamHandler(logging.StreamHandler, BaseHandler): + def __init__(self, config): + stream = config.get('stream') + logging.StreamHandler.__init__(self, stream) + BaseHandler.__init__(self, config) + +class RotatingFileHandler(logging.handlers.RotatingFileHandler, BaseHandler): + def __init__(self, config): + name = config.get('name') + if name is None: + raise ValueError('RotatingFileHandler: name not specified') + mode = config.get('mode', 'a') + maxBytes = config.get('maxBytes', 0) + backupCount = config.get('backupCount', 0) + logging.handlers.RotatingFileHandler.__init__(self, name, mode, maxBytes, backupCount) + BaseHandler.__init__(self, config) + +class FileHandler(logging.FileHandler, BaseHandler): + def __init__(self, config): + name = config.get('name') + if name is None: + raise ValueError('FileHandler: name not specified') + mode = config.get('mode', 'a') + logging.FileHandler.__init__(self, name, mode) + BaseHandler.__init__(self, config) + +def configLogger(logger, config): + for handler in logger.handlers: + logger.removeHandler(handler) + if 'level' in config: + logger.setLevel(config.level) + if 'handlers' in config: + for handler in config.handlers: + logger.addHandler(handler) + +def fileConfig(fname, *args, **kwargs): + cfg = Config(fname) + cfg.addNamespace(logging) + cfg.addNamespace(sys.modules[StreamHandler.__module__], 'logconfig') + + for name in cfg.formatters.keys(): + formatterConfig = cfg.formatters[name] + fmt = formatterConfig.get('format') + datefmt = formatterConfig.get('datefmt') + formatter = logging.Formatter(fmt, datefmt) + cfg.formatters[name] = formatter + + for name in cfg.handlers.keys(): + klass = cfg.handlers[name].get('class') + config = cfg.handlers[name].get('config') + cfg.handlers[name] = klass(config) + + for name in cfg.loggers.keys(): + loggerConfig = cfg.loggers[name] + logger = logging.getLogger(name) + configLogger(logger, loggerConfig) + + if 'root' in cfg: + configLogger(logging.getLogger(''), cfg.root) + +def testConfig(): + levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL] + loggers = ['', 'area1', 'area2'] + for i in xrange(1000): + logger = logging.getLogger(choice(loggers)) + level = choice(levels) + logger.log(level, "Message number %d", i) + +def main(args=None): + rv = 0 + if args is None: + args = sys.argv[1:] + parser = OptionParser(usage="usage: %prog [options] CONFIG-FILE") + + (options, args) = parser.parse_args(args) + try: + if len(args) == 0: + raise Usage("No configuration file specified") + fileConfig(args[0]) + testConfig() + except Usage, e: + parser.print_help() + print "\n%s: error: %s" % (get_prog_name(), e) + rv = 1 + except Exception, e: + print "\n%s: error: %s" % (get_prog_name(), e) + typ, val, tb = sys.exc_info() + import traceback + traceback.print_tb(tb) + rv = 2 + return rv + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/test/config_0_3_9/setup.py b/src/test/config_0_3_9/setup.py new file mode 100644 index 0000000..9c7dfab --- /dev/null +++ b/src/test/config_0_3_9/setup.py @@ -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 index 0000000..259697e --- /dev/null +++ b/src/test/config_0_3_9/styles.json @@ -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 index 0000000..393be86 --- /dev/null +++ b/src/test/config_0_3_9/test_config.py @@ -0,0 +1,439 @@ +# 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. +""" + +import unittest +# import test_support +import config +from config import Config, ConfigMerger, ConfigList +from config 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__": + unittest.main(exit=False) + pass + # test_main() diff --git a/src/test/debugTest.py b/src/test/debugTest.py new file mode 100755 index 0000000..bc84f53 --- /dev/null +++ b/src/test/debugTest.py @@ -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 index 0000000..2adaeb8 --- /dev/null +++ b/src/test/pyconfTest.py @@ -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 diff --git a/src/test/satHelpTest.py b/src/test/satHelpTest.py index 6eea8bd..b62de2a 100755 --- a/src/test/satHelpTest.py +++ b/src/test/satHelpTest.py @@ -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 index 0000000..7f884de --- /dev/null +++ b/src/test/test_pyconf.py @@ -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 + -- 2.39.2