import sys
import src
+import src.debug as DBG
+from src.salomeTools import _BaseCommand
-# Define all possible option for config command : sat config <options>
-parser = src.options.Options()
-parser.add_option('v', 'value', 'string', 'value',
- _("Optional: print the value of CONFIG_VARIABLE."))
-parser.add_option('e', 'edit', 'boolean', 'edit',
- _("Optional: edit the product configuration file."))
-parser.add_option('i', 'info', 'string', 'info',
- _("Optional: get information on a product."))
-parser.add_option('l', 'list', 'boolean', 'list',
- _("Optional: list all available applications."))
-parser.add_option('', 'show_patchs', 'boolean', 'show_patchs',
- _("Optional: synthetic view of all patches used in the application"))
-parser.add_option('c', 'copy', 'boolean', 'copy',
- _("""Optional: copy a config file (.pyconf) to the personal config files directory.
-\tWarning: the included files are not copied.
-\tIf a name is given the new config file takes the given name."""))
-parser.add_option('n', 'no_label', 'boolean', 'no_label',
- _("Internal use: do not print labels, Works only with --value and --list."))
-parser.add_option('', 'completion', 'boolean', 'completion',
- _("Internal use: print only keys, works only with --value."))
-parser.add_option('s', 'schema', 'boolean', 'schema',
- _("Internal use."))
class ConfigOpener:
'''Class that helps to find an application pyconf
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.
for v in sorted(vals):
sys.stdout.write("%s\n" % v)
-def description():
+
+########################################################################
+# Command class for command 'sat config etc.'
+########################################################################
+class Command(_BaseCommand):
+
+ def getParser(self):
+ # Define all possible option for config command : sat config <options>
+ parser = src.options.Options()
+ parser.add_option('v', 'value', 'string', 'value',
+ _("Optional: print the value of CONFIG_VARIABLE."))
+ parser.add_option('d', 'debug', 'string', 'debug',
+ _("Optional: print the debugging value of CONFIG_VARIABLE."))
+ parser.add_option('e', 'edit', 'boolean', 'edit',
+ _("Optional: edit the product configuration file."))
+ parser.add_option('i', 'info', 'string', 'info',
+ _("Optional: get information on a product."))
+ parser.add_option('l', 'list', 'boolean', 'list',
+ _("Optional: list all available applications."))
+ parser.add_option('', 'show_patchs', 'boolean', 'show_patchs',
+ _("Optional: synthetic view of all patches used in the application"))
+ parser.add_option('c', 'copy', 'boolean', 'copy',
+ _("""Optional: copy a config file (.pyconf) to the personal config files directory.
+ \tWarning: the included files are not copied.
+ \tIf a name is given the new config file takes the given name."""))
+ parser.add_option('n', 'no_label', 'boolean', 'no_label',
+ _("Internal use: do not print labels, Works only with --value and --list."))
+ parser.add_option('', 'completion', 'boolean', 'completion',
+ _("Internal use: print only keys, works only with --value."))
+ parser.add_option('s', 'schema', 'boolean', 'schema',
+ _("Internal use."))
+ return parser
+
+ def description(self):
'''method that is called when salomeTools is called with --help option.
:return: The text to display for the config command description.
example:
>> sat config SALOME-master --info ParaView""")
-
-
-def run(args, runner, logger):
- '''method that is called when salomeTools is called with config parameter.
- '''
- # Parse the options
- (options, args) = parser.parse_args(args)
-
- # Only useful for completion mechanism : print the keys of the config
- if options.schema:
- get_config_children(runner.cfg, args)
- return
-
- # case : print a value of the config
- if options.value:
- if options.value == ".":
- # if argument is ".", print all the config
- for val in sorted(runner.cfg.keys()):
- print_value(runner.cfg, val, not options.no_label, logger)
- else:
- print_value(runner.cfg, options.value, not options.no_label, logger,
- level=0, show_full_path=False)
-
- # case : edit user pyconf file or application file
- elif options.edit:
- editor = runner.cfg.USER.editor
- if ('APPLICATION' not in runner.cfg and
- 'open_application' not in runner.cfg): # edit user pyconf
- usercfg = os.path.join(runner.cfg.VARS.personalDir, 'SAT.pyconf')
- logger.write(_("Opening %s\n") % usercfg, 3)
- src.system.show_in_editor(editor, usercfg, logger)
- else:
- # search for file <application>.pyconf and open it
- for path in runner.cfg.PATHS.APPLICATIONPATH:
- pyconf_path = os.path.join(path, runner.cfg.VARS.application + ".pyconf")
- if os.path.exists(pyconf_path):
- logger.write(_("Opening %s\n") % pyconf_path, 3)
- src.system.show_in_editor(editor, pyconf_path, logger)
- break
-
- # case : give information about the product in parameter
- elif options.info:
- src.check_config_has_application(runner.cfg)
- if options.info in runner.cfg.APPLICATION.products:
- show_product_info(runner.cfg, options.info, logger)
- return
- raise src.SatException(
- _("%(product_name)s is not a product of %(application_name)s.") % \
- {'product_name' : options.info, 'application_name' : runner.cfg.VARS.application} )
-
- # case : copy an existing <application>.pyconf
- # to ~/.salomeTools/Applications/LOCAL_<application>.pyconf
- elif options.copy:
- # product is required
- src.check_config_has_application( runner.cfg )
-
- # get application file path
- source = runner.cfg.VARS.application + '.pyconf'
- source_full_path = ""
- for path in runner.cfg.PATHS.APPLICATIONPATH:
- # ignore personal directory
- if path == runner.cfg.VARS.personalDir:
- continue
- # loop on all directories that can have pyconf applications
- zz = os.path.join(path, source)
- if os.path.exists(zz):
- source_full_path = zz
- break
+
- if len(source_full_path) == 0:
- raise src.SatException(
- _("Config file for product %s not found\n") % source )
- else:
- if len(args) > 0:
- # a name is given as parameter, use it
- dest = args[0]
- elif 'copy_prefix' in runner.cfg.INTERNAL.config:
- # use prefix
- dest = (runner.cfg.INTERNAL.config.copy_prefix
- + runner.cfg.VARS.application)
- else:
- # use same name as source
- dest = runner.cfg.VARS.application
-
- # the full path
- dest_file = os.path.join(
- runner.cfg.VARS.personalDir, 'Applications', dest + '.pyconf' )
- if os.path.exists(dest_file):
- raise src.SatException(
- _("A personal application '%s' already exists") % dest )
+ def run(self, args):
+ '''method that is called when salomeTools is called with config parameter.
+ '''
+ runner = self.getRunner()
+ logger = self.getLogger()
+ parser = self.getParser()
+ config = self.getConfig()
+ # Parse the options
+ (options, args) = parser.parse_args(args)
+
+ # Only useful for completion mechanism : print the keys of the config
+ if options.schema:
+ get_config_children(config, args)
+ return
+
+ # case : print a value of the config
+ if options.value:
+ if options.value == ".":
+ # if argument is ".", print all the config
+ for val in sorted(config.keys()):
+ print_value(config, val, not options.no_label, logger)
+ else:
+ print_value(config, options.value, not options.no_label, logger,
+ level=0, show_full_path=False)
+
+ if options.debug:
+ print_debug(config, str(options.debug), not options.no_label, logger,
+ level=0, show_full_path=False)
+
+ # case : edit user pyconf file or application file
+ elif options.edit:
+ editor = config.USER.editor
+ if ('APPLICATION' not in config and
+ 'open_application' not in config): # edit user pyconf
+ usercfg = os.path.join(config.VARS.personalDir, 'SAT.pyconf')
+ logger.write(_("Opening %s\n") % usercfg, 3)
+ src.system.show_in_editor(editor, usercfg, logger)
+ else:
+ # search for file <application>.pyconf and open it
+ for path in config.PATHS.APPLICATIONPATH:
+ pyconf_path = os.path.join(path, config.VARS.application + ".pyconf")
+ if os.path.exists(pyconf_path):
+ logger.write(_("Opening %s\n") % pyconf_path, 3)
+ src.system.show_in_editor(editor, pyconf_path, logger)
+ break
+
+ # case : give information about the product in parameter
+ elif options.info:
+ src.check_config_has_application(config)
+ if options.info in config.APPLICATION.products:
+ show_product_info(config, options.info, logger)
+ return
+ raise src.SatException(
+ _("%(product_name)s is not a product of %(application_name)s.") % \
+ {'product_name' : options.info, 'application_name' : config.VARS.application} )
+
+ # case : copy an existing <application>.pyconf
+ # to ~/.salomeTools/Applications/LOCAL_<application>.pyconf
+ elif options.copy:
+ # product is required
+ src.check_config_has_application( config )
+
+ # get application file path
+ source = config.VARS.application + '.pyconf'
+ source_full_path = ""
+ for path in config.PATHS.APPLICATIONPATH:
+ # ignore personal directory
+ if path == config.VARS.personalDir:
+ continue
+ # loop on all directories that can have pyconf applications
+ zz = os.path.join(path, source)
+ if os.path.exists(zz):
+ source_full_path = zz
+ break
+
+ if len(source_full_path) == 0:
+ raise src.SatException(
+ _("Config file for product %s not found\n") % source )
+ else:
+ if len(args) > 0:
+ # a name is given as parameter, use it
+ dest = args[0]
+ elif 'copy_prefix' in config.INTERNAL.config:
+ # use prefix
+ dest = (config.INTERNAL.config.copy_prefix
+ + config.VARS.application)
+ else:
+ # use same name as source
+ dest = config.VARS.application
+
+ # the full path
+ dest_file = os.path.join(
+ config.VARS.personalDir, 'Applications', dest + '.pyconf' )
+ if os.path.exists(dest_file):
+ raise src.SatException(
+ _("A personal application '%s' already exists") % dest )
+
+ # perform the copy
+ shutil.copyfile(source_full_path, dest_file)
+ logger.write(_("%s has been created.\n") % dest_file)
+
+ # case : display all the available pyconf applications
+ elif options.list:
+ lproduct = list()
+ # search in all directories that can have pyconf applications
+ for path in config.PATHS.APPLICATIONPATH:
+ # print a header
+ if not options.no_label:
+ logger.write("------ %s\n" % src.printcolors.printcHeader(path))
+
+ if not os.path.exists(path):
+ logger.write(src.printcolors.printcError(
+ _("Directory not found")) + "\n" )
+ else:
+ for f in sorted(os.listdir(path)):
+ # ignore file that does not ends with .pyconf
+ if not f.endswith('.pyconf'):
+ continue
+
+ appliname = f[:-len('.pyconf')]
+ if appliname not in lproduct:
+ lproduct.append(appliname)
+ if path.startswith(config.VARS.personalDir) \
+ and not options.no_label:
+ logger.write("%s*\n" % appliname)
+ else:
+ logger.write("%s\n" % appliname)
+
+ logger.write("\n")
+ # case : give a synthetic view of all patches used in the application
+ elif options.show_patchs:
+ src.check_config_has_application(config)
+ # Print some informations
+ logger.write(_('Show the patchs of application %s\n') % \
+ src.printcolors.printcLabel(config.VARS.application), 3)
+ logger.write("\n", 2, False)
+ show_patchs(config, logger)
+
+ # case: print all the products name of the application (internal use for completion)
+ elif options.completion:
+ for product_name in config.APPLICATION.products.keys():
+ logger.write("%s\n" % product_name)
- # perform the copy
- shutil.copyfile(source_full_path, dest_file)
- logger.write(_("%s has been created.\n") % dest_file)
-
- # case : display all the available pyconf applications
- elif options.list:
- lproduct = list()
- # search in all directories that can have pyconf applications
- for path in runner.cfg.PATHS.APPLICATIONPATH:
- # print a header
- if not options.no_label:
- logger.write("------ %s\n" % src.printcolors.printcHeader(path))
-
- if not os.path.exists(path):
- logger.write(src.printcolors.printcError(
- _("Directory not found")) + "\n" )
- else:
- for f in sorted(os.listdir(path)):
- # ignore file that does not ends with .pyconf
- if not f.endswith('.pyconf'):
- continue
-
- appliname = f[:-len('.pyconf')]
- if appliname not in lproduct:
- lproduct.append(appliname)
- if path.startswith(runner.cfg.VARS.personalDir) \
- and not options.no_label:
- logger.write("%s*\n" % appliname)
- else:
- logger.write("%s\n" % appliname)
-
- logger.write("\n")
- # case : give a synthetic view of all patches used in the application
- elif options.show_patchs:
- src.check_config_has_application(runner.cfg)
- # Print some informations
- logger.write(_('Show the patchs of application %s\n') % \
- src.printcolors.printcLabel(runner.cfg.VARS.application), 3)
- logger.write("\n", 2, False)
- show_patchs(runner.cfg, logger)
-
- # case: print all the products name of the application (internal use for completion)
- elif options.completion:
- for product_name in runner.cfg.APPLICATION.products.keys():
- logger.write("%s\n" % product_name)
-
--- /dev/null
+#!/usr/bin/env python
+#-*- coding:utf-8 -*-
+# Copyright (C) 2010-2012 CEA/DEN
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import os
+import platform
+import datetime
+import shutil
+import sys
+
+import src
+import src.debug as DBG
+
+# Define all possible option for config command : sat config <options>
+parser = src.options.Options()
+parser.add_option('v', 'value', 'string', 'value',
+ _("Optional: print the value of CONFIG_VARIABLE."))
+parser.add_option('d', 'debug', 'string', 'debug',
+ _("Optional: print the debugging value of CONFIG_VARIABLE."))
+parser.add_option('e', 'edit', 'boolean', 'edit',
+ _("Optional: edit the product configuration file."))
+parser.add_option('i', 'info', 'string', 'info',
+ _("Optional: get information on a product."))
+parser.add_option('l', 'list', 'boolean', 'list',
+ _("Optional: list all available applications."))
+parser.add_option('', 'show_patchs', 'boolean', 'show_patchs',
+ _("Optional: synthetic view of all patches used in the application"))
+parser.add_option('c', 'copy', 'boolean', 'copy',
+ _("""Optional: copy a config file (.pyconf) to the personal config files directory.
+\tWarning: the included files are not copied.
+\tIf a name is given the new config file takes the given name."""))
+parser.add_option('n', 'no_label', 'boolean', 'no_label',
+ _("Internal use: do not print labels, Works only with --value and --list."))
+parser.add_option('', 'completion', 'boolean', 'completion',
+ _("Internal use: print only keys, works only with --value."))
+parser.add_option('s', 'schema', 'boolean', 'schema',
+ _("Internal use."))
+
+class ConfigOpener:
+ '''Class that helps to find an application pyconf
+ in all the possible directories (pathList)
+ '''
+ def __init__(self, pathList):
+ '''Initialization
+
+ :param pathList list: The list of paths where to search a pyconf.
+ '''
+ self.pathList = pathList
+
+ def __call__(self, name):
+ if os.path.isabs(name):
+ return src.pyconf.ConfigInputStream(open(name, 'rb'))
+ else:
+ return src.pyconf.ConfigInputStream(
+ open(os.path.join( self.get_path(name), name ), 'rb') )
+ raise IOError(_("Configuration file '%s' not found") % name)
+
+ def get_path( self, name ):
+ '''The method that returns the entire path of the pyconf searched
+ :param name str: The name of the searched pyconf.
+ '''
+ for path in self.pathList:
+ if os.path.exists(os.path.join(path, name)):
+ return path
+ raise IOError(_("Configuration file '%s' not found") % name)
+
+class ConfigManager:
+ '''Class that manages the read of all the configuration files of salomeTools
+ '''
+ def __init__(self, datadir=None):
+ pass
+
+ def _create_vars(self, application=None, command=None, datadir=None):
+ '''Create a dictionary that stores all information about machine,
+ user, date, repositories, etc...
+
+ :param application str: The application for which salomeTools is called.
+ :param command str: The command that is called.
+ :param datadir str: The repository that contain external data
+ for salomeTools.
+ :return: The dictionary that stores all information.
+ :rtype: dict
+ '''
+ var = {}
+ var['user'] = src.architecture.get_user()
+ var['salometoolsway'] = os.path.dirname(
+ os.path.dirname(os.path.abspath(__file__)))
+ var['srcDir'] = os.path.join(var['salometoolsway'], 'src')
+ var['internal_dir'] = os.path.join(var['srcDir'], 'internal_config')
+ var['sep']= os.path.sep
+
+ # datadir has a default location
+ var['datadir'] = os.path.join(var['salometoolsway'], 'data')
+ if datadir is not None:
+ var['datadir'] = datadir
+
+ var['personalDir'] = os.path.join(os.path.expanduser('~'),
+ '.salomeTools')
+ src.ensure_path_exists(var['personalDir'])
+
+ var['personal_applications_dir'] = os.path.join(var['personalDir'],
+ "Applications")
+ src.ensure_path_exists(var['personal_applications_dir'])
+
+ var['personal_products_dir'] = os.path.join(var['personalDir'],
+ "products")
+ src.ensure_path_exists(var['personal_products_dir'])
+
+ var['personal_archives_dir'] = os.path.join(var['personalDir'],
+ "Archives")
+ src.ensure_path_exists(var['personal_archives_dir'])
+
+ var['personal_jobs_dir'] = os.path.join(var['personalDir'],
+ "Jobs")
+ src.ensure_path_exists(var['personal_jobs_dir'])
+
+ var['personal_machines_dir'] = os.path.join(var['personalDir'],
+ "Machines")
+ src.ensure_path_exists(var['personal_machines_dir'])
+
+ # read linux distributions dictionary
+ distrib_cfg = src.pyconf.Config(os.path.join(var['srcDir'],
+ 'internal_config',
+ 'distrib.pyconf'))
+
+ # set platform parameters
+ dist_name = src.architecture.get_distribution(
+ codes=distrib_cfg.DISTRIBUTIONS)
+ dist_version = src.architecture.get_distrib_version(dist_name,
+ codes=distrib_cfg.VERSIONS)
+ dist = dist_name + dist_version
+
+ var['dist_name'] = dist_name
+ var['dist_version'] = dist_version
+ var['dist'] = dist
+ var['python'] = src.architecture.get_python_version()
+
+ var['nb_proc'] = src.architecture.get_nb_proc()
+ node_name = platform.node()
+ var['node'] = node_name
+ var['hostname'] = node_name
+
+ # set date parameters
+ dt = datetime.datetime.now()
+ var['date'] = dt.strftime('%Y%m%d')
+ var['datehour'] = dt.strftime('%Y%m%d_%H%M%S')
+ var['hour'] = dt.strftime('%H%M%S')
+
+ var['command'] = str(command)
+ var['application'] = str(application)
+
+ # Root dir for temporary files
+ var['tmp_root'] = os.sep + 'tmp' + os.sep + var['user']
+ # particular win case
+ if src.architecture.is_windows() :
+ var['tmp_root'] = os.path.expanduser('~') + os.sep + 'tmp'
+
+ return var
+
+ def get_command_line_overrides(self, options, sections):
+ '''get all the overwrites that are in the command line
+
+ :param options: the options from salomeTools class
+ initialization (like -l5 or --overwrite)
+ :param sections str: The config section to overwrite.
+ :return: The list of all the overwrites to apply.
+ :rtype: list
+ '''
+ # when there are no options or not the overwrite option,
+ # return an empty list
+ if options is None or options.overwrite is None:
+ return []
+
+ over = []
+ for section in sections:
+ # only overwrite the sections that correspond to the option
+ over.extend(filter(lambda l: l.startswith(section + "."),
+ options.overwrite))
+ return over
+
+ def get_config(self, application=None, options=None, command=None,
+ datadir=None):
+ '''get the config from all the configuration files.
+
+ :param application str: The application for which salomeTools is called.
+ :param options class Options: The general salomeToos
+ options (--overwrite or -l5, for example)
+ :param command str: The command that is called.
+ :param datadir str: The repository that contain
+ external data for salomeTools.
+ :return: The final config.
+ :rtype: class 'src.pyconf.Config'
+ '''
+
+ # create a ConfigMerger to handle merge
+ merger = src.pyconf.ConfigMerger()#MergeHandler())
+
+ # create the configuration instance
+ cfg = src.pyconf.Config()
+
+ # =====================================================================
+ # create VARS section
+ var = self._create_vars(application=application, command=command,
+ datadir=datadir)
+ # add VARS to config
+ cfg.VARS = src.pyconf.Mapping(cfg)
+ for variable in var:
+ cfg.VARS[variable] = var[variable]
+
+ # apply overwrite from command line if needed
+ for rule in self.get_command_line_overrides(options, ["VARS"]):
+ exec('cfg.' + rule) # this cannot be factorized because of the exec
+
+ # =====================================================================
+ # Load INTERNAL config
+ # read src/internal_config/salomeTools.pyconf
+ src.pyconf.streamOpener = ConfigOpener([
+ os.path.join(cfg.VARS.srcDir, 'internal_config')])
+ try:
+ internal_cfg = src.pyconf.Config(open(os.path.join(cfg.VARS.srcDir,
+ 'internal_config', 'salomeTools.pyconf')))
+ except src.pyconf.ConfigError as e:
+ raise src.SatException(_("Error in configuration file:"
+ " salomeTools.pyconf\n %(error)s") % \
+ {'error': str(e) })
+
+ merger.merge(cfg, internal_cfg)
+
+ # apply overwrite from command line if needed
+ for rule in self.get_command_line_overrides(options, ["INTERNAL"]):
+ exec('cfg.' + rule) # this cannot be factorized because of the exec
+
+ # =====================================================================
+ # Load LOCAL config file
+ # search only in the data directory
+ src.pyconf.streamOpener = ConfigOpener([cfg.VARS.datadir])
+ try:
+ local_cfg = src.pyconf.Config(open(os.path.join(cfg.VARS.datadir,
+ 'local.pyconf')),
+ PWD = ('LOCAL', cfg.VARS.datadir) )
+ except src.pyconf.ConfigError as e:
+ raise src.SatException(_("Error in configuration file: "
+ "local.pyconf\n %(error)s") % \
+ {'error': str(e) })
+ except IOError as error:
+ e = str(error)
+ raise src.SatException( e );
+ merger.merge(cfg, local_cfg)
+
+ # When the key is "default", put the default value
+ if cfg.LOCAL.base == "default":
+ cfg.LOCAL.base = os.path.abspath(
+ os.path.join(cfg.VARS.salometoolsway,
+ "..",
+ "BASE"))
+ if cfg.LOCAL.workdir == "default":
+ cfg.LOCAL.workdir = os.path.abspath(
+ os.path.join(cfg.VARS.salometoolsway,
+ ".."))
+ if cfg.LOCAL.log_dir == "default":
+ cfg.LOCAL.log_dir = os.path.abspath(
+ os.path.join(cfg.VARS.salometoolsway,
+ "..",
+ "LOGS"))
+
+ if cfg.LOCAL.archive_dir == "default":
+ cfg.LOCAL.archive_dir = os.path.abspath(
+ os.path.join(cfg.VARS.salometoolsway,
+ "..",
+ "ARCHIVES"))
+
+ # apply overwrite from command line if needed
+ for rule in self.get_command_line_overrides(options, ["LOCAL"]):
+ exec('cfg.' + rule) # this cannot be factorized because of the exec
+
+ # =====================================================================
+ # Load the PROJECTS
+ projects_cfg = src.pyconf.Config()
+ projects_cfg.addMapping("PROJECTS",
+ src.pyconf.Mapping(projects_cfg),
+ "The projects\n")
+ projects_cfg.PROJECTS.addMapping("projects",
+ src.pyconf.Mapping(cfg.PROJECTS),
+ "The projects definition\n")
+
+ for project_pyconf_path in cfg.PROJECTS.project_file_paths:
+ if not os.path.exists(project_pyconf_path):
+ msg = _("WARNING: The project file %s cannot be found. "
+ "It will be ignored\n") % project_pyconf_path
+ sys.stdout.write(msg)
+ continue
+ project_name = os.path.basename(project_pyconf_path)[:-len(".pyconf")]
+ try:
+ project_pyconf_dir = os.path.dirname(project_pyconf_path)
+ project_cfg = src.pyconf.Config(open(project_pyconf_path),
+ PWD=("", project_pyconf_dir))
+ except Exception as e:
+ msg = _("ERROR: Error in configuration file: %(file_path)s\n %(error)s\n") % \
+ {'file_path' : project_pyconf_path, 'error': str(e) }
+ sys.stdout.write(msg)
+ continue
+ projects_cfg.PROJECTS.projects.addMapping(project_name,
+ src.pyconf.Mapping(projects_cfg.PROJECTS.projects),
+ "The %s project\n" % project_name)
+ projects_cfg.PROJECTS.projects[project_name]=project_cfg
+ projects_cfg.PROJECTS.projects[project_name]["file_path"] = \
+ project_pyconf_path
+
+ merger.merge(cfg, projects_cfg)
+
+ # apply overwrite from command line if needed
+ for rule in self.get_command_line_overrides(options, ["PROJECTS"]):
+ exec('cfg.' + rule) # this cannot be factorized because of the exec
+
+ # =====================================================================
+ # Create the paths where to search the application configurations,
+ # the product configurations, the products archives,
+ # the jobs configurations and the machines configurations
+ cfg.addMapping("PATHS", src.pyconf.Mapping(cfg), "The paths\n")
+ cfg.PATHS["APPLICATIONPATH"] = src.pyconf.Sequence(cfg.PATHS)
+ cfg.PATHS.APPLICATIONPATH.append(cfg.VARS.personal_applications_dir, "")
+
+ cfg.PATHS["PRODUCTPATH"] = src.pyconf.Sequence(cfg.PATHS)
+ cfg.PATHS.PRODUCTPATH.append(cfg.VARS.personal_products_dir, "")
+ cfg.PATHS["ARCHIVEPATH"] = src.pyconf.Sequence(cfg.PATHS)
+ cfg.PATHS.ARCHIVEPATH.append(cfg.VARS.personal_archives_dir, "")
+ cfg.PATHS["JOBPATH"] = src.pyconf.Sequence(cfg.PATHS)
+ cfg.PATHS.JOBPATH.append(cfg.VARS.personal_jobs_dir, "")
+ cfg.PATHS["MACHINEPATH"] = src.pyconf.Sequence(cfg.PATHS)
+ cfg.PATHS.MACHINEPATH.append(cfg.VARS.personal_machines_dir, "")
+
+ # initialise the path with local directory
+ cfg.PATHS["ARCHIVEPATH"].append(cfg.LOCAL.archive_dir, "")
+
+ # Loop over the projects in order to complete the PATHS variables
+ for project in cfg.PROJECTS.projects:
+ for PATH in ["APPLICATIONPATH",
+ "PRODUCTPATH",
+ "ARCHIVEPATH",
+ "JOBPATH",
+ "MACHINEPATH"]:
+ if PATH not in cfg.PROJECTS.projects[project]:
+ continue
+ cfg.PATHS[PATH].append(cfg.PROJECTS.projects[project][PATH], "")
+
+ # apply overwrite from command line if needed
+ for rule in self.get_command_line_overrides(options, ["PATHS"]):
+ exec('cfg.' + rule) # this cannot be factorized because of the exec
+
+ # =====================================================================
+ # Load APPLICATION config file
+ if application is not None:
+ # search APPLICATION file in all directories in configPath
+ cp = cfg.PATHS.APPLICATIONPATH
+ src.pyconf.streamOpener = ConfigOpener(cp)
+ do_merge = True
+ try:
+ application_cfg = src.pyconf.Config(application + '.pyconf')
+ except IOError as e:
+ raise src.SatException(_("%s, use 'config --list' to get the"
+ " list of available applications.") % e)
+ except src.pyconf.ConfigError as e:
+ if (not ('-e' in parser.parse_args()[1])
+ or ('--edit' in parser.parse_args()[1])
+ and command == 'config'):
+ raise src.SatException(
+ _("Error in configuration file: (1)s.pyconf\n %(2)s") % \
+ { 'application': application, 'error': str(e) } )
+ else:
+ sys.stdout.write(src.printcolors.printcWarning(
+ "There is an error in the file %s.pyconf.\n" % \
+ cfg.VARS.application))
+ do_merge = False
+ except Exception as e:
+ if ( not('-e' in parser.parse_args()[1]) or
+ ('--edit' in parser.parse_args()[1]) and
+ command == 'config' ):
+ sys.stdout.write(src.printcolors.printcWarning("%s\n" % str(e)))
+ raise src.SatException(
+ _("Error in configuration file: %s.pyconf\n") % application )
+ else:
+ sys.stdout.write(src.printcolors.printcWarning(
+ "ERROR: in file %s.pyconf. Opening the file with the default viewer\n" % \
+ cfg.VARS.application))
+ sys.stdout.write("\n%s\n" % src.printcolors.printcWarning(str(e)))
+ do_merge = False
+
+ else:
+ cfg['open_application'] = 'yes'
+
+ # =====================================================================
+ # Load product config files in PRODUCTS section
+ products_cfg = src.pyconf.Config()
+ products_cfg.addMapping("PRODUCTS",
+ src.pyconf.Mapping(products_cfg),
+ "The products\n")
+ if application is not None:
+ src.pyconf.streamOpener = ConfigOpener(cfg.PATHS.PRODUCTPATH)
+ for product_name in application_cfg.APPLICATION.products.keys():
+ # Loop on all files that are in softsDir directory
+ # and read their config
+ product_file_name = product_name + ".pyconf"
+ product_file_path = src.find_file_in_lpath(product_file_name, cfg.PATHS.PRODUCTPATH)
+ if product_file_path:
+ products_dir = os.path.dirname(product_file_path)
+ try:
+ prod_cfg = src.pyconf.Config(open(product_file_path),
+ PWD=("", products_dir))
+ prod_cfg.from_file = product_file_path
+ products_cfg.PRODUCTS[product_name] = prod_cfg
+ except Exception as e:
+ msg = _(
+ "WARNING: Error in configuration file: %(prod)s\n %(error)s" % \
+ {'prod' : product_name, 'error': str(e) })
+ sys.stdout.write(msg)
+
+ merger.merge(cfg, products_cfg)
+
+ # apply overwrite from command line if needed
+ for rule in self.get_command_line_overrides(options, ["PRODUCTS"]):
+ exec('cfg.' + rule) # this cannot be factorized because of the exec
+
+ if do_merge:
+ merger.merge(cfg, application_cfg)
+
+ # default launcher name ('salome')
+ if ('profile' in cfg.APPLICATION and
+ 'launcher_name' not in cfg.APPLICATION.profile):
+ cfg.APPLICATION.profile.launcher_name = 'salome'
+
+ # apply overwrite from command line if needed
+ for rule in self.get_command_line_overrides(options,
+ ["APPLICATION"]):
+ # this cannot be factorized because of the exec
+ exec('cfg.' + rule)
+
+ # =====================================================================
+ # load USER config
+ self.set_user_config_file(cfg)
+ user_cfg_file = self.get_user_config_file()
+ user_cfg = src.pyconf.Config(open(user_cfg_file))
+ merger.merge(cfg, user_cfg)
+
+ # apply overwrite from command line if needed
+ for rule in self.get_command_line_overrides(options, ["USER"]):
+ exec('cfg.' + rule) # this cannot be factorize because of the exec
+
+ return cfg
+
+ def set_user_config_file(self, config):
+ '''Set the user config file name and path.
+ If necessary, build it from another one or create it from scratch.
+
+ :param config class 'src.pyconf.Config': The global config
+ (containing all pyconf).
+ '''
+ # get the expected name and path of the file
+ self.config_file_name = 'SAT.pyconf'
+ self.user_config_file_path = os.path.join(config.VARS.personalDir,
+ self.config_file_name)
+
+ # if pyconf does not exist, create it from scratch
+ if not os.path.isfile(self.user_config_file_path):
+ self.create_config_file(config)
+
+ def create_config_file(self, config):
+ '''This method is called when there are no user config file.
+ It build it from scratch.
+
+ :param config class 'src.pyconf.Config': The global config.
+ :return: the config corresponding to the file created.
+ :rtype: config class 'src.pyconf.Config'
+ '''
+
+ cfg_name = self.get_user_config_file()
+
+ user_cfg = src.pyconf.Config()
+ #
+ user_cfg.addMapping('USER', src.pyconf.Mapping(user_cfg), "")
+
+ user_cfg.USER.addMapping('cvs_user', config.VARS.user,
+ "This is the user name used to access salome cvs base.\n")
+ user_cfg.USER.addMapping('svn_user', config.VARS.user,
+ "This is the user name used to access salome svn base.\n")
+ user_cfg.USER.addMapping('output_verbose_level', 3,
+ "This is the default output_verbose_level you want."
+ " 0=>no output, 5=>debug.\n")
+ user_cfg.USER.addMapping('publish_dir',
+ os.path.join(os.path.expanduser('~'),
+ 'websupport',
+ 'satreport'),
+ "")
+ user_cfg.USER.addMapping('editor',
+ 'vi',
+ "This is the editor used to "
+ "modify configuration files\n")
+ user_cfg.USER.addMapping('browser',
+ 'firefox',
+ "This is the browser used to "
+ "read html documentation\n")
+ user_cfg.USER.addMapping('pdf_viewer',
+ 'evince',
+ "This is the pdf_viewer used "
+ "to read pdf documentation\n")
+# CNC 25/10/17 : plus nécessaire a priori
+# user_cfg.USER.addMapping("base",
+# src.pyconf.Reference(
+# user_cfg,
+# src.pyconf.DOLLAR,
+# 'workdir + $VARS.sep + "BASE"'),
+# "The products installation base (could be "
+# "ignored if this key exists in the local.pyconf"
+# " file of salomTools).\n")
+
+ #
+ src.ensure_path_exists(config.VARS.personalDir)
+ src.ensure_path_exists(os.path.join(config.VARS.personalDir,
+ 'Applications'))
+
+ f = open(cfg_name, 'w')
+ user_cfg.__save__(f)
+ f.close()
+
+ return user_cfg
+
+ def get_user_config_file(self):
+ '''Get the user config file
+ :return: path to the user config file.
+ :rtype: str
+ '''
+ if not self.user_config_file_path:
+ raise src.SatException(
+ _("Error in get_user_config_file: missing user config file path") )
+ return self.user_config_file_path
+
+def check_path(path, ext=[]):
+ '''Construct a text with the input path and "not found" if it does not
+ exist.
+
+ :param path Str: the path to check.
+ :param ext List: An extension. Verify that the path extension
+ is in the list
+ :return: The string of the path with information
+ :rtype: Str
+ '''
+ # check if file exists
+ if not os.path.exists(path):
+ return "'%s' %s" % (path, src.printcolors.printcError(_("** not found")))
+
+ # check extension
+ if len(ext) > 0:
+ fe = os.path.splitext(path)[1].lower()
+ if fe not in ext:
+ return "'%s' %s" % (path, src.printcolors.printcError(_("** bad extension")))
+
+ return path
+
+def show_product_info(config, name, logger):
+ '''Display on the terminal and logger information about a product.
+
+ :param config Config: the global configuration.
+ :param name Str: The name of the product
+ :param logger Logger: The logger instance to use for the display
+ '''
+
+ logger.write(_("%s is a product\n") % src.printcolors.printcLabel(name), 2)
+ pinfo = src.product.get_product_config(config, name)
+
+ if "depend" in pinfo:
+ src.printcolors.print_value(logger,
+ "depends on",
+ ', '.join(pinfo.depend), 2)
+
+ if "opt_depend" in pinfo:
+ src.printcolors.print_value(logger,
+ "optional",
+ ', '.join(pinfo.opt_depend), 2)
+
+ # information on pyconf
+ logger.write("\n", 2)
+ logger.write(src.printcolors.printcLabel("configuration:") + "\n", 2)
+ if "from_file" in pinfo:
+ src.printcolors.print_value(logger,
+ "pyconf file path",
+ pinfo.from_file,
+ 2)
+ if "section" in pinfo:
+ src.printcolors.print_value(logger,
+ "section",
+ pinfo.section,
+ 2)
+
+ # information on prepare
+ logger.write("\n", 2)
+ logger.write(src.printcolors.printcLabel("prepare:") + "\n", 2)
+
+ is_dev = src.product.product_is_dev(pinfo)
+ method = pinfo.get_source
+ if is_dev:
+ method += " (dev)"
+ src.printcolors.print_value(logger, "get method", method, 2)
+
+ if method == 'cvs':
+ src.printcolors.print_value(logger, "server", pinfo.cvs_info.server, 2)
+ src.printcolors.print_value(logger, "base module",
+ pinfo.cvs_info.module_base, 2)
+ src.printcolors.print_value(logger, "source", pinfo.cvs_info.source, 2)
+ src.printcolors.print_value(logger, "tag", pinfo.cvs_info.tag, 2)
+
+ elif method == 'svn':
+ src.printcolors.print_value(logger, "repo", pinfo.svn_info.repo, 2)
+
+ elif method == 'git':
+ src.printcolors.print_value(logger, "repo", pinfo.git_info.repo, 2)
+ src.printcolors.print_value(logger, "tag", pinfo.git_info.tag, 2)
+
+ elif method == 'archive':
+ src.printcolors.print_value(logger,
+ "get from",
+ check_path(pinfo.archive_info.archive_name),
+ 2)
+
+ if 'patches' in pinfo:
+ for patch in pinfo.patches:
+ src.printcolors.print_value(logger, "patch", check_path(patch), 2)
+
+ if src.product.product_is_fixed(pinfo):
+ src.printcolors.print_value(logger, "install_dir",
+ check_path(pinfo.install_dir), 2)
+
+ if src.product.product_is_native(pinfo) or src.product.product_is_fixed(pinfo):
+ return
+
+ # information on compilation
+ if src.product.product_compiles(pinfo):
+ logger.write("\n", 2)
+ logger.write(src.printcolors.printcLabel("compile:") + "\n", 2)
+ src.printcolors.print_value(logger,
+ "compilation method",
+ pinfo.build_source,
+ 2)
+
+ if pinfo.build_source == "script" and "compil_script" in pinfo:
+ src.printcolors.print_value(logger,
+ "Compilation script",
+ pinfo.compil_script,
+ 2)
+
+ if 'nb_proc' in pinfo:
+ src.printcolors.print_value(logger, "make -j", pinfo.nb_proc, 2)
+
+ src.printcolors.print_value(logger,
+ "source dir",
+ check_path(pinfo.source_dir),
+ 2)
+ if 'install_dir' in pinfo:
+ src.printcolors.print_value(logger,
+ "build dir",
+ check_path(pinfo.build_dir),
+ 2)
+ src.printcolors.print_value(logger,
+ "install dir",
+ check_path(pinfo.install_dir),
+ 2)
+ else:
+ logger.write(" %s\n" % src.printcolors.printcWarning(_("no install dir")) , 2)
+ else:
+ logger.write("\n", 2)
+ msg = _("This product does not compile")
+ logger.write("%s\n" % msg, 2)
+
+ # information on environment
+ logger.write("\n", 2)
+ logger.write(src.printcolors.printcLabel("environ :") + "\n", 2)
+ if "environ" in pinfo and "env_script" in pinfo.environ:
+ src.printcolors.print_value(logger,
+ "script",
+ check_path(pinfo.environ.env_script),
+ 2)
+
+ zz = src.environment.SalomeEnviron(config,
+ src.fileEnviron.ScreenEnviron(logger),
+ False)
+ zz.set_python_libdirs()
+ zz.set_a_product(name, logger)
+
+def show_patchs(config, logger):
+ '''Prints all the used patchs in the application.
+
+ :param config Config: the global configuration.
+ :param logger Logger: The logger instance to use for the display
+ '''
+ len_max = max([len(p) for p in config.APPLICATION.products]) + 2
+ for product in config.APPLICATION.products:
+ product_info = src.product.get_product_config(config, product)
+ if src.product.product_has_patches(product_info):
+ logger.write("%s: " % product, 1)
+ logger.write(src.printcolors.printcInfo(
+ " " * (len_max - len(product) -2) +
+ "%s\n" % product_info.patches[0]),
+ 1)
+ if len(product_info.patches) > 1:
+ for patch in product_info.patches[1:]:
+ logger.write(src.printcolors.printcInfo(len_max*" " +
+ "%s\n" % patch), 1)
+ logger.write("\n", 1)
+
+def print_value(config, path, show_label, logger, level=0, show_full_path=False):
+ '''Prints a value from the configuration. Prints recursively the values
+ under the initial path.
+
+ :param config class 'src.pyconf.Config': The configuration
+ from which the value is displayed.
+ :param path str : the path in the configuration of the value to print.
+ :param show_label boolean: if True, do a basic display.
+ (useful for bash completion)
+ :param logger Logger: the logger instance
+ :param level int: The number of spaces to add before display.
+ :param show_full_path :
+ '''
+
+ # Make sure that the path does not ends with a point
+ if path.endswith('.'):
+ path = path[:-1]
+
+ # display all the path or not
+ if show_full_path:
+ vname = path
+ else:
+ vname = path.split('.')[-1]
+
+ # number of spaces before the display
+ tab_level = " " * level
+
+ # call to the function that gets the value of the path.
+ try:
+ val = config.getByPath(path)
+ except Exception as e:
+ logger.write(tab_level)
+ logger.write("%s: ERROR %s\n" % (src.printcolors.printcLabel(vname),
+ src.printcolors.printcError(str(e))))
+ return
+
+ # in this case, display only the value
+ if show_label:
+ logger.write(tab_level)
+ logger.write("%s: " % src.printcolors.printcLabel(vname))
+
+ # The case where the value has under values,
+ # do a recursive call to the function
+ if dir(val).__contains__('keys'):
+ if show_label: logger.write("\n")
+ for v in sorted(val.keys()):
+ print_value(config, path + '.' + v, show_label, logger, level + 1)
+ elif val.__class__ == src.pyconf.Sequence or isinstance(val, list):
+ # in this case, value is a list (or a Sequence)
+ if show_label: logger.write("\n")
+ index = 0
+ for v in val:
+ print_value(config, path + "[" + str(index) + "]",
+ show_label, logger, level + 1)
+ index = index + 1
+ else: # case where val is just a str
+ logger.write("%s\n" % val)
+
+def print_debug(config, aPath, show_label, logger, level=0, show_full_path=False):
+ path = str(aPath)
+ if path == "." :
+ val = config
+ path = ""
+ else:
+ if path.endswith('.'): # Make sure that the path does not ends with a point
+ path = path[:-1]
+ val = config.getByPath(path)
+
+ outStream = DBG.OutStream()
+ DBG.saveConfigDbg(val, outStream, path=path)
+ res = outStream.value
+ logger.write(res)
+ return
+
+
+def get_config_children(config, args):
+ '''Gets the names of the children of the given parameter.
+ Useful only for completion mechanism
+
+ :param config Config: The configuration where to read the values
+ :param args: The path in the config from which get the keys
+ '''
+ vals = []
+ rootkeys = config.keys()
+
+ if len(args) == 0:
+ # no parameter returns list of root keys
+ vals = rootkeys
+ else:
+ parent = args[0]
+ pos = parent.rfind('.')
+ if pos < 0:
+ # Case where there is only on key as parameter.
+ # For example VARS
+ vals = [m for m in rootkeys if m.startswith(parent)]
+ else:
+ # Case where there is a part from a key
+ # for example VARS.us (for VARS.user)
+ head = parent[0:pos]
+ tail = parent[pos+1:]
+ try:
+ a = config.getByPath(head)
+ if dir(a).__contains__('keys'):
+ vals = map(lambda x: head + '.' + x,
+ [m for m in a.keys() if m.startswith(tail)])
+ except:
+ pass
+
+ for v in sorted(vals):
+ sys.stdout.write("%s\n" % v)
+
+def description():
+ '''method that is called when salomeTools is called with --help option.
+
+ :return: The text to display for the config command description.
+ :rtype: str
+ '''
+ return _("""\
+The config command allows manipulation and operation on config files.
+
+example:
+>> sat config SALOME-master --info ParaView""")
+
+
+def run(args, runner, logger):
+ '''method that is called when salomeTools is called with config parameter.
+ '''
+ # Parse the options
+ (options, args) = parser.parse_args(args)
+
+ # Only useful for completion mechanism : print the keys of the config
+ if options.schema:
+ get_config_children(runner.cfg, args)
+ return
+
+ # case : print a value of the config
+ if options.value:
+ if options.value == ".":
+ # if argument is ".", print all the config
+ for val in sorted(runner.cfg.keys()):
+ print_value(runner.cfg, val, not options.no_label, logger)
+ else:
+ print_value(runner.cfg, options.value, not options.no_label, logger,
+ level=0, show_full_path=False)
+
+ if options.debug:
+ print_debug(runner.cfg, str(options.debug), not options.no_label, logger,
+ level=0, show_full_path=False)
+
+ # case : edit user pyconf file or application file
+ elif options.edit:
+ editor = runner.cfg.USER.editor
+ if ('APPLICATION' not in runner.cfg and
+ 'open_application' not in runner.cfg): # edit user pyconf
+ usercfg = os.path.join(runner.cfg.VARS.personalDir, 'SAT.pyconf')
+ logger.write(_("Opening %s\n") % usercfg, 3)
+ src.system.show_in_editor(editor, usercfg, logger)
+ else:
+ # search for file <application>.pyconf and open it
+ for path in runner.cfg.PATHS.APPLICATIONPATH:
+ pyconf_path = os.path.join(path, runner.cfg.VARS.application + ".pyconf")
+ if os.path.exists(pyconf_path):
+ logger.write(_("Opening %s\n") % pyconf_path, 3)
+ src.system.show_in_editor(editor, pyconf_path, logger)
+ break
+
+ # case : give information about the product in parameter
+ elif options.info:
+ src.check_config_has_application(runner.cfg)
+ if options.info in runner.cfg.APPLICATION.products:
+ show_product_info(runner.cfg, options.info, logger)
+ return
+ raise src.SatException(
+ _("%(product_name)s is not a product of %(application_name)s.") % \
+ {'product_name' : options.info, 'application_name' : runner.cfg.VARS.application} )
+
+ # case : copy an existing <application>.pyconf
+ # to ~/.salomeTools/Applications/LOCAL_<application>.pyconf
+ elif options.copy:
+ # product is required
+ src.check_config_has_application( runner.cfg )
+
+ # get application file path
+ source = runner.cfg.VARS.application + '.pyconf'
+ source_full_path = ""
+ for path in runner.cfg.PATHS.APPLICATIONPATH:
+ # ignore personal directory
+ if path == runner.cfg.VARS.personalDir:
+ continue
+ # loop on all directories that can have pyconf applications
+ zz = os.path.join(path, source)
+ if os.path.exists(zz):
+ source_full_path = zz
+ break
+
+ if len(source_full_path) == 0:
+ raise src.SatException(
+ _("Config file for product %s not found\n") % source )
+ else:
+ if len(args) > 0:
+ # a name is given as parameter, use it
+ dest = args[0]
+ elif 'copy_prefix' in runner.cfg.INTERNAL.config:
+ # use prefix
+ dest = (runner.cfg.INTERNAL.config.copy_prefix
+ + runner.cfg.VARS.application)
+ else:
+ # use same name as source
+ dest = runner.cfg.VARS.application
+
+ # the full path
+ dest_file = os.path.join(
+ runner.cfg.VARS.personalDir, 'Applications', dest + '.pyconf' )
+ if os.path.exists(dest_file):
+ raise src.SatException(
+ _("A personal application '%s' already exists") % dest )
+
+ # perform the copy
+ shutil.copyfile(source_full_path, dest_file)
+ logger.write(_("%s has been created.\n") % dest_file)
+
+ # case : display all the available pyconf applications
+ elif options.list:
+ lproduct = list()
+ # search in all directories that can have pyconf applications
+ for path in runner.cfg.PATHS.APPLICATIONPATH:
+ # print a header
+ if not options.no_label:
+ logger.write("------ %s\n" % src.printcolors.printcHeader(path))
+
+ if not os.path.exists(path):
+ logger.write(src.printcolors.printcError(
+ _("Directory not found")) + "\n" )
+ else:
+ for f in sorted(os.listdir(path)):
+ # ignore file that does not ends with .pyconf
+ if not f.endswith('.pyconf'):
+ continue
+
+ appliname = f[:-len('.pyconf')]
+ if appliname not in lproduct:
+ lproduct.append(appliname)
+ if path.startswith(runner.cfg.VARS.personalDir) \
+ and not options.no_label:
+ logger.write("%s*\n" % appliname)
+ else:
+ logger.write("%s\n" % appliname)
+
+ logger.write("\n")
+ # case : give a synthetic view of all patches used in the application
+ elif options.show_patchs:
+ src.check_config_has_application(runner.cfg)
+ # Print some informations
+ logger.write(_('Show the patchs of application %s\n') % \
+ src.printcolors.printcLabel(runner.cfg.VARS.application), 3)
+ logger.write("\n", 2, False)
+ show_patchs(runner.cfg, logger)
+
+ # case: print all the products name of the application (internal use for completion)
+ elif options.completion:
+ for product_name in runner.cfg.APPLICATION.products.keys():
+ logger.write("%s\n" % product_name)
+
+
# Define all possible option for log command : sat log <options>
parser = src.options.Options()
-parser.add_option('t', 'terminal', 'boolean', 'terminal', "Optional: "
- "Terminal log.")
-parser.add_option('l', 'last', 'boolean', 'last', "Show the log of the last "
- "Optional: launched command.")
-parser.add_option('', 'last_terminal', 'boolean', 'last_terminal', "Show the "
- "log of the last compilations"
- "Optional: launched command.")
-parser.add_option('f', 'full', 'boolean', 'full', "Optional: Show the logs of "
- "ALL the launched commands.")
-parser.add_option('c', 'clean', 'int', 'clean', "Optional: Erase the n most "
- "ancient log files.")
-parser.add_option('n', 'no_browser', 'boolean', 'no_browser', "Optional: Do not"
- " launch the browser at the end of the command. Only update "
- "the hat file.")
+parser.add_option(
+ 't', 'terminal', 'boolean', 'terminal',
+ "Optional: Show sat instances logs, no browser.")
+parser.add_option(
+ 'l', 'last', 'boolean', 'last',
+ "Show the log of the last launched command.")
+parser.add_option(
+ 'x', 'last_terminal', 'boolean', 'last_terminal',
+ """Optional: Show compile log of products, no browser.""")
+parser.add_option(
+ 'f', 'full', 'boolean', 'full',
+ "Optional: Show the logs of ALL the launched commands.")
+parser.add_option(
+ 'c', 'clean', 'int', 'clean',
+ "Optional: Erase the n most ancient log files.")
+parser.add_option(
+ 'n', 'no_browser', 'boolean', 'no_browser',
+ "Optional: Do not launch the browser at the end of the command. Only update the hat file.")
def get_last_log_file(logDir, notShownCommands):
'''Used in case of last option. Get the last log command file path.
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
_("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):
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
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 = []
# 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)
# 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))
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()
# @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
# @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)
##
# 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
"""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):
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))
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):
# 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:
'''
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
try:
return eval(s)
except Exception as e:
- raise ConfigError(str(e))
+ raise ConfigError("Config path not found: '%s'" % path)
class Sequence(Container):
"""
except ConfigError:
pass
if not found:
- raise ConfigError("unable to resolve %r" % path)
+ raise ConfigError("ConfigList path not found '%r'" % path)
return rv
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
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__), "..") )
# 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
_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
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
gettext.install('salomeTools', os.path.join(srcdir, 'i18n'))
DBG.write("setLocale", os.environ["LANG"])
-# Define all possible option for salomeTools command : sat <options> <args>
-parser = src.options.Options()
-parser.add_option('h', 'help', 'boolean', 'help',
- _("shows global help or help on a specific command."))
-parser.add_option('o', 'overwrite', 'list', "overwrite",
- _("overwrites a configuration parameters."))
-parser.add_option('g', 'debug', 'boolean', 'debug_mode',
- _("run salomeTools in debug mode."))
-parser.add_option('v', 'verbose', 'int', "output_verbose_level",
- _("change output verbose level (default is 3)."))
-parser.add_option('b', 'batch', 'boolean', "batch",
- _("batch mode (no question)."))
-parser.add_option('t', 'all_in_terminal', 'boolean', "all_in_terminal",
- _("all traces in the terminal (for example compilation logs)."))
-parser.add_option('l', 'logs_paths_in_file', 'string', "logs_paths_in_file",
- _("put the command result and paths to log files."))
+
+########################################################################
+# _BaseCmd class
+########################################################################
+class _BaseCommand(object):
+ '''_BaseCmd is the base class for all inherited commands of salomeTools
+ instancied as classes 'Command' in files '.../commands/*.py'
+ '''
+ def __init__(self, name):
+ self.name = name
+ self.runner = None # runner (as caller) usually as Sat instance
+ self.logger = None # logger (from caller) usually as Sat instance logger
+ self.config = None # config usually loaded with _getConfig method
+
+ def getClassName(self):
+ """
+ returns 'config.Command' or prepare.Command' for example
+ as inherited classes 'Command' in files
+ '.../commands/config.py' or '.../commands/prepare.py'
+ """
+ return "%s.%s" % (self.name, self.__class__.__name__)
+
+ def __repr__(self):
+ tmp = PP.pformat(self.__dict__)
+ res = "%s(\n %s)\n" % (self.getClassName(), tmp[1:-1])
+ return res
+
+ def setRunner(self, runner):
+ """set who owns me, and use me whith method run()"""
+ self.runner = runner
+
+ def setLogger(self, logger):
+ """set logger for run command"""
+ self.logger = logger
+
+ def getLogger(self):
+ if self.logger is None: # could use owner Sat instance logger
+ return self.runner.getLogger()
+ else: # could use local logger
+ return self.logger
+
+ def getRunner(self):
+ if self.runner is None:
+ raise Exception("have to set runner attribute, fix it.")
+ else:
+ return self.runner
+
+ def getParser(self):
+ raise Exception("_BaseCmd class have not to be instancied, only for inheritage")
+
+ def description(self):
+ '''method that is called when salomeTools is called with --help option.
+
+ :return: The text to display for the config command description.
+ :rtype: str
+ '''
+ raise Exception("_BaseCmd class have not to be instancied, only for inheritage")
+ def run(self, args, runner, logger):
+ '''method that is called when salomeTools is called with self.name parameter.
+ '''
+ raise Exception("_BaseCmd class have not to be instancied, only for inheritage")
+
+
+ def getConfig(self):
+ if self.config is None:
+ self.config = self._getConfig()
+ return self.config
+
+
+ def _getConfig(self):
+ '''The function that will load the configuration (all pyconf)
+ and return the config from files .pyconf
+ '''
+ if self.config is not None:
+ raise Exception("config existing yet in 's' instance" % self.getClassName())
+
+ # Get the arguments in a list and remove the empty elements
+ print "command.runner.arguments", self.runner.arguments
+
+ self.parser = self.getParser()
+ try:
+ options, args = self.parser.parse_args(self.runner.arguments[1:])
+ DBG.write("%s args", args)
+ DBG.write("%s options", options)
+ except Exception as exc:
+ write_exception(exc)
+ sys.exit(src.KOSYS)
+
+ self.arguments = args # args are postfixes options: args[0] is the 'commands' command
+ self.options = options # the options passed to salomeTools
+
+ print "command.arguments", self.arguments
+ print "command.options", self.options
+
+ if type(args) == type(''):
+ # split by spaces without considering spaces in quotes
+ argv_0 = re.findall(r'(?:"[^"]*"|[^\s"])+', args)
+ else:
+ argv_0 = args
+
+ if argv_0 != ['']:
+ while "" in argv_0: argv_0.remove("")
+
+ # Format the argv list in order to prevent strings
+ # that contain a blank to be separated
+ argv = []
+ elem_old = ""
+ for elem in argv_0:
+ if argv == [] or elem_old.startswith("-") or elem.startswith("-"):
+ argv.append(elem)
+ else:
+ argv[-1] += " " + elem
+ elem_old = elem
+
+ # if it is provided by the command line, get the application
+ appliToLoad = None
+ if argv not in [[''], []] and argv[0][0] != "-":
+ appliToLoad = argv[0].rstrip('*')
+ argv = argv[1:]
+
+ # Check if the global options of salomeTools have to be changed
+ if options:
+ options_save = self.options
+ self.options = options
+
+ # read the configuration from all the pyconf files
+ cfgManager = commands.config.ConfigManager()
+ config = cfgManager.get_config(datadir=self.datadir,
+ application=appliToLoad,
+ options=self.options,
+ command=__nameCmd__)
+
+ # Set the verbose mode if called
+ if verbose > -1:
+ verbose_save = self.options.output_verbose_level
+ self.options.__setattr__("output_verbose_level", verbose)
+
+ # Set batch mode if called
+ if batch:
+ batch_save = self.options.batch
+ self.options.__setattr__("batch", True)
+
+ # set output level
+ if self.options.output_verbose_level is not None:
+ config.USER.output_verbose_level = self.options.output_verbose_level
+ if config.USER.output_verbose_level < 1:
+ config.USER.output_verbose_level = 0
+ silent = (config.USER.output_verbose_level == 0)
+
+ # create log file
+ micro_command = False
+ if logger_add_link:
+ micro_command = True
+ logger_command = src.logger.Logger(config,
+ silent_sysstd=silent,
+ all_in_terminal=self.options.all_in_terminal,
+ micro_command=micro_command)
+
+ # Check that the path given by the logs_paths_in_file option
+ # is a file path that can be written
+ if self.options.logs_paths_in_file and not micro_command:
+ try:
+ self.options.logs_paths_in_file = os.path.abspath(
+ self.options.logs_paths_in_file)
+ dir_file = os.path.dirname(self.options.logs_paths_in_file)
+ if not os.path.exists(dir_file):
+ os.makedirs(dir_file)
+ if os.path.exists(self.options.logs_paths_in_file):
+ os.remove(self.options.logs_paths_in_file)
+ file_test = open(self.options.logs_paths_in_file, "w")
+ file_test.close()
+ except Exception as e:
+ msg = _("WARNING: the logs_paths_in_file option will "
+ "not be taken into account.\nHere is the error:")
+ logger_command.write("%s\n%s\n\n" % (
+ src.printcolors.printcWarning(msg),
+ str(e)))
+ self.options.logs_paths_in_file = None
+
+ return config
+
+########################################################################
+# Sat class
+########################################################################
class Sat(object):
'''The main class that stores all the commands of salomeTools
'''
'''
# Read the salomeTools prefixes options before the 'commands' tag
# sat <options> <args>
- # (the list of possible options is at the beginning of this file)
+ # (the list of possible options is at the beginning of this file)
+ DBG.push_debug(True)
+ self.parser = self._getParser()
try:
if type(opt) is not list: # as string 'sat --help' for example'
opts = opt.split()
else:
opts = opt
- options, args = parser.parse_args(opts)
- # DBG.write("Sat args", args)
- # DBG.write("Sat options", options)
+ options, args = self.parser.parse_args(opts)
+ DBG.write("Sat args", args)
+ DBG.write("Sat options", options)
except Exception as exc:
write_exception(exc)
sys.exit(src.KOSYS)
- # initialization of class attributes
- self.__dict__ = dict()
- self.cfg = None # the config that will be read using pyconf module
+ self.logger = None # the logger that will be use
+ self.config = None # the config that will be read using pyconf module
self.arguments = args # args are postfixes options: args[0] is the 'commands' command
self.options = options # the options passed to salomeTools
self.datadir = datadir # default value will be <salomeTools root>/data
- # set the commands by calling the dedicated function
- self._setCommands(cmdsdir)
-
- '''
- # done with execute_command, to avoid sys.exit
- # if the help option has been called, print help and exit
- if options.help:
- try:
- self.print_help(args)
- sys.exit(src.OKSYS)
- except Exception as exc:
- write_exception(exc)
- DBG.write("args", args, True)
- sys.exit(src.KOSYS)
- '''
+ # contains commands classes needed (think micro commands)
+ # if useful 'a la demande'
+ self.commands = {}
- def __getattr__(self, name):
- '''overwrite of __getattr__ function in order to display
- a customized message in case of a wrong call
- :param name str: The name of the attribute
- '''
- if name in self.__dict__:
- return self.__dict__[name]
- else:
- raise AttributeError("'%s'" % name + _(" is not a valid command"))
+ def __repr__(self):
+ aDict = {
+ "arguments": self.arguments,
+ "options": self.options,
+ "datadir": self.datadir,
+ "commands": sorted(self.commands.keys()),
+ }
+ tmp = PP.pformat(aDict)
+ res = "Sat(\n %s\n)\n" % tmp[1:-1]
+ return res
+
+ def getLogger(self):
+ return self.logger
+
+
+ def _getParser(self):
+ """
+ Define all possible <options> for salomeTools/sat command: 'sat <options> <args>'
+ (internal use only)
+ """
+ import src.options
+ parser = src.options.Options()
+ parser.add_option('h', 'help', 'boolean', 'help',
+ _("shows global help or help on a specific command."))
+ parser.add_option('o', 'overwrite', 'list', "overwrite",
+ _("overwrites a configuration parameters."))
+ parser.add_option('g', 'debug', 'boolean', 'debug_mode',
+ _("run salomeTools in debug mode."))
+ parser.add_option('v', 'verbose', 'int', "output_verbose_level",
+ _("change output verbose level (default is 3)."))
+ parser.add_option('b', 'batch', 'boolean', "batch",
+ _("batch mode (no question)."))
+ parser.add_option('t', 'all_in_terminal', 'boolean', "all_in_terminal",
+ _("all traces in the terminal (for example compilation logs)."))
+ parser.add_option('l', 'logs_paths_in_file', 'string', "logs_paths_in_file",
+ _("put the command result and paths to log files."))
+ return parser
+
+
+ def _getCommand(self, name):
+ """
+ create and add Command 'name' as instance of class in dict self.commands
+ create only one instance
+ """
+ if name not in _COMMANDS_NAMES:
+ raise AttributeError(_("command not valid: '%s'") % name)
+ if name in self.commands.keys():
+ raise AttributeError(_("command existing yet: '%s', use getCommand") % name)
+ file_, pathname, description = imp.find_module(name, [cmdsdir])
+ module = imp.load_module(name, file_, pathname, description)
+ cmdInstance = module.Command(name)
+ cmdInstance.setRunner(self) # self is runner, owns cmdInstance
+ DBG.write("new command", cmdInstance)
+ return cmdInstance
+
+ def getCommand(self, name):
+ """
+ returns inherited instance of _BaseCmd for command 'name'
+ if not existing as self.commands[name], create it.
+
+ example:
+ returns Commamd() from commamd.config
+ """
+ if name not in self.commands.keys():
+ self.commands[name] = self._getCommand(name)
+ return self.commands[name]
+
def execute_command(self, opt=None):
"""select first argument as a command in directory 'commands', and launch on arguments
return src.OKSYS
# the command called
- command = args[0]
- # get dynamically the command function to call
- fun_command = self.__getattr__(command)
+ cmdName = args[0]
+ # create/get dynamically the command instance to call its 'run' method
+ cmdInstance = self.getCommand(cmdName)
# Run the command using the arguments
- exitCode = fun_command(args[1:])
- return exitCode
-
- def _setCommands(self, dirPath):
- '''set class attributes corresponding to all commands that are
- in the dirPath directory
-
- :param dirPath str: The directory path containing the commands
- '''
- # loop on the commands name
- for nameCmd in lCommand:
-
- # Exception for the jobs command that requires the paramiko module
- if nameCmd == "jobs":
- try:
- saveout = sys.stderr
- ff = tempfile.TemporaryFile()
- sys.stderr = ff
- import paramiko
- sys.stderr = saveout
- except:
- sys.stderr = saveout
- continue
-
- # load the module that has name nameCmd in dirPath
- (file_, pathname, description) = imp.find_module(nameCmd, [dirPath])
- module = imp.load_module(nameCmd, file_, pathname, description)
-
- def run_command(args='',
- options=None,
- batch = False,
- verbose = -1,
- logger_add_link = None):
- '''The function that will load the configuration (all pyconf)
- and return the function run of the command corresponding to module
-
- :param args str: The arguments of the command
- '''
- # Make sure the internationalization is available
- # gettext.install('salomeTools', os.path.join(srcdir, 'i18n'))
-
- # Get the arguments in a list and remove the empty elements
- if type(args) == type(''):
- # split by spaces without considering spaces in quotes
- argv_0 = re.findall(r'(?:"[^"]*"|[^\s"])+', args)
- else:
- argv_0 = args
-
- if argv_0 != ['']:
- while "" in argv_0: argv_0.remove("")
-
- # Format the argv list in order to prevent strings
- # that contain a blank to be separated
- argv = []
- elem_old = ""
- for elem in argv_0:
- if argv == [] or elem_old.startswith("-") or elem.startswith("-"):
- argv.append(elem)
- else:
- argv[-1] += " " + elem
- elem_old = elem
-
- # if it is provided by the command line, get the application
- appliToLoad = None
- if argv not in [[''], []] and argv[0][0] != "-":
- appliToLoad = argv[0].rstrip('*')
- argv = argv[1:]
-
- # Check if the global options of salomeTools have to be changed
- if options:
- options_save = self.options
- self.options = options
-
- # read the configuration from all the pyconf files
- cfgManager = commands.config.ConfigManager()
- self.cfg = cfgManager.get_config(datadir=self.datadir,
- application=appliToLoad,
- options=self.options,
- command=__nameCmd__)
-
- # Set the verbose mode if called
- if verbose > -1:
- verbose_save = self.options.output_verbose_level
- self.options.__setattr__("output_verbose_level", verbose)
-
- # Set batch mode if called
- if batch:
- batch_save = self.options.batch
- self.options.__setattr__("batch", True)
-
- # set output level
- if self.options.output_verbose_level is not None:
- self.cfg.USER.output_verbose_level = self.options.output_verbose_level
- if self.cfg.USER.output_verbose_level < 1:
- self.cfg.USER.output_verbose_level = 0
- silent = (self.cfg.USER.output_verbose_level == 0)
-
- # create log file
- micro_command = False
- if logger_add_link:
- micro_command = True
- logger_command = src.logger.Logger(self.cfg,
- silent_sysstd=silent,
- all_in_terminal=self.options.all_in_terminal,
- micro_command=micro_command)
-
- # Check that the path given by the logs_paths_in_file option
- # is a file path that can be written
- if self.options.logs_paths_in_file and not micro_command:
- try:
- self.options.logs_paths_in_file = os.path.abspath(
- self.options.logs_paths_in_file)
- dir_file = os.path.dirname(self.options.logs_paths_in_file)
- if not os.path.exists(dir_file):
- os.makedirs(dir_file)
- if os.path.exists(self.options.logs_paths_in_file):
- os.remove(self.options.logs_paths_in_file)
- file_test = open(self.options.logs_paths_in_file, "w")
- file_test.close()
- except Exception as e:
- msg = _("WARNING: the logs_paths_in_file option will "
- "not be taken into account.\nHere is the error:")
- logger_command.write("%s\n%s\n\n" % (
- src.printcolors.printcWarning(msg),
- str(e)))
- self.options.logs_paths_in_file = None
-
- options_launched = ""
- res = None
- try:
- # Execute the hooks (if there is any)
- # and run method of the command
- self.run_hook(__nameCmd__, C_PRE_HOOK, logger_command)
- res = __module__.run(argv, self, logger_command)
- self.run_hook(__nameCmd__, C_POST_HOOK, logger_command)
- if res is None:
- res = 0
-
- except Exception as e:
- # Get error
- logger_command.write("\n***** ", 1)
- logger_command.write(
- src.printcolors.printcError("salomeTools ERROR:"), 1)
- logger_command.write("\n" + str(e) + "\n\n", 1)
- # get stack
- __, __, exc_traceback = sys.exc_info()
- fp = tempfile.TemporaryFile()
- traceback.print_tb(exc_traceback, file=fp)
- fp.seek(0)
- stack = fp.read()
- verbosity = 5
- if self.options.debug_mode:
- verbosity = 1
- logger_command.write("TRACEBACK:\n%s" % stack.replace('"',"'"),
- verbosity)
- finally:
- # set res if it is not set in the command
- if res is None:
- res = 1
-
- # come back to the original global options
- if options:
- options_launched = get_text_from_options(self.options)
- self.options = options_save
-
- # come back in the original batch mode if
- # batch argument was called
- if batch:
- self.options.__setattr__("batch", batch_save)
-
- # come back in the original verbose mode if
- # verbose argument was called
- if verbose > -1:
- self.options.__setattr__("output_verbose_level",
- verbose_save)
- # put final attributes in xml log file
- # (end time, total time, ...) and write it
- launchedCommand = ' '.join([self.cfg.VARS.salometoolsway +
- os.path.sep +
- 'sat',
- options_launched,
- __nameCmd__,
- ' '.join(argv_0)])
- launchedCommand = launchedCommand.replace('"', "'")
-
- # Add a link to the parent command
- if logger_add_link is not None:
- logger_add_link.add_link(logger_command.logFileName,
- __nameCmd__,
- res,
- launchedCommand)
- logger_add_link.l_logFiles += logger_command.l_logFiles
-
- # Put the final attributes corresponding to end time and
- # Write the file to the hard drive
- logger_command.end_write(
- {"launchedCommand" : launchedCommand})
-
- if res != 0:
- res = 1
-
- # print the log file path if
- # the maximum verbose mode is invoked
- if not micro_command:
- logger_command.write("\nPath to the xml log file:\n", 5)
- logger_command.write("%s\n\n" % \
- src.printcolors.printcInfo(logger_command.logFilePath), 5)
-
- # If the logs_paths_in_file was called, write the result
- # and log files in the given file path
- if self.options.logs_paths_in_file and not micro_command:
- file_res = open(self.options.logs_paths_in_file, "w")
- file_res.write(str(res) + "\n")
- for i, filepath in enumerate(logger_command.l_logFiles):
- file_res.write(filepath)
- if i < len(logger_command.l_logFiles):
- file_res.write("\n")
- file_res.flush()
-
- return res
-
- # Make sure that run_command will be redefined
- # at each iteration of the loop
- globals_up = {}
- globals_up.update(run_command.__globals__)
- globals_up.update({'__nameCmd__': nameCmd, '__module__' : module})
- func = types.FunctionType(run_command.__code__,
- globals_up,
- run_command.__name__,
- run_command.__defaults__,
- run_command.__closure__)
-
- # set the attribute corresponding to the command
- self.__setattr__(nameCmd, func)
-
- def run_hook(self, cmd_name, hook_type, logger):
- '''Execute a hook file for a given command regarding the fact
- it is pre or post
-
- :param cmd_name str: The the command on which execute the hook
- :param hook_type str: pre or post
- :param logger Logger: the logging instance to use for the prints
- '''
- # The hooks must be defined in the application pyconf
- # So, if there is no application, do not do anything
- if not src.config_has_application(self.cfg):
- return
-
- # The hooks must be defined in the application pyconf in the
- # APPLICATION section, hook : { command : 'script_path.py'}
- if "hook" not in self.cfg.APPLICATION \
- or cmd_name not in self.cfg.APPLICATION.hook:
- return
-
- # Get the hook_script path and verify that it exists
- hook_script_path = self.cfg.APPLICATION.hook[cmd_name]
- if not os.path.exists(hook_script_path):
- raise src.SatException(_("Hook script not found: %s") %
- hook_script_path)
-
- # Try to execute the script, catch the exception if it fails
- try:
- # import the module (in the sense of python)
- pymodule = imp.load_source(cmd_name, hook_script_path)
-
- # format a message to be printed at hook execution
- msg = src.printcolors.printcWarning(_("Run hook script"))
- msg = "%s: %s\n" % (msg,
- src.printcolors.printcInfo(hook_script_path))
-
- # run the function run_pre_hook if this function is called
- # before the command, run_post_hook if it is called after
- if hook_type == C_PRE_HOOK and "run_pre_hook" in dir(pymodule):
- logger.write(msg, 1)
- pymodule.run_pre_hook(self.cfg, logger)
- elif hook_type == C_POST_HOOK and "run_post_hook" in dir(pymodule):
- logger.write(msg, 1)
- pymodule.run_post_hook(self.cfg, logger)
-
- except Exception as exc:
- msg = _("Unable to run hook script: %s") % hook_script_path
- msg += "\n" + str(exc)
- raise src.SatException(msg)
+ exitCode = cmdInstance.run(args[1:])
+ return exitCode
def print_help(self, opt):
'''Prints help for a command. Function called when "sat -h <command>"
# 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
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
"""
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):
--- /dev/null
+#!/usr/bin/env python
+#-*- coding:utf-8 -*-
+
+# Copyright (C) 2010-2018 CEA/DEN
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+'''This file is the main entry file to salomeTools
+'''
+
+import os
+import sys
+import re
+import tempfile
+import imp
+import types
+import gettext
+import traceback
+import subprocess as SP
+
+#################################
+# NOT MAIN allowed
+#################################
+if __name__ == "__main__":
+ sys.stderr.write("\nERROR: 'salomeTools.py' is not main command entry for sat: use 'sat' instead.\n\n")
+ KOSYS = 1 # avoid import src
+ sys.exit(KOSYS)
+
+
+import src.debug as DBG # Easy print stderr (for DEBUG only)
+
+# get path to src
+rootdir = os.path.realpath( os.path.join(os.path.dirname(__file__), "..") )
+DBG.write("sat rootdir", rootdir)
+srcdir = os.path.join(rootdir, "src")
+cmdsdir = os.path.join(rootdir, "commands")
+
+# load resources for internationalization
+gettext.install('salomeTools', os.path.join(srcdir, 'i18n'))
+
+# salomeTools imports
+import src
+import commands.config
+
+# The possible hooks :
+# pre is for hooks to be executed before commands
+# post is for hooks to be executed after commands
+C_PRE_HOOK = "pre"
+C_POST_HOOK = "post"
+
+_LANG = os.environ["LANG"] # original locale
+
+def find_command_list(dirPath):
+ '''Parse files in dirPath that end with .py : it gives commands list
+
+ :param dirPath str: The directory path where to search the commands
+ :return: cmd_list : the list containing the commands name
+ :rtype: list
+ '''
+ cmd_list = []
+ for item in os.listdir(dirPath):
+ if item in ["__init__.py"]: #avoid theses files
+ continue
+ if item.endswith('.py'):
+ cmd_list.append(item[:-len('.py')])
+ return cmd_list
+
+# The list of valid salomeTools commands
+#lCommand = ['config', 'compile', 'prepare',...]
+lCommand = find_command_list(cmdsdir)
+
+def getCommandsList():
+ """Gives commands list (as basename of files commands/*.py)
+ """
+ return lCommand
+
+def launchSat(command):
+ """launch sat as subprocess popen
+ command as string ('sat --help' for example)
+ used for unittest, or else...
+ returns tuple (stdout, stderr)
+ """
+ if "sat" not in command.split()[0]:
+ raise Exception(_("Not a valid command for launchSat: '%s'") % command)
+ env = dict(os.environ)
+ env["PATH"] = rootdir + ":" + env["PATH"]
+ res =SP.Popen(command, shell=True, env=env, stdout=SP.PIPE, stderr=SP.PIPE).communicate()
+ return res
+
+def setNotLocale():
+ """force english at any moment"""
+ os.environ["LANG"] = ''
+ gettext.install('salomeTools', os.path.join(srcdir, 'i18n'))
+ DBG.write("setNotLocale", os.environ["LANG"])
+
+def setLocale():
+ """reset initial locale at any moment
+ 'fr' or else 'TODO' from initial environment var '$LANG'
+ """
+ os.environ["LANG"] = _LANG
+ gettext.install('salomeTools', os.path.join(srcdir, 'i18n'))
+ DBG.write("setLocale", os.environ["LANG"])
+
+# Define all possible option for salomeTools command : sat <options> <args>
+parser = src.options.Options()
+parser.add_option('h', 'help', 'boolean', 'help',
+ _("shows global help or help on a specific command."))
+parser.add_option('o', 'overwrite', 'list', "overwrite",
+ _("overwrites a configuration parameters."))
+parser.add_option('g', 'debug', 'boolean', 'debug_mode',
+ _("run salomeTools in debug mode."))
+parser.add_option('v', 'verbose', 'int', "output_verbose_level",
+ _("change output verbose level (default is 3)."))
+parser.add_option('b', 'batch', 'boolean', "batch",
+ _("batch mode (no question)."))
+parser.add_option('t', 'all_in_terminal', 'boolean', "all_in_terminal",
+ _("all traces in the terminal (for example compilation logs)."))
+parser.add_option('l', 'logs_paths_in_file', 'string', "logs_paths_in_file",
+ _("put the command result and paths to log files."))
+
+class Sat(object):
+ '''The main class that stores all the commands of salomeTools
+ '''
+ def __init__(self, opt='', datadir=None):
+ '''Initialization
+
+ :param opt str or list: The sat options
+ :param: datadir str : the directory that contain all the external
+ data (like software pyconf and software scripts)
+ '''
+ # Read the salomeTools prefixes options before the 'commands' tag
+ # sat <options> <args>
+ # (the list of possible options is at the beginning of this file)
+ try:
+ if type(opt) is not list: # as string 'sat --help' for example'
+ opts = opt.split()
+ else:
+ opts = opt
+ options, args = parser.parse_args(opts)
+ # DBG.write("Sat args", args)
+ # DBG.write("Sat options", options)
+ except Exception as exc:
+ write_exception(exc)
+ sys.exit(src.KOSYS)
+
+ # initialization of class attributes
+ self.__dict__ = dict()
+ self.cfg = None # the config that will be read using pyconf module
+ self.arguments = args # args are postfixes options: args[0] is the 'commands' command
+ self.options = options # the options passed to salomeTools
+ self.datadir = datadir # default value will be <salomeTools root>/data
+ # set the commands by calling the dedicated function
+ self._setCommands(cmdsdir)
+
+ '''
+ # done with execute_command, to avoid sys.exit
+ # if the help option has been called, print help and exit
+ if options.help:
+ try:
+ self.print_help(args)
+ sys.exit(src.OKSYS)
+ except Exception as exc:
+ write_exception(exc)
+ DBG.write("args", args, True)
+ sys.exit(src.KOSYS)
+ '''
+
+ def __getattr__(self, name):
+ '''overwrite of __getattr__ function in order to display
+ a customized message in case of a wrong call
+
+ :param name str: The name of the attribute
+ '''
+ if name in self.__dict__:
+ return self.__dict__[name]
+ else:
+ raise AttributeError("'%s'" % name + _(" is not a valid command"))
+
+ def execute_command(self, opt=None):
+ """select first argument as a command in directory 'commands', and launch on arguments
+
+ :param opt str, optionnal: The sat options (as sys.argv)
+ """
+ if opt is not None:
+ args = opt
+ else:
+ args = self.arguments
+
+ # print general help and returns
+ if len(args) == 0:
+ print_help()
+ return src.OKSYS
+
+ # if the help option has been called, print command help and returns
+ if self.options.help:
+ self.print_help(self.arguments)
+ return src.OKSYS
+
+ # the command called
+ command = args[0]
+ # get dynamically the command function to call
+ fun_command = self.__getattr__(command)
+ # Run the command using the arguments
+ exitCode = fun_command(args[1:])
+ return exitCode
+
+ def _setCommands(self, dirPath):
+ '''set class attributes corresponding to all commands that are
+ in the dirPath directory
+
+ :param dirPath str: The directory path containing the commands
+ '''
+ # loop on the commands name
+ for nameCmd in lCommand:
+
+ # Exception for the jobs command that requires the paramiko module
+ if nameCmd == "jobs":
+ try:
+ saveout = sys.stderr
+ ff = tempfile.TemporaryFile()
+ sys.stderr = ff
+ import paramiko
+ sys.stderr = saveout
+ except:
+ sys.stderr = saveout
+ continue
+
+ # load the module that has name nameCmd in dirPath
+ (file_, pathname, description) = imp.find_module(nameCmd, [dirPath])
+ module = imp.load_module(nameCmd, file_, pathname, description)
+
+ def run_command(args='',
+ options=None,
+ batch = False,
+ verbose = -1,
+ logger_add_link = None):
+ '''The function that will load the configuration (all pyconf)
+ and return the function run of the command corresponding to module
+
+ :param args str: The arguments of the command
+ '''
+ # Make sure the internationalization is available
+ # gettext.install('salomeTools', os.path.join(srcdir, 'i18n'))
+
+ # Get the arguments in a list and remove the empty elements
+ if type(args) == type(''):
+ # split by spaces without considering spaces in quotes
+ argv_0 = re.findall(r'(?:"[^"]*"|[^\s"])+', args)
+ else:
+ argv_0 = args
+
+ if argv_0 != ['']:
+ while "" in argv_0: argv_0.remove("")
+
+ # Format the argv list in order to prevent strings
+ # that contain a blank to be separated
+ argv = []
+ elem_old = ""
+ for elem in argv_0:
+ if argv == [] or elem_old.startswith("-") or elem.startswith("-"):
+ argv.append(elem)
+ else:
+ argv[-1] += " " + elem
+ elem_old = elem
+
+ # if it is provided by the command line, get the application
+ appliToLoad = None
+ if argv not in [[''], []] and argv[0][0] != "-":
+ appliToLoad = argv[0].rstrip('*')
+ argv = argv[1:]
+
+ # Check if the global options of salomeTools have to be changed
+ if options:
+ options_save = self.options
+ self.options = options
+
+ # read the configuration from all the pyconf files
+ cfgManager = commands.config.ConfigManager()
+ self.cfg = cfgManager.get_config(datadir=self.datadir,
+ application=appliToLoad,
+ options=self.options,
+ command=__nameCmd__)
+
+ # Set the verbose mode if called
+ if verbose > -1:
+ verbose_save = self.options.output_verbose_level
+ self.options.__setattr__("output_verbose_level", verbose)
+
+ # Set batch mode if called
+ if batch:
+ batch_save = self.options.batch
+ self.options.__setattr__("batch", True)
+
+ # set output level
+ if self.options.output_verbose_level is not None:
+ self.cfg.USER.output_verbose_level = self.options.output_verbose_level
+ if self.cfg.USER.output_verbose_level < 1:
+ self.cfg.USER.output_verbose_level = 0
+ silent = (self.cfg.USER.output_verbose_level == 0)
+
+ # create log file
+ micro_command = False
+ if logger_add_link:
+ micro_command = True
+ logger_command = src.logger.Logger(self.cfg,
+ silent_sysstd=silent,
+ all_in_terminal=self.options.all_in_terminal,
+ micro_command=micro_command)
+
+ # Check that the path given by the logs_paths_in_file option
+ # is a file path that can be written
+ if self.options.logs_paths_in_file and not micro_command:
+ try:
+ self.options.logs_paths_in_file = os.path.abspath(
+ self.options.logs_paths_in_file)
+ dir_file = os.path.dirname(self.options.logs_paths_in_file)
+ if not os.path.exists(dir_file):
+ os.makedirs(dir_file)
+ if os.path.exists(self.options.logs_paths_in_file):
+ os.remove(self.options.logs_paths_in_file)
+ file_test = open(self.options.logs_paths_in_file, "w")
+ file_test.close()
+ except Exception as e:
+ msg = _("WARNING: the logs_paths_in_file option will "
+ "not be taken into account.\nHere is the error:")
+ logger_command.write("%s\n%s\n\n" % (
+ src.printcolors.printcWarning(msg),
+ str(e)))
+ self.options.logs_paths_in_file = None
+
+ options_launched = ""
+ res = None
+ try:
+ # Execute the hooks (if there is any)
+ # and run method of the command
+ self.run_hook(__nameCmd__, C_PRE_HOOK, logger_command)
+ res = __module__.run(argv, self, logger_command)
+ self.run_hook(__nameCmd__, C_POST_HOOK, logger_command)
+ if res is None:
+ res = 0
+
+ except Exception as e:
+ # Get error
+ logger_command.write("\n***** ", 1)
+ logger_command.write(
+ src.printcolors.printcError("salomeTools ERROR:"), 1)
+ logger_command.write("\n" + str(e) + "\n\n", 1)
+ # get stack
+ __, __, exc_traceback = sys.exc_info()
+ fp = tempfile.TemporaryFile()
+ traceback.print_tb(exc_traceback, file=fp)
+ fp.seek(0)
+ stack = fp.read()
+ verbosity = 5
+ if self.options.debug_mode:
+ verbosity = 1
+ logger_command.write("TRACEBACK:\n%s" % stack.replace('"',"'"),
+ verbosity)
+ finally:
+ # set res if it is not set in the command
+ if res is None:
+ res = 1
+
+ # come back to the original global options
+ if options:
+ options_launched = get_text_from_options(self.options)
+ self.options = options_save
+
+ # come back in the original batch mode if
+ # batch argument was called
+ if batch:
+ self.options.__setattr__("batch", batch_save)
+
+ # come back in the original verbose mode if
+ # verbose argument was called
+ if verbose > -1:
+ self.options.__setattr__("output_verbose_level",
+ verbose_save)
+ # put final attributes in xml log file
+ # (end time, total time, ...) and write it
+ launchedCommand = ' '.join([self.cfg.VARS.salometoolsway +
+ os.path.sep +
+ 'sat',
+ options_launched,
+ __nameCmd__,
+ ' '.join(argv_0)])
+ launchedCommand = launchedCommand.replace('"', "'")
+
+ # Add a link to the parent command
+ if logger_add_link is not None:
+ logger_add_link.add_link(logger_command.logFileName,
+ __nameCmd__,
+ res,
+ launchedCommand)
+ logger_add_link.l_logFiles += logger_command.l_logFiles
+
+ # Put the final attributes corresponding to end time and
+ # Write the file to the hard drive
+ logger_command.end_write(
+ {"launchedCommand" : launchedCommand})
+
+ if res != 0:
+ res = 1
+
+ # print the log file path if
+ # the maximum verbose mode is invoked
+ if not micro_command:
+ logger_command.write("\nPath to the xml log file:\n", 5)
+ logger_command.write("%s\n\n" % \
+ src.printcolors.printcInfo(logger_command.logFilePath), 5)
+
+ # If the logs_paths_in_file was called, write the result
+ # and log files in the given file path
+ if self.options.logs_paths_in_file and not micro_command:
+ file_res = open(self.options.logs_paths_in_file, "w")
+ file_res.write(str(res) + "\n")
+ for i, filepath in enumerate(logger_command.l_logFiles):
+ file_res.write(filepath)
+ if i < len(logger_command.l_logFiles):
+ file_res.write("\n")
+ file_res.flush()
+
+ return res
+
+ # Make sure that run_command will be redefined
+ # at each iteration of the loop
+ globals_up = {}
+ globals_up.update(run_command.__globals__)
+ globals_up.update({'__nameCmd__': nameCmd, '__module__' : module})
+ func = types.FunctionType(run_command.__code__,
+ globals_up,
+ run_command.__name__,
+ run_command.__defaults__,
+ run_command.__closure__)
+
+ # set the attribute corresponding to the command
+ self.__setattr__(nameCmd, func)
+
+ def run_hook(self, cmd_name, hook_type, logger):
+ '''Execute a hook file for a given command regarding the fact
+ it is pre or post
+
+ :param cmd_name str: The the command on which execute the hook
+ :param hook_type str: pre or post
+ :param logger Logger: the logging instance to use for the prints
+ '''
+ # The hooks must be defined in the application pyconf
+ # So, if there is no application, do not do anything
+ if not src.config_has_application(self.cfg):
+ return
+
+ # The hooks must be defined in the application pyconf in the
+ # APPLICATION section, hook : { command : 'script_path.py'}
+ if "hook" not in self.cfg.APPLICATION \
+ or cmd_name not in self.cfg.APPLICATION.hook:
+ return
+
+ # Get the hook_script path and verify that it exists
+ hook_script_path = self.cfg.APPLICATION.hook[cmd_name]
+ if not os.path.exists(hook_script_path):
+ raise src.SatException(_("Hook script not found: %s") %
+ hook_script_path)
+
+ # Try to execute the script, catch the exception if it fails
+ try:
+ # import the module (in the sense of python)
+ pymodule = imp.load_source(cmd_name, hook_script_path)
+
+ # format a message to be printed at hook execution
+ msg = src.printcolors.printcWarning(_("Run hook script"))
+ msg = "%s: %s\n" % (msg,
+ src.printcolors.printcInfo(hook_script_path))
+
+ # run the function run_pre_hook if this function is called
+ # before the command, run_post_hook if it is called after
+ if hook_type == C_PRE_HOOK and "run_pre_hook" in dir(pymodule):
+ logger.write(msg, 1)
+ pymodule.run_pre_hook(self.cfg, logger)
+ elif hook_type == C_POST_HOOK and "run_post_hook" in dir(pymodule):
+ logger.write(msg, 1)
+ pymodule.run_post_hook(self.cfg, logger)
+
+ except Exception as exc:
+ msg = _("Unable to run hook script: %s") % hook_script_path
+ msg += "\n" + str(exc)
+ raise src.SatException(msg)
+
+ def print_help(self, opt):
+ '''Prints help for a command. Function called when "sat -h <command>"
+
+ :param argv str: the options passed (to get the command name)
+ '''
+ # if no command as argument (sat -h)
+ if len(opt)==0:
+ print_help()
+ return
+
+ # get command name
+ command = opt[0]
+ # read the configuration from all the pyconf files
+ cfgManager = commands.config.ConfigManager()
+ self.cfg = cfgManager.get_config(datadir=self.datadir)
+
+ # Check if this command exists
+ if not hasattr(self, command):
+ raise src.SatException(_("Command '%s' does not exist") % command)
+
+ # load the module
+ module = self.get_module(command)
+
+ msg = self.get_module_help(module)
+
+ if isStdoutPipe():
+ # clean color if the terminal is redirected by user
+ # ex: sat compile appli > log.txt
+ msg = src.printcolors.cleancolor(msg)
+ print(msg)
+ return
+
+ def get_module_help(self, module):
+ """get help for a command
+ as 'sat --help config' for example
+ """
+ # get salomeTools version
+ msg = get_version() + "\n\n"
+
+ # print the description of the command that is done in the command file
+ if hasattr( module, "description" ):
+ msg += src.printcolors.printcHeader( _("Description:") ) + "\n"
+ msg += module.description() + "\n\n"
+
+ # print the description of the command options
+ if hasattr( module, "parser" ) :
+ msg += module.parser.get_help() + "\n"
+ return msg
+
+
+ def get_module(self, module):
+ '''Loads a command. Function called only by print_help
+
+ :param module str: the command to load
+ '''
+ # Check if this command exists
+ if not hasattr(self, module):
+ raise src.SatException(_("Command '%s' does not exist") % module)
+
+ # load the module
+ (file_, pathname, description) = imp.find_module(module, [cmdsdir])
+ module = imp.load_module(module, file_, pathname, description)
+ return module
+
+
+def get_text_from_options(options):
+ text_options = ""
+ for attr in dir(options):
+ if attr.startswith("__"):
+ continue
+ if options.__getattr__(attr) != None:
+ option_contain = options.__getattr__(attr)
+ if type(option_contain)==type([]):
+ option_contain = ",".join(option_contain)
+ if type(option_contain)==type(True):
+ option_contain = ""
+ text_options+= "--%s %s " % (attr, option_contain)
+ return text_options
+
+
+def isStdoutPipe():
+ """check if the terminal is redirected by user (elsewhere a tty)
+ example:
+ >> sat compile appli > log.txt
+ """
+ return not ('isatty' in dir(sys.stdout) and sys.stdout.isatty())
+
+def get_version():
+ """get version colored string
+ """
+ cfgManager = commands.config.ConfigManager()
+ cfg = cfgManager.get_config()
+ # print the key corresponding to salomeTools version
+ msg = src.printcolors.printcHeader( _("Version: ") ) + \
+ cfg.INTERNAL.sat_version
+ return msg
+
+def get_help():
+ """get general help colored string
+ """
+ # read the config
+ msg = get_version() + "\n\n"
+ msg += src.printcolors.printcHeader(_("Usage: ")) + \
+ "sat [sat_options] <command> [product] [command_options]\n\n"
+ msg += parser.get_help() + "\n"
+ msg += src.printcolors.printcHeader(_("Available commands are:")) + "\n\n"
+ for command in lCommand:
+ msg += " - %s\n" % (command)
+ msg += "\n"
+ # Explain how to get the help for a specific command
+ msg += src.printcolors.printcHeader(
+ _("Getting the help for a specific command: ")) + \
+ "sat --help <command>\n"
+ return msg
+
+def print_help():
+ """prints salomeTools general help
+ """
+ msg = get_help()
+ if isStdoutPipe():
+ # clean color if the terminal is redirected by user
+ # ex: sat compile appli > log.txt
+ msg = src.printcolors.cleancolor(msg)
+ print(msg)
+ return
+
+def write_exception(exc):
+ '''write in stderr exception in case of error in a command
+
+ :param exc exception: the exception to print
+ '''
+ sys.stderr.write("\n***** ")
+ sys.stderr.write(src.printcolors.printcError("salomeTools ERROR:"))
+ sys.stderr.write("\n" + str(exc) + "\n")
+
+
+
+
--- /dev/null
+
+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"
+ }
+}
--- /dev/null
+#!/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
--- /dev/null
+#TODO
+switch pyconf.py 0.3.7.1 -> 0.3.9, here for test
--- /dev/null
+Copyright (C) 2004-2010 by Vinay Sajip. All Rights Reserved.\r
+\r
+Permission to use, copy, modify, and distribute this software and its\r
+documentation for any purpose and without fee is hereby granted,\r
+provided that the above copyright notice appear in all copies and that\r
+both that copyright notice and this permission notice appear in\r
+supporting documentation, and that the name of Vinay Sajip\r
+not be used in advertising or publicity pertaining to distribution\r
+of the software without specific, written prior permission.\r
+\r
+VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING\r
+ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL\r
+VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR\r
+ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN\r
+AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR\r
+IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--- /dev/null
+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
--- /dev/null
+This module is intended to provide configuration functionality for Python\r
+programs.\r
+\r
+Change History\r
+--------------\r
+\r
+Version Date Description\r
+=============================================================================\r
+0.3.9 11 May 2010 Fixed parsing bug which caused failure for numbers with\r
+ exponents.\r
+-----------------------------------------------------------------------------\r
+0.3.8 03 Mar 2010 Fixed parsing bug which caused failure for negative\r
+ numbers in sequences. Improved resolution logic.\r
+-----------------------------------------------------------------------------\r
+0.3.7 05 Oct 2007 Added Mapping.__delitem__ (patch by John Drummond).\r
+ Mapping.__getattribute__ no longer returns "" when\r
+ asked for "__class__" - doing so causes pickle to\r
+ crash (reported by Jamila Gunawardena).\r
+ Allow negative numbers (reported by Gary Schoep; had\r
+ already been fixed but not yet released).\r
+-----------------------------------------------------------------------------\r
+0.3.6 09 Mar 2006 Made classes derive from object (previously they were\r
+ old-style classes).\r
+ Changed ConfigMerger to use a more flexible merge\r
+ strategy.\r
+ Multiline strings (using """ or ''') are now supported.\r
+ A typo involving raising a ConfigError was fixed.\r
+ Patches received with thanks from David Janes & Tim\r
+ Desjardins (BlogMatrix) and Erick Tryzelaar.\r
+-----------------------------------------------------------------------------\r
+0.3.5 27 Dec 2004 Added ConfigOutputStream to provide better Unicode\r
+ output support. Altered save code to put platform-\r
+ dependent newlines for Unicode.\r
+-----------------------------------------------------------------------------\r
+0.3.4 11 Nov 2004 Added ConfigInputStream to provide better Unicode\r
+ support.\r
+ Added ConfigReader.setStream().\r
+-----------------------------------------------------------------------------\r
+0.3.3 09 Nov 2004 Renamed config.get() to getByPath(), and likewise for\r
+ ConfigList.\r
+ Added Mapping.get() to work like dict.get().\r
+ Added logconfig.py and logconfig.cfg to distribution.\r
+-----------------------------------------------------------------------------\r
+0.3.2 04 Nov 2004 Simplified parseMapping().\r
+ Allowed Config.__init__ to accept a string as well as a\r
+ stream. If a string is passed in, streamOpener is used\r
+ to obtain the stream to be used.\r
+-----------------------------------------------------------------------------\r
+0.3.1 04 Nov 2004 Changed addNamespace/removeNamespace to make name\r
+ specification easier.\r
+ Refactored save(), added Container.writeToStream and\r
+ Container.writeValue() to help with this.\r
+-----------------------------------------------------------------------------\r
+0.3 03 Nov 2004 Added test harness (test_config.py)\r
+ Fixed bugs in bracket parsing.\r
+ Refactored internal classes.\r
+ Added merging functionality.\r
+-----------------------------------------------------------------------------\r
+0.2 01 Nov 2004 Added support for None.\r
+ Stream closed in load() and save().\r
+ Added support for changing configuration.\r
+ Fixed bugs in identifier parsing and isword().\r
+-----------------------------------------------------------------------------\r
+0.1 31 Oct 2004 Initial implementation (for community feedback)\r
+-----------------------------------------------------------------------------\r
+\r
+-----------------------------------------------------------------------------\r
+COPYRIGHT\r
+-----------------------------------------------------------------------------\r
+Copyright 2004-2010 by Vinay Sajip. All Rights Reserved.\r
+\r
+Permission to use, copy, modify, and distribute this software and its\r
+documentation for any purpose and without fee is hereby granted,\r
+provided that the above copyright notice appear in all copies and that\r
+both that copyright notice and this permission notice appear in\r
+supporting documentation, and that the name of Vinay Sajip\r
+not be used in advertising or publicity pertaining to distribution\r
+of the software without specific, written prior permission.\r
+VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING\r
+ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL\r
+VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR\r
+ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN\r
+AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR\r
+IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\r
--- /dev/null
+# Copyright 2004-2010 by Vinay Sajip. All Rights Reserved.\r
+#\r
+# Permission to use, copy, modify, and distribute this software and its\r
+# documentation for any purpose and without fee is hereby granted,\r
+# provided that the above copyright notice appear in all copies and that\r
+# both that copyright notice and this permission notice appear in\r
+# supporting documentation, and that the name of Vinay Sajip\r
+# not be used in advertising or publicity pertaining to distribution\r
+# of the software without specific, written prior permission.\r
+# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING\r
+# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL\r
+# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR\r
+# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER\r
+# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\r
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\r
+\r
+"""\r
+This is a configuration module for Python.\r
+\r
+This module should work under Python versions >= 2.2, and cannot be used with\r
+earlier versions since it uses new-style classes.\r
+\r
+Development and testing has only been carried out (so far) on Python 2.3.4 and\r
+Python 2.4.2. See the test module (test_config.py) included in the\r
+U{distribution<http://www.red-dove.com/python_config.html|_blank>} (follow the\r
+download link).\r
+\r
+A simple example - with the example configuration file::\r
+\r
+ messages:\r
+ [\r
+ {\r
+ stream : `sys.stderr`\r
+ message: 'Welcome'\r
+ name: 'Harry'\r
+ }\r
+ {\r
+ stream : `sys.stdout`\r
+ message: 'Welkom'\r
+ name: 'Ruud'\r
+ }\r
+ {\r
+ stream : $messages[0].stream\r
+ message: 'Bienvenue'\r
+ name: Yves\r
+ }\r
+ ]\r
+\r
+a program to read the configuration would be::\r
+\r
+ from config import Config\r
+\r
+ f = file('simple.cfg')\r
+ cfg = Config(f)\r
+ for m in cfg.messages:\r
+ s = '%s, %s' % (m.message, m.name)\r
+ try:\r
+ print >> m.stream, s\r
+ except IOError, e:\r
+ print e\r
+\r
+which, when run, would yield the console output::\r
+\r
+ Welcome, Harry\r
+ Welkom, Ruud\r
+ Bienvenue, Yves\r
+\r
+See U{this tutorial<http://www.red-dove.com/python_config.html|_blank>} for more\r
+information.\r
+\r
+@version: 0.3.9\r
+\r
+@author: Vinay Sajip\r
+\r
+@copyright: Copyright (C) 2004-2010 Vinay Sajip. All Rights Reserved.\r
+\r
+\r
+@var streamOpener: The default stream opener. This is a factory function which\r
+takes a string (e.g. filename) and returns a stream suitable for reading. If\r
+unable to open the stream, an IOError exception should be thrown.\r
+\r
+The default value of this variable is L{defaultStreamOpener}. For an example\r
+of how it's used, see test_config.py (search for streamOpener).\r
+"""\r
+\r
+__author__ = "Vinay Sajip <vinay_sajip@red-dove.com>"\r
+__status__ = "alpha"\r
+__version__ = "0.3.9"\r
+__date__ = "11 May 2010"\r
+\r
+from types import StringType, UnicodeType\r
+\r
+import codecs\r
+import logging\r
+import os\r
+import sys\r
+\r
+WORD = 'a'\r
+NUMBER = '9'\r
+STRING = '"'\r
+EOF = ''\r
+LCURLY = '{'\r
+RCURLY = '}'\r
+LBRACK = '['\r
+LBRACK2 = 'a['\r
+RBRACK = ']'\r
+LPAREN = '('\r
+LPAREN2 = '(('\r
+RPAREN = ')'\r
+DOT = '.'\r
+COMMA = ','\r
+COLON = ':'\r
+AT = '@'\r
+PLUS = '+'\r
+MINUS = '-'\r
+STAR = '*'\r
+SLASH = '/'\r
+MOD = '%'\r
+BACKTICK = '`'\r
+DOLLAR = '$'\r
+TRUE = 'True'\r
+FALSE = 'False'\r
+NONE = 'None'\r
+\r
+WORDCHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_"\r
+\r
+if sys.platform == 'win32':\r
+ NEWLINE = '\r\n'\r
+elif os.name == 'mac':\r
+ NEWLINE = '\r'\r
+else:\r
+ NEWLINE = '\n'\r
+\r
+try:\r
+ import encodings.utf_32\r
+ has_utf32 = True\r
+except:\r
+ has_utf32 = False\r
+\r
+try:\r
+ from logging.handlers import NullHandler\r
+except ImportError:\r
+ class NullHandler(logging.Handler):\r
+ def emit(self, record):\r
+ pass\r
+\r
+logger = logging.getLogger(__name__)\r
+if not logger.handlers:\r
+ logger.addHandler(NullHandler())\r
+\r
+class ConfigInputStream(object):\r
+ """\r
+ An input stream which can read either ANSI files with default encoding\r
+ or Unicode files with BOMs.\r
+\r
+ Handles UTF-8, UTF-16LE, UTF-16BE. Could handle UTF-32 if Python had\r
+ built-in support.\r
+ """\r
+ def __init__(self, stream):\r
+ """\r
+ Initialize an instance.\r
+\r
+ @param stream: The underlying stream to be read. Should be seekable.\r
+ @type stream: A stream (file-like object).\r
+ """\r
+ encoding = None\r
+ signature = stream.read(4)\r
+ used = -1\r
+ if has_utf32:\r
+ if signature == codecs.BOM_UTF32_LE:\r
+ encoding = 'utf-32le'\r
+ elif signature == codecs.BOM_UTF32_BE:\r
+ encoding = 'utf-32be'\r
+ if encoding is None:\r
+ if signature[:3] == codecs.BOM_UTF8:\r
+ used = 3\r
+ encoding = 'utf-8'\r
+ elif signature[:2] == codecs.BOM_UTF16_LE:\r
+ used = 2\r
+ encoding = 'utf-16le'\r
+ elif signature[:2] == codecs.BOM_UTF16_BE:\r
+ used = 2\r
+ encoding = 'utf-16be'\r
+ else:\r
+ used = 0\r
+ if used >= 0:\r
+ stream.seek(used)\r
+ if encoding:\r
+ reader = codecs.getreader(encoding)\r
+ stream = reader(stream)\r
+ self.stream = stream\r
+ self.encoding = encoding\r
+\r
+ def read(self, size):\r
+ if (size == 0) or (self.encoding is None):\r
+ rv = self.stream.read(size)\r
+ else:\r
+ rv = u''\r
+ while size > 0:\r
+ rv += self.stream.read(1)\r
+ size -= 1\r
+ return rv\r
+\r
+ def close(self):\r
+ self.stream.close()\r
+\r
+ def readline(self):\r
+ if self.encoding is None:\r
+ line = ''\r
+ else:\r
+ line = u''\r
+ while True:\r
+ c = self.stream.read(1)\r
+ if c:\r
+ line += c\r
+ if c == '\n':\r
+ break\r
+ return line\r
+\r
+class ConfigOutputStream(object):\r
+ """\r
+ An output stream which can write either ANSI files with default encoding\r
+ or Unicode files with BOMs.\r
+\r
+ Handles UTF-8, UTF-16LE, UTF-16BE. Could handle UTF-32 if Python had\r
+ built-in support.\r
+ """\r
+\r
+ def __init__(self, stream, encoding=None):\r
+ """\r
+ Initialize an instance.\r
+\r
+ @param stream: The underlying stream to be written.\r
+ @type stream: A stream (file-like object).\r
+ @param encoding: The desired encoding.\r
+ @type encoding: str\r
+ """\r
+ if encoding is not None:\r
+ encoding = str(encoding).lower()\r
+ self.encoding = encoding\r
+ if encoding == "utf-8":\r
+ stream.write(codecs.BOM_UTF8)\r
+ elif encoding == "utf-16be":\r
+ stream.write(codecs.BOM_UTF16_BE)\r
+ elif encoding == "utf-16le":\r
+ stream.write(codecs.BOM_UTF16_LE)\r
+ elif encoding == "utf-32be":\r
+ stream.write(codecs.BOM_UTF32_BE)\r
+ elif encoding == "utf-32le":\r
+ stream.write(codecs.BOM_UTF32_LE)\r
+\r
+ if encoding is not None:\r
+ writer = codecs.getwriter(encoding)\r
+ stream = writer(stream)\r
+ self.stream = stream\r
+\r
+ def write(self, data):\r
+ self.stream.write(data)\r
+\r
+ def flush(self):\r
+ self.stream.flush()\r
+\r
+ def close(self):\r
+ self.stream.close()\r
+\r
+def defaultStreamOpener(name):\r
+ """\r
+ This function returns a read-only stream, given its name. The name passed\r
+ in should correspond to an existing stream, otherwise an exception will be\r
+ raised.\r
+\r
+ This is the default value of L{streamOpener}; assign your own callable to\r
+ streamOpener to return streams based on names. For example, you could use\r
+ urllib2.urlopen().\r
+\r
+ @param name: The name of a stream, most commonly a file name.\r
+ @type name: str\r
+ @return: A stream with the specified name.\r
+ @rtype: A read-only stream (file-like object)\r
+ """\r
+ return ConfigInputStream(file(name, 'rb'))\r
+\r
+streamOpener = None\r
+\r
+class ConfigError(Exception):\r
+ """\r
+ This is the base class of exceptions raised by this module.\r
+ """\r
+ pass\r
+\r
+class ConfigFormatError(ConfigError):\r
+ """\r
+ This is the base class of exceptions raised due to syntax errors in\r
+ configurations.\r
+ """\r
+ pass\r
+\r
+class ConfigResolutionError(ConfigError):\r
+ """\r
+ This is the base class of exceptions raised due to semantic errors in\r
+ configurations.\r
+ """\r
+ pass\r
+\r
+def isWord(s):\r
+ """\r
+ See if a passed-in value is an identifier. If the value passed in is not a\r
+ string, False is returned. An identifier consists of alphanumerics or\r
+ underscore characters.\r
+\r
+ Examples::\r
+\r
+ isWord('a word') ->False\r
+ isWord('award') -> True\r
+ isWord(9) -> False\r
+ isWord('a_b_c_') ->True\r
+\r
+ @note: isWord('9abc') will return True - not exactly correct, but adequate\r
+ for the way it's used here.\r
+\r
+ @param s: The name to be tested\r
+ @type s: any\r
+ @return: True if a word, else False\r
+ @rtype: bool\r
+ """\r
+ if type(s) != type(''):\r
+ return False\r
+ s = s.replace('_', '')\r
+ return s.isalnum()\r
+\r
+def makePath(prefix, suffix):\r
+ """\r
+ Make a path from a prefix and suffix.\r
+\r
+ Examples::\r
+\r
+ makePath('', 'suffix') -> 'suffix'\r
+ makePath('prefix', 'suffix') -> 'prefix.suffix'\r
+ makePath('prefix', '[1]') -> 'prefix[1]'\r
+\r
+ @param prefix: The prefix to use. If it evaluates as false, the suffix\r
+ is returned.\r
+ @type prefix: str\r
+ @param suffix: The suffix to use. It is either an identifier or an\r
+ index in brackets.\r
+ @type suffix: str\r
+ @return: The path concatenation of prefix and suffix, with a\r
+ dot if the suffix is not a bracketed index.\r
+ @rtype: str\r
+\r
+ """\r
+ if not prefix:\r
+ rv = suffix\r
+ elif suffix[0] == '[':\r
+ rv = prefix + suffix\r
+ else:\r
+ rv = prefix + '.' + suffix\r
+ return rv\r
+\r
+\r
+class Container(object):\r
+ """\r
+ This internal class is the base class for mappings and sequences.\r
+\r
+ @ivar path: A string which describes how to get\r
+ to this instance from the root of the hierarchy.\r
+\r
+ Example::\r
+\r
+ a.list.of[1].or['more'].elements\r
+ """\r
+ def __init__(self, parent):\r
+ """\r
+ Initialize an instance.\r
+\r
+ @param parent: The parent of this instance in the hierarchy.\r
+ @type parent: A L{Container} instance.\r
+ """\r
+ object.__setattr__(self, 'parent', parent)\r
+\r
+ def setPath(self, path):\r
+ """\r
+ Set the path for this instance.\r
+ @param path: The path - a string which describes how to get\r
+ to this instance from the root of the hierarchy.\r
+ @type path: str\r
+ """\r
+ object.__setattr__(self, 'path', path)\r
+\r
+ def evaluate(self, item):\r
+ """\r
+ Evaluate items which are instances of L{Reference} or L{Expression}.\r
+\r
+ L{Reference} instances are evaluated using L{Reference.resolve},\r
+ and L{Expression} instances are evaluated using\r
+ L{Expression.evaluate}.\r
+\r
+ @param item: The item to be evaluated.\r
+ @type item: any\r
+ @return: If the item is an instance of L{Reference} or L{Expression},\r
+ the evaluated value is returned, otherwise the item is returned\r
+ unchanged.\r
+ """\r
+ if isinstance(item, Reference):\r
+ item = item.resolve(self)\r
+ elif isinstance(item, Expression):\r
+ item = item.evaluate(self)\r
+ return item\r
+\r
+ def writeToStream(self, stream, indent, container):\r
+ """\r
+ Write this instance to a stream at the specified indentation level.\r
+\r
+ Should be redefined in subclasses.\r
+\r
+ @param stream: The stream to write to\r
+ @type stream: A writable stream (file-like object)\r
+ @param indent: The indentation level\r
+ @type indent: int\r
+ @param container: The container of this instance\r
+ @type container: L{Container}\r
+ @raise NotImplementedError: If a subclass does not override this\r
+ """\r
+ raise NotImplementedError\r
+\r
+ def writeValue(self, value, stream, indent):\r
+ if isinstance(self, Mapping):\r
+ indstr = ' '\r
+ else:\r
+ indstr = indent * ' '\r
+ if isinstance(value, Reference) or isinstance(value, Expression):\r
+ stream.write('%s%r%s' % (indstr, value, NEWLINE))\r
+ else:\r
+ if (type(value) is StringType): # and not isWord(value):\r
+ value = repr(value)\r
+ stream.write('%s%s%s' % (indstr, value, NEWLINE))\r
+\r
+class Mapping(Container):\r
+ """\r
+ This internal class implements key-value mappings in configurations.\r
+ """\r
+\r
+ def __init__(self, parent=None):\r
+ """\r
+ Initialize an instance.\r
+\r
+ @param parent: The parent of this instance in the hierarchy.\r
+ @type parent: A L{Container} instance.\r
+ """\r
+ Container.__init__(self, parent)\r
+ object.__setattr__(self, 'path', '')\r
+ object.__setattr__(self, 'data', {})\r
+ object.__setattr__(self, 'order', []) # to preserve ordering\r
+ object.__setattr__(self, 'comments', {})\r
+\r
+ def __delitem__(self, key):\r
+ """\r
+ Remove an item\r
+ """\r
+ data = object.__getattribute__(self, 'data')\r
+ if key not in data:\r
+ raise AttributeError(key)\r
+ order = object.__getattribute__(self, 'order')\r
+ comments = object.__getattribute__(self, 'comments')\r
+ del data[key]\r
+ order.remove(key)\r
+ del comments[key]\r
+\r
+ def __getitem__(self, key):\r
+ data = object.__getattribute__(self, 'data')\r
+ if key not in data:\r
+ raise AttributeError(key)\r
+ rv = data[key]\r
+ return self.evaluate(rv)\r
+\r
+ __getattr__ = __getitem__\r
+\r
+ def __getattribute__(self, name):\r
+ if name == "__dict__":\r
+ return {}\r
+ if name in ["__methods__", "__members__"]:\r
+ return []\r
+ #if name == "__class__":\r
+ # return ''\r
+ data = object.__getattribute__(self, "data")\r
+ useData = data.has_key(name)\r
+ if useData:\r
+ rv = getattr(data, name)\r
+ else:\r
+ rv = object.__getattribute__(self, name)\r
+ if rv is None:\r
+ raise AttributeError(name)\r
+ return rv\r
+\r
+ def iteritems(self):\r
+ for key in self.keys():\r
+ yield(key, self[key])\r
+ raise StopIteration\r
+\r
+ def __contains__(self, item):\r
+ order = object.__getattribute__(self, 'order')\r
+ return item in order\r
+\r
+ def addMapping(self, key, value, comment, setting=False):\r
+ """\r
+ Add a key-value mapping with a comment.\r
+\r
+ @param key: The key for the mapping.\r
+ @type key: str\r
+ @param value: The value for the mapping.\r
+ @type value: any\r
+ @param comment: The comment for the key (can be None).\r
+ @type comment: str\r
+ @param setting: If True, ignore clashes. This is set\r
+ to true when called from L{__setattr__}.\r
+ @raise ConfigFormatError: If an existing key is seen\r
+ again and setting is False.\r
+ """\r
+ data = object.__getattribute__(self, 'data')\r
+ order = object.__getattribute__(self, 'order')\r
+ comments = object.__getattribute__(self, 'comments')\r
+\r
+ data[key] = value\r
+ if key not in order:\r
+ order.append(key)\r
+ elif not setting:\r
+ raise ConfigFormatError("repeated key: %s" % key)\r
+ comments[key] = comment\r
+\r
+ def __setattr__(self, name, value):\r
+ self.addMapping(name, value, None, True)\r
+\r
+ __setitem__ = __setattr__\r
+\r
+ def keys(self):\r
+ """\r
+ Return the keys in a similar way to a dictionary.\r
+ """\r
+ return object.__getattribute__(self, 'order')\r
+\r
+ def get(self, key, default=None):\r
+ """\r
+ Allows a dictionary-style get operation.\r
+ """\r
+ if key in self:\r
+ return self[key]\r
+ return default\r
+\r
+ def __str__(self):\r
+ return str(object.__getattribute__(self, 'data'))\r
+\r
+ def __repr__(self):\r
+ return repr(object.__getattribute__(self, 'data'))\r
+\r
+ def __len__(self):\r
+ return len(object.__getattribute__(self, 'order'))\r
+\r
+ def __iter__(self):\r
+ return self.iterkeys()\r
+\r
+ def iterkeys(self):\r
+ order = object.__getattribute__(self, 'order')\r
+ return order.__iter__()\r
+\r
+ def writeToStream(self, stream, indent, container):\r
+ """\r
+ Write this instance to a stream at the specified indentation level.\r
+\r
+ Should be redefined in subclasses.\r
+\r
+ @param stream: The stream to write to\r
+ @type stream: A writable stream (file-like object)\r
+ @param indent: The indentation level\r
+ @type indent: int\r
+ @param container: The container of this instance\r
+ @type container: L{Container}\r
+ """\r
+ indstr = indent * ' '\r
+ if len(self) == 0:\r
+ stream.write(' { }%s' % NEWLINE)\r
+ else:\r
+ if isinstance(container, Mapping):\r
+ stream.write(NEWLINE)\r
+ stream.write('%s{%s' % (indstr, NEWLINE))\r
+ self.save(stream, indent + 1)\r
+ stream.write('%s}%s' % (indstr, NEWLINE))\r
+\r
+ def save(self, stream, indent=0):\r
+ """\r
+ Save this configuration to the specified stream.\r
+ @param stream: A stream to which the configuration is written.\r
+ @type stream: A write-only stream (file-like object).\r
+ @param indent: The indentation level for the output.\r
+ @type indent: int\r
+ """\r
+ indstr = indent * ' '\r
+ order = object.__getattribute__(self, 'order')\r
+ data = object.__getattribute__(self, 'data')\r
+ maxlen = 0 # max(map(lambda x: len(x), order))\r
+ for key in order:\r
+ comment = self.comments[key]\r
+ if isWord(key):\r
+ skey = key\r
+ else:\r
+ skey = repr(key)\r
+ if comment:\r
+ stream.write('%s#%s' % (indstr, comment))\r
+ stream.write('%s%-*s :' % (indstr, maxlen, skey))\r
+ value = data[key]\r
+ if isinstance(value, Container):\r
+ value.writeToStream(stream, indent, self)\r
+ else:\r
+ self.writeValue(value, stream, indent)\r
+\r
+class Config(Mapping):\r
+ """\r
+ This class represents a configuration, and is the only one which clients\r
+ need to interface to, under normal circumstances.\r
+ """\r
+\r
+ class Namespace(object):\r
+ """\r
+ This internal class is used for implementing default namespaces.\r
+\r
+ An instance acts as a namespace.\r
+ """\r
+ def __init__(self):\r
+ self.sys = sys\r
+ self.os = os\r
+\r
+ def __repr__(self):\r
+ return "<Namespace('%s')>" % ','.join(self.__dict__.keys())\r
+\r
+ def __init__(self, streamOrFile=None, parent=None):\r
+ """\r
+ Initializes an instance.\r
+\r
+ @param streamOrFile: If specified, causes this instance to be loaded\r
+ from the stream (by calling L{load}). If a string is provided, it is\r
+ passed to L{streamOpener} to open a stream. Otherwise, the passed\r
+ value is assumed to be a stream and used as is.\r
+ @type streamOrFile: A readable stream (file-like object) or a name.\r
+ @param parent: If specified, this becomes the parent of this instance\r
+ in the configuration hierarchy.\r
+ @type parent: a L{Container} instance.\r
+ """\r
+ Mapping.__init__(self, parent)\r
+ object.__setattr__(self, 'reader', ConfigReader(self))\r
+ object.__setattr__(self, 'namespaces', [Config.Namespace()])\r
+ object.__setattr__(self, 'resolving', set())\r
+ if streamOrFile is not None:\r
+ if isinstance(streamOrFile, StringType) or isinstance(streamOrFile, UnicodeType):\r
+ global streamOpener\r
+ if streamOpener is None:\r
+ streamOpener = defaultStreamOpener\r
+ streamOrFile = streamOpener(streamOrFile)\r
+ load = object.__getattribute__(self, "load")\r
+ load(streamOrFile)\r
+\r
+ def load(self, stream):\r
+ """\r
+ Load the configuration from the specified stream. Multiple streams can\r
+ be used to populate the same instance, as long as there are no\r
+ clashing keys. The stream is closed.\r
+ @param stream: A stream from which the configuration is read.\r
+ @type stream: A read-only stream (file-like object).\r
+ @raise ConfigError: if keys in the loaded configuration clash with\r
+ existing keys.\r
+ @raise ConfigFormatError: if there is a syntax error in the stream.\r
+ """\r
+ reader = object.__getattribute__(self, 'reader')\r
+ #object.__setattr__(self, 'root', reader.load(stream))\r
+ reader.load(stream)\r
+ stream.close()\r
+\r
+ def addNamespace(self, ns, name=None):\r
+ """\r
+ Add a namespace to this configuration which can be used to evaluate\r
+ (resolve) dotted-identifier expressions.\r
+ @param ns: The namespace to be added.\r
+ @type ns: A module or other namespace suitable for passing as an\r
+ argument to vars().\r
+ @param name: A name for the namespace, which, if specified, provides\r
+ an additional level of indirection.\r
+ @type name: str\r
+ """\r
+ namespaces = object.__getattribute__(self, 'namespaces')\r
+ if name is None:\r
+ namespaces.append(ns)\r
+ else:\r
+ setattr(namespaces[0], name, ns)\r
+\r
+ def removeNamespace(self, ns, name=None):\r
+ """\r
+ Remove a namespace added with L{addNamespace}.\r
+ @param ns: The namespace to be removed.\r
+ @param name: The name which was specified when L{addNamespace} was\r
+ called.\r
+ @type name: str\r
+ """\r
+ namespaces = object.__getattribute__(self, 'namespaces')\r
+ if name is None:\r
+ namespaces.remove(ns)\r
+ else:\r
+ delattr(namespaces[0], name)\r
+\r
+ def save(self, stream, indent=0):\r
+ """\r
+ Save this configuration to the specified stream. The stream is\r
+ closed if this is the top-level configuration in the hierarchy.\r
+ L{Mapping.save} is called to do all the work.\r
+ @param stream: A stream to which the configuration is written.\r
+ @type stream: A write-only stream (file-like object).\r
+ @param indent: The indentation level for the output.\r
+ @type indent: int\r
+ """\r
+ Mapping.save(self, stream, indent)\r
+ if indent == 0:\r
+ stream.close()\r
+\r
+ def getByPath(self, path):\r
+ """\r
+ Obtain a value in the configuration via its path.\r
+ @param path: The path of the required value\r
+ @type path: str\r
+ @return the value at the specified path.\r
+ @rtype: any\r
+ @raise ConfigError: If the path is invalid\r
+ """\r
+ s = 'self.' + path\r
+ try:\r
+ return eval(s)\r
+ except Exception, e:\r
+ raise ConfigError(str(e))\r
+\r
+class Sequence(Container):\r
+ """\r
+ This internal class implements a value which is a sequence of other values.\r
+ """\r
+ class SeqIter(object):\r
+ """\r
+ This internal class implements an iterator for a L{Sequence} instance.\r
+ """\r
+ def __init__(self, seq):\r
+ self.seq = seq\r
+ self.limit = len(object.__getattribute__(seq, 'data'))\r
+ self.index = 0\r
+\r
+ def __iter__(self):\r
+ return self\r
+\r
+ def next(self):\r
+ if self.index >= self.limit:\r
+ raise StopIteration\r
+ rv = self.seq[self.index]\r
+ self.index += 1\r
+ return rv\r
+\r
+ def __init__(self, parent=None):\r
+ """\r
+ Initialize an instance.\r
+\r
+ @param parent: The parent of this instance in the hierarchy.\r
+ @type parent: A L{Container} instance.\r
+ """\r
+ Container.__init__(self, parent)\r
+ object.__setattr__(self, 'data', [])\r
+ object.__setattr__(self, 'comments', [])\r
+\r
+ def append(self, item, comment):\r
+ """\r
+ Add an item to the sequence.\r
+\r
+ @param item: The item to add.\r
+ @type item: any\r
+ @param comment: A comment for the item.\r
+ @type comment: str\r
+ """\r
+ data = object.__getattribute__(self, 'data')\r
+ comments = object.__getattribute__(self, 'comments')\r
+ data.append(item)\r
+ comments.append(comment)\r
+\r
+ def __getitem__(self, index):\r
+ data = object.__getattribute__(self, 'data')\r
+ try:\r
+ rv = data[index]\r
+ except (IndexError, KeyError, TypeError):\r
+ raise ConfigResolutionError('%r is not a valid index for %r' % (index, object.__getattribute__(self, 'path')))\r
+ if not isinstance(rv, list):\r
+ rv = self.evaluate(rv)\r
+ else:\r
+ # deal with a slice\r
+ result = []\r
+ for a in rv:\r
+ result.append(self.evaluate(a))\r
+ rv = result\r
+ return rv\r
+\r
+ def __iter__(self):\r
+ return Sequence.SeqIter(self)\r
+\r
+ def __repr__(self):\r
+ return repr(object.__getattribute__(self, 'data'))\r
+\r
+ def __str__(self):\r
+ return str(self[:]) # using the slice evaluates the contents\r
+\r
+ def __len__(self):\r
+ return len(object.__getattribute__(self, 'data'))\r
+\r
+ def writeToStream(self, stream, indent, container):\r
+ """\r
+ Write this instance to a stream at the specified indentation level.\r
+\r
+ Should be redefined in subclasses.\r
+\r
+ @param stream: The stream to write to\r
+ @type stream: A writable stream (file-like object)\r
+ @param indent: The indentation level\r
+ @type indent: int\r
+ @param container: The container of this instance\r
+ @type container: L{Container}\r
+ """\r
+ indstr = indent * ' '\r
+ if len(self) == 0:\r
+ stream.write(' [ ]%s' % NEWLINE)\r
+ else:\r
+ if isinstance(container, Mapping):\r
+ stream.write(NEWLINE)\r
+ stream.write('%s[%s' % (indstr, NEWLINE))\r
+ self.save(stream, indent + 1)\r
+ stream.write('%s]%s' % (indstr, NEWLINE))\r
+\r
+ def save(self, stream, indent):\r
+ """\r
+ Save this instance to the specified stream.\r
+ @param stream: A stream to which the configuration is written.\r
+ @type stream: A write-only stream (file-like object).\r
+ @param indent: The indentation level for the output, > 0\r
+ @type indent: int\r
+ """\r
+ if indent == 0:\r
+ raise ConfigError("sequence cannot be saved as a top-level item")\r
+ data = object.__getattribute__(self, 'data')\r
+ comments = object.__getattribute__(self, 'comments')\r
+ indstr = indent * ' '\r
+ for i in xrange(0, len(data)):\r
+ value = data[i]\r
+ comment = comments[i]\r
+ if comment:\r
+ stream.write('%s#%s' % (indstr, comment))\r
+ if isinstance(value, Container):\r
+ value.writeToStream(stream, indent, self)\r
+ else:\r
+ self.writeValue(value, stream, indent)\r
+\r
+class Reference(object):\r
+ """\r
+ This internal class implements a value which is a reference to another value.\r
+ """\r
+ def __init__(self, config, type, ident):\r
+ """\r
+ Initialize an instance.\r
+\r
+ @param config: The configuration which contains this reference.\r
+ @type config: A L{Config} instance.\r
+ @param type: The type of reference.\r
+ @type type: BACKTICK or DOLLAR\r
+ @param ident: The identifier which starts the reference.\r
+ @type ident: str\r
+ """\r
+ self.config = config\r
+ self.type = type\r
+ self.elements = [ident]\r
+\r
+ def addElement(self, type, ident):\r
+ """\r
+ Add an element to the reference.\r
+\r
+ @param type: The type of reference.\r
+ @type type: BACKTICK or DOLLAR\r
+ @param ident: The identifier which continues the reference.\r
+ @type ident: str\r
+ """\r
+ self.elements.append((type, ident))\r
+\r
+ def findConfig(self, container):\r
+ """\r
+ Find the closest enclosing configuration to the specified container.\r
+\r
+ @param container: The container to start from.\r
+ @type container: L{Container}\r
+ @return: The closest enclosing configuration, or None.\r
+ @rtype: L{Config}\r
+ """\r
+ while (container is not None) and not isinstance(container, Config):\r
+ container = object.__getattribute__(container, 'parent')\r
+ return container\r
+\r
+ def resolve(self, container):\r
+ """\r
+ Resolve this instance in the context of a container.\r
+\r
+ @param container: The container to resolve from.\r
+ @type container: L{Container}\r
+ @return: The resolved value.\r
+ @rtype: any\r
+ @raise ConfigResolutionError: If resolution fails.\r
+ """\r
+ rv = None\r
+ path = object.__getattribute__(container, 'path')\r
+ current = self.findConfig(container)\r
+ while current is not None:\r
+ if self.type == BACKTICK:\r
+ namespaces = object.__getattribute__(current, 'namespaces')\r
+ found = False\r
+ s = str(self)[1:-1]\r
+ for ns in namespaces:\r
+ try:\r
+ try:\r
+ rv = eval(s, vars(ns))\r
+ except TypeError: #Python 2.7 - vars is a dictproxy\r
+ rv = eval(s, {}, vars(ns))\r
+ found = True\r
+ break\r
+ except:\r
+ logger.debug("unable to resolve %r in %r", s, ns)\r
+ pass\r
+ if found:\r
+ break\r
+ else:\r
+ firstkey = self.elements[0]\r
+ if firstkey in current.resolving:\r
+ current.resolving.remove(firstkey)\r
+ raise ConfigResolutionError("Circular reference: %r" % firstkey)\r
+ current.resolving.add(firstkey)\r
+ key = firstkey\r
+ try:\r
+ rv = current[key]\r
+ for item in self.elements[1:]:\r
+ key = item[1]\r
+ rv = rv[key]\r
+ current.resolving.remove(firstkey)\r
+ break\r
+ except ConfigResolutionError:\r
+ raise\r
+ except:\r
+ logger.debug("Unable to resolve %r: %s", key, sys.exc_info()[1])\r
+ rv = None\r
+ pass\r
+ current.resolving.discard(firstkey)\r
+ current = self.findConfig(object.__getattribute__(current, 'parent'))\r
+ if current is None:\r
+ raise ConfigResolutionError("unable to evaluate %r in the configuration %s" % (self, path))\r
+ return rv\r
+\r
+ def __str__(self):\r
+ s = self.elements[0]\r
+ for tt, tv in self.elements[1:]:\r
+ if tt == DOT:\r
+ s += '.%s' % tv\r
+ else:\r
+ s += '[%r]' % tv\r
+ if self.type == BACKTICK:\r
+ return BACKTICK + s + BACKTICK\r
+ else:\r
+ return DOLLAR + s\r
+\r
+ def __repr__(self):\r
+ return self.__str__()\r
+\r
+class Expression(object):\r
+ """\r
+ This internal class implements a value which is obtained by evaluating an expression.\r
+ """\r
+ def __init__(self, op, lhs, rhs):\r
+ """\r
+ Initialize an instance.\r
+\r
+ @param op: the operation expressed in the expression.\r
+ @type op: PLUS, MINUS, STAR, SLASH, MOD\r
+ @param lhs: the left-hand-side operand of the expression.\r
+ @type lhs: any Expression or primary value.\r
+ @param rhs: the right-hand-side operand of the expression.\r
+ @type rhs: any Expression or primary value.\r
+ """\r
+ self.op = op\r
+ self.lhs = lhs\r
+ self.rhs = rhs\r
+\r
+ def __str__(self):\r
+ return '%r %s %r' % (self.lhs, self.op, self.rhs)\r
+\r
+ def __repr__(self):\r
+ return self.__str__()\r
+\r
+ def evaluate(self, container):\r
+ """\r
+ Evaluate this instance in the context of a container.\r
+\r
+ @param container: The container to evaluate in from.\r
+ @type container: L{Container}\r
+ @return: The evaluated value.\r
+ @rtype: any\r
+ @raise ConfigResolutionError: If evaluation fails.\r
+ @raise ZeroDivideError: If division by zero occurs.\r
+ @raise TypeError: If the operation is invalid, e.g.\r
+ subtracting one string from another.\r
+ """\r
+ lhs = self.lhs\r
+ if isinstance(lhs, Reference):\r
+ lhs = lhs.resolve(container)\r
+ elif isinstance(lhs, Expression):\r
+ lhs = lhs.evaluate(container)\r
+ rhs = self.rhs\r
+ if isinstance(rhs, Reference):\r
+ rhs = rhs.resolve(container)\r
+ elif isinstance(rhs, Expression):\r
+ rhs = rhs.evaluate(container)\r
+ op = self.op\r
+ if op == PLUS:\r
+ rv = lhs + rhs\r
+ elif op == MINUS:\r
+ rv = lhs - rhs\r
+ elif op == STAR:\r
+ rv = lhs * rhs\r
+ elif op == SLASH:\r
+ rv = lhs / rhs\r
+ else:\r
+ rv = lhs % rhs\r
+ return rv\r
+\r
+class ConfigReader(object):\r
+ """\r
+ This internal class implements a parser for configurations.\r
+ """\r
+\r
+ def __init__(self, config):\r
+ self.filename = None\r
+ self.config = config\r
+ self.lineno = 0\r
+ self.colno = 0\r
+ self.lastc = None\r
+ self.last_token = None\r
+ self.commentchars = '#'\r
+ self.whitespace = ' \t\r\n'\r
+ self.quotes = '\'"'\r
+ self.punct = ':-+*/%,.{}[]()@`$'\r
+ self.digits = '0123456789'\r
+ self.wordchars = '%s' % WORDCHARS # make a copy\r
+ self.identchars = self.wordchars + self.digits\r
+ self.pbchars = []\r
+ self.pbtokens = []\r
+ self.comment = None\r
+\r
+ def location(self):\r
+ """\r
+ Return the current location (filename, line, column) in the stream\r
+ as a string.\r
+\r
+ Used when printing error messages,\r
+\r
+ @return: A string representing a location in the stream being read.\r
+ @rtype: str\r
+ """\r
+ return "%s(%d,%d)" % (self.filename, self.lineno, self.colno)\r
+\r
+ def getChar(self):\r
+ """\r
+ Get the next char from the stream. Update line and column numbers\r
+ appropriately.\r
+\r
+ @return: The next character from the stream.\r
+ @rtype: str\r
+ """\r
+ if self.pbchars:\r
+ c = self.pbchars.pop()\r
+ else:\r
+ c = self.stream.read(1)\r
+ self.colno += 1\r
+ if c == '\n':\r
+ self.lineno += 1\r
+ self.colno = 1\r
+ return c\r
+\r
+ def __repr__(self):\r
+ return "<ConfigReader at 0x%08x>" % id(self)\r
+\r
+ __str__ = __repr__\r
+\r
+ def getToken(self):\r
+ """\r
+ Get a token from the stream. String values are returned in a form\r
+ where you need to eval() the returned value to get the actual\r
+ string. The return value is (token_type, token_value).\r
+\r
+ Multiline string tokenizing is thanks to David Janes (BlogMatrix)\r
+\r
+ @return: The next token.\r
+ @rtype: A token tuple.\r
+ """\r
+ if self.pbtokens:\r
+ return self.pbtokens.pop()\r
+ stream = self.stream\r
+ self.comment = None\r
+ token = ''\r
+ tt = EOF\r
+ while True:\r
+ c = self.getChar()\r
+ if not c:\r
+ break\r
+ elif c == '#':\r
+ self.comment = stream.readline()\r
+ self.lineno += 1\r
+ continue\r
+ if c in self.quotes:\r
+ token = c\r
+ quote = c\r
+ tt = STRING\r
+ escaped = False\r
+ multiline = False\r
+ c1 = self.getChar()\r
+ if c1 == quote:\r
+ c2 = self.getChar()\r
+ if c2 == quote:\r
+ multiline = True\r
+ token += quote\r
+ token += quote\r
+ else:\r
+ self.pbchars.append(c2)\r
+ self.pbchars.append(c1)\r
+ else:\r
+ self.pbchars.append(c1)\r
+ while True:\r
+ c = self.getChar()\r
+ if not c:\r
+ break\r
+ token += c\r
+ if (c == quote) and not escaped:\r
+ if not multiline or (len(token) >= 6 and token.endswith(token[:3]) and token[-4] != '\\'):\r
+ break\r
+ if c == '\\':\r
+ escaped = not escaped\r
+ else:\r
+ escaped = False\r
+ if not c:\r
+ raise ConfigFormatError('%s: Unterminated quoted string: %r, %r' % (self.location(), token, c))\r
+ break\r
+ if c in self.whitespace:\r
+ self.lastc = c\r
+ continue\r
+ elif c in self.punct:\r
+ token = c\r
+ tt = c\r
+ if (self.lastc == ']') or (self.lastc in self.identchars):\r
+ if c == '[':\r
+ tt = LBRACK2\r
+ elif c == '(':\r
+ tt = LPAREN2\r
+ break\r
+ elif c in self.digits:\r
+ token = c\r
+ tt = NUMBER\r
+ in_exponent=False\r
+ while True:\r
+ c = self.getChar()\r
+ if not c:\r
+ break\r
+ if c in self.digits:\r
+ token += c\r
+ elif (c == '.') and token.find('.') < 0 and not in_exponent:\r
+ token += c\r
+ elif (c == '-') and token.find('-') < 0 and in_exponent:\r
+ token += c\r
+ elif (c in 'eE') and token.find('e') < 0 and\\r
+ token.find('E') < 0:\r
+ token += c\r
+ in_exponent = True\r
+ else:\r
+ if c and (c not in self.whitespace):\r
+ self.pbchars.append(c)\r
+ break\r
+ break\r
+ elif c in self.wordchars:\r
+ token = c\r
+ tt = WORD\r
+ c = self.getChar()\r
+ while c and (c in self.identchars):\r
+ token += c\r
+ c = self.getChar()\r
+ if c: # and c not in self.whitespace:\r
+ self.pbchars.append(c)\r
+ if token == "True":\r
+ tt = TRUE\r
+ elif token == "False":\r
+ tt = FALSE\r
+ elif token == "None":\r
+ tt = NONE\r
+ break\r
+ else:\r
+ raise ConfigFormatError('%s: Unexpected character: %r' % (self.location(), c))\r
+ if token:\r
+ self.lastc = token[-1]\r
+ else:\r
+ self.lastc = None\r
+ self.last_token = tt\r
+ return (tt, token)\r
+\r
+ def load(self, stream, parent=None, suffix=None):\r
+ """\r
+ Load the configuration from the specified stream.\r
+\r
+ @param stream: A stream from which to load the configuration.\r
+ @type stream: A stream (file-like object).\r
+ @param parent: The parent of the configuration (to which this reader\r
+ belongs) in the hierarchy. Specified when the configuration is\r
+ included in another one.\r
+ @type parent: A L{Container} instance.\r
+ @param suffix: The suffix of this configuration in the parent\r
+ configuration. Should be specified whenever the parent is not None.\r
+ @raise ConfigError: If parent is specified but suffix is not.\r
+ @raise ConfigFormatError: If there are syntax errors in the stream.\r
+ """\r
+ if parent is not None:\r
+ if suffix is None:\r
+ raise ConfigError("internal error: load called with parent but no suffix")\r
+ self.config.setPath(makePath(object.__getattribute__(parent, 'path'), suffix))\r
+ self.setStream(stream)\r
+ self.token = self.getToken()\r
+ self.parseMappingBody(self.config)\r
+ if self.token[0] != EOF:\r
+ raise ConfigFormatError('%s: expecting EOF, found %r' % (self.location(), self.token[1]))\r
+\r
+ def setStream(self, stream):\r
+ """\r
+ Set the stream to the specified value, and prepare to read from it.\r
+\r
+ @param stream: A stream from which to load the configuration.\r
+ @type stream: A stream (file-like object).\r
+ """\r
+ self.stream = stream\r
+ if hasattr(stream, 'name'):\r
+ filename = stream.name\r
+ else:\r
+ filename = '?'\r
+ self.filename = filename\r
+ self.lineno = 1\r
+ self.colno = 1\r
+\r
+ def match(self, t):\r
+ """\r
+ Ensure that the current token type matches the specified value, and\r
+ advance to the next token.\r
+\r
+ @param t: The token type to match.\r
+ @type t: A valid token type.\r
+ @return: The token which was last read from the stream before this\r
+ function is called.\r
+ @rtype: a token tuple - see L{getToken}.\r
+ @raise ConfigFormatError: If the token does not match what's expected.\r
+ """\r
+ if self.token[0] != t:\r
+ raise ConfigFormatError("%s: expecting %s, found %r" % (self.location(), t, self.token[1]))\r
+ rv = self.token\r
+ self.token = self.getToken()\r
+ return rv\r
+\r
+ def parseMappingBody(self, parent):\r
+ """\r
+ Parse the internals of a mapping, and add entries to the provided\r
+ L{Mapping}.\r
+\r
+ @param parent: The mapping to add entries to.\r
+ @type parent: A L{Mapping} instance.\r
+ """\r
+ while self.token[0] in [WORD, STRING]:\r
+ self.parseKeyValuePair(parent)\r
+\r
+ def parseKeyValuePair(self, parent):\r
+ """\r
+ Parse a key-value pair, and add it to the provided L{Mapping}.\r
+\r
+ @param parent: The mapping to add entries to.\r
+ @type parent: A L{Mapping} instance.\r
+ @raise ConfigFormatError: if a syntax error is found.\r
+ """\r
+ comment = self.comment\r
+ tt, tv = self.token\r
+ if tt == WORD:\r
+ key = tv\r
+ suffix = tv\r
+ elif tt == STRING:\r
+ key = eval(tv)\r
+ suffix = '[%s]' % tv\r
+ else:\r
+ msg = "%s: expecting word or string, found %r"\r
+ raise ConfigFormatError(msg % (self.location(), tv))\r
+ self.token = self.getToken()\r
+ # for now, we allow key on its own as a short form of key : True\r
+ if self.token[0] == COLON:\r
+ self.token = self.getToken()\r
+ value = self.parseValue(parent, suffix)\r
+ else:\r
+ value = True\r
+ try:\r
+ parent.addMapping(key, value, comment)\r
+ except Exception, e:\r
+ raise ConfigFormatError("%s: %s, %r" % (self.location(), e,\r
+ self.token[1]))\r
+ tt = self.token[0]\r
+ if tt not in [EOF, WORD, STRING, RCURLY, COMMA]:\r
+ msg = "%s: expecting one of EOF, WORD, STRING,\\r
+RCURLY, COMMA, found %r"\r
+ raise ConfigFormatError(msg % (self.location(), self.token[1]))\r
+ if tt == COMMA:\r
+ self.token = self.getToken()\r
+\r
+ def parseValue(self, parent, suffix):\r
+ """\r
+ Parse a value.\r
+\r
+ @param parent: The container to which the value will be added.\r
+ @type parent: A L{Container} instance.\r
+ @param suffix: The suffix for the value.\r
+ @type suffix: str\r
+ @return: The value\r
+ @rtype: any\r
+ @raise ConfigFormatError: if a syntax error is found.\r
+ """\r
+ tt = self.token[0]\r
+ if tt in [STRING, WORD, NUMBER, LPAREN, DOLLAR,\r
+ TRUE, FALSE, NONE, BACKTICK, MINUS]:\r
+ rv = self.parseScalar()\r
+ elif tt == LBRACK:\r
+ rv = self.parseSequence(parent, suffix)\r
+ elif tt in [LCURLY, AT]:\r
+ rv = self.parseMapping(parent, suffix)\r
+ else:\r
+ raise ConfigFormatError("%s: unexpected input: %r" %\r
+ (self.location(), self.token[1]))\r
+ return rv\r
+\r
+ def parseSequence(self, parent, suffix):\r
+ """\r
+ Parse a sequence.\r
+\r
+ @param parent: The container to which the sequence will be added.\r
+ @type parent: A L{Container} instance.\r
+ @param suffix: The suffix for the value.\r
+ @type suffix: str\r
+ @return: a L{Sequence} instance representing the sequence.\r
+ @rtype: L{Sequence}\r
+ @raise ConfigFormatError: if a syntax error is found.\r
+ """\r
+ rv = Sequence(parent)\r
+ rv.setPath(makePath(object.__getattribute__(parent, 'path'), suffix))\r
+ self.match(LBRACK)\r
+ comment = self.comment\r
+ tt = self.token[0]\r
+ while tt in [STRING, WORD, NUMBER, LCURLY, LBRACK, LPAREN, DOLLAR,\r
+ TRUE, FALSE, NONE, BACKTICK, MINUS]:\r
+ suffix = '[%d]' % len(rv)\r
+ value = self.parseValue(parent, suffix)\r
+ rv.append(value, comment)\r
+ tt = self.token[0]\r
+ comment = self.comment\r
+ if tt == COMMA:\r
+ self.match(COMMA)\r
+ tt = self.token[0]\r
+ comment = self.comment\r
+ continue\r
+ self.match(RBRACK)\r
+ return rv\r
+\r
+ def parseMapping(self, parent, suffix):\r
+ """\r
+ Parse a mapping.\r
+\r
+ @param parent: The container to which the mapping will be added.\r
+ @type parent: A L{Container} instance.\r
+ @param suffix: The suffix for the value.\r
+ @type suffix: str\r
+ @return: a L{Mapping} instance representing the mapping.\r
+ @rtype: L{Mapping}\r
+ @raise ConfigFormatError: if a syntax error is found.\r
+ """\r
+ if self.token[0] == LCURLY:\r
+ self.match(LCURLY)\r
+ rv = Mapping(parent)\r
+ rv.setPath(\r
+ makePath(object.__getattribute__(parent, 'path'), suffix))\r
+ self.parseMappingBody(rv)\r
+ self.match(RCURLY)\r
+ else:\r
+ self.match(AT)\r
+ tt, fn = self.match(STRING)\r
+ rv = Config(eval(fn), parent)\r
+ return rv\r
+\r
+ def parseScalar(self):\r
+ """\r
+ Parse a scalar - a terminal value such as a string or number, or\r
+ an L{Expression} or L{Reference}.\r
+\r
+ @return: the parsed scalar\r
+ @rtype: any scalar\r
+ @raise ConfigFormatError: if a syntax error is found.\r
+ """\r
+ lhs = self.parseTerm()\r
+ tt = self.token[0]\r
+ while tt in [PLUS, MINUS]:\r
+ self.match(tt)\r
+ rhs = self.parseTerm()\r
+ lhs = Expression(tt, lhs, rhs)\r
+ tt = self.token[0]\r
+ return lhs\r
+\r
+ def parseTerm(self):\r
+ """\r
+ Parse a term in an additive expression (a + b, a - b)\r
+\r
+ @return: the parsed term\r
+ @rtype: any scalar\r
+ @raise ConfigFormatError: if a syntax error is found.\r
+ """\r
+ lhs = self.parseFactor()\r
+ tt = self.token[0]\r
+ while tt in [STAR, SLASH, MOD]:\r
+ self.match(tt)\r
+ rhs = self.parseFactor()\r
+ lhs = Expression(tt, lhs, rhs)\r
+ tt = self.token[0]\r
+ return lhs\r
+\r
+ def parseFactor(self):\r
+ """\r
+ Parse a factor in an multiplicative expression (a * b, a / b, a % b)\r
+\r
+ @return: the parsed factor\r
+ @rtype: any scalar\r
+ @raise ConfigFormatError: if a syntax error is found.\r
+ """\r
+ tt = self.token[0]\r
+ if tt in [NUMBER, WORD, STRING, TRUE, FALSE, NONE]:\r
+ rv = self.token[1]\r
+ if tt != WORD:\r
+ rv = eval(rv)\r
+ self.match(tt)\r
+ elif tt == LPAREN:\r
+ self.match(LPAREN)\r
+ rv = self.parseScalar()\r
+ self.match(RPAREN)\r
+ elif tt == DOLLAR:\r
+ self.match(DOLLAR)\r
+ rv = self.parseReference(DOLLAR)\r
+ elif tt == BACKTICK:\r
+ self.match(BACKTICK)\r
+ rv = self.parseReference(BACKTICK)\r
+ self.match(BACKTICK)\r
+ elif tt == MINUS:\r
+ self.match(MINUS)\r
+ rv = -self.parseScalar()\r
+ else:\r
+ raise ConfigFormatError("%s: unexpected input: %r" %\r
+ (self.location(), self.token[1]))\r
+ return rv\r
+\r
+ def parseReference(self, type):\r
+ """\r
+ Parse a reference.\r
+\r
+ @return: the parsed reference\r
+ @rtype: L{Reference}\r
+ @raise ConfigFormatError: if a syntax error is found.\r
+ """\r
+ word = self.match(WORD)\r
+ rv = Reference(self.config, type, word[1])\r
+ while self.token[0] in [DOT, LBRACK2]:\r
+ self.parseSuffix(rv)\r
+ return rv\r
+\r
+ def parseSuffix(self, ref):\r
+ """\r
+ Parse a reference suffix.\r
+\r
+ @param ref: The reference of which this suffix is a part.\r
+ @type ref: L{Reference}.\r
+ @raise ConfigFormatError: if a syntax error is found.\r
+ """\r
+ tt = self.token[0]\r
+ if tt == DOT:\r
+ self.match(DOT)\r
+ word = self.match(WORD)\r
+ ref.addElement(DOT, word[1])\r
+ else:\r
+ self.match(LBRACK2)\r
+ tt, tv = self.token\r
+ if tt not in [NUMBER, STRING]:\r
+ raise ConfigFormatError("%s: expected number or string, found %r" % (self.location(), tv))\r
+ self.token = self.getToken()\r
+ tv = eval(tv)\r
+ self.match(RBRACK)\r
+ ref.addElement(LBRACK, tv)\r
+\r
+def defaultMergeResolve(map1, map2, key):\r
+ """\r
+ A default resolver for merge conflicts. Returns a string\r
+ indicating what action to take to resolve the conflict.\r
+\r
+ @param map1: The map being merged into.\r
+ @type map1: L{Mapping}.\r
+ @param map2: The map being used as the merge operand.\r
+ @type map2: L{Mapping}.\r
+ @param key: The key in map2 (which also exists in map1).\r
+ @type key: str\r
+ @return: One of "merge", "append", "mismatch" or "overwrite"\r
+ indicating what action should be taken. This should\r
+ be appropriate to the objects being merged - e.g.\r
+ there is no point returning "merge" if the two objects\r
+ are instances of L{Sequence}.\r
+ @rtype: str\r
+ """\r
+ obj1 = map1[key]\r
+ obj2 = map2[key]\r
+ if isinstance(obj1, Mapping) and isinstance(obj2, Mapping):\r
+ rv = "merge"\r
+ elif isinstance(obj1, Sequence) and isinstance(obj2, Sequence):\r
+ rv = "append"\r
+ else:\r
+ rv = "mismatch"\r
+ return rv\r
+\r
+def overwriteMergeResolve(map1, map2, key):\r
+ """\r
+ An overwriting resolver for merge conflicts. Calls L{defaultMergeResolve},\r
+ but where a "mismatch" is detected, returns "overwrite" instead.\r
+\r
+ @param map1: The map being merged into.\r
+ @type map1: L{Mapping}.\r
+ @param map2: The map being used as the merge operand.\r
+ @type map2: L{Mapping}.\r
+ @param key: The key in map2 (which also exists in map1).\r
+ @type key: str\r
+ """\r
+ rv = defaultMergeResolve(map1, map2, key)\r
+ if rv == "mismatch":\r
+ rv = "overwrite"\r
+ return rv\r
+\r
+class ConfigMerger(object):\r
+ """\r
+ This class is used for merging two configurations. If a key exists in the\r
+ merge operand but not the merge target, then the entry is copied from the\r
+ merge operand to the merge target. If a key exists in both configurations,\r
+ then a resolver (a callable) is called to decide how to handle the\r
+ conflict.\r
+ """\r
+\r
+ def __init__(self, resolver=defaultMergeResolve):\r
+ """\r
+ Initialise an instance.\r
+\r
+ @param resolver:\r
+ @type resolver: A callable which takes the argument list\r
+ (map1, map2, key) where map1 is the mapping being merged into,\r
+ map2 is the merge operand and key is the clashing key. The callable\r
+ should return a string indicating how the conflict should be resolved.\r
+ For possible return values, see L{defaultMergeResolve}. The default\r
+ value preserves the old behaviour\r
+ """\r
+ self.resolver = resolver\r
+\r
+ def merge(self, merged, mergee):\r
+ """\r
+ Merge two configurations. The second configuration is unchanged,\r
+ and the first is changed to reflect the results of the merge.\r
+\r
+ @param merged: The configuration to merge into.\r
+ @type merged: L{Config}.\r
+ @param mergee: The configuration to merge.\r
+ @type mergee: L{Config}.\r
+ """\r
+ self.mergeMapping(merged, mergee)\r
+\r
+ def mergeMapping(self, map1, map2):\r
+ """\r
+ Merge two mappings recursively. The second mapping is unchanged,\r
+ and the first is changed to reflect the results of the merge.\r
+\r
+ @param map1: The mapping to merge into.\r
+ @type map1: L{Mapping}.\r
+ @param map2: The mapping to merge.\r
+ @type map2: L{Mapping}.\r
+ """\r
+ keys = map1.keys()\r
+ for key in map2.keys():\r
+ if key not in keys:\r
+ map1[key] = map2[key]\r
+ else:\r
+ obj1 = map1[key]\r
+ obj2 = map2[key]\r
+ decision = self.resolver(map1, map2, key)\r
+ if decision == "merge":\r
+ self.mergeMapping(obj1, obj2)\r
+ elif decision == "append":\r
+ self.mergeSequence(obj1, obj2)\r
+ elif decision == "overwrite":\r
+ map1[key] = obj2\r
+ elif decision == "mismatch":\r
+ self.handleMismatch(obj1, obj2)\r
+ else:\r
+ msg = "unable to merge: don't know how to implement %r"\r
+ raise ValueError(msg % decision)\r
+\r
+ def mergeSequence(self, seq1, seq2):\r
+ """\r
+ Merge two sequences. The second sequence is unchanged,\r
+ and the first is changed to have the elements of the second\r
+ appended to it.\r
+\r
+ @param seq1: The sequence to merge into.\r
+ @type seq1: L{Sequence}.\r
+ @param seq2: The sequence to merge.\r
+ @type seq2: L{Sequence}.\r
+ """\r
+ data1 = object.__getattribute__(seq1, 'data')\r
+ data2 = object.__getattribute__(seq2, 'data')\r
+ for obj in data2:\r
+ data1.append(obj)\r
+ comment1 = object.__getattribute__(seq1, 'comments')\r
+ comment2 = object.__getattribute__(seq2, 'comments')\r
+ for obj in comment2:\r
+ comment1.append(obj)\r
+\r
+ def handleMismatch(self, obj1, obj2):\r
+ """\r
+ Handle a mismatch between two objects.\r
+\r
+ @param obj1: The object to merge into.\r
+ @type obj1: any\r
+ @param obj2: The object to merge.\r
+ @type obj2: any\r
+ """\r
+ raise ConfigError("unable to merge %r with %r" % (obj1, obj2))\r
+\r
+class ConfigList(list):\r
+ """\r
+ This class implements an ordered list of configurations and allows you\r
+ to try getting the configuration from each entry in turn, returning\r
+ the first successfully obtained value.\r
+ """\r
+\r
+ def getByPath(self, path):\r
+ """\r
+ Obtain a value from the first configuration in the list which defines\r
+ it.\r
+\r
+ @param path: The path of the value to retrieve.\r
+ @type path: str\r
+ @return: The value from the earliest configuration in the list which\r
+ defines it.\r
+ @rtype: any\r
+ @raise ConfigError: If no configuration in the list has an entry with\r
+ the specified path.\r
+ """\r
+ found = False\r
+ rv = None\r
+ for entry in self:\r
+ try:\r
+ rv = entry.getByPath(path)\r
+ found = True\r
+ break\r
+ except ConfigError:\r
+ pass\r
+ if not found:\r
+ raise ConfigError("unable to resolve %r" % path)\r
+ return rv\r
--- /dev/null
+# Configuration file for logconfig.py\r
+\r
+# root logger configuration\r
+root:\r
+{\r
+ level : `DEBUG`\r
+ handlers : [$handlers.console, $handlers.file]\r
+}\r
+formatters: {\r
+ brief:\r
+ {\r
+ format: '%(levelname)-8s: %(name)s: %(message)s'\r
+ }\r
+ precise:\r
+ {\r
+ format: '%(asctime)s %(name)-15s %(levelname)-8s %(message)s'\r
+ }\r
+}\r
+handlers:\r
+{\r
+ console:\r
+ {\r
+ class : `logconfig.StreamHandler`\r
+ config:\r
+ {\r
+ level : `INFO`\r
+ stream : `sys.stdout`\r
+ formatter: $formatters.brief\r
+ }\r
+ }\r
+ file:\r
+ {\r
+ class : `logconfig.RotatingFileHandler`\r
+ config:\r
+ {\r
+ name: 'logconfig.log'\r
+ maxBytes: 1024\r
+ backupCount: 3\r
+ formatter: $formatters.precise\r
+ }\r
+ }\r
+ debugfile:\r
+ {\r
+ class : `logconfig.FileHandler`\r
+ config:\r
+ {\r
+ name: 'logconfig-detail.log'\r
+ mode: 'a'\r
+ formatter: $formatters.precise\r
+ }\r
+ }\r
+}\r
+loggers:\r
+{\r
+ area1:\r
+ {\r
+ level : `ERROR`\r
+ handlers: [$handlers.debugfile]\r
+ }\r
+ area2:\r
+ {\r
+ level : `CRITICAL`\r
+ handlers: [$handlers.debugfile]\r
+ }\r
+}
\ No newline at end of file
--- /dev/null
+#!/usr/bin/env python\r
+#\r
+# Copyright 2001-2004 by Vinay Sajip. All Rights Reserved.\r
+#\r
+# Permission to use, copy, modify, and distribute this software and its\r
+# documentation for any purpose and without fee is hereby granted,\r
+# provided that the above copyright notice appear in all copies and that\r
+# both that copyright notice and this permission notice appear in\r
+# supporting documentation, and that the name of Vinay Sajip\r
+# not be used in advertising or publicity pertaining to distribution\r
+# of the software without specific, written prior permission.\r
+# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING\r
+# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL\r
+# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR\r
+# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER\r
+# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\r
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\r
+#\r
+# This file is part of the Python config distribution. See\r
+# http://www.red-dove.com/python_config.html\r
+#\r
+"""\r
+A test for the config module through seeing how to use it to configure logging.\r
+\r
+Copyright (C) 2004 Vinay Sajip. All Rights Reserved.\r
+"""\r
+\r
+from config import Config\r
+from optparse import OptionParser, get_prog_name\r
+from random import choice\r
+import logging\r
+import logging.handlers\r
+import sys\r
+\r
+class Usage(Exception):\r
+ pass\r
+\r
+class BaseHandler:\r
+ def __init__(self, config):\r
+ if 'level' in config:\r
+ self.setLevel(config.level)\r
+ if 'formatter' in config:\r
+ self.setFormatter(config.formatter)\r
+\r
+class StreamHandler(logging.StreamHandler, BaseHandler):\r
+ def __init__(self, config):\r
+ stream = config.get('stream')\r
+ logging.StreamHandler.__init__(self, stream)\r
+ BaseHandler.__init__(self, config)\r
+\r
+class RotatingFileHandler(logging.handlers.RotatingFileHandler, BaseHandler):\r
+ def __init__(self, config):\r
+ name = config.get('name')\r
+ if name is None:\r
+ raise ValueError('RotatingFileHandler: name not specified')\r
+ mode = config.get('mode', 'a')\r
+ maxBytes = config.get('maxBytes', 0)\r
+ backupCount = config.get('backupCount', 0)\r
+ logging.handlers.RotatingFileHandler.__init__(self, name, mode, maxBytes, backupCount)\r
+ BaseHandler.__init__(self, config)\r
+\r
+class FileHandler(logging.FileHandler, BaseHandler):\r
+ def __init__(self, config):\r
+ name = config.get('name')\r
+ if name is None:\r
+ raise ValueError('FileHandler: name not specified')\r
+ mode = config.get('mode', 'a')\r
+ logging.FileHandler.__init__(self, name, mode)\r
+ BaseHandler.__init__(self, config)\r
+\r
+def configLogger(logger, config):\r
+ for handler in logger.handlers:\r
+ logger.removeHandler(handler)\r
+ if 'level' in config:\r
+ logger.setLevel(config.level)\r
+ if 'handlers' in config:\r
+ for handler in config.handlers:\r
+ logger.addHandler(handler)\r
+\r
+def fileConfig(fname, *args, **kwargs):\r
+ cfg = Config(fname)\r
+ cfg.addNamespace(logging)\r
+ cfg.addNamespace(sys.modules[StreamHandler.__module__], 'logconfig')\r
+\r
+ for name in cfg.formatters.keys():\r
+ formatterConfig = cfg.formatters[name]\r
+ fmt = formatterConfig.get('format')\r
+ datefmt = formatterConfig.get('datefmt')\r
+ formatter = logging.Formatter(fmt, datefmt)\r
+ cfg.formatters[name] = formatter\r
+\r
+ for name in cfg.handlers.keys():\r
+ klass = cfg.handlers[name].get('class')\r
+ config = cfg.handlers[name].get('config')\r
+ cfg.handlers[name] = klass(config)\r
+\r
+ for name in cfg.loggers.keys():\r
+ loggerConfig = cfg.loggers[name]\r
+ logger = logging.getLogger(name)\r
+ configLogger(logger, loggerConfig)\r
+\r
+ if 'root' in cfg:\r
+ configLogger(logging.getLogger(''), cfg.root)\r
+\r
+def testConfig():\r
+ levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL]\r
+ loggers = ['', 'area1', 'area2']\r
+ for i in xrange(1000):\r
+ logger = logging.getLogger(choice(loggers))\r
+ level = choice(levels)\r
+ logger.log(level, "Message number %d", i)\r
+\r
+def main(args=None):\r
+ rv = 0\r
+ if args is None:\r
+ args = sys.argv[1:]\r
+ parser = OptionParser(usage="usage: %prog [options] CONFIG-FILE")\r
+\r
+ (options, args) = parser.parse_args(args)\r
+ try:\r
+ if len(args) == 0:\r
+ raise Usage("No configuration file specified")\r
+ fileConfig(args[0])\r
+ testConfig()\r
+ except Usage, e:\r
+ parser.print_help()\r
+ print "\n%s: error: %s" % (get_prog_name(), e)\r
+ rv = 1\r
+ except Exception, e:\r
+ print "\n%s: error: %s" % (get_prog_name(), e)\r
+ typ, val, tb = sys.exc_info()\r
+ import traceback\r
+ traceback.print_tb(tb)\r
+ rv = 2\r
+ return rv\r
+\r
+if __name__ == "__main__":\r
+ sys.exit(main())\r
--- /dev/null
+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"],
+ )
--- /dev/null
+{
+ "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]
+ }]
+ ]
+}
--- /dev/null
+# Copyright 2004-2010 by Vinay Sajip. All Rights Reserved.\r
+#\r
+# Permission to use, copy, modify, and distribute this software and its\r
+# documentation for any purpose and without fee is hereby granted,\r
+# provided that the above copyright notice appear in all copies and that\r
+# both that copyright notice and this permission notice appear in\r
+# supporting documentation, and that the name of Vinay Sajip\r
+# not be used in advertising or publicity pertaining to distribution\r
+# of the software without specific, written prior permission.\r
+# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING\r
+# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL\r
+# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR\r
+# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER\r
+# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\r
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\r
+\r
+"""\r
+Test harness for the configuration module 'config' for Python.\r
+"""\r
+\r
+import unittest\r
+# import test_support\r
+import config\r
+from config import Config, ConfigMerger, ConfigList\r
+from config import ConfigError, ConfigFormatError, ConfigResolutionError\r
+import logging\r
+from StringIO import StringIO\r
+\r
+STREAMS = {\r
+ "simple_1" :\r
+"""\r
+message: 'Hello, world!'\r
+""",\r
+ "malformed_1" :\r
+"""\r
+123\r
+""",\r
+ "malformed_2" :\r
+"""\r
+[ 123, 'abc' ]\r
+""",\r
+ "malformed_3" :\r
+"""\r
+{ a : 7, b : 1.3, c : 'test' }\r
+""",\r
+ "malformed_4" :\r
+"""\r
+test: $a [7] # note space before bracket\r
+""",\r
+ "malformed_5" :\r
+"""\r
+test: 'abc'\r
+test: 'def'\r
+""",\r
+ "wellformed_1" :\r
+"""\r
+test: $a[7] # note no space before bracket\r
+""",\r
+ "boolean_1":\r
+"""\r
+test : False\r
+another_test: True\r
+""",\r
+ "boolean_2":\r
+"""\r
+test : false\r
+another_test: true\r
+""",\r
+ "none_1":\r
+"""\r
+test : None\r
+""",\r
+ "none_2":\r
+"""\r
+test : none\r
+""",\r
+ "number_1":\r
+"""\r
+root: 1\r
+stream: 1.7\r
+neg: -1\r
+negfloat: -2.0\r
+posexponent: 2.0999999e-08\r
+negexponent: -2.0999999e-08\r
+exponent: 2.0999999e08\r
+""",\r
+ "sequence_1":\r
+"""\r
+mixed: [ "VALIGN", [ 0, 0 ], [ -1, -1 ], "TOP" ]\r
+simple: [1, 2]\r
+nested: [1, [2, 3], [4, [5, 6]]]\r
+""",\r
+ "include_1":\r
+"""\r
+included: @'include_2'\r
+""",\r
+ "include_2":\r
+"""\r
+test: 123\r
+another_test: 'abc'\r
+""",\r
+ "expr_1":\r
+"""\r
+value1 : 10\r
+value2 : 5\r
+value3 : 'abc'\r
+value4 : 'ghi'\r
+value5 : 0\r
+value6 : { 'a' : $value1, 'b': $value2 }\r
+derived1 : $value1 + $value2\r
+derived2 : $value1 - $value2\r
+derived3 : $value1 * $value2\r
+derived4 : $value1 / $value2\r
+derived5 : $value1 % $value2\r
+derived6 : $value3 + $value4\r
+derived7 : $value3 + 'def' + $value4\r
+derived8 : $value3 - $value4 # meaningless\r
+derived9 : $value1 / $value5 # div by zero\r
+derived10 : $value1 % $value5 # div by zero\r
+derived11 : $value17 # doesn't exist\r
+derived12 : $value6.a + $value6.b\r
+""",\r
+ "eval_1":\r
+"""\r
+stderr : `sys.stderr`\r
+stdout : `sys.stdout`\r
+stdin : `sys.stdin`\r
+debug : `debug`\r
+DEBUG : `DEBUG`\r
+derived: $DEBUG * 10\r
+""",\r
+ "merge_1":\r
+"""\r
+value1: True\r
+value3: [1, 2, 3]\r
+value5: [ 7 ]\r
+value6: { 'a' : 1, 'c' : 3 }\r
+""",\r
+ "merge_2":\r
+"""\r
+value2: False\r
+value4: [4, 5, 6]\r
+value5: ['abc']\r
+value6: { 'b' : 2, 'd' : 4 }\r
+""",\r
+ "merge_3":\r
+"""\r
+value1: True\r
+value2: 3\r
+value3: [1, 3, 5]\r
+value4: [1, 3, 5]\r
+""",\r
+ "merge_4":\r
+"""\r
+value1: False\r
+value2: 4\r
+value3: [2, 4, 6]\r
+value4: [2, 4, 6]\r
+""",\r
+ "list_1":\r
+"""\r
+verbosity : 1\r
+""",\r
+ "list_2":\r
+"""\r
+verbosity : 2\r
+program_value: 4\r
+""",\r
+ "list_3":\r
+"""\r
+verbosity : 3\r
+suite_value: 5\r
+""",\r
+ "get_1":\r
+"""\r
+value1 : 123\r
+value2 : 'abcd'\r
+value3 : True\r
+value4 : None\r
+value5:\r
+{\r
+ value1 : 123\r
+ value2 : 'abcd'\r
+ value3 : True\r
+ value4 : None\r
+}\r
+""",\r
+ "multiline_1":\r
+"""\r
+value1: '''Value One\r
+Value Two\r
+'''\r
+value2: \"\"\"Value Three\r
+Value Four\"\"\"\r
+"""\r
+}\r
+\r
+def makeStream(name):\r
+ s = StringIO(STREAMS[name])\r
+ s.name = name\r
+ return s\r
+\r
+class OutStream(StringIO):\r
+ def close(self):\r
+ self.value = self.getvalue()\r
+ StringIO.close(self)\r
+\r
+class TestConfig(unittest.TestCase):\r
+\r
+ def setUp(self):\r
+ self.cfg = Config(None)\r
+\r
+ def tearDown(self):\r
+ del self.cfg\r
+\r
+ def testCreation(self):\r
+ self.assertEqual(0, len(self.cfg)) # should be empty\r
+\r
+ def testSimple(self):\r
+ self.cfg.load(makeStream("simple_1"))\r
+ self.failUnless('message' in self.cfg)\r
+ self.failIf('root' in self.cfg)\r
+ self.failIf('stream' in self.cfg)\r
+ self.failIf('load' in self.cfg)\r
+ self.failIf('save' in self.cfg)\r
+\r
+ def testValueOnly(self):\r
+ self.assertRaises(ConfigError, self.cfg.load,\r
+ makeStream("malformed_1"))\r
+ self.assertRaises(ConfigError, self.cfg.load,\r
+ makeStream("malformed_2"))\r
+ self.assertRaises(ConfigError, self.cfg.load,\r
+ makeStream("malformed_3"))\r
+\r
+ def testBadBracket(self):\r
+ self.assertRaises(ConfigError, self.cfg.load,\r
+ makeStream("malformed_4"))\r
+\r
+ def testDuplicate(self):\r
+ self.assertRaises(ConfigError, self.cfg.load,\r
+ makeStream("malformed_5"))\r
+\r
+ def testGoodBracket(self):\r
+ self.cfg.load(makeStream("wellformed_1"))\r
+\r
+ def testBoolean(self):\r
+ self.cfg.load(makeStream("boolean_1"))\r
+ self.assertEqual(True, self.cfg.another_test)\r
+ self.assertEqual(False, self.cfg.test)\r
+\r
+ def testNotBoolean(self):\r
+ self.cfg.load(makeStream("boolean_2"))\r
+ self.assertEqual('true', self.cfg.another_test)\r
+ self.assertEqual('false', self.cfg.test)\r
+\r
+ def testNone(self):\r
+ self.cfg.load(makeStream("none_1"))\r
+ self.assertEqual(None, self.cfg.test)\r
+\r
+ def testNotNone(self):\r
+ self.cfg.load(makeStream("none_2"))\r
+ self.assertEqual('none', self.cfg.test)\r
+\r
+ def testNumber(self):\r
+ self.cfg.load(makeStream("number_1"))\r
+ self.assertEqual(1, self.cfg.root)\r
+ self.assertEqual(1.7, self.cfg.stream)\r
+ self.assertEqual(-1, self.cfg.neg)\r
+ self.assertEqual(-2.0, self.cfg.negfloat)\r
+ self.assertAlmostEqual(-2.0999999e-08, self.cfg.negexponent)\r
+ self.assertAlmostEqual(2.0999999e-08, self.cfg.posexponent)\r
+ self.assertAlmostEqual(2.0999999e08, self.cfg.exponent)\r
+\r
+ def testChange(self):\r
+ self.cfg.load(makeStream("simple_1"))\r
+ self.cfg.message = 'Goodbye, cruel world!'\r
+ self.assertEqual('Goodbye, cruel world!', self.cfg.message)\r
+\r
+ def testSave(self):\r
+ self.cfg.load(makeStream("simple_1"))\r
+ self.cfg.message = 'Goodbye, cruel world!'\r
+ out = OutStream()\r
+ self.cfg.save(out)\r
+ self.assertEqual("message : 'Goodbye, cruel world!'" + config.NEWLINE,\r
+ out.value)\r
+\r
+ def testInclude(self):\r
+ config.streamOpener = makeStream\r
+ self.cfg = Config("include_1")\r
+ config.streamOpener = config.defaultStreamOpener\r
+ out = OutStream()\r
+ self.cfg.save(out)\r
+ s = "included :%s{%s test : 123%s another_test : 'abc'%s}%s" % (5 *\r
+ (config.NEWLINE,))\r
+ self.assertEqual(s, out.value)\r
+\r
+ def testExpression(self):\r
+ self.cfg.load(makeStream("expr_1"))\r
+ self.assertEqual(15, self.cfg.derived1)\r
+ self.assertEqual(5, self.cfg.derived2)\r
+ self.assertEqual(50, self.cfg.derived3)\r
+ self.assertEqual(2, self.cfg.derived4)\r
+ self.assertEqual(0, self.cfg.derived5)\r
+ self.assertEqual('abcghi', self.cfg.derived6)\r
+ self.assertEqual('abcdefghi', self.cfg.derived7)\r
+ self.assertRaises(TypeError, lambda x: x.derived8, self.cfg)\r
+ self.assertRaises(ZeroDivisionError, lambda x: x.derived9, self.cfg)\r
+ self.assertRaises(ZeroDivisionError, lambda x: x.derived10, self.cfg)\r
+ self.assertRaises(ConfigResolutionError,\r
+ lambda x: x.derived11, self.cfg)\r
+ self.assertEqual(15, self.cfg.derived12)\r
+\r
+ def testEval(self):\r
+ import sys, logging\r
+ self.cfg.load(makeStream("eval_1"))\r
+ self.assertEqual(sys.stderr, self.cfg.stderr)\r
+ self.assertEqual(sys.stdout, self.cfg.stdout)\r
+ self.assertEqual(sys.stdin, self.cfg.stdin)\r
+ self.assertRaises(ConfigResolutionError, lambda x: x.debug, self.cfg)\r
+ self.cfg.addNamespace(logging.Logger)\r
+ self.assertEqual(logging.Logger.debug.im_func, self.cfg.debug)\r
+ self.assertRaises(ConfigResolutionError, lambda x: x.DEBUG, self.cfg)\r
+ self.cfg.addNamespace(logging)\r
+ self.assertEqual(logging.DEBUG, self.cfg.DEBUG)\r
+ self.cfg.removeNamespace(logging.Logger)\r
+ self.assertEqual(logging.debug, self.cfg.debug)\r
+ self.assertEqual(logging.DEBUG * 10, self.cfg.derived)\r
+\r
+ def testFunctions(self):\r
+ makePath = config.makePath\r
+ isWord = config.isWord\r
+ self.assertEqual('suffix', makePath('', 'suffix'))\r
+ self.assertEqual('suffix', makePath(None, 'suffix'))\r
+ self.assertEqual('prefix.suffix', makePath('prefix', 'suffix'))\r
+ self.assertEqual('prefix[1]', makePath('prefix', '[1]'))\r
+ self.failUnless(isWord('a9'))\r
+ self.failUnless(isWord('9a')) #perverse, but there you go\r
+ self.failIf(isWord(9))\r
+ self.failIf(isWord(None))\r
+ self.failIf(isWord(self))\r
+ self.failIf(isWord(''))\r
+\r
+ def testMerge(self):\r
+ cfg1 = Config()\r
+ cfg1.load(makeStream("merge_1"))\r
+ cfg2 = Config(makeStream("merge_2"))\r
+ ConfigMerger().merge(cfg1, cfg2)\r
+ merged = cfg1\r
+ cfg1 = Config()\r
+ cfg1.load(makeStream("merge_1"))\r
+ for i in xrange(0, 5):\r
+ key = 'value%d' % (i + 1,)\r
+ self.failUnless(key in merged)\r
+ self.assertEqual(len(cfg1.value5) + len(cfg2.value5),\r
+ len(merged.value5))\r
+ cfg3 = Config()\r
+ cfg3.load(makeStream("merge_3"))\r
+ cfg4 = Config(makeStream("merge_4"))\r
+ merger = ConfigMerger()\r
+ self.assertRaises(ConfigError, merger.merge, cfg3, cfg4)\r
+\r
+ cfg3 = Config(makeStream("merge_3"))\r
+ cfg4 = Config(makeStream("merge_4"))\r
+ merger = ConfigMerger(config.overwriteMergeResolve)\r
+ merger.merge(cfg3, cfg4)\r
+ self.assertEqual(False, cfg3['value1'])\r
+ self.assertEqual(4, cfg3['value2'])\r
+\r
+ def customMergeResolve(map1, map2, key):\r
+ if key == "value3":\r
+ rv = "overwrite"\r
+ else:\r
+ rv = config.overwriteMergeResolve(map1, map2, key)\r
+ return rv\r
+\r
+ cfg3 = Config(makeStream("merge_3"))\r
+ cfg4 = Config(makeStream("merge_4"))\r
+ merger = ConfigMerger(customMergeResolve)\r
+ merger.merge(cfg3, cfg4)\r
+ self.assertEqual("[2, 4, 6]", str(cfg3.value3))\r
+ self.assertEqual("[1, 3, 5, 2, 4, 6]", str(cfg3.value4))\r
+\r
+ def testList(self):\r
+ list = ConfigList()\r
+ list.append(Config(makeStream("list_1")))\r
+ list.append(Config(makeStream("list_2")))\r
+ list.append(Config(makeStream("list_3")))\r
+ self.assertEqual(1, list.getByPath('verbosity'))\r
+ self.assertEqual(4, list.getByPath('program_value'))\r
+ self.assertEqual(5, list.getByPath('suite_value'))\r
+ self.assertRaises(ConfigError, list.getByPath, 'nonexistent_value')\r
+\r
+ def testGet(self):\r
+ cfg = self.cfg\r
+ cfg.load(makeStream("get_1"))\r
+ self.assertEqual(123, cfg.get('value1'))\r
+ self.assertEqual(123, cfg.get('value1', -123))\r
+ self.assertEqual(-123, cfg.get('value11', -123))\r
+ self.assertEqual('abcd', cfg.get('value2'))\r
+ self.failUnless(cfg.get('value3'))\r
+ self.failIf(cfg.get('value4') is not None)\r
+ self.assertEqual(123, cfg.value5.get('value1'))\r
+ self.assertEqual(123, cfg.value5.get('value1', -123))\r
+ self.assertEqual(-123, cfg.value5.get('value11', -123))\r
+ self.assertEqual('abcd', cfg.value5.get('value2'))\r
+ self.failUnless(cfg.value5.get('value3'))\r
+ self.failIf(cfg.value5.get('value4') is not None)\r
+\r
+ def testMultiline(self):\r
+ cfg = self.cfg\r
+ cfg.load(makeStream("multiline_1"))\r
+ self.assertEqual("Value One\nValue Two\n", cfg.get('value1'))\r
+ self.assertEqual("Value Three\nValue Four", cfg.get('value2'))\r
+\r
+ def testSequence(self):\r
+ cfg = self.cfg\r
+ strm = makeStream("sequence_1")\r
+ cfg.load(strm)\r
+ self.assertEqual(str(cfg.simple), "[1, 2]")\r
+ self.assertEqual(str(cfg.nested), "[1, [2, 3], [4, [5, 6]]]")\r
+ self.assertEqual(str(cfg.mixed), "['VALIGN', [0, 0], [-1, -1], 'TOP']")\r
+\r
+ def testJSON(self):\r
+ data = StringIO('dummy: ' + open('styles.json', 'r').read())\r
+ self.cfg.load(data)\r
+\r
+def init_logging():\r
+ logging.basicConfig(level=logging.DEBUG, filename="test_config.log",\r
+ filemode="w", format="%(asctime)s %(levelname)-5s %(name)-10s %(message)s")\r
+"""\r
+def test_main():\r
+ init_logging()\r
+ test_support.run_unittest(TestConfig)\r
+"""\r
+\r
+if __name__ == "__main__":\r
+ unittest.main(exit=False)\r
+ pass\r
+ # test_main()\r
--- /dev/null
+#!/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
+
--- /dev/null
+#!/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
# one shot setUp() for this TestCase
# DBG.push_debug(True)
SAT.setNotLocale() # test english
+ return
def test_010(self):
cmd = "sat --help"
# one shot tearDown() for this TestCase
SAT.setLocale() # end test english
# DBG.pop_debug()
+ return
if __name__ == '__main__':
unittest.main(exit=False)
--- /dev/null
+#!/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
+