1
2
3
4 """
5 The basic interface and implementation for a plugin manager.
6
7 Also define the basic mechanism to add functionalities to the base
8 PluginManager. A few *principles* to follow in this case:
9
10 If the new functionalities do not overlap the ones already
11 implemented, then they must be implemented as a Decorator class of the
12 base plugin. This should be done by inheriting the
13 ``PluginManagerDecorator``.
14
15 If this previous way is not possible, then the functionalities should
16 be added as a subclass of ``PluginManager``.
17
18 The first method is highly prefered since it makes it possible to have
19 a more flexible design where one can pick several functionalities and
20 litterally *add* them to get an object corresponding to one's precise
21 needs.
22 """
23
24 import sys, os
25 import logging
26 import ConfigParser
27 import types
28
29 from IPlugin import IPlugin
30
31
32
33
34 PLUGIN_NAME_FORBIDEN_STRING=";;"
35
37 """
38 Gather some info about a plugin such as its name, author,
39 description...
40 """
41
42 - def __init__(self, plugin_name, plugin_path):
43 """
44 Set the namle and path of the plugin as well as the default
45 values for other usefull variables.
46
47 .. warning:: The ``path`` attribute is the full path to the
48 plugin if it is organised as a directory or the full path
49 to a file without the ``.py`` extension if the plugin is
50 defined by a simple file. In the later case, the actual
51 plugin is reached via ``plugin_info.path+'.py'``.
52
53 """
54 self.name = plugin_name
55 self.path = plugin_path
56 self.author = "Unknown"
57 self.version = "?.?"
58 self.website = "None"
59 self.copyright = "Unknown"
60 self.description = ""
61 self.plugin_object = None
62 self.category = None
63
65 """
66 Return the activated state of the plugin object.
67 Makes it possible to define a property.
68 """
69 return self.plugin_object.is_activated
70 is_activated = property(fget=_getIsActivated)
71
73 """
74 Set the version of the plugin.
75
76 Used by subclasses to provide different handling of the
77 version number.
78 """
79 self.version = vstring
80
82 """
83 Manage several plugins by ordering them in several categories.
84
85 The mechanism for searching and loading the plugins is already
86 implemented in this class so that it can be used directly (hence
87 it can be considered as a bit more than a mere interface)
88
89 The file describing a plugin should be written in the sytax
90 compatible with Python's ConfigParser module as in the following
91 example::
92
93 [Core Information]
94 Name= My plugin Name
95 Module=the_name_of_the_pluginto_load_with_no_py_ending
96
97 [Documentation]
98 Description=What my plugin broadly does
99 Author= My very own name
100 Website= My very own website
101 Version=the_version_number_of_the_plugin
102 """
103
104 - def __init__(self,
105 categories_filter={"Default":IPlugin},
106 directories_list=None,
107 plugin_info_ext="yapsy-plugin"):
108 """
109 Initialize the mapping of the categories and set the list of
110 directories where plugins may be. This can also be set by
111 direct call the methods:
112
113 - ``setCategoriesFilter`` for ``categories_filter``
114 - ``setPluginPlaces`` for ``directories_list``
115 - ``setPluginInfoExtension`` for ``plugin_info_ext``
116
117 You may look at these function's documentation for the meaning
118 of each corresponding arguments.
119 """
120 self.setPluginInfoClass(PluginInfo)
121 self.setCategoriesFilter(categories_filter)
122 self.setPluginPlaces(directories_list)
123 self.setPluginInfoExtension(plugin_info_ext)
124
126 """
127 Set the categories of plugins to be looked for as well as the
128 way to recognise them.
129
130 The ``categories_filter`` first defines the various categories
131 in which the plugins will be stored via its keys and it also
132 defines the interface tha has to be inherited by the actual
133 plugin class belonging to each category.
134 """
135 self.categories_interfaces = categories_filter.copy()
136
137 self.category_mapping = {}
138
139
140 self._category_file_mapping = {}
141 for categ in categories_filter.keys():
142 self.category_mapping[categ] = []
143 self._category_file_mapping[categ] = []
144
145
147 """
148 Set the class that holds PluginInfo. The class should inherit
149 from ``PluginInfo``.
150 """
151 self._plugin_info_cls = picls
152
154 """
155 Get the class that holds PluginInfo. The class should inherit
156 from ``PluginInfo``.
157 """
158 return self._plugin_info_cls
159
161 """
162 Set the list of directories where to look for plugin places.
163 """
164 if directories_list is None:
165 directories_list = [os.path.dirname(__file__)]
166 self.plugins_places = directories_list
167
169 """
170 Set the extension that identifies a plugin info file.
171
172 The ``plugin_info_ext`` is the extension that will have the
173 informative files describing the plugins and that are used to
174 actually detect the presence of a plugin (see
175 ``collectPlugins``).
176 """
177 self.plugin_info_ext = plugin_info_ext
178
180 """
181 Return the list of all categories.
182 """
183 return self.category_mapping.keys()
184
186 """
187 Return the list of all plugins belonging to a category.
188 """
189 return self.category_mapping[category_name]
190
191
193 """
194 Gather the core information (name, and module to be loaded)
195 about a plugin described by it's info file (found at
196 'directory/filename').
197
198 Return an instance of ``self.plugin_info_cls`` and the
199 config_parser used to gather the core data *in a tuple*, if the
200 required info could be localised, else return ``(None,None)``.
201
202 .. note:: This is supposed to be used internally by subclasses
203 and decorators.
204
205 """
206
207 candidate_infofile = os.path.join(directory,filename)
208
209 config_parser = ConfigParser.SafeConfigParser()
210 try:
211 config_parser.read(candidate_infofile)
212 except:
213 logging.debug("Could not parse the plugin file %s" % candidate_infofile)
214 return (None, None)
215
216 if not config_parser.has_section("Core"):
217 logging.debug("Plugin info file has no 'Core' section (in %s)" % candidate_infofile)
218 return (None, None)
219 if not config_parser.has_option("Core","Name") or not config_parser.has_option("Core","Module"):
220 logging.debug("Plugin info file has no 'Name' or 'Module' section (in %s)" % candidate_infofile)
221 return (None, None)
222
223 name = config_parser.get("Core", "Name")
224 name = name.strip()
225 if PLUGIN_NAME_FORBIDEN_STRING in name:
226 logging.debug("Plugin name contains forbiden character: %s (in %s)" % (PLUGIN_NAME_FORBIDEN_STRING,
227 candidate_infofile))
228 return (None, None)
229
230 plugin_info = self._plugin_info_cls(name,
231 os.path.join(directory,config_parser.get("Core", "Module")))
232 return (plugin_info,config_parser)
233
235 """
236 Gather some basic documentation about the plugin described by
237 it's info file (found at 'directory/filename').
238
239 Return an instance of ``self.plugin_info_cls`` gathering the
240 required informations.
241
242 See also:
243
244 ``self._gatherCorePluginInfo``
245 """
246 plugin_info,config_parser = self._gatherCorePluginInfo(directory, filename)
247 if plugin_info is None:
248 return None
249
250 if config_parser.has_section("Documentation"):
251 if config_parser.has_option("Documentation","Author"):
252 plugin_info.author = config_parser.get("Documentation", "Author")
253 if config_parser.has_option("Documentation","Version"):
254 plugin_info.setVersion(config_parser.get("Documentation", "Version"))
255 if config_parser.has_option("Documentation","Website"):
256 plugin_info.website = config_parser.get("Documentation", "Website")
257 if config_parser.has_option("Documentation","Copyright"):
258 plugin_info.copyright = config_parser.get("Documentation", "Copyright")
259 if config_parser.has_option("Documentation","Description"):
260 plugin_info.description = config_parser.get("Documentation", "Description")
261 return plugin_info
262
264 """
265 Walk through the plugins' places and look for plugins.
266
267 Return the number of plugins found.
268 """
269
270 self._candidates = []
271 for directory in map(os.path.abspath,self.plugins_places):
272
273 if not os.path.isdir(directory):
274 logging.debug("%s skips %s (not a directory)" % (self.__class__.__name__,directory))
275 continue
276
277 logging.debug("%s walks into directory: %s" % (self.__class__.__name__,directory))
278 for item in os.walk(directory):
279 dirpath = item[0]
280 for filename in item[2]:
281
282 if not filename.endswith(".%s" % self.plugin_info_ext):
283 continue
284 candidate_infofile = os.path.join(dirpath,filename)
285 logging.debug("""%s found a candidate:
286 %s""" % (self.__class__.__name__, candidate_infofile))
287
288 plugin_info = self.gatherBasicPluginInfo(dirpath,filename)
289 if plugin_info is None:
290 logging.debug("""Candidate rejected:
291 %s""" % candidate_infofile)
292 continue
293
294
295
296
297 if os.path.isdir(plugin_info.path):
298 candidate_filepath = os.path.join(plugin_info.path,"__init__")
299 elif os.path.isfile(plugin_info.path+".py"):
300 candidate_filepath = plugin_info.path
301 else:
302 continue
303
304 self._candidates.append((candidate_infofile, candidate_filepath, plugin_info))
305 return len(self._candidates)
306
308 """
309 Load the candidate plugins that have been identified through a
310 previous call to locatePlugins. For each plugin candidate
311 look for its category, load it and store it in the appropriate
312 slot of the ``category_mapping``.
313
314 If a callback function is specified, call it before every load
315 attempt. The ``plugin_info`` instance is passed as an argument to
316 the callback.
317 """
318
319 if not hasattr(self, '_candidates'):
320 raise ValueError("locatePlugins must be called before loadPlugins")
321
322 for candidate_infofile, candidate_filepath, plugin_info in self._candidates:
323
324
325
326 if callback is not None:
327 callback(plugin_info)
328
329
330 candidate_globals = {"__file__":candidate_filepath+".py"}
331 if "__init__" in os.path.basename(candidate_filepath):
332 sys.path.append(plugin_info.path)
333 try:
334 execfile(candidate_filepath+".py",candidate_globals)
335 except Exception,e:
336 logging.debug("Unable to execute the code in plugin: %s" % candidate_filepath)
337 logging.debug("\t The following problem occured: %s %s " % (os.linesep, e))
338 if "__init__" in os.path.basename(candidate_filepath):
339 sys.path.remove(plugin_info.path)
340 continue
341
342 if "__init__" in os.path.basename(candidate_filepath):
343 sys.path.remove(plugin_info.path)
344
345 for element in candidate_globals.values():
346 current_category = None
347 for category_name in self.categories_interfaces.keys():
348 try:
349 is_correct_subclass = issubclass(element, self.categories_interfaces[category_name])
350 except:
351 continue
352 if is_correct_subclass:
353 if element is not self.categories_interfaces[category_name]:
354 current_category = category_name
355 break
356 if current_category is not None:
357 if not (candidate_infofile in self._category_file_mapping[current_category]):
358
359 plugin_info.plugin_object = element()
360 plugin_info.category = current_category
361 self.category_mapping[current_category].append(plugin_info)
362 self._category_file_mapping[current_category].append(candidate_infofile)
363 current_category = None
364 break
365
366
367
368 delattr(self, '_candidates')
369
371 """
372 Walk through the plugins' places and look for plugins. Then
373 for each plugin candidate look for its category, load it and
374 stores it in the appropriate slot of the category_mapping.
375 """
376
377 self.locatePlugins()
378 self.loadPlugins()
379
380
382 """
383 Get the plugin correspoding to a given category and name
384 """
385 if self.category_mapping.has_key(category):
386 for item in self.category_mapping[category]:
387 if item.name == name:
388 return item
389 return None
390
392 """
393 Activate a plugin corresponding to a given category + name.
394 """
395 pta_item = self.getPluginByName(name,category)
396 if pta_item is not None:
397 plugin_to_activate = pta_item.plugin_object
398 if plugin_to_activate is not None:
399 logging.debug("Activating plugin: %s.%s"% (category,name))
400 plugin_to_activate.activate()
401 return plugin_to_activate
402 return None
403
404
406 """
407 Desactivate a plugin corresponding to a given category + name.
408 """
409 if self.category_mapping.has_key(category):
410 plugin_to_deactivate = None
411 for item in self.category_mapping[category]:
412 if item.name == name:
413 plugin_to_deactivate = item.plugin_object
414 break
415 if plugin_to_deactivate is not None:
416 logging.debug("Deactivating plugin: %s.%s"% (category,name))
417 plugin_to_deactivate.deactivate()
418 return plugin_to_deactivate
419 return None
420
421
423 """
424 Make it possible to add several responsibilities to a plugin
425 manager object in a more flexible way than by mere
426 subclassing. This is indeed an implementation of the Decorator
427 Design Patterns.
428
429
430 There is also an additional mechanism that allows for the
431 automatic creation of the object to be decorated when this object
432 is an instance of PluginManager (and not an instance of its
433 subclasses). This way we can keep the plugin managers creation
434 simple when the user don't want to mix a lot of 'enhancements' on
435 the base class.
436 """
437
438 - def __init__(self,decorated_object=None,
439
440
441 categories_filter={"Default":IPlugin},
442 directories_list=[os.path.dirname(__file__)],
443 plugin_info_ext="yapsy-plugin"):
444 """
445 Mimics the PluginManager's __init__ method and wraps an
446 instance of this class into this decorator class.
447
448 - *If the decorated_object is not specified*, then we use the
449 PluginManager class to create the 'base' manager, and to do
450 so we will use the arguments: ``categories_filter``,
451 ``directories_list``, and ``plugin_info_ext`` or their
452 default value if they are not given.
453
454 - *If the decorated object is given*, these last arguments are
455 simply **ignored** !
456
457 All classes (and especially subclasses of this one) that want
458 to be a decorator must accept the decorated manager as an
459 object passed to the init function under the exact keyword
460 ``decorated_object``.
461 """
462
463 if decorated_object is None:
464 logging.debug("Creating a default PluginManager instance to be decorated.")
465 decorated_object = PluginManager(categories_filter,
466 directories_list,
467 plugin_info_ext)
468 self._component = decorated_object
469
471 """
472 Decorator trick copied from:
473 http://www.pasteur.fr/formation/infobio/python/ch18s06.html
474 """
475
476 return getattr(self._component,name)
477
478
480 """
481 This function will usually be a shortcut to successively call
482 ``self.locatePlugins`` and then ``self.loadPlugins`` which are
483 very likely to be redefined in each new decorator.
484
485 So in order for this to keep on being a "shortcut" and not a
486 real pain, I'm redefining it here.
487 """
488 self.locatePlugins()
489 self.loadPlugins()
490
491
493 """
494 Singleton version of the most basic plugin manager.
495
496 Being a singleton, this class should not be initialised explicitly
497 and the ``get`` classmethod must be called instead.
498
499 To call one of this class's methods you have to use the ``get``
500 method in the following way:
501 ``PluginManagerSingleton.get().themethodname(theargs)``
502
503 To set up the various coonfigurables variables of the
504 PluginManager's behaviour please call explicitly the following
505 methods:
506
507 - ``setCategoriesFilter`` for ``categories_filter``
508 - ``setPluginPlaces`` for ``directories_list``
509 - ``setPluginInfoExtension`` for ``plugin_info_ext``
510 """
511
512 __instance = None
513
514 __decoration_chain = None
515
517 """
518 Initialisation: this class should not be initialised
519 explicitly and the ``get`` classmethod must be called instead.
520
521 To set up the various configurables variables of the
522 PluginManager's behaviour please call explicitly the following
523 methods:
524
525 - ``setCategoriesFilter`` for ``categories_filter``
526 - ``setPluginPlaces`` for ``directories_list``
527 - ``setPluginInfoExtension`` for ``plugin_info_ext``
528 """
529 if self.__instance is not None:
530 raise Exception("Singleton can't be created twice !")
531
533 """
534 Set the functionalities handled by the plugin manager by
535 giving a list of ``PluginManager`` decorators.
536
537 This function shouldn't be called several time in a same
538 process, but if it is only the first call will have an effect.
539
540 It also has an effect only if called before the initialisation
541 of the singleton.
542
543 In cases where the function is indeed going to change anything
544 the ``True`` value is return, in all other cases, the ``False``
545 value is returned.
546 """
547 if self.__decoration_chain is None and self.__instance is None:
548 logging.debug("Setting up a specific behaviour for the PluginManagerSingleton")
549 self.__decoration_chain = list_of_pmd
550 return True
551 else:
552 logging.debug("Useless call to setBehaviour: the singleton is already instanciated of already has a behaviour.")
553 return False
554 setBehaviour = classmethod(setBehaviour)
555
556
576 get = classmethod(get)
577