A little bug fix
[jcd.git] / PluginLoader.py
blob49d19fd78ddc5fe9d587897b52996dab6be6fb85
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3 # vim: expandtab:shiftwidth=4:fileencoding=utf-8 :
5 # Copyright ® 2008 Fulvio Satta
7 # If you want contact me, send an email to Yota_VGA@users.sf.net
9 # This file is part of jcd
11 # jcd is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # jcd is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with this program; if not, write to the Free Software
23 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
25 #TODO: Test, test, test
26 #TODO: Polishing
27 #TODO: Comment better
28 #TODO: Complete the syncronization part
29 #TODO: Manage the plugin unloading correctly
30 #TODO: Complete the reloading part
32 ##########################
33 ##### IMPORT SECTION #####
34 ##########################
36 import warnings as _warnings
38 from Syncronized import AutoLock as _AutoLock
39 from Syncronized import RWAutoLock as _RWAutoLock
40 from Syncronized import RWSyncronized as _RWSyncronized
42 from SignalSlot import SignalBase as _SignalBase
44 from threading import RLock as _RLock
46 #####################################
47 ##### EXTERNAL VARIABLE SECTION #####
48 #####################################
50 #Plugin list
51 plugins = []
53 #Inited plugin list
54 loaded = []
56 #Directorys checked
57 dir_list = []
59 #Module mutex
60 mutex = _RLock()
62 #User blacks
63 user_blacks = []
65 #Auto blacks
66 auto_blacks = []
68 ######################################
69 ##### INTERNAL FUNCTIONS SECTION #####
70 ######################################
72 #Import a plugin
73 def _import(plugin, dir, plugname):
74 import imp
75 import os
76 import sys
78 #Get the paths
79 fullpath = os.path.join(dir, plugin.replace(u'.', os.sep))
80 path, name = os.path.split(fullpath)
82 #If the plugin is already exported return it
83 try:
84 return sys.modules[plugname]
85 except KeyError:
86 pass
88 #Find the module
89 fp, pathname, description = imp.find_module(name, [path])
91 #Import the module
92 try:
93 return imp.load_module(plugname, fp, pathname, description)
94 finally:
95 if fp:
96 fp.close()
98 #Load a plugin, but not init that
99 def _load(plugin, dir):
100 from codecs import open
102 lines = open(plugin, encoding = 'utf8').readlines()
103 lines = [line.strip() for line in lines if line[0] != u'#' and line.strip()]
105 try:
106 module = _import(lines[0], dir, plugin)
107 except Exception, e:
108 _warnings.warn('Plugin %s have an import error: %s, %s, %s' % (
109 os.path.join(dir, plugin),
110 type(e), e, e.args),
111 ImportWarning, stacklevel = 2)
112 except:
113 _warnings.warn('Plugin %s have an import error' %
114 os.path.join(dir, plugin),
115 ImportWarning, stacklevel = 2)
116 else:
117 return module
119 return None
121 ######################################
122 ##### EXTERNAL FUNCTIONS SECTION #####
123 ######################################
125 #Auboblack the modules
126 @_AutoLock(mutex)
127 def filterBlacks(plugins):
128 import os
130 refilter = False
131 new_plugins = []
132 black_plugins = []
133 new_black_plugins = []
135 for plugin in plugins:
136 noblack = True
137 try:
138 black = hasattr(plugin[0], 'loadable') and not plugin.loadable(plugin[0])
139 except Exception, e:
140 _warnings.warn('Plugin %s have a loadable error: %s, %s, %s' % (
141 plugin[0].__name__,
142 type(e), e, e.args),
143 ImportWarning, stacklevel = 2)
144 except:
145 _warnings.warn('Plugin %s have a loadable error' %
146 plugin[0].__name__,
147 ImportWarning, stacklevel = 2)
149 if noblack:
150 new_plugins.append(plugin)
151 else:
152 black_plugins.append(plugin)
153 refilter = True
155 if refilter:
156 new_plugins, new_black_plugins = filterBlacks(new_plugins)
158 return new_plugins, black_plugins + new_black_plugins
160 #Find the plugins.
161 @_AutoLock(mutex)
162 def findPlugins(dirs, numbers, order, blackList = []):
163 from glob import glob
164 import os
166 #Default values
167 modules = {}
168 modules_names = {}
170 #For each dir
171 for dir in dirs:
172 #Get the plugins in the directory
173 dir = os.path.abspath(os.path.expanduser(dir))
174 plugins = glob(os.path.join(dir, '*.plug'))
176 #For each plugin
177 for plugin in plugins:
178 #If the plugin is in the black list go to the next
179 if plugin in blackList:
180 user_blacks.append(plugin)
181 continue
183 #Try to load the module
184 module = _load(plugin, dir)
185 if not module:
186 continue
188 #Get the priority number
189 number = 0
190 try:
191 number = module.priority
192 except AttributeError:
193 pass
194 try:
195 number = numbers[plugin]
196 except KeyError:
197 pass
199 #If an order is defined follow the order, else append the module
200 #as the last of the list
201 try:
202 index = order.index(plugin)
203 except ValueError:
204 modules[number] = modules.get(number, []) + [module]
205 else:
206 actuals = modules_names.get(number, [])
207 modules_actuals = modules.get(number, [])
208 n = 0
209 for name in order[:index]:
210 if name in actuals:
211 n += 1
212 modules_actuals.insert(n, module)
213 actuals.insert(n, plugin)
214 modules[number] = modules_actuals
215 modules_names[number] = actuals
217 return modules
219 #Flat the modules in a list
220 def flattingModules(modules):
221 r = []
223 numbers = modules.keys()
224 numbers.sort()
226 for number in numbers:
227 r += zip(modules[number], [number] * len(modules[number]))
229 return r
231 #Remove the plugins in remove but not in mantain from sys.modules
232 @_AutoLock(mutex)
233 def removePlugins(mantain, remove):
234 import sys
236 for i in remove:
237 if module not in mantain:
238 try:
239 del sys.modules[module.__name__]
240 except KeyError:
241 pass
243 #Set the directory lists and load the plugins
244 @_AutoLock(mutex)
245 def setDirs(dirs, pluginOrderingList, blackList = [], autoremotion = True):
246 global plugins, dir_list, numbered_plugins, auto_blacks
248 numbers = dict(pluginOrderingList)
249 order = [i[0] for i in pluginOrderingList]
250 old_plugins = plugins + auto_blacks
252 dir_list = dirs
253 numbered = findPlugins(dirs, numbers, order)
254 plugins = flattingModules(numbered)
256 if autoremotion:
257 removePlugins(plugins, old_plugins)
259 plugins, auto_blacks = filterBlacks(plugins)
261 return [plugin for plugin in old_plugins if plugin not in plugins]
263 #Select a group of plugins
264 @_AutoLock(mutex)
265 def getPlugins(selector, mantainNumbers = False):
266 if mantainNumbers:
267 return [plugin for plugin in plugins if selector(plugin)]
268 return [plugin[0] for plugin in plugins if selector(plugin[0])]
270 #Reload a plugin
271 #WARNING: Very low level function! You must know what you do!
272 @_AutoLock(mutex)
273 def reload(plugin):
274 import os
275 import imp
277 #The original settings
278 fullpath = plugin.__file__
279 path, name = os.path.split(fullpath)
280 name = name.rsplit('.', 1)[0]
282 #Search the module
283 fp, pathname, description = imp.find_module(name, [path])
285 #Reimport the module
286 try:
287 return imp.load_module(plugin.__name__, fp, pathname, description)
288 finally:
289 if fp:
290 fp.close()
292 ##################################
293 ##### PLUGIN CLASSES SECTION #####
294 ##################################
296 class Loader:
297 class __implementation(_RWSyncronized, _SignalBase):
298 def __init__(self):
299 _RWSyncronized.__init__(self)
300 _SignalBase.__init__(self)
302 def setupSignals(self):
303 self.addSignal('reloadPlugins')
305 __imp = __implementation()
307 @_AutoLock(__imp)
308 def registerObject(self, object):
309 self.__imp.slots['reloadPlugins'].append(object.reloadPlugins)
311 #Init a plugin
312 @_AutoLock(mutex)
313 def loadPlugin(self, plugin):
314 if plugin not in loaded:
315 if hasattr(plugin, 'init'):
316 plugin.init()
317 loaded.append(plugin)
319 #Init many plugins
320 def loadPlugins(self, plugins):
321 for plugin in plugins:
322 self.loadPlugin(plugin)
324 #Sett all the plugins as unloadables
325 @_AutoLock(mutex)
326 @_RWAutoLock(__imp, write = True)
327 def startReloading(self):
328 global loaded
329 self.__imp.oldplugins = loaded
330 self.__imp.emit('reloadPlugins')
331 loaded = []
333 #Unload all the plugins non reloaded
334 @_AutoLock(mutex)
335 @_RWAutoLock(__imp, write = True)
336 def stopReloading(self):
337 for plugin in self.__imp.oldplugins:
338 if plugin not in loaded and hasattr(plugin, 'deinit'):
339 plugin.deinit()
341 #Lock plugin unloading
342 def lock(self, wait = None):
343 mutex.lock()
344 try:
345 return self.__imp.readLock(wait)
346 except:
347 mutex.unlock()
349 #unlock plugin unloading
350 def unlock(self):
351 self.__imp.writeLock()
352 mutex.unlock()
354 ##############################
355 ##### DECORATORS SECTION #####
356 ##############################
358 #Decorator for Loader sincronization
359 def LockPluginUnloading(f):
360 loader = Loader()
362 def function(*args, **kwargs):
363 loader.lock()
364 try:
365 return f(*args, **kwargs)
366 finally:
367 loader.unlock()
369 return function
371 ########################
372 ##### TEST SECTION #####
373 ########################
375 if __name__ == '__main__':
376 order = {1 : ('plugins/example.plug')}
378 setDirs(['plugins'], flattingModules(order))
380 plugin = getPlugins(lambda plugin: plugin.task['name'] == 'ExampleTask')[0]
381 loader = Loader()
382 loader.loadPlugin(plugin)
383 loader.loadPlugin(plugin)
385 loader.startReloading()
386 loader.loadPlugin(plugin)
387 loader.stopReloading()
389 print plugins