From 2565306c95fec5b755679d3966ce9a55566303f4 Mon Sep 17 00:00:00 2001 From: Bryan Kowal Date: Wed, 6 Nov 2013 19:28:33 -0600 Subject: [PATCH] Issue #2540 - finish implementing the python override capability. The pure-python solution will be checked in later. Amend: addressed review comments Amend: update DR number Change-Id: I5ae0c1140e0c90be002e3e3711fc4311ce1a05f0 Former-commit-id: 8e1c0fb8eb3e42559c91fb7a534f91650f347a70 [formerly 8e1c0fb8eb3e42559c91fb7a534f91650f347a70 [formerly 315ca002de4045f0d7ae6b298012365a052a7430]] Former-commit-id: 75e6914df1c6e7f6b52ab2294c0fd305349a9fb3 Former-commit-id: 5c38f0bfd623ce684050a8333cd82783ba9d7c8f --- .../base/python/LocalizationUtil.py | 18 +- .../common_static/base/python/PathManager.py | 5 +- .../base/python/PythonOverrider.py | 165 +++++++++++++----- 3 files changed, 138 insertions(+), 50 deletions(-) diff --git a/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/LocalizationUtil.py b/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/LocalizationUtil.py index 2374c3b2a4..aaed389ccd 100644 --- a/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/LocalizationUtil.py +++ b/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/LocalizationUtil.py @@ -30,12 +30,16 @@ # Date Ticket# Engineer Description # ------------ ---------- ----------- -------------------------- # 03/12/13 mnash Initial Creation. +# 11/06/13 2086 bkowal Add the containing directory to the +# PYTHONPATH long enough to import +# the module. # # # import os import imp +import sys from com.raytheon.uf.common.localization import LocalizationContext_LocalizationType as JavaLocalizationType, LocalizationContext_LocalizationLevel as JavaLocalizationLevel @@ -80,7 +84,19 @@ def loadModule(filename): @summary: This function takes a filename and find the module, loads it and returns that module ''' + addedToPath = False + path = os.path.splitext(filename)[0] + directory = os.path.dirname(filename) + # ensure the module containing directory is on the python path. + if sys.path.count(directory) == 0: + sys.path.append(directory) + addedToPath = True filename = os.path.split(path)[1] fp, pathname, description = imp.find_module(filename) - return imp.load_module(filename, fp, pathname, description) \ No newline at end of file + module = imp.load_module(filename, fp, pathname, description) + + if addedToPath: + sys.path.remove(directory) + + return module \ No newline at end of file diff --git a/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/PathManager.py b/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/PathManager.py index c837793394..b14e774763 100644 --- a/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/PathManager.py +++ b/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/PathManager.py @@ -28,6 +28,7 @@ # Date Ticket# Engineer Description # ------------ ---------- ----------- -------------------------- # 03/18/13 mnash Initial Creation. +# 11/08/13 2086 bkowal Declare lists using [] instead of list() # # # @@ -110,7 +111,7 @@ class PathManager(IPathManager.IPathManager): else : jfiles = self.jpathManager.listStaticFiles(name, extArr, recursive, filesOnly) if jfiles is not None : - files = list() + files = [] for file in jfiles : files.append(LocalizationFile(file)) return files @@ -121,7 +122,7 @@ class PathManager(IPathManager.IPathManager): @summary: This method returns the list of available levels. ''' jLevels = self.jpathManager.getAvailableLevels() - levels = list() + levels = [] for level in jLevels : levels.append(level.name()) return levels diff --git a/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/PythonOverrider.py b/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/PythonOverrider.py index d000e0a75b..003d5e5c00 100644 --- a/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/PythonOverrider.py +++ b/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/PythonOverrider.py @@ -28,17 +28,19 @@ # Date Ticket# Engineer Description # ------------ ---------- ----------- -------------------------- # 03/12/13 mnash Initial Creation. +# 11/04/13 2086 bkowal Updated to merge classes - both legacy and non-legacy. +# Minimum to Maximum level of retrieval can now be specified. # # # import os import imp -import inspect +import types from PathManager import PathManager import LocalizationUtil -def override(name, loctype): +def importModule(name, loctype='COMMON_STATIC', level=None): """ Takes a name (filename and localization path) and the localization type and finds the file and overrides it, and returns the module @@ -46,20 +48,37 @@ def override(name, loctype): Args: name : the name and path of the file in localization loctype : a string representation of the localization type + level : a string representation of the localization level (BASE, SITE, etc.) Returns: a module that has all the correct methods after being overridden """ pathManager = PathManager() tieredFiles = pathManager.getTieredLocalizationFile(loctype, name) - levels = pathManager.getAvailableLevels() - lfiles = list() - for level in levels : - if tieredFiles.has_key(level) : - lfiles.append(tieredFiles[level].getPath()) + availableLevels = pathManager.getAvailableLevels() + if level == None: + levels = availableLevels + else: + # ensure that the specified level is actually a legitimate level + if level not in availableLevels: + raise LookupError('An invalid level has been specified!') + + levels = [] + try: + levelIdx = availableLevels.index(level) + levels = availableLevels[:levelIdx + 1] + except ValueError: + # ignore; the exception should never be thrown, we verify that the specified level + # is valid in the previous if statement + pass + + lfiles = [] + for _level in levels : + if _level in tieredFiles: + lfiles.append(tieredFiles[_level].getPath()) themodule = _internalOverride(lfiles) return themodule - + def _internalOverride(files): """ Takes the files and overrides them @@ -69,50 +88,102 @@ def _internalOverride(files): Returns: a new module that contains all the necessary elements - """ + """ themodule = imp.new_module('tmpmodule') # modules = list of all the modules for module in files : # load each module, temporarily tmpmodule = LocalizationUtil.loadModule(module) - the_module = _combineMembers(tmpmodule, themodule) + themodule = _combineMembers(tmpmodule, themodule) return themodule -def _combineMembers(tocombine, finalmodule): - """ - Loops over the necessary parts of each module and decides how to combine them - - Args: - tocombine : the module to combine in - finalmodule : the module that is being combined into - - Returns: - a new python module that was created above - """ - # get the functions - members = inspect.getmembers(tocombine, inspect.isfunction) - for member in members : - finalmodule.__setattr__(member[0], member[1]) - - # get the classes - classes = inspect.getmembers(tocombine, inspect.isclass) - for clazz in classes: - finalmodule.__setattr__(clazz[0], clazz[1]) - +def _combineMembers(tocombine, combinationresult): for attr in dir(tocombine): - if attr.startswith('__') == False or attr.startswith('_') == False: - if hasattr(finalmodule, attr): - if isinstance(attr, dict): - # simply update dicts with the new keys - getattr(finalmodule, attr).update(getattr(tocombine, attr)) - if isinstance(attr, list): - listattr = getattr(tocombine, attr) - for i in listattr: - # override each element in the list if it exists - getattr(finalmodule, attr)[i] = listattr[i] - else : - finalmodule.__setattr__(attr, getattr(tocombine, attr)) - else : - if inspect.ismethod(attr) == False and inspect.isbuiltin(attr) == False and inspect.isclass(attr) == False: - finalmodule.__setattr__(attr, getattr(tocombine, attr)) - return finalmodule + if attr.startswith('__') or attr.startswith('_') or isType(attr, types.BuiltinFunctionType): + # skip + continue + + # is the element a class? + if isType(getattr(tocombine, attr), types.ClassType): + combinationresult = _mergeClasses(tocombine, combinationresult, attr) + else: + # absolute override + combinationresult = _mergeAttributes(tocombine, combinationresult, attr) + + return combinationresult + +def _mergeClasses(source, target, className): + sourceClass = getattr(source, className) + targetClass = getattr(target, className, None) + + if (targetClass == None): + return _mergeAttributes(source, target, className) + + legacyMode = (hasattr(sourceClass, '__class__') == False) + + # verify that both classes are either legacy for current style. + if ((hasattr(targetClass, '__class__') == False) != legacyMode): + raise Exception("A legacy python class cannot be merged with a non-legacy python class!") + + # ensure that the classes are not exactly the same (breaks the legacy merge). + if compareClasses(sourceClass, targetClass): + # nothing to merge + return target + + for attr in dir(sourceClass): + # include private attributes because this is a class? + # methods cannot just be merged into a class, so skip them. + if isType(attr, types.BuiltinFunctionType) or isType(attr, types.MethodType) or \ + attr.startswith('__') or attr.startswith('_'): + continue + + # do we need to worry about nested classes? + if isType(getattr(sourceClass, attr), types.ClassType): + target = _mergeClasses(source, target, attr) + + attributeName = className + '.' + attr + target = _mergeAttributes(source, target, attributeName) + + # complete the merge / override of methods. + exec(_buildMergeDirective(className, legacyMode)) + return _mergeAttributes(source, target, className) + +def _buildMergeDirective(className, legacyMode): + if (legacyMode): + return 'source.' + className + '.__bases__ = (target.' + className + ',)' + else: + return 'source.' + className + ' = type("' + className + \ + '", (target.' + className + ',), dict(source.' + className + '.__dict__))' + +def isType(object, type): + return type(object) == type + +def compareClasses(clazz1, clazz2): + clazz1Attr = dir(clazz1) + clazz2Attr = dir(clazz2) + + if (len(clazz1Attr) != len(clazz2Attr)): + return False + + i = 0 + while i < len(clazz1Attr): + # compare the names + if (clazz1Attr[i] != clazz2Attr[i]): + return False + + # compare the attributes directly + attr1 = getattr(clazz1, clazz1Attr[i]) + attr2 = getattr(clazz2, clazz2Attr[i]) + if (attr1 != attr2): + return False + + i += 1 + + return True + +def _mergeAttributes(source, target, attributeName): + + mergeDirective = 'target.' + attributeName + ' = source.' + attributeName + exec(mergeDirective) + + return target \ No newline at end of file