From bb7248c414cb9aa77940f8a3f0e37f4969912adb Mon Sep 17 00:00:00 2001 From: Serge Rehbinder Date: Tue, 26 Jan 2016 15:11:58 +0100 Subject: [PATCH 1/1] salomeTools-5 FIRST COMMIT --- data/distrib.pyconf | 57 ++ sat | 1 + src/common/__init__.py | 23 + src/common/architecture.py | 144 +++ src/common/config_pyconf.py | 1693 +++++++++++++++++++++++++++++++++++ src/common/options.py | 168 ++++ src/common/printcolors.py | 112 +++ src/config.py | 214 +++++ src/salomeTools.py | 156 ++++ 9 files changed, 2568 insertions(+) create mode 100644 data/distrib.pyconf create mode 120000 sat create mode 100644 src/common/__init__.py create mode 100644 src/common/architecture.py create mode 100644 src/common/config_pyconf.py create mode 100644 src/common/options.py create mode 100755 src/common/printcolors.py create mode 100644 src/config.py create mode 100755 src/salomeTools.py diff --git a/data/distrib.pyconf b/data/distrib.pyconf new file mode 100644 index 0000000..18e6edd --- /dev/null +++ b/data/distrib.pyconf @@ -0,0 +1,57 @@ +#!/usr/bin/env python +#-*- coding:utf-8 -*- + +# distribution codes +# Code to use to replace the value returned by: lsb_release -si +# If no code is found an error will be raised. +DISTRIBUTIONS : +{ + "bullxServer": "BS" + "CentOS": "CO" + "Debian": "DB" + "Fedora": "FD" + "LinuxMint": "MN" + "Mageia": "MG" + "MandrivaLinux": "MD" + "RedHatEnterpriseServer": "RH" + "Ubuntu": "UB" + "openSUSE project":"OS" +} + +# versions substitution +# Code to use for release, to the replace value returned by: lsb_release -sr +# If no code is found the value is used. +VERSIONS : +{ + "DB": + { + "unstable": "sid" + "5.0": "05" + "4.0": "04" + "6.0.7" : "06" + "6.0.10": "06" + "7.1" : "07" + "7.5" : "07" + } + "MD": + { + "2008.0": "08" + "2010.0": "10" + "2010.1": "10" + "2010.2": "10" + "2007.1": "07" + "2006.0": "06" + "4" : "06" + } + "MG": + { + "3": "03" + "4": "04" + "5": "05" + "6": "06" + } + "CO": + { + "7.1.1503": "7.1" + } +} diff --git a/sat b/sat new file mode 120000 index 0000000..804194a --- /dev/null +++ b/sat @@ -0,0 +1 @@ +src/salomeTools.py \ No newline at end of file diff --git a/src/common/__init__.py b/src/common/__init__.py new file mode 100644 index 0000000..e6ddc37 --- /dev/null +++ b/src/common/__init__.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +#-*- coding:utf-8 -*- +# Copyright (C) 2010-2013 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 + + +from . import config_pyconf +from . import architecture +from . import printcolors +from . import options \ No newline at end of file diff --git a/src/common/architecture.py b/src/common/architecture.py new file mode 100644 index 0000000..241e50f --- /dev/null +++ b/src/common/architecture.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python +#-*- coding:utf-8 -*- +# Copyright (C) 2010-2013 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 + +''' +In this file : all the stuff that can change with the architecture on which SAT is running +''' + +import os, sys, platform, pwd + +def is_windows(): + '''method that checks windows OS + :rtype: boolean + ''' + return platform.system() == 'Windows' + +def get_user(): + '''method that gets the username that launched sat + :rtype: str + ''' + # In windows case, the USERNAME environment variable has to be set + if is_windows(): + if not os.environ.has_key('USERNAME'): + raise Exception('USERNAME environment variable not set') + return os.environ['USERNAME'] + else: # linux + return pwd.getpwuid(os.getuid())[0] + +def _lsb_release(args): + '''Get system information with lsb_release. + :param args str: The arguments to give to lsb_release. + :return: The distribution. + :rtype: str + ''' + try: + path = '/usr/local/bin:/usr/bin:/bin' + lsb_path = os.getenv("LSB_PATH") + if lsb_path is not None: + path = lsb_path + ":" + path + + from subprocess import Popen, PIPE + res = Popen(['lsb_release', args], env={'PATH': path}, stdout=PIPE).communicate()[0][:-1] + # in case of python3, convert byte to str + if isinstance(res, bytes): + res = res.decode() + return res + except OSError: + sys.stderr.write(_("lsb_release not installed\n")) + sys.stderr.write(_("You can define $LSB_PATH to give the path to lsb_release\n")) + sys.exit(-1) + +def get_distribution(codes): + '''Gets the code for the distribution + :param codes L{Mapping}: The map containing distribution correlation table. + :return: The distribution on which salomeTools is running, regarding the distribution correlation table contained in codes variable. + :rtype: str + ''' + if is_windows(): + return "Win" + + # Call to lsb_release + distrib = _lsb_release('-si') + if codes is not None and distrib in codes: + distrib = codes[distrib] + else: + sys.stderr.write(_("Unknown distribution: '%s'\n") % distrib) + sys.stderr.write(_("Please add your distribution to data/distrib.pyconf\n")) + sys.exit(-1) + + return distrib + + +def get_distrib_version(distrib, codes): + '''Gets the version of the distribution + :param distrib str: The distribution on which the version will be found. + :param codes L{Mapping}: The map containing distribution correlation table. + :return: The version of the distribution on which salomeTools is running, regarding the distribution correlation table contained in codes variable. + :rtype: str + ''' + + if is_windows(): + return platform.release() + + # Call to lsb_release + version = _lsb_release('-sr') + if distrib in codes: + if version in codes[distrib]: + version = codes[distrib][version] + + return version + +def get_nb_bit(): + '''Gets the number of bytes. + :return: the number of bytes of the OS on which salomeTools is running. + :rtype: str + ''' + + # The platform python module gives the answer + nb_bit = platform.architecture()[0] + if nb_bit == "64bit": + nb_bit = "64" + elif nb_bit == "32bit": + nb_bit = "32" + else: + sys.stderr.write(_("Unknown architecture: '%s'\n") % nb_bit) + sys.exit(-1) + + return nb_bit + +def get_python_version(): + '''Gets the version of the running python. + :return: the version of the running python. + :rtype: str + ''' + + # The platform python module gives the answer + return platform.python_version() + +def get_nb_proc(): + '''Gets the number of processors of the machine on which salomeTools is running. + :return: the number of processors. + :rtype: str + ''' + + try : + import multiprocessing + nb_proc=multiprocessing.cpu_count() + except : + nb_proc=int(os.sysconf('SC_NPROCESSORS_ONLN')) + return nb_proc \ No newline at end of file diff --git a/src/common/config_pyconf.py b/src/common/config_pyconf.py new file mode 100644 index 0000000..89b66bd --- /dev/null +++ b/src/common/config_pyconf.py @@ -0,0 +1,1693 @@ +#!/usr/bin/env python +#-*- coding:utf-8 -*- + + + +# Copyright 2004-2007 by Vinay Sajip. All Rights Reserved. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose and without fee is hereby granted, +# provided that the above copyright notice appear in all copies and that +# both that copyright notice and this permission notice appear in +# supporting documentation, and that the name of Vinay Sajip +# not be used in advertising or publicity pertaining to distribution +# of the software without specific, written prior permission. +# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL +# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR +# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER +# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +""" +This is a configuration module for Python. + +This module should work under Python versions >= 2.2, and cannot be used with +earlier versions since it uses new-style classes. + +Development and testing has only been carried out (so far) on Python 2.3.4 and +Python 2.4.2. See the test module (test_config.py) included in the +U{distribution} (follow the +download link). + +A simple example - with the example configuration file:: + + messages: + [ + { + stream : `sys.stderr` + message: 'Welcome' + name: 'Harry' + } + { + stream : `sys.stdout` + message: 'Welkom' + name: 'Ruud' + } + { + stream : $messages[0].stream + message: 'Bienvenue' + name: Yves + } + ] + +a program to read the configuration would be:: + + from config import Config + + f = file('simple.cfg') + cfg = Config(f) + for m in cfg.messages: + s = '%s, %s' % (m.message, m.name) + try: + print >> m.stream, s + except IOError, e: + print e + +which, when run, would yield the console output:: + + Welcome, Harry + Welkom, Ruud + Bienvenue, Yves + +See U{this tutorial} for more +information. + +#modified for salomeTools +@version: 0.3.7.1 + +@author: Vinay Sajip + +@copyright: Copyright (C) 2004-2007 Vinay Sajip. All Rights Reserved. + + +@var streamOpener: The default stream opener. This is a factory function which +takes a string (e.g. filename) and returns a stream suitable for reading. If +unable to open the stream, an IOError exception should be thrown. + +The default value of this variable is L{defaultStreamOpener}. For an example +of how it's used, see test_config.py (search for streamOpener). +""" + +__author__ = "Vinay Sajip " +__status__ = "alpha" +__version__ = "0.3.7.1" #modified for salomeTools +__date__ = "05 October 2007" + +import codecs +import os +import sys + +WORD = 'a' +NUMBER = '9' +STRING = '"' +EOF = '' +LCURLY = '{' +RCURLY = '}' +LBRACK = '[' +LBRACK2 = 'a[' +RBRACK = ']' +LPAREN = '(' +LPAREN2 = '((' +RPAREN = ')' +DOT = '.' +COMMA = ',' +COLON = ':' +AT = '@' +PLUS = '+' +MINUS = '-' +STAR = '*' +SLASH = '/' +MOD = '%' +BACKTICK = '`' +DOLLAR = '$' +TRUE = 'True' +FALSE = 'False' +NONE = 'None' + +WORDCHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_" + +if sys.platform == 'win32': + NEWLINE = '\r\n' +elif os.name == 'mac': + NEWLINE = '\r' +else: + NEWLINE = '\n' + +try: + has_utf32 = True +except: + has_utf32 = False + +class ConfigInputStream(object): + """ + An input stream which can read either ANSI files with default encoding + or Unicode files with BOMs. + + Handles UTF-8, UTF-16LE, UTF-16BE. Could handle UTF-32 if Python had + built-in support. + """ + def __init__(self, stream): + """ + Initialize an instance. + + @param stream: The underlying stream to be read. Should be seekable. + @type stream: A stream (file-like object). + """ + encoding = None + signature = stream.read(4) + used = -1 + if has_utf32: + if signature == codecs.BOM_UTF32_LE: + encoding = 'utf-32le' + elif signature == codecs.BOM_UTF32_BE: + encoding = 'utf-32be' + if encoding is None: + if signature[:3] == codecs.BOM_UTF8: + used = 3 + encoding = 'utf-8' + elif signature[:2] == codecs.BOM_UTF16_LE: + used = 2 + encoding = 'utf-16le' + elif signature[:2] == codecs.BOM_UTF16_BE: + used = 2 + encoding = 'utf-16be' + else: + used = 0 + if used >= 0: + stream.seek(used) + if encoding: + reader = codecs.getreader(encoding) + stream = reader(stream) + self.stream = stream + self.encoding = encoding + + def read(self, size): + if (size == 0) or (self.encoding is None): + rv = self.stream.read(size) + else: + rv = u'' + while size > 0: + rv += self.stream.read(1) + size -= 1 + return rv + + def close(self): + self.stream.close() + + def readline(self): + if self.encoding is None: + line = '' + else: + line = u'' + while True: + c = self.stream.read(1) + if isinstance(c, bytes): + c = c.decode() + if c: + line += c + if c == '\n': + break + return line + +class ConfigOutputStream(object): + """ + An output stream which can write either ANSI files with default encoding + or Unicode files with BOMs. + + Handles UTF-8, UTF-16LE, UTF-16BE. Could handle UTF-32 if Python had + built-in support. + """ + + def __init__(self, stream, encoding=None): + """ + Initialize an instance. + + @param stream: The underlying stream to be written. + @type stream: A stream (file-like object). + @param encoding: The desired encoding. + @type encoding: str + """ + if encoding is not None: + encoding = str(encoding).lower() + self.encoding = encoding + if encoding == "utf-8": + stream.write(codecs.BOM_UTF8) + elif encoding == "utf-16be": + stream.write(codecs.BOM_UTF16_BE) + elif encoding == "utf-16le": + stream.write(codecs.BOM_UTF16_LE) + elif encoding == "utf-32be": + stream.write(codecs.BOM_UTF32_BE) + elif encoding == "utf-32le": + stream.write(codecs.BOM_UTF32_LE) + + if encoding is not None: + writer = codecs.getwriter(encoding) + stream = writer(stream) + self.stream = stream + + def write(self, data): + self.stream.write(data) + + def flush(self): + self.stream.flush() + + def close(self): + self.stream.close() + +def defaultStreamOpener(name): + """ + This function returns a read-only stream, given its name. The name passed + in should correspond to an existing stream, otherwise an exception will be + raised. + + This is the default value of L{streamOpener}; assign your own callable to + streamOpener to return streams based on names. For example, you could use + urllib2.urlopen(). + + @param name: The name of a stream, most commonly a file name. + @type name: str + @return: A stream with the specified name. + @rtype: A read-only stream (file-like object) + """ + return ConfigInputStream(open(name, 'rb')) + +streamOpener = None + +__resolveOverwrite__ = True + +class ConfigError(Exception): + """ + This is the base class of exceptions raised by this module. + """ + pass + +class ConfigFormatError(ConfigError): + """ + This is the base class of exceptions raised due to syntax errors in + configurations. + """ + pass + +class ConfigResolutionError(ConfigError): + """ + This is the base class of exceptions raised due to semantic errors in + configurations. + """ + pass + +def isWord(s): + """ + See if a passed-in value is an identifier. If the value passed in is not a + string, False is returned. An identifier consists of alphanumerics or + underscore characters. + + Examples:: + + isWord('a word') ->False + isWord('award') -> True + isWord(9) -> False + isWord('a_b_c_') ->True + + @note: isWord('9abc') will return True - not exactly correct, but adequate + for the way it's used here. + + @param s: The name to be tested + @type s: any + @return: True if a word, else False + @rtype: bool + """ + if type(s) != type(''): + return False + s = s.replace('_', '') + return s.isalnum() + +def makePath(prefix, suffix): + """ + Make a path from a prefix and suffix. + + Examples:: + + makePath('', 'suffix') -> 'suffix' + makePath('prefix', 'suffix') -> 'prefix.suffix' + makePath('prefix', '[1]') -> 'prefix[1]' + + @param prefix: The prefix to use. If it evaluates as false, the suffix + is returned. + @type prefix: str + @param suffix: The suffix to use. It is either an identifier or an + index in brackets. + @type suffix: str + @return: The path concatenation of prefix and suffix, with a + dot if the suffix is not a bracketed index. + @rtype: str + + """ + if not prefix: + rv = suffix + elif suffix[0] == '[': + rv = prefix + suffix + else: + rv = prefix + '.' + suffix + return rv + + +class Container(object): + """ + This internal class is the base class for mappings and sequences. + + @ivar path: A string which describes how to get + to this instance from the root of the hierarchy. + + Example:: + + a.list.of[1].or['more'].elements + """ + def __init__(self, parent): + """ + Initialize an instance. + + @param parent: The parent of this instance in the hierarchy. + @type parent: A L{Container} instance. + """ + object.__setattr__(self, 'parent', parent) + + def setPath(self, path): + """ + Set the path for this instance. + @param path: The path - a string which describes how to get + to this instance from the root of the hierarchy. + @type path: str + """ + object.__setattr__(self, 'path', path) + + def evaluate(self, item): + """ + Evaluate items which are instances of L{Reference} or L{Expression}. + + L{Reference} instances are evaluated using L{Reference.resolve}, + and L{Expression} instances are evaluated using + L{Expression.evaluate}. + + @param item: The item to be evaluated. + @type item: any + @return: If the item is an instance of L{Reference} or L{Expression}, + the evaluated value is returned, otherwise the item is returned + unchanged. + """ + if isinstance(item, Reference): + item = item.resolve(self) + elif isinstance(item, Expression): + item = item.evaluate(self) + return item + + def writeToStream(self, stream, indent, container): + """ + Write this instance to a stream at the specified indentation level. + + Should be redefined in subclasses. + + @param stream: The stream to write to + @type stream: A writable stream (file-like object) + @param indent: The indentation level + @type indent: int + @param container: The container of this instance + @type container: L{Container} + @raise NotImplementedError: If a subclass does not override this + """ + raise NotImplementedError + + def writeValue(self, value, stream, indent): + if isinstance(self, Mapping): + indstr = ' ' + else: + indstr = indent * ' ' + if isinstance(value, Reference) or isinstance(value, Expression): + stream.write('%s%r%s' % (indstr, value, NEWLINE)) + else: + if isinstance(value, str): # and not isWord(value): + value = repr(value) + stream.write('%s%s%s' % (indstr, value, NEWLINE)) + +class Mapping(Container): + """ + This internal class implements key-value mappings in configurations. + """ + + def __init__(self, parent=None): + """ + Initialize an instance. + + @param parent: The parent of this instance in the hierarchy. + @type parent: A L{Container} instance. + """ + Container.__init__(self, parent) + object.__setattr__(self, 'path', '') + object.__setattr__(self, 'data', {}) + object.__setattr__(self, 'order', []) # to preserve ordering + object.__setattr__(self, 'comments', {}) + + def __delitem__(self, key): + """ + Remove an item + """ + data = object.__getattribute__(self, 'data') + if key not in data: + raise AttributeError(key) + order = object.__getattribute__(self, 'order') + comments = object.__getattribute__(self, 'comments') + del data[key] + order.remove(key) + del comments[key] + + def __getitem__(self, key): + data = object.__getattribute__(self, 'data') + if key not in data: + raise AttributeError(key) + rv = data[key] + return self.evaluate(rv) + + __getattr__ = __getitem__ + + ''' + def __getattribute__(self, name): + if name == "__dict__": + return {} + if name in ["__methods__", "__members__"]: + return [] + #if name == "__class__": + # return '' + data = object.__getattribute__(self, "data") + useData = data.has_key(name) + if useData: + rv = getattr(data, name) + else: + rv = object.__getattribute__(self, name) + if rv is None: + raise AttributeError(name) + return rv + ''' + + def iteritems(self): + for key in self.keys(): + yield(key, self[key]) + raise StopIteration + + def __contains__(self, item): + order = object.__getattribute__(self, 'order') + return item in order + + def addMapping(self, key, value, comment, setting=False): + """ + Add a key-value mapping with a comment. + + @param key: The key for the mapping. + @type key: str + @param value: The value for the mapping. + @type value: any + @param comment: The comment for the key (can be None). + @type comment: str + @param setting: If True, ignore clashes. This is set + to true when called from L{__setattr__}. + @raise ConfigFormatError: If an existing key is seen + again and setting is False. + """ + data = object.__getattribute__(self, 'data') + order = object.__getattribute__(self, 'order') + comments = object.__getattribute__(self, 'comments') + + data[key] = value + if key not in order: + order.append(key) + elif not setting: + raise ConfigFormatError("repeated key: %s" % key) + comments[key] = comment + + def __setattr__(self, name, value): + self.addMapping(name, value, None, True) + + __setitem__ = __setattr__ + + def keys(self): + """ + Return the keys in a similar way to a dictionary. + """ + return object.__getattribute__(self, 'order') + + def get(self, key, default=None): + """ + Allows a dictionary-style get operation. + """ + if key in self: + return self[key] + return default + + def __str__(self): + return str(object.__getattribute__(self, 'data')) + + def __repr__(self): + return repr(object.__getattribute__(self, 'data')) + + def __len__(self): + return len(object.__getattribute__(self, 'order')) + + def __iter__(self): + return self.iterkeys() + + def iterkeys(self): + order = object.__getattribute__(self, 'order') + return order.__iter__() + + def writeToStream(self, stream, indent, container): + """ + Write this instance to a stream at the specified indentation level. + + Should be redefined in subclasses. + + @param stream: The stream to write to + @type stream: A writable stream (file-like object) + @param indent: The indentation level + @type indent: int + @param container: The container of this instance + @type container: L{Container} + """ + indstr = indent * ' ' + if len(self) == 0: + stream.write(' { }%s' % NEWLINE) + else: + if isinstance(container, Mapping): + stream.write(NEWLINE) + stream.write('%s{%s' % (indstr, NEWLINE)) + self.__save__(stream, indent + 1) + stream.write('%s}%s' % (indstr, NEWLINE)) + + def __save__(self, stream, indent=0): + """ + Save this configuration to the specified stream. + @param stream: A stream to which the configuration is written. + @type stream: A write-only stream (file-like object). + @param indent: The indentation level for the output. + @type indent: int + """ + indstr = indent * ' ' + order = object.__getattribute__(self, 'order') + data = object.__getattribute__(self, 'data') + maxlen = 0 # max(map(lambda x: len(x), order)) + for key in order: + comment = self.comments[key] + if isWord(key): + skey = key + else: + skey = repr(key) + if comment: + stream.write('%s#%s' % (indstr, comment)) + stream.write('%s%-*s :' % (indstr, maxlen, skey)) + value = data[key] + if isinstance(value, Container): + value.writeToStream(stream, indent, self) + else: + self.writeValue(value, stream, indent) + +class Config(Mapping): + """ + This class represents a configuration, and is the only one which clients + need to interface to, under normal circumstances. + """ + + class Namespace(object): + """ + This internal class is used for implementing default namespaces. + + An instance acts as a namespace. + """ + def __init__(self): + self.sys = sys + self.os = os + + def __init__(self, streamOrFile=None, parent=None): + """ + Initializes an instance. + + @param streamOrFile: If specified, causes this instance to be loaded + from the stream (by calling L{load}). If a string is provided, it is + passed to L{streamOpener} to open a stream. Otherwise, the passed + value is assumed to be a stream and used as is. + @type streamOrFile: A readable stream (file-like object) or a name. + @param parent: If specified, this becomes the parent of this instance + in the configuration hierarchy. + @type parent: a L{Container} instance. + """ + Mapping.__init__(self, parent) + object.__setattr__(self, 'reader', ConfigReader(self)) + object.__setattr__(self, 'namespaces', [Config.Namespace()]) + if streamOrFile is not None: + if isinstance(streamOrFile, str) or isinstance(streamOrFile, unicode): + global streamOpener + if streamOpener is None: + streamOpener = defaultStreamOpener + streamOrFile = streamOpener(streamOrFile) + load = object.__getattribute__(self, "load") + load(streamOrFile) + + def load(self, stream): + """ + Load the configuration from the specified stream. Multiple streams can + be used to populate the same instance, as long as there are no + clashing keys. The stream is closed. + @param stream: A stream from which the configuration is read. + @type stream: A read-only stream (file-like object). + @raise ConfigError: if keys in the loaded configuration clash with + existing keys. + @raise ConfigFormatError: if there is a syntax error in the stream. + """ + reader = object.__getattribute__(self, 'reader') + reader.load(stream) + stream.close() + + def addNamespace(self, ns, name=None): + """ + Add a namespace to this configuration which can be used to evaluate + (resolve) dotted-identifier expressions. + @param ns: The namespace to be added. + @type ns: A module or other namespace suitable for passing as an + argument to vars(). + @param name: A name for the namespace, which, if specified, provides + an additional level of indirection. + @type name: str + """ + namespaces = object.__getattribute__(self, 'namespaces') + if name is None: + namespaces.append(ns) + else: + setattr(namespaces[0], name, ns) + + def removeNamespace(self, ns, name=None): + """ + Remove a namespace added with L{addNamespace}. + @param ns: The namespace to be removed. + @param name: The name which was specified when L{addNamespace} was + called. + @type name: str + """ + namespaces = object.__getattribute__(self, 'namespaces') + if name is None: + namespaces.remove(ns) + else: + delattr(namespaces[0], name) + + def __save__(self, stream, indent=0, no_close=False): + """ + Save this configuration to the specified stream. The stream is + closed if this is the top-level configuration in the hierarchy. + L{Mapping.__save__} is called to do all the work. + @param stream: A stream to which the configuration is written. + @type stream: A write-only stream (file-like object). + @param indent: The indentation level for the output. + @type indent: int + """ + Mapping.__save__(self, stream, indent) + if indent == 0 and not no_close: + stream.close() + + def getByPath(self, path): + """ + Obtain a value in the configuration via its path. + @param path: The path of the required value + @type path: str + @return the value at the specified path. + @rtype: any + @raise ConfigError: If the path is invalid + """ + s = 'self.' + path + try: + return eval(s) + except Exception as e: + raise ConfigError(str(e)) + +class Sequence(Container): + """ + This internal class implements a value which is a sequence of other values. + """ + class SeqIter(object): + """ + This internal class implements an iterator for a L{Sequence} instance. + """ + def __init__(self, seq): + self.seq = seq + self.limit = len(object.__getattribute__(seq, 'data')) + self.index = 0 + + def __iter__(self): + return self + + def next(self): + if self.index >= self.limit: + raise StopIteration + rv = self.seq[self.index] + self.index += 1 + return rv + + def __init__(self, parent=None): + """ + Initialize an instance. + + @param parent: The parent of this instance in the hierarchy. + @type parent: A L{Container} instance. + """ + Container.__init__(self, parent) + object.__setattr__(self, 'data', []) + object.__setattr__(self, 'comments', []) + + def append(self, item, comment): + """ + Add an item to the sequence. + + @param item: The item to add. + @type item: any + @param comment: A comment for the item. + @type comment: str + """ + data = object.__getattribute__(self, 'data') + comments = object.__getattribute__(self, 'comments') + data.append(item) + comments.append(comment) + + def __getitem__(self, index): + data = object.__getattribute__(self, 'data') + try: + rv = data[index] + except (IndexError, KeyError, TypeError): + raise ConfigResolutionError('%r is not a valid index for %r' % (index, object.__getattribute__(self, 'path'))) + if not isinstance(rv, list): + rv = self.evaluate(rv) + else: + # deal with a slice + result = [] + for a in rv: + result.append(self.evaluate(a)) + rv = result + return rv + + def __iter__(self): + return Sequence.SeqIter(self) + + def __repr__(self): + return repr(object.__getattribute__(self, 'data')) + + def __str__(self): + return str(self[:]) # using the slice evaluates the contents + + def __len__(self): + return len(object.__getattribute__(self, 'data')) + + def writeToStream(self, stream, indent, container): + """ + Write this instance to a stream at the specified indentation level. + + Should be redefined in subclasses. + + @param stream: The stream to write to + @type stream: A writable stream (file-like object) + @param indent: The indentation level + @type indent: int + @param container: The container of this instance + @type container: L{Container} + """ + indstr = indent * ' ' + if len(self) == 0: + stream.write(' [ ]%s' % NEWLINE) + else: + if isinstance(container, Mapping): + stream.write(NEWLINE) + stream.write('%s[%s' % (indstr, NEWLINE)) + self.__save__(stream, indent + 1) + stream.write('%s]%s' % (indstr, NEWLINE)) + + def __save__(self, stream, indent): + """ + Save this instance to the specified stream. + @param stream: A stream to which the configuration is written. + @type stream: A write-only stream (file-like object). + @param indent: The indentation level for the output, > 0 + @type indent: int + """ + if indent == 0: + raise ConfigError("sequence cannot be saved as a top-level item") + data = object.__getattribute__(self, 'data') + comments = object.__getattribute__(self, 'comments') + indstr = indent * ' ' + for i in xrange(0, len(data)): + value = data[i] + comment = comments[i] + if comment: + stream.write('%s#%s' % (indstr, comment)) + if isinstance(value, Container): + value.writeToStream(stream, indent, self) + else: + self.writeValue(value, stream, indent) + +class Reference(object): + """ + This internal class implements a value which is a reference to another value. + """ + def __init__(self, config, type, ident): + """ + Initialize an instance. + + @param config: The configuration which contains this reference. + @type config: A L{Config} instance. + @param type: The type of reference. + @type type: BACKTICK or DOLLAR + @param ident: The identifier which starts the reference. + @type ident: str + """ + self.config = config + self.type = type + self.elements = [ident] + + def addElement(self, type, ident): + """ + Add an element to the reference. + + @param type: The type of reference. + @type type: BACKTICK or DOLLAR + @param ident: The identifier which continues the reference. + @type ident: str + """ + self.elements.append((type, ident)) + + def findConfig(self, container): + """ + Find the closest enclosing configuration to the specified container. + + @param container: The container to start from. + @type container: L{Container} + @return: The closest enclosing configuration, or None. + @rtype: L{Config} + """ + while (container is not None) and not isinstance(container, Config): + container = object.__getattribute__(container, 'parent') + return container + + def resolve(self, container): + """ + Resolve this instance in the context of a container. + + @param container: The container to resolve from. + @type container: L{Container} + @return: The resolved value. + @rtype: any + @raise ConfigResolutionError: If resolution fails. + """ + rv = None + path = object.__getattribute__(container, 'path') + current = container + while current is not None: + if self.type == BACKTICK: + namespaces = object.__getattribute__(current, 'namespaces') + found = False + for ns in namespaces: + try: + rv = eval(str(self)[1:-1], vars(ns)) + found = True + break + except: + pass + if found: + break + else: + key = self.elements[0] + try: + rv = current[key] + for item in self.elements[1:]: + key = item[1] + rv = rv[key] + break + except: + rv = None + pass + current = object.__getattribute__(current, 'parent') + if current is None: + raise ConfigResolutionError("unable to evaluate %r in the configuration %s" % (self, path)) + return rv + + def __str__(self): + s = self.elements[0] + for tt, tv in self.elements[1:]: + if tt == DOT: + s += '.%s' % tv + else: + s += '[%r]' % tv + if self.type == BACKTICK: + return BACKTICK + s + BACKTICK + else: + return DOLLAR + s + + def __repr__(self): + return self.__str__() + +class Expression(object): + """ + This internal class implements a value which is obtained by evaluating an expression. + """ + def __init__(self, op, lhs, rhs): + """ + Initialize an instance. + + @param op: the operation expressed in the expression. + @type op: PLUS, MINUS, STAR, SLASH, MOD + @param lhs: the left-hand-side operand of the expression. + @type lhs: any Expression or primary value. + @param rhs: the right-hand-side operand of the expression. + @type rhs: any Expression or primary value. + """ + self.op = op + self.lhs = lhs + self.rhs = rhs + + def __str__(self): + return '%r %s %r' % (self.lhs, self.op, self.rhs) + + def __repr__(self): + return self.__str__() + + def evaluate(self, container): + """ + Evaluate this instance in the context of a container. + + @param container: The container to evaluate in from. + @type container: L{Container} + @return: The evaluated value. + @rtype: any + @raise ConfigResolutionError: If evaluation fails. + @raise ZeroDivideError: If division by zero occurs. + @raise TypeError: If the operation is invalid, e.g. + subtracting one string from another. + """ + lhs = self.lhs + if isinstance(lhs, Reference): + lhs = lhs.resolve(container) + elif isinstance(lhs, Expression): + lhs = lhs.evaluate(container) + rhs = self.rhs + if isinstance(rhs, Reference): + rhs = rhs.resolve(container) + elif isinstance(rhs, Expression): + rhs = rhs.evaluate(container) + op = self.op + if op == PLUS: + rv = lhs + rhs + elif op == MINUS: + rv = lhs - rhs + elif op == STAR: + rv = lhs * rhs + elif op == SLASH: + rv = lhs / rhs + else: + rv = lhs % rhs + return rv + +class ConfigReader(object): + """ + This internal class implements a parser for configurations. + """ + + def __init__(self, config): + self.filename = None + self.config = config + self.lineno = 0 + self.colno = 0 + self.lastc = None + self.last_token = None + self.commentchars = '#' + self.whitespace = ' \t\r\n' + self.quotes = '\'"' + self.punct = ':-+*/%,.{}[]()@`$' + self.digits = '0123456789' + self.wordchars = '%s' % WORDCHARS # make a copy + self.identchars = self.wordchars + self.digits + self.pbchars = [] + self.pbtokens = [] + self.comment = None + + def location(self): + """ + Return the current location (filename, line, column) in the stream + as a string. + + Used when printing error messages, + + @return: A string representing a location in the stream being read. + @rtype: str + """ + return "%s(%d,%d)" % (self.filename, self.lineno, self.colno) + + def getChar(self): + """ + Get the next char from the stream. Update line and column numbers + appropriately. + + @return: The next character from the stream. + @rtype: str + """ + if self.pbchars: + c = self.pbchars.pop() + if isinstance(c,bytes): + c = c.decode() + else: + c = self.stream.read(1) + if isinstance(c,bytes): + c = c.decode() + self.colno += 1 + if c == '\n': + self.lineno += 1 + self.colno = 1 + return c + + def __repr__(self): + return "" % id(self) + + __str__ = __repr__ + + def getToken(self): + """ + Get a token from the stream. String values are returned in a form + where you need to eval() the returned value to get the actual + string. The return value is (token_type, token_value). + + Multiline string tokenizing is thanks to David Janes (BlogMatrix) + + @return: The next token. + @rtype: A token tuple. + """ + if self.pbtokens: + return self.pbtokens.pop() + stream = self.stream + self.comment = None + token = '' + tt = EOF + while True: + c = self.getChar() + if not c: + break + elif c == '#': + if self.comment : + self.comment += '#' + stream.readline() + else : + self.comment = stream.readline() + self.lineno += 1 + continue + if c in self.quotes: + token = c + quote = c + tt = STRING + escaped = False + multiline = False + c1 = self.getChar() + if c1 == quote: + c2 = self.getChar() + if c2 == quote: + multiline = True + token += quote + token += quote + else: + self.pbchars.append(c2) + self.pbchars.append(c1) + else: + self.pbchars.append(c1) + while True: + c = self.getChar() + if not c: + break + token += c + if (c == quote) and not escaped: + if not multiline or (len(token) >= 6 and token.endswith(token[:3]) and token[-4] != '\\'): + break + if c == '\\': + escaped = not escaped + else: + escaped = False + if not c: + raise ConfigFormatError('%s: Unterminated quoted string: %r, %r' % (self.location(), token, c)) + break + if c in self.whitespace: + self.lastc = c + continue + elif c in self.punct: + token = c + tt = c + if (self.lastc == ']') or (self.lastc in self.identchars): + if c == '[': + tt = LBRACK2 + elif c == '(': + tt = LPAREN2 + break + elif c in self.digits: + token = c + tt = NUMBER + while True: + c = self.getChar() + if not c: + break + if c in self.digits: + token += c + elif (c == '.') and token.find('.') < 0: + token += c + else: + if c and (c not in self.whitespace): + self.pbchars.append(c) + break + break + elif c in self.wordchars: + token = c + tt = WORD + c = self.getChar() + while c and (c in self.identchars): + token += c + c = self.getChar() + if c: # and c not in self.whitespace: + self.pbchars.append(c) + if token == "True": + tt = TRUE + elif token == "False": + tt = FALSE + elif token == "None": + tt = NONE + break + else: + raise ConfigFormatError('%s: Unexpected character: %r' % (self.location(), c)) + if token: + self.lastc = token[-1] + else: + self.lastc = None + self.last_token = tt + return (tt, token) + + def load(self, stream, parent=None, suffix=None): + """ + Load the configuration from the specified stream. + + @param stream: A stream from which to load the configuration. + @type stream: A stream (file-like object). + @param parent: The parent of the configuration (to which this reader + belongs) in the hierarchy. Specified when the configuration is + included in another one. + @type parent: A L{Container} instance. + @param suffix: The suffix of this configuration in the parent + configuration. Should be specified whenever the parent is not None. + @raise ConfigError: If parent is specified but suffix is not. + @raise ConfigFormatError: If there are syntax errors in the stream. + """ + if parent is not None: + if suffix is None: + raise ConfigError("internal error: load called with parent but no suffix") + self.config.setPath(makePath(object.__getattribute__(parent, 'path'), suffix)) + self.setStream(stream) + self.token = self.getToken() + self.parseMappingBody(self.config) + if self.token[0] != EOF: + raise ConfigFormatError('%s: expecting EOF, found %r' % (self.location(), self.token[1])) + + def setStream(self, stream): + """ + Set the stream to the specified value, and prepare to read from it. + + @param stream: A stream from which to load the configuration. + @type stream: A stream (file-like object). + """ + self.stream = stream + if hasattr(stream, 'name'): + filename = stream.name + else: + filename = '?' + self.filename = filename + self.lineno = 1 + self.colno = 1 + + def match(self, t): + """ + Ensure that the current token type matches the specified value, and + advance to the next token. + + @param t: The token type to match. + @type t: A valid token type. + @return: The token which was last read from the stream before this + function is called. + @rtype: a token tuple - see L{getToken}. + @raise ConfigFormatError: If the token does not match what's expected. + """ + if self.token[0] != t: + raise ConfigFormatError("%s: expecting %s, found %r" % (self.location(), t, self.token[1])) + rv = self.token + self.token = self.getToken() + return rv + + def parseMappingBody(self, parent): + """ + Parse the internals of a mapping, and add entries to the provided + L{Mapping}. + + @param parent: The mapping to add entries to. + @type parent: A L{Mapping} instance. + """ + while self.token[0] in [WORD, STRING]: + self.parseKeyValuePair(parent) + + def parseKeyValuePair(self, parent): + """ + Parse a key-value pair, and add it to the provided L{Mapping}. + + @param parent: The mapping to add entries to. + @type parent: A L{Mapping} instance. + @raise ConfigFormatError: if a syntax error is found. + """ + comment = self.comment + tt, tv = self.token + if tt == WORD: + key = tv + suffix = tv + elif tt == STRING: + key = eval(tv) + suffix = '[%s]' % tv + else: + msg = "%s: expecting word or string, found %r" + raise ConfigFormatError(msg % (self.location(), tv)) + self.token = self.getToken() + # for now, we allow key on its own as a short form of key : True + if self.token[0] == COLON: + self.token = self.getToken() + value = self.parseValue(parent, suffix) + else: + value = True + try: + parent.addMapping(key, value, comment) + except Exception as e: + raise ConfigFormatError("%s: %s, %r" % (self.location(), e, + self.token[1])) + tt = self.token[0] + if tt not in [EOF, WORD, STRING, RCURLY, COMMA]: + msg = "%s: expecting one of EOF, WORD, STRING, \ +RCURLY, COMMA, found %r" + raise ConfigFormatError(msg % (self.location(), self.token[1])) + if tt == COMMA: + self.token = self.getToken() + + def parseValue(self, parent, suffix): + """ + Parse a value. + + @param parent: The container to which the value will be added. + @type parent: A L{Container} instance. + @param suffix: The suffix for the value. + @type suffix: str + @return: The value + @rtype: any + @raise ConfigFormatError: if a syntax error is found. + """ + tt = self.token[0] + if tt in [STRING, WORD, NUMBER, LPAREN, DOLLAR, + TRUE, FALSE, NONE, BACKTICK, MINUS]: + rv = self.parseScalar() + elif tt == LBRACK: + rv = self.parseSequence(parent, suffix) + elif tt in [LCURLY, AT]: + rv = self.parseMapping(parent, suffix) + else: + raise ConfigFormatError("%s: unexpected input: %r" % + (self.location(), self.token[1])) + return rv + + def parseSequence(self, parent, suffix): + """ + Parse a sequence. + + @param parent: The container to which the sequence will be added. + @type parent: A L{Container} instance. + @param suffix: The suffix for the value. + @type suffix: str + @return: a L{Sequence} instance representing the sequence. + @rtype: L{Sequence} + @raise ConfigFormatError: if a syntax error is found. + """ + rv = Sequence(parent) + rv.setPath(makePath(object.__getattribute__(parent, 'path'), suffix)) + self.match(LBRACK) + comment = self.comment + tt = self.token[0] + while tt in [STRING, WORD, NUMBER, LCURLY, LBRACK, LPAREN, DOLLAR, + TRUE, FALSE, NONE, BACKTICK]: + suffix = '[%d]' % len(rv) + value = self.parseValue(parent, suffix) + rv.append(value, comment) + tt = self.token[0] + comment = self.comment + if tt == COMMA: + self.match(COMMA) + tt = self.token[0] + comment = self.comment + continue + self.match(RBRACK) + return rv + + def parseMapping(self, parent, suffix): + """ + Parse a mapping. + + @param parent: The container to which the mapping will be added. + @type parent: A L{Container} instance. + @param suffix: The suffix for the value. + @type suffix: str + @return: a L{Mapping} instance representing the mapping. + @rtype: L{Mapping} + @raise ConfigFormatError: if a syntax error is found. + """ + if self.token[0] == LCURLY: + self.match(LCURLY) + rv = Mapping(parent) + rv.setPath( + makePath(object.__getattribute__(parent, 'path'), suffix)) + self.parseMappingBody(rv) + self.match(RCURLY) + else: + self.match(AT) + _, fn = self.match(STRING) + rv = Config(eval(fn), parent) + return rv + + def parseScalar(self): + """ + Parse a scalar - a terminal value such as a string or number, or + an L{Expression} or L{Reference}. + + @return: the parsed scalar + @rtype: any scalar + @raise ConfigFormatError: if a syntax error is found. + """ + lhs = self.parseTerm() + tt = self.token[0] + while tt in [PLUS, MINUS]: + self.match(tt) + rhs = self.parseTerm() + lhs = Expression(tt, lhs, rhs) + tt = self.token[0] + return lhs + + def parseTerm(self): + """ + Parse a term in an additive expression (a + b, a - b) + + @return: the parsed term + @rtype: any scalar + @raise ConfigFormatError: if a syntax error is found. + """ + lhs = self.parseFactor() + tt = self.token[0] + while tt in [STAR, SLASH, MOD]: + self.match(tt) + rhs = self.parseFactor() + lhs = Expression(tt, lhs, rhs) + tt = self.token[0] + return lhs + + def parseFactor(self): + """ + Parse a factor in an multiplicative expression (a * b, a / b, a % b) + + @return: the parsed factor + @rtype: any scalar + @raise ConfigFormatError: if a syntax error is found. + """ + tt = self.token[0] + if tt in [NUMBER, WORD, STRING, TRUE, FALSE, NONE]: + rv = self.token[1] + if tt != WORD: + rv = eval(rv) + self.match(tt) + elif tt == LPAREN: + self.match(LPAREN) + rv = self.parseScalar() + self.match(RPAREN) + elif tt == DOLLAR: + self.match(DOLLAR) + rv = self.parseReference(DOLLAR) + elif tt == BACKTICK: + self.match(BACKTICK) + rv = self.parseReference(BACKTICK) + self.match(BACKTICK) + elif tt == MINUS: + self.match(MINUS) + rv = -self.parseScalar() + else: + raise ConfigFormatError("%s: unexpected input: %r" % + (self.location(), self.token[1])) + return rv + + def parseReference(self, type): + """ + Parse a reference. + + @return: the parsed reference + @rtype: L{Reference} + @raise ConfigFormatError: if a syntax error is found. + """ + word = self.match(WORD) + rv = Reference(self.config, type, word[1]) + while self.token[0] in [DOT, LBRACK2]: + self.parseSuffix(rv) + return rv + + def parseSuffix(self, ref): + """ + Parse a reference suffix. + + @param ref: The reference of which this suffix is a part. + @type ref: L{Reference}. + @raise ConfigFormatError: if a syntax error is found. + """ + tt = self.token[0] + if tt == DOT: + self.match(DOT) + word = self.match(WORD) + ref.addElement(DOT, word[1]) + else: + self.match(LBRACK2) + tt, tv = self.token + if tt not in [NUMBER, STRING]: + raise ConfigFormatError("%s: expected number or string, found %r" % (self.location(), tv)) + self.token = self.getToken() + tv = eval(tv) + self.match(RBRACK) + ref.addElement(LBRACK, tv) + +def defaultMergeResolve(map1, map2, key): + """ + A default resolver for merge conflicts. Returns a string + indicating what action to take to resolve the conflict. + + @param map1: The map being merged into. + @type map1: L{Mapping}. + @param map2: The map being used as the merge operand. + @type map2: L{Mapping}. + @param key: The key in map2 (which also exists in map1). + @type key: str + @return: One of "merge", "append", "mismatch" or "overwrite" + indicating what action should be taken. This should + be appropriate to the objects being merged - e.g. + there is no point returning "merge" if the two objects + are instances of L{Sequence}. + @rtype: str + """ + obj1 = map1[key] + obj2 = map2[key] + if isinstance(obj1, Mapping) and isinstance(obj2, Mapping): + rv = "merge" + elif isinstance(obj1, Sequence) and isinstance(obj2, Sequence): + rv = "append" + else: + rv = "mismatch" + return rv + +def overwriteMergeResolve(map1, map2, key): + """ + An overwriting resolver for merge conflicts. Calls L{defaultMergeResolve}, + but where a "mismatch" is detected, returns "overwrite" instead. + + @param map1: The map being merged into. + @type map1: L{Mapping}. + @param map2: The map being used as the merge operand. + @type map2: L{Mapping}. + @param key: The key in map2 (which also exists in map1). + @type key: str + """ + rv = defaultMergeResolve(map1, map2, key) + if rv == "mismatch": + rv = "overwrite" + return rv + +class ConfigMerger(object): + """ + This class is used for merging two configurations. If a key exists in the + merge operand but not the merge target, then the entry is copied from the + merge operand to the merge target. If a key exists in both configurations, + then a resolver (a callable) is called to decide how to handle the + conflict. + """ + + def __init__(self, resolver=defaultMergeResolve): + """ + Initialise an instance. + + @param resolver: + @type resolver: A callable which takes the argument list + (map1, map2, key) where map1 is the mapping being merged into, + map2 is the merge operand and key is the clashing key. The callable + should return a string indicating how the conflict should be resolved. + For possible return values, see L{defaultMergeResolve}. The default + value preserves the old behaviour + """ + self.resolver = resolver + + def merge(self, merged, mergee): + """ + Merge two configurations. The second configuration is unchanged, + and the first is changed to reflect the results of the merge. + + @param merged: The configuration to merge into. + @type merged: L{Config}. + @param mergee: The configuration to merge. + @type mergee: L{Config}. + """ + self.mergeMapping(merged, mergee) + + def overwriteKeys(self, map1, seq2): + """ + Renint variables. The second mapping is unchanged, + and the first is changed depending the keys of the second mapping. + @param map1: The mapping to reinit keys into. + @type map1: L{Mapping}. + @param map2: The mapping container reinit information. + @type map2: L{Mapping}. + """ + + overwrite_list = object.__getattribute__(seq2, 'data') + for overwrite_instruction in overwrite_list: + object.__setattr__(overwrite_instruction, 'parent', map1) + if "__condition__" in overwrite_instruction.keys(): + overwrite_condition = overwrite_instruction["__condition__"] + if eval(overwrite_condition, globals(), map1): + for key in overwrite_instruction.keys(): + if key == "__condition__": + continue + try: + exec( 'map1.' + key + " = " + repr(overwrite_instruction[key])) + except: + exec('map1.' + key + " = " + str(overwrite_instruction[key])) + else: + for key in overwrite_instruction.keys(): + try: + exec('map1.' + key + " = " + repr(overwrite_instruction[key])) + except: + exec('map1.' + key + " = " + str(overwrite_instruction[key])) + + def mergeMapping(self, map1, map2): + """ + Merge two mappings recursively. The second mapping is unchanged, + and the first is changed to reflect the results of the merge. + + @param map1: The mapping to merge into. + @type map1: L{Mapping}. + @param map2: The mapping to merge. + @type map2: L{Mapping}. + """ + keys = map1.keys() + global __resolveOverwrite__ + for key in map2.keys(): + if __resolveOverwrite__ and key == "__overwrite__": + self.overwriteKeys(map1,map2[key]) + + elif key not in keys: + map1[key] = map2[key] + if isinstance(map1[key], Container) : + object.__setattr__(map1[key], 'parent', map1) + else: + obj1 = map1[key] + obj2 = map2[key] + decision = self.resolver(map1, map2, key) + if decision == "merge": + self.mergeMapping(obj1, obj2) + elif decision == "append": + self.mergeSequence(obj1, obj2) + elif decision == "overwrite": + map1[key] = obj2 + if isinstance(map1[key], Container): + object.__setattr__(map1[key], 'parent', map1) + elif decision == "mismatch": + self.handleMismatch(obj1, obj2) + else: + msg = "unable to merge: don't know how to implement %r" + raise ValueError(msg % decision) + + def mergeSequence(self, seq1, seq2): + """ + Merge two sequences. The second sequence is unchanged, + and the first is changed to have the elements of the second + appended to it. + + @param seq1: The sequence to merge into. + @type seq1: L{Sequence}. + @param seq2: The sequence to merge. + @type seq2: L{Sequence}. + """ + data1 = object.__getattribute__(seq1, 'data') + data2 = object.__getattribute__(seq2, 'data') + for obj in data2: + data1.append(obj) + comment1 = object.__getattribute__(seq1, 'comments') + comment2 = object.__getattribute__(seq2, 'comments') + for obj in comment2: + comment1.append(obj) + + def handleMismatch(self, obj1, obj2): + """ + Handle a mismatch between two objects. + + @param obj1: The object to merge into. + @type obj1: any + @param obj2: The object to merge. + @type obj2: any + """ + raise ConfigError("unable to merge %r with %r" % (obj1, obj2)) + +class ConfigList(list): + """ + This class implements an ordered list of configurations and allows you + to try getting the configuration from each entry in turn, returning + the first successfully obtained value. + """ + + def getByPath(self, path): + """ + Obtain a value from the first configuration in the list which defines + it. + + @param path: The path of the value to retrieve. + @type path: str + @return: The value from the earliest configuration in the list which + defines it. + @rtype: any + @raise ConfigError: If no configuration in the list has an entry with + the specified path. + """ + found = False + rv = None + for entry in self: + try: + rv = entry.getByPath(path) + found = True + break + except ConfigError: + pass + if not found: + raise ConfigError("unable to resolve %r" % path) + return rv diff --git a/src/common/options.py b/src/common/options.py new file mode 100644 index 0000000..4a0f59d --- /dev/null +++ b/src/common/options.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python +#-*- coding:utf-8 -*- +# Copyright (C) 2010-2013 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 +'''The Options class that manages the access to all options passed as parameters in salomeTools command lines +''' +import getopt +import sys +from . import printcolors + +class OptResult(object): + '''An instance of this class will be the object manipulated in code of all salomeTools command + The aim of this class is to have an elegant syntax to manipulate the options. + ex: + print(options.level) + 5 + ''' + def __init__(self): + '''Initialization + ''' + self.__dict__ = dict() + + def __getattr__(self, name): + '''Overwrite of the __getattr__ function to customize it for option usage + :param name str: The attribute to get the value. + :return: the value corresponding to the attribute. + :rtype: str,int,list,boolean + ''' + if name in self.__dict__: + return self.__dict__[name] + else: + raise AttributeError(name + " is not a valid option") + + def __setattr__(self, name, value): + '''Overwrite of the __setattr__ function to customize it for option usage + :param name str: The attribute to set. + :param value str: The value corresponding to the attribute. + :return: Nothing. + :rtype: N\A + ''' + object.__setattr__(self,name,value) + +class Options: + '''Class to manage all salomeTools options + ''' + def __init__(self): + '''Initialization + ''' + # The options field stocks all options of a command in a list that contains dicts + self.options = [] + # The list of available option type + self.availableOptions = ["boolean", "string", "int", "float", "long", "list", "list2"] + + def add_option(self, shortName, longName, optionType, destName, helpString=""): + '''Method to add an option to a command. It gets all attributes of an option and append it in the options field + :param shortName str: The short name of the option (ex "l" for level option). + :param longName str: The long name of the option (ex "level" for level option). + :param optionType str: The type of the option (ex "int"). + :param destName str: The name that will be used in the code. + :param helpString str: The text to display when user ask for help on a command. + :return: Nothing. + :rtype: N\A + ''' + option = dict() + option['shortName'] = shortName + option['longName'] = longName + + if optionType not in self.availableOptions: + print("error optionType", optionType, "not available.") + sys.exit(-1) + + option['optionType'] = optionType + option['destName'] = destName + option['helpString'] = helpString + option['result'] = None + self.options.append(option) + + def print_help(self): + '''Method that display all options stored in self.options and there help + :return: Nothing. + :rtype: N\A + ''' + # Do nothing if there are no options + if len(self.options) == 0: + return + + # for all options, print its values. "shortname" is an optional field of the options + print(printcolors.printcHeader("Available options are:")) + for option in self.options: + if 'shortName' in option and len(option['shortName']) > 0: + print(" -%(shortName)1s, --%(longName)s (%(optionType)s)\n\t%(helpString)s\n" % option) + else: + print(" --%(longName)s (%(optionType)s)\n\t%(helpString)s\n" % option) + + def parse_args(self, argList=None): + '''Method that instantiates the class OptResult that gives access to all options in the code + :param argList list: the raw list of arguments that were passed + :return: optResult, args : optResult is the option instance to manipulate in the code. args is the full raw list of passed options + :rtype: (class 'common.options.OptResult',list) + ''' + if argList is None: + argList = sys.argv[1:] + + # format shortNameOption and longNameOption to make right arguments to getopt.getopt function + shortNameOption = "" + longNameOption = [] + for option in self.options: + shortNameOption = shortNameOption + option['shortName'] + if option['shortName'] != "" and option['optionType'] != "boolean": + shortNameOption = shortNameOption + ":" + + if option['longName'] != "": + if option['optionType'] != "boolean": + longNameOption.append(option['longName'] + "=") + else: + longNameOption.append(option['longName']) + + # call to getopt.getopt function to get the option passed in the command regarding the available options + optlist, args = getopt.getopt(argList, shortNameOption, longNameOption) + + # instantiate and completing the optResult that will be returned + optResult = OptResult() + for option in self.options: + shortOption = "-" + option['shortName'] + longOption = "--" + option['longName'] + optionType = option['optionType'] + for opt in optlist: + if opt[0] in [shortOption, longOption]: + if optionType == "string": + option['result'] = opt[1] + elif optionType == "boolean": + option['result'] = True + elif optionType == "int": + option['result'] = int(opt[1]) + elif optionType == "float": + option['result'] = float(opt[1]) + elif optionType == "long": + option['result'] = long(opt[1]) + elif optionType == "list": + if option['result'] is None: + option['result'] = list() + option['result'].append(opt[1]) + elif optionType == "list2": + if option['result'] is None: + option['result'] = list() + if opt[1].find(",") == -1: + option['result'].append(opt[1]) + else: + elts = filter(lambda l: len(l) > 0, opt[1].split(",")) + option['result'].extend(elts) + + optResult.__setattr__(option['destName'], option['result']) + + return optResult, args + diff --git a/src/common/printcolors.py b/src/common/printcolors.py new file mode 100755 index 0000000..bad832a --- /dev/null +++ b/src/common/printcolors.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +#-*- coding:utf-8 -*- +# Copyright (C) 2010-2013 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 +'''In this file is stored the mechanism that manage color prints in the terminal +''' + +# define constant to use in scripts +COLOR_ERROR = 'ERROR' +COLOR_WARNING = 'WARNING' +COLOR_SUCCESS = 'SUCCESS' +COLOR_LABEL = 'LABEL' +COLOR_HEADER = 'HEADER' +COLOR_INFO = 'INFO' +COLOR_HIGLIGHT = 'HIGHLIGHT' + +# the color map to use to print the colors +__colormap__ = { + COLOR_ERROR: '\033[1m\033[31m', + COLOR_SUCCESS: '\033[1m\033[32m', + COLOR_WARNING: '\033[33m', + COLOR_HEADER: '\033[34m', + COLOR_INFO: '\033[35m', + COLOR_LABEL: '\033[36m', + COLOR_HIGLIGHT: '\033[97m\033[43m' +} + +# list of available codes +__code_range__ = [1, 4] + list(range(30, 38)) + list(range(40, 48)) + list(range(90, 98)) + list(range(100, 108)) + +# print a text with colors +def printc(txt, code=''): + # no code means 'auto mode' + if code == '': + striptxt = txt.strip().upper() + if striptxt == "OK": + code = COLOR_SUCCESS + elif striptxt in ["KO", "NO"] or striptxt.startswith("ERR"): + code = COLOR_ERROR + else: + return txt + + # no code => output the originial text + if code not in __colormap__.keys() or __colormap__[code] == '': + return txt + + return __colormap__[code] + txt + '\033[0m' + +def printcInfo(txt): + return printc(txt, COLOR_INFO) + +def printcError(txt): + return printc(txt, COLOR_ERROR) + +def printcWarning(txt): + return printc(txt, COLOR_WARNING) + +def printcHeader(txt): + return printc(txt, COLOR_HEADER) + +def printcLabel(txt): + return printc(txt, COLOR_LABEL) + +def printcSuccess(txt): + return printc(txt, COLOR_SUCCESS) + +def printcHighlight(txt): + return printc(txt, COLOR_HIGLIGHT) + +def cleancolor(message): + message = message.replace('\033[0m', '') + for i in __code_range__: + message = message.replace('\033[%dm' % i, '') + return message + +# shortcut method to print a label and a value with the info color +def print_value(logger, label, value, level=1, suffix=""): + if logger is None: + print(" %s = %s %s" % (label, printcInfo(str(value)), suffix)) + else: + logger.write(" %s = %s %s\n" % (label, printcInfo(str(value)), suffix), level) + +def print_color_range(start, end): + for k in range(start, end+1): + print("\033[%dm%3d\033[0m" % (k, k),) + print + +# This method prints the color map +def print_color_map(): + print("colormap:") + print("{") + for k in sorted(__colormap__.keys()): + codes = __colormap__[k].split('\033[') + codes = filter(lambda l: len(l) > 0, codes) + codes = map(lambda l: l[:-1], codes) + print(printc(" %s: '%s', " % (k, ';'.join(codes)), k)) + print("}") + + diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..2ea6d18 --- /dev/null +++ b/src/config.py @@ -0,0 +1,214 @@ +#!/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 sys +import common +import platform +import datetime + +# Define all possible option for config command : sat config +parser = common.options.Options() +parser.add_option('v', 'value', 'string', 'value', "print the value of CONFIG_VARIABLE.") + +''' +class MergeHandler: + def __init__(self): + pass + + def __call__(self, map1, map2, key): + if '__overwrite__' in map2 and key in map2.__overwrite__: + return "overwrite" + else: + return common.config_pyconf.overwriteMergeResolve(map1, map2, key) +''' + +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'] = common.architecture.get_user() + var['salometoolsway'] = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + var['srcDir'] = os.path.join(var['salometoolsway'], 'src') + 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') + + # read linux distributions dictionary + distrib_cfg = common.config_pyconf.Config(os.path.join(var['dataDir'], "distrib.pyconf")) + + # set platform parameters + dist_name = common.architecture.get_distribution(codes=distrib_cfg.DISTRIBUTIONS) + dist_version = common.architecture.get_distrib_version(dist_name, codes=distrib_cfg.VERSIONS) + dist = dist_name + dist_version + + # Forcing architecture with env variable ARCH on Windows + if common.architecture.is_windows() and "ARCH" in os.environ : + bitsdict={"Win32":"32","Win64":"64"} + nb_bits = bitsdict[os.environ["ARCH"]] + else : + nb_bits = common.architecture.get_nb_bit() + + var['dist_name'] = dist_name + var['dist_version'] = dist_version + var['dist'] = dist + var['arch'] = dist + '_' + nb_bits + var['bits'] = nb_bits + var['python'] = common.architecture.get_python_version() + + var['nb_proc'] = common.architecture.get_nb_proc() + node_name = platform.node() + var['node'] = node_name + var['hostname'] = node_name + # particular win case + if common.architecture.is_windows() : + var['hostname'] = node_name+'-'+nb_bits + + # 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 common.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 : TO DO + :param sections str: The command that is called. + :return: The list of all the overwrites of the command line. + :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: + over.extend(filter(lambda l: l.startswith(section + "."), options.overwrite)) + return over + + def getConfig(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 TODO + :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 'common.config_pyconf.Config' + ''' + + # create a ConfigMerger to handle merge + merger = common.config_pyconf.ConfigMerger()#MergeHandler()) + + # create the configuration instance + cfg = common.config_pyconf.Config() + + # ======================================================================================= + # create VARS section + var = self._create_vars(application=application, command=command, dataDir=dataDir) + # add VARS to config + cfg.VARS = common.config_pyconf.Mapping(cfg) + for variable in var: + cfg.VARS[variable] = var[variable] + + for rule in self.get_command_line_overrides(options, ["VARS"]): + exec('cfg.' + rule) # this cannot be factorized because of the exec + + return cfg + + +def print_value(config, path, show_label, level=0, show_full_path=False): + '''Prints a value from the configuration. Prints recursively the values under the initial path. + :param config class 'common.config_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 level int: The number of spaces to add before display. + :param show_full_path : + :return: The final config. + :rtype: class 'common.config_pyconf.Config' + ''' + + # 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: + sys.stdout.write(tab_level) + sys.stdout.write("%s: ERROR %s\n" % (common.printcolors.printcLabel(vname), common.printcolors.printcError(str(e)))) + return + + # in this case, display only the value + if show_label: + sys.stdout.write(tab_level) + sys.stdout.write("%s: " % common.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: sys.stdout.write("\n") + for v in sorted(val.keys()): + print_value(config, path + '.' + v, show_label, level + 1) + elif val.__class__ == common.config_pyconf.Sequence or isinstance(val, list): # in this case, value is a list (or a Sequence) + if show_label: sys.stdout.write("\n") + index = 0 + for v in val: + print_value(config, path + "[" + str(index) + "]", show_label, level + 1) + index = index + 1 + else: # case where val is just a str + sys.stdout.write("%s\n" % val) + +def run(args, runner): + (options, args) = parser.parse_args(args) + print('Je suis dans la commande config ! Bien joué ! COUCOU') + if options.value: + print_value(runner.cfg, options.value, True, level=0, show_full_path=False) + + runner.config_copy('-v VARS') \ No newline at end of file diff --git a/src/salomeTools.py b/src/salomeTools.py new file mode 100755 index 0000000..bbfa571 --- /dev/null +++ b/src/salomeTools.py @@ -0,0 +1,156 @@ +#!/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 + +'''This file is the main entry file to salomeTools +''' + +import os +import sys +import imp +import types +import common.options +import config + +def copy_func(f, name=None): + ''' + return a function with same code, globals, defaults, closure, and + name (or provide a new name) + ''' + + fn = types.FunctionType(f.__code__, f.__globals__, name or f.__name__, + f.__defaults__, f.__closure__) + # in case f was given attrs (note this dict is a shallow copy): + fn.__dict__.update(f.__dict__) + return fn + +# get path to salomeTools sources +srcdir = os.path.dirname(os.path.realpath(__file__)) + +def find_command_list(): + cmd_list = [] + for item in os.listdir(srcdir): + if item.endswith('.py') and item!='salomeTools.py': + cmd_list.append(item[:-len('.py')]) + return cmd_list + +# The list of valid salomeTools commands +#lCommand = ['config', 'compile', 'prepare'] +lCommand = find_command_list() + +# Define all possible option for salomeTools command : sat