From 38e0db898a4ae670a8fd132ccbe7df2fe870bf14 Mon Sep 17 00:00:00 2001 From: Bryan Kowal Date: Mon, 20 Jan 2014 13:05:25 -0600 Subject: [PATCH] Issue #2712 - improved class merging in Python Overrider. Created PythonOverriderInterface. Amend: Wrap ModuleUtil in try..finally to ensure changes to the path are undone Amend: Fixed formatting in PythonOverriderCore Change-Id: I8c4d2702ef634d2b038ba20ccffc610db8a6c580 Former-commit-id: 87a367ab05abc1a9c993096f2a62d452411e188a [formerly db00372029addfef69ba973712692eae1e572125] [formerly c85b15121014028f37b7be39beefd783223efc72] [formerly 87a367ab05abc1a9c993096f2a62d452411e188a [formerly db00372029addfef69ba973712692eae1e572125] [formerly c85b15121014028f37b7be39beefd783223efc72] [formerly d2e40770dd866683ab4cec23fdc064c0b4cccb43 [formerly c85b15121014028f37b7be39beefd783223efc72 [formerly b788b07aecdc4039ef0ce657d7d9a04ab88e0069]]]] Former-commit-id: d2e40770dd866683ab4cec23fdc064c0b4cccb43 Former-commit-id: 23a43fa42e636baa6ba04bd787564cd0123128fb [formerly 5c5ccfc65deba2da6389acbcd1b153ab5be94470] [formerly c7afdc35878088f48892c5546a83a73128a9ff77 [formerly cf82007019b3f710ec24370ac4e74bd6c22a3f24]] Former-commit-id: c979ede9026ddbdd6ecd96f6664ec6cae30a643e [formerly 29b48d3ef674817e269c2350d29444be09ee802d] Former-commit-id: 744084f871d5087687718cc118102a25d124b7e3 --- .../common_static/base/python/ModuleUtil.py | 20 ++--- .../base/python/PythonOverriderCore.py | 43 ++++++--- .../base/python/PythonOverriderInterface.py | 89 +++++++++++++++++++ .../base/python/MasterInterface.py | 6 +- 4 files changed, 133 insertions(+), 25 deletions(-) create mode 100644 edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/PythonOverriderInterface.py diff --git a/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/ModuleUtil.py b/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/ModuleUtil.py index 40bc2b9d39..60c3409fb1 100644 --- a/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/ModuleUtil.py +++ b/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/ModuleUtil.py @@ -29,6 +29,9 @@ # Date Ticket# Engineer Description # ------------ ---------- ----------- -------------------------- # 11/11/13 bkowal Initial Creation. +# 01/20/14 2712 bkowal Always add the directory path for +# the desired module to the beginning +# of the system path. # import os, sys, imp @@ -39,19 +42,16 @@ 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) - module = imp.load_module(filename, fp, pathname, description) - - if addedToPath: - sys.path.remove(directory) + sys.path.insert(0, directory) + try: + filename = os.path.split(path)[1] + fp, pathname, description = imp.find_module(filename) + module = imp.load_module(filename, fp, pathname, description) + finally: + sys.path.pop(0) return module \ No newline at end of file diff --git a/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/PythonOverriderCore.py b/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/PythonOverriderCore.py index 82c08542ee..bd082df239 100644 --- a/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/PythonOverriderCore.py +++ b/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/PythonOverriderCore.py @@ -30,11 +30,14 @@ # Date Ticket# Engineer Description # ------------ ---------- ----------- -------------------------- # 11/12/13 bkowal Initial Creation. +# 01/20/14 2712 bkowal Improve python class merging. Classes +# will now truly override each other +# instead of extending each other. # # # -import os, imp, types +import os, imp, types, inspect import ModuleUtil def _internalOverride(files): @@ -111,29 +114,41 @@ def _mergeClasses(source, target, className): 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 isinstance(attr, types.BuiltinFunctionType) \ - or isinstance(attr, types.MethodType) \ - or attr.startswith('__') or attr.startswith('_'): - continue + if isinstance(getattr(sourceClass, attr), types.BuiltinFunctionType) \ + or isinstance(getattr(sourceClass, attr), types.MethodType) \ + or attr.startswith('__'): + continue # do we need to worry about nested classes? if isinstance(getattr(sourceClass, attr), types.ClassType) \ - or isinstance(getattr(sourceClass, attr), types.TypeType): + or isinstance(getattr(sourceClass, attr), types.TypeType): target = _mergeClasses(source, target, attr) attributeName = className + '.' + attr target = _mergeAttributes(source, target, attributeName) + + # make new class "extend" the original class + for attr in dir(targetClass): + if attr != '__init__' \ + and isinstance(getattr(targetClass, attr), types.MethodType) \ + and not attr in dir(sourceClass): + # complete the merge / override of methods for any method that + # the new class does not implement + + # retrieve the implementation of the method (this is different from + # retrieving the method, itself) + exec('method = target.' + className + '.' + attr + '.im_func') + # copy the method implementation to the other class and give it + # the same name as the original + classMethodDirective = _buildReplaceClassMethodDirective(className, attr) + exec(classMethodDirective) - # 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 _buildReplaceClassMethodDirective(className, methodName): + replaceDirective = 'setattr(source.' + className + ', "' + methodName + '", method)' + + return replaceDirective def _compareClasses(clazz1, clazz2): clazz1Attr = dir(clazz1) diff --git a/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/PythonOverriderInterface.py b/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/PythonOverriderInterface.py new file mode 100644 index 0000000000..97c7e8e6c2 --- /dev/null +++ b/edexOsgi/com.raytheon.uf.common.localization.python/utility/common_static/base/python/PythonOverriderInterface.py @@ -0,0 +1,89 @@ +# # +# This software was developed and / or modified by Raytheon Company, +# pursuant to Contract DG133W-05-CQ-1067 with the US Government. +# +# U.S. EXPORT CONTROLLED TECHNICAL DATA +# This software product contains export-restricted data whose +# export/transfer/disclosure is restricted by U.S. law. Dissemination +# to non-U.S. persons whether in the United States or abroad requires +# an export license or other authorization. +# +# Contractor Name: Raytheon Company +# Contractor Address: 6825 Pine Street, Suite 340 +# Mail Stop B8 +# Omaha, NE 68106 +# 402.291.0100 +# +# See the AWIPS II Master Rights File ("Master Rights File.pdf") for +# further licensing information. +# # + +# +# A proxy to Python Overrider that utilizes the capabilities of the +# Python RollbackMasterInterface. The objective of this class is +# to prevent the MasterInterface from completing imports because the +# MasterInterface will overwrite modules instead of merging them. +# +# +# SOFTWARE HISTORY +# +# Date Ticket# Engineer Description +# ------------ ---------- ----------- -------------------------- +# 01/14/2014 #2766 bkowal Initial Creation. +# +# +# + +import os, sys, string, traceback +import RollbackMasterInterface +import PythonOverrider + +class PythonOverriderInterface(RollbackMasterInterface.RollbackMasterInterface): + def __init__(self, scriptPath, localizationPath=None): + super(PythonOverriderInterface, self).__init__(scriptPath) + self._localizationPath = localizationPath + + def importModules(self): + modulesToImport = [] + + for s in self._scriptPath.split(os.path.pathsep): + if os.path.exists(s): + scriptfiles = os.listdir(s) + + for filename in scriptfiles: + split = string.split(filename, ".") + if len(split) == 2 and len(split[0]) > 0 and split[1] == "py" and not filename.endswith("Interface.py"): + if not split[0] in modulesToImport: + modulesToImport.append(split[0]) + + for moduleName in modulesToImport: + self._importModule(moduleName) + + def addModule(self, moduleName): + if not moduleName in self.scripts: + self.scripts.append(moduleName) + self.reloadModules() + + def reloadModules(self): + for script in self.scripts: + # first remove all references to the existing module + if sys.modules.has_key(script): + self.clearModuleAttributes(script) + sys.modules.pop(script) + + # now use PythonOverrider to re-import the module + self._importModule(script) + + def _importModule(self, moduleName): + scriptName = moduleName + '.py' + if self._localizationPath: + scriptName = os.path.join(self._localizationPath, scriptName) + try: + importedModule = PythonOverrider.importModule(scriptName) + except Exception, e: + msg = moduleName + "\n" + traceback.format_exc() + self.addImportError(msg) + return + + if not moduleName in self.scripts: + self.scripts.append(moduleName) \ No newline at end of file diff --git a/edexOsgi/com.raytheon.uf.common.python/utility/common_static/base/python/MasterInterface.py b/edexOsgi/com.raytheon.uf.common.python/utility/common_static/base/python/MasterInterface.py index 40d6655c57..6fbd63863d 100644 --- a/edexOsgi/com.raytheon.uf.common.python/utility/common_static/base/python/MasterInterface.py +++ b/edexOsgi/com.raytheon.uf.common.python/utility/common_static/base/python/MasterInterface.py @@ -34,7 +34,8 @@ # 10/20/08 njensen Initial Creation. # 01/17/13 1486 dgilling Make a new-style class. # 09/23/13 16614 njensen Fixed reload method -# +# 01/20/14 2712 bkowal It is now possible to add errors +# from a subclass. # # @@ -141,6 +142,9 @@ class MasterInterface(object): self.__importErrors = [] return returnList + def addImportError(self, error): + self.__importErrors.append(error) + def reloadModule(self, moduleName): if sys.modules.has_key(moduleName): # From the python documentation: