Module EDFactoryPlugin
[hide private]
[frames] | no frames]

Source Code for Module EDFactoryPlugin

  1  # coding: utf8 
  2  # 
  3  #    Project: The EDNA Kernel 
  4  #             http://www.edna-site.org 
  5  # 
  6  #    File: "$Id$" 
  7  # 
  8  #    Copyright (C) 2008-2011 European Synchrotron Radiation Facility 
  9  #                            Grenoble, France 
 10  # 
 11  #    Principal author:    Olof Svensson (svensson@esrf.fr)  
 12  # 
 13  #    Contributing author: Marie-Francoise Incardona (incardon@esrf.fr) 
 14  #                         Jérôme Kieffer (jerome.kieffer@esrf.fr) 
 15  # 
 16  #    This program is free software: you can redistribute it and/or modify 
 17  #    it under the terms of the GNU Lesser General Public License as published 
 18  #    by the Free Software Foundation, either version 3 of the License, or 
 19  #    (at your option) any later version. 
 20  # 
 21  #    This program is distributed in the hope that it will be useful, 
 22  #    but WITHOUT ANY WARRANTY; without even the implied warranty of 
 23  #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 24  #    GNU Lesser General Public License for more details. 
 25  # 
 26  #    You should have received a copy of the GNU General Public License 
 27  #    and the GNU Lesser General Public License  along with this program.   
 28  #    If not, see <http://www.gnu.org/licenses/>. 
 29  # 
 30  from __future__ import with_statement 
 31   
 32  __authors__ = ["Marie-Francoise Incardona", "Olof Svensson", "Jérôme Kieffer" ] 
 33  __contact__ = "svensson@esrf.fr" 
 34  __license__ = "LGPLv3+" 
 35  __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" 
 36  __date__ = "20120213" 
 37   
 38  import os, sys, hashlib 
 39  from EDThreading import Semaphore 
 40  from EDLogging   import EDLogging 
 41  from EDVerbose   import EDVerbose 
 42  from EDUtilsPath import EDUtilsPath 
 43  from EDModule    import EDModule 
 44  from XSDataCommon import XSDataDictionary 
 45  from XSDataCommon import XSDataKeyValuePair 
 46  from XSDataCommon import XSDataString 
47 48 -class EDFactoryPlugin(EDLogging):
49 """ 50 This class provides a factory for loading plugins and/or modules. By default all plugins/modules located 51 in $EDNA_HOME can be loaded with this class. A plugin or module located elsewhere can be loaded 52 provided one of its parent directories is added with the method "addPluginRootDirectory". 53 54 All plugins or modules with file name ending with \*.py are located. If the file name starts 55 with "EDPlugin" or "XSData" a warning is issued if multiple modules/plugins with the same name are located. 56 57 Both the loadPlugin and loadModule methods will automatically add the module location to the 58 application python path, if it not already exists on the path. 59 60 The EDFactoryPlugin also adds the path to a "src" directory if the following scheme for 61 locating the plugins is followed: 62 63 EDNA project, e.g. mxv1, mxExecPlugins etc:: 64 | 65 |-conf 66 |-datamodel 67 |-src 68 | |-XSData[project name].py 69 |-tests 70 | |-data 71 | |-testsuite 72 | | |-EDTestSuite[for the project] 73 | 74 |-plugins 75 | |-EDPlugin[name of plugin]-v1.0 76 | | |-plugins 77 | | | |-EDPlugin[name of plugin]v10.py 78 | | | |-EDPlugin[another name of plugin]v10.py 79 | | |-tests 80 | | | |-data 81 | | | |-testsuite 82 | | | | |-EDTestCase[for a plugin] 83 84 The "src" directory above is used for storing the data bindings for a project common data model, 85 e.g. XSDataMXv1.py, or in general for code used by all the plugins. The path to the "src" 86 directory is added using a relative path from the plugin location: ../../../src 87 The path to the src directory is only added if the src directory exists. 88 89 In order to improve speed, a cache of the modules and their location is saved to disk 90 the first time a module/plugin is located. The default location of this cache 91 file is in $EDNA_HOME, and the default name is ".XSDataDictionaryModule.xml". 92 93 If a cache files is present, and if a module/plugin cannot be loaded, the cache is 94 updated by searching all plugin root directories again. 95 96 If a directory contains the file ".ednaignore" in this directory and sub-directories are ignored. 97 """ 98 99 # class variables 100 IGNORE_FILE = ".ednaignore" 101 _dictLoadedModules = {} 102 __dictConfFiles = {None:None} 103 __dictProjectRootDir = {None:None} 104 __semaphoreStatic = Semaphore() 105 __edFactoryPlugin = None 106
107 - def __init__(self):
108 EDLogging.__init__(self) 109 # Default plugin root directory: $EDNA_HOME 110 self.__listPluginRootDirectory = [EDUtilsPath.EDNA_HOME] 111 for oneProjectDir in EDUtilsPath._EDNA_PROJECTS.values(): 112 if os.path.isdir(oneProjectDir): 113 self.__listPluginRootDirectory.append(os.path.abspath(oneProjectDir)) 114 self.__dictModuleLocation = None
115 116
117 - def __initModuleDictionary(self):
118 """ 119 This private method initialises the dictionary with all plugins. If the path to 120 the dictionary cache file exists the plugins are loaded, otherwise 121 the plugin root directories are searched and the dictionary is 122 written to the cache file. 123 """ 124 if (os.path.exists(EDUtilsPath.getEdnaPluginCachePath())): 125 self.loadModuleDictionaryFromDisk(EDUtilsPath.getEdnaPluginCachePath()) 126 else: 127 self.__searchRootDirectories() 128 self.saveModuleDictionaryToDisk(EDUtilsPath.getEdnaPluginCachePath())
129 130 131
132 - def saveModuleDictionaryToDisk(self, _strPath):
133 """ 134 This method saves the module dictionary to disk in form of XML. 135 This method should be private but is kept public in order to be unit tested. 136 137 @param _strPath: Path to the module dictionary XML file 138 @type _strPath: python string 139 """ 140 xsDataDictionaryPlugin = XSDataDictionary() 141 strEdnaHome = EDUtilsPath.EDNA_HOME 142 for strModule in self.__dictModuleLocation: 143 xsDataKeyValuePair = XSDataKeyValuePair() 144 xsDataKeyValuePair.setKey(XSDataString(strModule)) 145 strModuleLocation = self.__dictModuleLocation[ strModule ] 146 # Remove the path up to $EDNA_HOME 147 strModuleLocationStripped = strModuleLocation.replace(strEdnaHome, "") 148 if (strModuleLocationStripped.startswith("/") or strModuleLocationStripped.startswith("\\")): 149 strModuleLocationStripped = strModuleLocationStripped[1:] 150 xsDataKeyValuePair.setValue(XSDataString(strModuleLocationStripped)) 151 xsDataDictionaryPlugin.addKeyValuePair(xsDataKeyValuePair) 152 try: 153 xsDataDictionaryPlugin.exportToFile(_strPath) 154 except Exception: 155 self.warning("The module cache could not be written to disk.")
156 157
158 - def loadModuleDictionaryFromDisk(self, _strPath):
159 """ 160 Loads the cache from disk. 161 162 @param _strPath: Path to the module dictionary XML file 163 @type _strPath: python string 164 """ 165 #strPath = None 166 strEdnaHome = EDUtilsPath.EDNA_HOME 167 self.__dictModuleLocation = {} 168 try: 169 xsDataDictionaryPlugin = XSDataDictionary.parseFile(_strPath) 170 for xsDataKeyValuePair in xsDataDictionaryPlugin.getKeyValuePair(): 171 strModuleName = xsDataKeyValuePair.getKey().getValue() 172 strModuleLocationRelative = xsDataKeyValuePair.getValue().getValue() 173 strModuleLocationAbsolute = os.path.abspath(os.path.join(strEdnaHome, strModuleLocationRelative)) 174 if not os.path.exists(strModuleLocationAbsolute): 175 raise BaseException("Path loaded from disk does not exist: %s" % strModuleLocationAbsolute) 176 self.__dictModuleLocation[ strModuleName ] = strModuleLocationAbsolute 177 except BaseException, oExcpetionType: 178 self.warning("Error when reading module cache from disk: %s" % str(oExcpetionType)) 179 self.warning("Forcing reload of module locations.") 180 self.__searchRootDirectories() 181 self.saveModuleDictionaryToDisk(_strPath)
182 183
184 - def getModuleLocation(self, _strModuleName):
185 """ 186 This method returns the location of a module, e.g. XSDataCommon. 187 188 @param _strModuleName: Name of the module 189 @type _strModuleName: python string 190 191 @return: Path to the module location 192 @type: python string 193 """ 194 strModuleLocation = None 195 if (self.__dictModuleLocation is None): 196 with self.locked(): 197 if self.__dictModuleLocation is None: 198 self.__initModuleDictionary() 199 if (_strModuleName in self.__dictModuleLocation): 200 strModuleLocation = self.__dictModuleLocation[ _strModuleName ] 201 strDirectoryIgnored = self.checkDirectoriesForIgnoreFile(strModuleLocation) 202 if strDirectoryIgnored: 203 self.warning("Module location %s ignored because directory %s contains %s" % (strModuleLocation, strDirectoryIgnored, self.IGNORE_FILE)) 204 self.__searchRootDirectories() 205 self.saveModuleDictionaryToDisk(EDUtilsPath.getEdnaPluginCachePath()) 206 strModuleLocation = None 207 else: 208 with self.locked(): 209 # The module was not found - force reloading of all plugins 210 self.warning("Module %s not found, forcing reloading of all modules..." % _strModuleName) 211 self.__searchRootDirectories() 212 # Save the new dictionary in any case - even if the plugin might not be found. 213 self.saveModuleDictionaryToDisk(EDUtilsPath.getEdnaPluginCachePath()) 214 if (_strModuleName in self.__dictModuleLocation.keys()): 215 strModuleLocation = self.__dictModuleLocation[ _strModuleName ] 216 # Fix for bug #395 - update the saved cache 217 self.DEBUG("EDFactoryPlugin.loadModule: Updating the module cache file %s" % EDUtilsPath.getEdnaPluginCachePath()) 218 else: 219 self.DEBUG("EDFactoryPlugin.loadModule: module %s not found after forced reload of all modules." % _strModuleName) 220 return strModuleLocation
221 222
223 - def checkDirectoriesForIgnoreFile(self, _strDirectory):
224 # Check that this directory and all directories above till $EDNA_HOME should not be ignored 225 strDirectoryIgnored = None 226 bContinueSearching = True 227 strCurrentDirectory = _strDirectory 228 while bContinueSearching: 229 if os.path.exists(os.path.join(strCurrentDirectory, self.IGNORE_FILE)): 230 strDirectoryIgnored = strCurrentDirectory 231 bContinueSearching = False 232 # Move up a directory 233 strPreviousDirectory = strCurrentDirectory 234 strCurrentDirectory = os.path.dirname(strCurrentDirectory) 235 if strCurrentDirectory == EDUtilsPath.EDNA_HOME or strCurrentDirectory == strPreviousDirectory: 236 bContinueSearching = False 237 return strDirectoryIgnored
238 239 240
241 - def isPlugin(self, _strFileName):
242 """ 243 This method returns a True if the file name provided is considered to 244 be an EDNA plugin or module, i.e. it starts with either "EDPlugin" or "XSData" 245 and it ends with ".py", otherwise the method returns False. 246 247 @param _strFileName: Name of the file 248 @type _strFileName: python string 249 250 @return: True or False 251 @type: boolean 252 """ 253 bValue = False 254 if ((_strFileName.startswith("EDPlugin") or _strFileName.startswith("XSData")) and \ 255 _strFileName.endswith(".py")): 256 bValue = True 257 return bValue
258 259
260 - def __addPluginLocation(self, _strPluginRootDirectory, _strDirectoryVisit, _listDirectory):
261 """ 262 This method is called by the python walk command in the addPluginRootDirectory method. 263 264 It checks all the file names in the _listDirectory list if they corresponds to plugins or modules 265 using the method "isPlugin". If the file name corresponds to a plugin or module, the location of the 266 plugin or module is added to the self.__dictModuleLocation. 267 268 @param _strPluginRootDirectory: Name of the root directory 269 @type _strPluginRootDirectory: python string 270 271 @param _strDirectoryVisit: Name of the directory currently visited 272 @type _strDirectoryVisit: python string 273 274 @param _listDirectory: List of directory entries 275 @type _listDirectory: python list 276 """ 277 if (self.__dictModuleLocation is None): 278 self.__dictModuleLocation = {} 279 strDirectoryIgnored = self.checkDirectoriesForIgnoreFile(_strDirectoryVisit) 280 if strDirectoryIgnored: 281 self.DEBUG("Directory %s ignored because directory %s contains %s" % (_strDirectoryVisit, strDirectoryIgnored, self.IGNORE_FILE)) 282 else: 283 for strFileName in _listDirectory: 284 if strFileName.endswith(".py"): 285 # Strip off the ".py" exctension 286 strPluginName = strFileName[:-3] 287 if (strPluginName in self.__dictModuleLocation.keys() and self.isPlugin(strFileName)): 288 lstError = ["EDFactoryPlugin: Found multiple plugins/modules with the same name!", 289 "Plugin/module already loaded: %s, location: %s" % (strPluginName, self.__dictModuleLocation[ strPluginName ]), 290 "Duplicate plugin/module definition in : %s" % _strDirectoryVisit] 291 strError = os.linesep.join(lstError) 292 self.error(strError) 293 raise RuntimeError(strError) 294 else: 295 self.__dictModuleLocation[ strPluginName ] = _strDirectoryVisit
296 297
298 - def addPluginRootDirectory(self, _strPluginRootDirectory):
299 """ 300 This method can be called by an application if plugins or modules are supposed to be loaded 301 outside the EDNA_HOME directory. 302 303 @param _strPluginRootDirectory: Name of the root directory 304 @type _strPluginRootDirectory: python string 305 """ 306 with self.locked(): 307 self.__listPluginRootDirectory.append(_strPluginRootDirectory)
308 309
310 - def __searchRootDirectories(self):
311 """ 312 This method loops through all the root directories and recursively searchs for modules/plugins. 313 """ 314 self.__dictModuleLocation = {} 315 for strPluginRootDirectory in self.__listPluginRootDirectory: 316 # The following Python call will be deprecated in Python 3.0, see bug #262 317 os.path.walk(strPluginRootDirectory, self.__addPluginLocation, strPluginRootDirectory)
318 319
320 - def loadPlugin(self, _strPluginName):
321 """ 322 This method loads a plugin if it's present in the self.__dictModuleLocation. 323 324 @param _strPluginName: Name of the plugin to be loaded 325 @type _strPluginName: python string 326 """ 327 edPlugin = None 328 strModuleLocation = self.getModuleLocation(_strPluginName) 329 if (strModuleLocation is not None): 330 self.appendPath(strModuleLocation) 331 oModule = self.preImport(_strPluginName, strModuleLocation) 332 if oModule: 333 edPlugin = oModule.__dict__[ _strPluginName ]() 334 else: 335 self.error("Plugin %s couldn't be loaded from %s" % (_strPluginName, strModuleLocation)) 336 else: 337 self.error("Plugin not found: " + _strPluginName) 338 # self.warning('In EDFactoryPlugin: edPlugin is %s type %s' % (edPlugin, type(edPlugin))) 339 return edPlugin
340 341
342 - def loadModule(self, _strModuleName):
343 """ 344 This method loads a module, i.e. it adds the module to the python path. 345 346 @param _strModuleName: Name of the module to be loaded 347 @type _strModuleName: python string 348 """ 349 strModuleLocation = self.getModuleLocation(_strModuleName) 350 if (strModuleLocation is not None): 351 return self.preImport(_strModuleName, strModuleLocation)
352 353 354
355 - def appendPath(self, _strModuleLocation):
356 """ 357 This method appends the plugin "src" directory to the system path, if it's not already present. 358 359 @param _strModuleLocation: Path to the module location 360 @type _strModuleLocation: python string 361 """ 362 strSrcDirectory = EDUtilsPath.appendListOfPaths(_strModuleLocation, [ "..", "..", "..", "src" ]) 363 if (os.path.exists(strSrcDirectory)): 364 if (not strSrcDirectory in sys.path): 365 sys.path.append(strSrcDirectory)
366 367
368 - def getProjectRootDirectory(self, _strModuleName):
369 """ 370 This method returns the project root directory of a given module name. 371 A directory is considered to be a project root if it contains the following 372 directories: "conf", "src" and "plugins" 373 374 @param _strModuleName: Name of the module 375 @type _strModuleName: python string 376 377 @return: The project root directory 378 @type: python string 379 """ 380 strModuleLocation = self.getModuleLocation(_strModuleName) 381 if strModuleLocation not in self.__class__.__dictProjectRootDir: 382 with self.__class__.__semaphoreStatic: 383 if strModuleLocation not in self.__class__.__dictProjectRootDir: 384 strProjectRootDirectory = None 385 if (strModuleLocation is not None): 386 # Now start looking for "conf" and "plugins", max four iterations 387 bFoundRootDirectory = False 388 iMaxIterations = 4 389 strProjectRootDirectory = strModuleLocation 390 while ((not bFoundRootDirectory) and (iMaxIterations > 0)): 391 strProjectRootDirectory = os.path.abspath(os.path.join(strProjectRootDirectory, "..")) 392 edListDirectoryContent = EDUtilsPath.getFileList(strProjectRootDirectory) 393 if (("conf" in edListDirectoryContent) and \ 394 ("src" in edListDirectoryContent) and \ 395 ("plugins" in edListDirectoryContent)): 396 bFoundRootDirectory = True 397 iMaxIterations = iMaxIterations - 1 398 if (not bFoundRootDirectory): 399 strProjectRootDirectory = None 400 self.__class__.__dictProjectRootDir[strModuleLocation] = strProjectRootDirectory 401 return self.__class__.__dictProjectRootDir[strModuleLocation]
402 403
404 - def getProjectName(self, _strModuleName):
405 """ 406 This method returns the name of the project by finding the project root directory 407 and returning the basename. 408 409 @param _strModuleName: Name of the module 410 @type _strModuleName: python string 411 412 @return: The project name 413 @type: python string 414 """ 415 strProjectName = None 416 strProjectRootDirectory = self.getProjectRootDirectory(_strModuleName) 417 if (strProjectRootDirectory is not None): 418 strProjectName = os.path.basename(strProjectRootDirectory) 419 return strProjectName
420 421 422 @classmethod
423 - def preImport(cls, _strModuleName, _strPath=None, _strForceVersion=None, _strMethodVersion=None):
424 """ 425 Static method that import locally with a lock and keeps track of already imported module. 426 @param _strModuleName: Name of the module to import 427 @param _strPath: Path to the module to import 428 @param _strForceVersion: version string to enforce to. Should be compatible with the method given !!! 429 @param _strMethodVersion: property or method to get the version number (should return a string) 430 @return: reference to the module loaded 431 """ 432 oModule = None 433 EDVerbose.DEBUG("EDFactoryPlugin.preImport %s %s %s is loaded=%s" % (_strModuleName, _strPath, _strForceVersion, _strModuleName in cls._dictLoadedModules)) 434 if (_strModuleName not in cls._dictLoadedModules) or \ 435 (cls._dictLoadedModules[_strModuleName].module is None): 436 with cls.__semaphoreStatic: 437 if _strModuleName not in cls._dictLoadedModules: 438 edModule = EDModule(_strModuleName) 439 cls._dictLoadedModules[_strModuleName] = edModule 440 else: 441 edModule = cls._dictLoadedModules[_strModuleName] 442 oModule = edModule.preImport(_strPath, _strMethodVersion) 443 elif (_strForceVersion is not None) and \ 444 (cls._dictLoadedModules[_strModuleName].version < _strForceVersion): 445 if (cls._dictLoadedModules[_strModuleName].version == "") and (_strMethodVersion is not None): 446 cls._dictLoadedModules[_strModuleName].retrieveVersion(_strMethodVersion) 447 if (cls._dictLoadedModules[_strModuleName].version < _strForceVersion): 448 EDVerbose.WARNING("EDFactoryPlugin.preimport wrong module version: %s is %s not %s" % (_strModuleName, cls._dictLoadedModules[_strModuleName].version, _strForceVersion)) 449 cls.unImport(_strModuleName) 450 cls.preImport(_strModuleName, _strPath, _strForceVersion, _strMethodVersion) 451 oModule = cls._dictLoadedModules[_strModuleName].module 452 elif (cls._dictLoadedModules[_strModuleName].version == ""): 453 cls._dictLoadedModules[_strModuleName].retrieveVersion(_strMethodVersion) 454 oModule = cls._dictLoadedModules[_strModuleName].module 455 else: 456 oModule = cls._dictLoadedModules[_strModuleName].module 457 return oModule
458 459 460 @classmethod
461 - def unImport(cls, _strModuleName):
462 """ 463 Static method that remove a module from the imported modules. 464 @param _strModuleName: Name of the module to un-import 465 """ 466 if _strModuleName in cls._dictLoadedModules: 467 EDVerbose.DEBUG("EDFactoryPlugin.unImport: unload module %s." % _strModuleName) 468 with cls.__semaphoreStatic: 469 module = cls._dictLoadedModules.pop(_strModuleName) 470 module.unImport() 471 else: 472 EDVerbose.WARNING("EDFactoryPlugin.unImport: Module %s is not loaded. " % _strModuleName)
473 474 @classmethod
475 - def getFactoryPlugin(cls):
476 """ 477 This is a class method to provide compatibility with EDFactoryPluginStatic 478 @return: the static version of the factory plugin. 479 """ 480 if cls.__edFactoryPlugin is None: 481 with cls.__semaphoreStatic: 482 if (cls.__edFactoryPlugin is None): 483 cls.__edFactoryPlugin = EDFactoryPlugin() 484 return cls.__edFactoryPlugin
485 486 edFactoryPlugin = EDFactoryPlugin() 487