1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
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
100 IGNORE_FILE = ".ednaignore"
101 _dictLoadedModules = {}
102 __dictConfFiles = {None:None}
103 __dictProjectRootDir = {None:None}
104 __semaphoreStatic = Semaphore()
105 __edFactoryPlugin = None
106
115
116
129
130
131
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
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
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
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
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
210 self.warning("Module %s not found, forcing reloading of all modules..." % _strModuleName)
211 self.__searchRootDirectories()
212
213 self.saveModuleDictionaryToDisk(EDUtilsPath.getEdnaPluginCachePath())
214 if (_strModuleName in self.__dictModuleLocation.keys()):
215 strModuleLocation = self.__dictModuleLocation[ _strModuleName ]
216
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
224
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
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
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
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
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
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
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
317 os.path.walk(strPluginRootDirectory, self.__addPluginLocation, strPluginRootDirectory)
318
319
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
339 return edPlugin
340
341
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
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
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
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
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):
458
459
460 @classmethod
473
474 @classmethod
485
486 edFactoryPlugin = EDFactoryPlugin()
487