|
@@ -22,6 +22,8 @@ INIT_FILENAME = '__init__.py' # Loaded with settings
|
22
|
22
|
CONFSPEC_FILENAME_VARNAME = '__confspec__'
|
23
|
23
|
CONFSPEC_VARNAME = 'CONFSPEC'
|
24
|
24
|
LOADER_FILENAME_VARNAME = '__loader__'
|
|
25
|
+PLUGIN_DEPS_VARNAME = '__plugin_deps__'
|
|
26
|
+ACTIVATE_METHOD_NAME = '_activate'
|
25
|
27
|
|
26
|
28
|
class PluginError(Exception):
|
27
|
29
|
pass
|
|
@@ -39,8 +41,13 @@ class Plugin(object):
|
39
|
41
|
|
40
|
42
|
##@brief Stores plugin directories paths
|
41
|
43
|
_plugin_directories = None
|
42
|
|
-
|
|
44
|
+
|
|
45
|
+ ##@brief Stores Plugin instances indexed by name
|
43
|
46
|
_plugin_instances = dict()
|
|
47
|
+
|
|
48
|
+ ##@brief Attribute used by load_all and load methods to detect circular
|
|
49
|
+ #dependencies
|
|
50
|
+ _load_called = []
|
44
|
51
|
|
45
|
52
|
##@brief Plugin class constructor
|
46
|
53
|
#
|
|
@@ -56,6 +63,7 @@ class Plugin(object):
|
56
|
63
|
self.path = self.plugin_path(plugin_name)
|
57
|
64
|
self.module = None
|
58
|
65
|
self.__confspecs = dict()
|
|
66
|
+ self.loaded = False
|
59
|
67
|
|
60
|
68
|
# Importing __init__.py
|
61
|
69
|
plugin_module = '%s.%s' % ( VIRTUAL_PACKAGE_NAME,
|
|
@@ -120,21 +128,86 @@ class Plugin(object):
|
120
|
128
|
filename = self.path + filename
|
121
|
129
|
loader = SourceFileLoader(module_name, filename)
|
122
|
130
|
return loader.load_module()
|
123
|
|
-
|
124
|
|
- ##@brief Register hooks etc
|
125
|
|
- def load(self):
|
|
131
|
+
|
|
132
|
+ ##@brief Check dependencies of plugin
|
|
133
|
+ #@return A list of plugin name to be loaded before
|
|
134
|
+ def check_deps(self):
|
|
135
|
+ try:
|
|
136
|
+ res = getattr(self.module, PLUGIN_DEPS_VARNAME)
|
|
137
|
+ except AttributeError:
|
|
138
|
+ return list()
|
|
139
|
+ result = list()
|
|
140
|
+ errors = list()
|
|
141
|
+ for plugin_name in res:
|
|
142
|
+ try:
|
|
143
|
+ result.append(self.get(plugin_name))
|
|
144
|
+ except PluginError:
|
|
145
|
+ errors.append(plugin_name)
|
|
146
|
+ if len(errors) > 0:
|
|
147
|
+ raise PluginError( "Bad dependencie for '%s' :"%self.name,
|
|
148
|
+ ', '.join(errors))
|
|
149
|
+ return result
|
|
150
|
+
|
|
151
|
+ ##@brief Check if the plugin should be activated
|
|
152
|
+ #
|
|
153
|
+ #Try to fetch a function called @ref ACTIVATE_METHOD_NAME in __init__.py
|
|
154
|
+ #of a plugin. If none found assert that the plugin can be loaded, else
|
|
155
|
+ #the method is called. If it returns anything else that True, the plugin
|
|
156
|
+ #is noted as not activable
|
|
157
|
+ #
|
|
158
|
+ # @note Maybe we have to exit everything if a plugin cannot be loaded...
|
|
159
|
+ def activable(self):
|
126
|
160
|
from lodel import logger
|
127
|
161
|
try:
|
128
|
|
- test_fun = self.module._activate
|
|
162
|
+ test_fun = getattr(self.module, ACTIVATE_METHOD_NAME)
|
129
|
163
|
except AttributeError:
|
130
|
|
- logger.debug("No _activate method found for plugin %s. Assuming plugin is ready to be loaded" % self.name)
|
|
164
|
+ msg = "No %s method found for plugin %s. Assuming plugin is ready to be loaded"
|
|
165
|
+ msg %= (ACTIVATE_METHOD_NAME, self.name)
|
|
166
|
+ logger.debug(msg)
|
131
|
167
|
test_fun = lambda:True
|
132
|
|
- res = test_fun()
|
133
|
|
- if not(res is True):
|
134
|
|
- raise PluginError(res)
|
|
168
|
+ return test_fun()
|
|
169
|
+
|
|
170
|
+ ##@brief Load a plugin
|
|
171
|
+ #
|
|
172
|
+ #Loading a plugin mean importing a file. The filename is defined in the
|
|
173
|
+ #plugin's __init__.py file in a LOADER_FILENAME_VARNAME variable.
|
|
174
|
+ #
|
|
175
|
+ #The loading process has to take care of other things :
|
|
176
|
+ #- loading dependencies (other plugins)
|
|
177
|
+ #- check that the plugin can be activated using Plugin.activate() method
|
|
178
|
+ #- avoid circular dependencies infinite loop
|
|
179
|
+ def _load(self):
|
|
180
|
+ if self.loaded:
|
|
181
|
+ return
|
|
182
|
+ from lodel import logger
|
|
183
|
+ #Test that plugin "wants" to be activated
|
|
184
|
+ activable = self.activable()
|
|
185
|
+ if not(activable is True):
|
|
186
|
+ msg = "Plugin %s is not activable : %s"
|
|
187
|
+ msg %= (self.name, activable)
|
|
188
|
+ raise PluginError(activable)
|
135
|
189
|
|
|
190
|
+ #Circular dependencie detection
|
|
191
|
+ if self.name in self._load_called:
|
|
192
|
+ raise PluginError("Circular dependencie in Plugin detected. Abording")
|
|
193
|
+ else:
|
|
194
|
+ self._load_called.append(self.name)
|
|
195
|
+
|
|
196
|
+ #Dependencie load
|
|
197
|
+ for dependencie in self.check_deps():
|
|
198
|
+ activable = dependencie.activable()
|
|
199
|
+ if activable is True:
|
|
200
|
+ dependencie._load()
|
|
201
|
+ else:
|
|
202
|
+ msg = "Plugin {plugin_name} not activable because it depends on plugin {dep_name} that is not activable : {reason}"
|
|
203
|
+ msg = msg.format(
|
|
204
|
+ plugin_name = self.name,
|
|
205
|
+ dep_name = dependencie.name,
|
|
206
|
+ reason = activable)
|
|
207
|
+
|
|
208
|
+ #Loading the plugin
|
136
|
209
|
try:
|
137
|
|
- return self._import_from_init_var(LOADER_FILENAME_VARNAME)
|
|
210
|
+ self._import_from_init_var(LOADER_FILENAME_VARNAME)
|
138
|
211
|
except AttributeError:
|
139
|
212
|
msg = "Malformed plugin {plugin}. No {varname} found in __init__.py"
|
140
|
213
|
msg = msg.format(
|
|
@@ -148,13 +221,21 @@ class Plugin(object):
|
148
|
221
|
expt = str(e))
|
149
|
222
|
raise PluginError(msg)
|
150
|
223
|
logger.debug("Plugin '%s' loaded" % self.name)
|
151
|
|
-
|
|
224
|
+ self.loaded = True
|
|
225
|
+
|
|
226
|
+ ##@brief Call load method on every pre-loaded plugins
|
|
227
|
+ #
|
|
228
|
+ # Called by loader to trigger hooks registration.
|
|
229
|
+ # This method have to avoid circular dependencies infinite loops. For this
|
|
230
|
+ # purpose a class attribute _load_called exists.
|
|
231
|
+ # @throw PluginError
|
152
|
232
|
@classmethod
|
153
|
233
|
def load_all(cls):
|
154
|
234
|
errors = dict()
|
|
235
|
+ cls._load_called = []
|
155
|
236
|
for name, plugin in cls._plugin_instances.items():
|
156
|
237
|
try:
|
157
|
|
- plugin.load()
|
|
238
|
+ plugin._load()
|
158
|
239
|
except PluginError as e:
|
159
|
240
|
errors[name] = e
|
160
|
241
|
if len(errors) > 0:
|
|
@@ -163,14 +244,17 @@ class Plugin(object):
|
163
|
244
|
msg += "\n\t%20s : %s" % (name,e)
|
164
|
245
|
msg += "\n"
|
165
|
246
|
raise PluginError(msg)
|
166
|
|
-
|
|
247
|
+
|
|
248
|
+ ##@return a copy of __confspecs attr
|
167
|
249
|
@property
|
168
|
250
|
def confspecs(self):
|
169
|
251
|
return copy.copy(self.__confspecs)
|
170
|
252
|
|
171
|
253
|
##@brief Register a new plugin
|
172
|
254
|
#
|
173
|
|
- # preload
|
|
255
|
+ #@param plugin_name str : The plugin name
|
|
256
|
+ #@return a Plugin instance
|
|
257
|
+ #@throw PluginError
|
174
|
258
|
@classmethod
|
175
|
259
|
def register(cls, plugin_name):
|
176
|
260
|
if plugin_name in cls._plugin_instances:
|
|
@@ -181,6 +265,11 @@ class Plugin(object):
|
181
|
265
|
cls._plugin_instances[plugin_name] = plugin
|
182
|
266
|
return plugin
|
183
|
267
|
|
|
268
|
+ ##@brief Plugins instances accessor
|
|
269
|
+ #
|
|
270
|
+ #@param plugin_name str: The plugin name
|
|
271
|
+ #@return a Plugin instance
|
|
272
|
+ #@throw PluginError if plugin not found
|
184
|
273
|
@classmethod
|
185
|
274
|
def get(cls, plugin_name):
|
186
|
275
|
try:
|