Package yapsy :: Module PluginManager
[hide private]

Source Code for Module yapsy.PluginManager

  1  #!/usr/bin/python 
  2  # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t -*- 
  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  # A forbiden string that can later be used to describe lists of 
 33  # plugins for instance (see ``ConfigurablePluginManager``) 
 34  PLUGIN_NAME_FORBIDEN_STRING=";;" 
 35   
36 -class PluginInfo(object):
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
64 - def _getIsActivated(self):
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
72 - def setVersion(self, vstring):
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
81 -class PluginManager(object):
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
125 - def setCategoriesFilter(self, categories_filter):
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 # prepare the mapping from categories to plugin lists 137 self.category_mapping = {} 138 # also maps the plugin info files (useful to avoid loading 139 # twice the same plugin...) 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
146 - def setPluginInfoClass(self,picls):
147 """ 148 Set the class that holds PluginInfo. The class should inherit 149 from ``PluginInfo``. 150 """ 151 self._plugin_info_cls = picls
152
153 - def getPluginInfoClass(self):
154 """ 155 Get the class that holds PluginInfo. The class should inherit 156 from ``PluginInfo``. 157 """ 158 return self._plugin_info_cls
159
160 - def setPluginPlaces(self, directories_list):
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
168 - def setPluginInfoExtension(self,plugin_info_ext):
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
179 - def getCategories(self):
180 """ 181 Return the list of all categories. 182 """ 183 return self.category_mapping.keys()
184
185 - def getPluginsOfCategory(self,category_name):
186 """ 187 Return the list of all plugins belonging to a category. 188 """ 189 return self.category_mapping[category_name]
190 191
192 - def _gatherCorePluginInfo(self, directory, filename):
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 # now we can consider the file as a serious candidate 207 candidate_infofile = os.path.join(directory,filename) 208 # parse the information file to get info about the plugin 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 # check if the basic info is available 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 # check that the given name is valid 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 # start collecting essential info 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
234 - def gatherBasicPluginInfo(self, directory,filename):
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 # collect additional (but usually quite usefull) information 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
263 - def locatePlugins(self):
264 """ 265 Walk through the plugins' places and look for plugins. 266 267 Return the number of plugins found. 268 """ 269 # print "%s.locatePlugins" % self.__class__ 270 self._candidates = [] 271 for directory in map(os.path.abspath,self.plugins_places): 272 # first of all, is it a directory :) 273 if not os.path.isdir(directory): 274 logging.debug("%s skips %s (not a directory)" % (self.__class__.__name__,directory)) 275 continue 276 # iteratively walks through the directory 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 # eliminate the obvious non plugin files 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 # print candidate_infofile 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 # now determine the path of the file to execute, 294 # depending on wether the path indicated is a 295 # directory or a file 296 # print plugin_info.path 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 # print candidate_filepath 304 self._candidates.append((candidate_infofile, candidate_filepath, plugin_info)) 305 return len(self._candidates)
306
307 - def loadPlugins(self, callback=None):
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 # print "%s.loadPlugins" % self.__class__ 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 # if a callback exists, call it before attempting to load 324 # the plugin so that a message can be displayed to the 325 # user 326 if callback is not None: 327 callback(plugin_info) 328 # now execute the file and get its content into a 329 # specific dictionnary 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 # now try to find and initialise the first subclass of the correct plugin interface 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 # we found a new plugin: initialise it and search for the next one 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 # Remove candidates list since we don't need them any more and 367 # don't need to take up the space 368 delattr(self, '_candidates')
369
370 - def collectPlugins(self):
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 # print "%s.collectPlugins" % self.__class__ 377 self.locatePlugins() 378 self.loadPlugins()
379 380
381 - def getPluginByName(self,name,category="Default"):
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
391 - def activatePluginByName(self,name,category="Default"):
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
405 - def deactivatePluginByName(self,name,category="Default"):
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
422 -class PluginManagerDecorator(object):
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 # The following args will only be used if we need to 440 # create a default PluginManager 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
470 - def __getattr__(self,name):
471 """ 472 Decorator trick copied from: 473 http://www.pasteur.fr/formation/infobio/python/ch18s06.html 474 """ 475 # print "looking for %s in %s" % (name, self.__class__) 476 return getattr(self._component,name)
477 478
479 - def collectPlugins(self):
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
492 -class PluginManagerSingleton(object):
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
516 - def __init__(self):
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
532 - def setBehaviour(self,list_of_pmd):
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
557 - def get(self):
558 """ 559 Actually create an instance 560 """ 561 if self.__instance is None: 562 if self.__decoration_chain is not None: 563 # Get the object to be decorated 564 # print self.__decoration_chain 565 pm = self.__decoration_chain[0]() 566 for cls_item in self.__decoration_chain[1:]: 567 # print cls_item 568 pm = cls_item(decorated_manager=pm) 569 # Decorate the whole object 570 self.__instance = pm 571 else: 572 # initialise the 'inner' PluginManagerDecorator 573 self.__instance = PluginManager() 574 logging.debug("PluginManagerSingleton initialised") 575 return self.__instance
576 get = classmethod(get)
577