Package elisa :: Package core :: Module plugin_registry
[hide private]
[frames] | no frames]

Source Code for Module elisa.core.plugin_registry

  1  # -*- coding: utf-8 -*- 
  2  # Elisa - Home multimedia server 
  3  # Copyright (C) 2006-2008 Fluendo Embedded S.L. (www.fluendo.com). 
  4  # All rights reserved. 
  5  # 
  6  # This file is available under one of two license agreements. 
  7  # 
  8  # This file is licensed under the GPL version 3. 
  9  # See "LICENSE.GPL" in the root of this distribution including a special 
 10  # exception to use Elisa with Fluendo's plugins. 
 11  # 
 12  # The GPL part of Elisa is also available under a commercial licensing 
 13  # agreement from Fluendo. 
 14  # See "LICENSE.Elisa" in the root directory of this distribution package 
 15  # for details on that license. 
 16   
 17  """ 
 18  Plugins load/management support 
 19  """ 
 20   
 21   
 22  __maintainer__ = 'Philippe Normand <philippe@fluendo.com>' 
 23   
 24  from elisa.core import log, config 
 25  from elisa.core.plugin import Plugin 
 26  from elisa.core.component import ComponentError, UnMetDependency, InitializeFailure 
 27  from elisa.core.component import UnSupportedPlatform 
 28  from elisa.core.utils import classinit, misc 
 29  from elisa.core import common 
 30  from elisa.extern import path 
 31  from sets import Set 
 32  import pkg_resources 
 33  import os, sys, re, gc 
 34  import inspect 
 35  import types 
 36  import new 
 37  from twisted.internet import defer 
 38   
 39  # FIXME: why is this shortcut not in common ? 
40 -def get_component_class(component_path):
41 """ This is a shortcut to L{elisa.core.plugin_registry.PluginRegistry.get_component_class} 42 43 @rtype: L{elisa.core.component.Component} class 44 """ 45 plugin_registry = common.application.plugin_registry 46 component_class = plugin_registry.get_component_class(component_path) 47 return component_class
48
49 -class PluginNotFound(Exception):
50
51 - def __init__(self, plugin_name):
52 Exception.__init__(self) 53 self.plugin_name = plugin_name
54
55 - def __str__(self):
56 return "Plugin %r not found" % self.plugin_name
57
58 -class ComponentNotFound(Exception):
59
60 - def __init__(self, component_name):
61 Exception.__init__(self) 62 self.component_name = component_name
63
64 - def __str__(self):
65 return "Component %r not found" % self.component_name
66
67 -class PluginRegistry(log.Loggable):
68 """ 69 The PluginRegistry is responsible to find all the Plugins. Plugins 70 enabled in the Application config file will be loaded by 71 instantiating their class. 72 73 The registry can create components by first searching them by name 74 in Plugins component registries and instantiating them directly. 75 Component instances are not handled by the PluginRegistry. 76 77 @ivar _plugin_classes: Plugin classes found by the Registry 78 @type _plugin_classes: dict mapping Plugin names to Plugin classes 79 @ivar _plugin_instances: Plugins currently instantiated 80 @type _plugin_instances: L{elisa.core.plugin.Plugin} list 81 @ivar _app_config: Application's config 82 @type _app_config: L{elisa.core.config.Config} 83 """ 84 85 # Allows property fget/fset/fdel/doc overriding 86 __metaclass__ = classinit.ClassInitMeta 87 __classinit__ = classinit.build_properties 88
89 - def __init__(self, application_config):
90 """ Initialize the PluginRegistry instance variables and the 91 local plugin directory. 92 93 @param application_config: Application's config 94 @type application_config: L{elisa.core.config.Config} 95 """ 96 log.Loggable.__init__(self) 97 self.debug("Creating") 98 99 self._plugin_directories = [] 100 self._plugin_classes = {} 101 self._plugin_instances = {} 102 self._app_config = application_config 103 self._init_local_plugins_dir()
104
105 - def _init_local_plugins_dir(self):
106 """ 107 Register the local plugins directory in pkg_resources. The 108 directory name is found in the application's config in 109 'plugins_dir' option which is in the 'general' section 110 """ 111 default = '~/.elisa/plugins' 112 113 expanded_default = os.path.expanduser(default) 114 if not os.path.exists(expanded_default): 115 try: 116 os.makedirs(expanded_default) 117 except OSError, e: 118 self.warning("Could not make directory %r: %s",expand_default, 119 e) 120 default = '' 121 else: 122 default_pkg = os.path.join(expanded_default, '__init__.py') 123 if not os.path.exists(default_pkg): 124 try: 125 open(default_pkg,'w').close() 126 except IOError, error: 127 self.warning("Could not create %r: %r", default_pkg, 128 error) 129 default = '' 130 131 directories = misc.env_var_explode_list('ELISA_PLUGIN_PATH', default) 132 133 for directory in directories: 134 if not directory: 135 continue 136 if directory.startswith('~'): 137 directory = os.path.expanduser(directory) 138 else: 139 directory = os.path.abspath(directory) 140 141 if directory.endswith(os.path.sep): 142 directory = directory[:-1] 143 144 if not os.path.exists(os.path.join(directory, '__init__.py')): 145 self.warning("__init__.py file missing in %r; " 146 "skipping plugin directory registration", directory) 147 else: 148 self.debug("Plugin directory: %r", directory) 149 self._plugin_directories.append(directory) 150 151 # set up our custom environment where plugins are located 152 environment = pkg_resources.Environment(self._plugin_directories) 153 154 working_set = pkg_resources.working_set 155 156 # cope with old versions of setuptools 157 if hasattr(working_set, 'find_plugins'): 158 distributions, errors = working_set.find_plugins(environment) 159 map(working_set.add, distributions) 160 if errors: 161 self.warning("Couldn't load: %s" % errors) 162 else: 163 self.warning("Please upgrade setuptools if you want to "\ 164 "be able to use additional plugins "\ 165 "from %s" % self._plugin_directories)
166
167 - def plugins__get(self):
168 return self._plugin_instances
169
170 - def plugin_classes__get(self):
171 return self._plugin_classes
172
173 - def register_plugin(self, plugin_class):
174 """ Add the given plugin class to our internal plugins list. 175 176 @param plugin_class: Plugin's class 177 @type plugin_class: class 178 """ 179 plugin_class.load_config() 180 if common.application.translator: 181 plugin_class.load_translations(common.application.translator) 182 183 try: 184 errors = plugin_class.initialize() 185 except (UnMetDependency,InitializeFailure), error: 186 self.warning(error) 187 else: 188 name = plugin_class.name 189 self._plugin_classes[name] = plugin_class 190 self.info("Loaded the plugin %r", name) 191 192 names = plugin_class.components.keys() 193 names.sort() 194 component_names = ', '.join(names) 195 self.debug("Components available in %r plugin: %s", name, 196 component_names)
197
198 - def unregister_plugin(self, plugin_class):
199 """ Remove the given plugin class from our internal plugins list. 200 201 @param plugin_class: Plugin's class 202 @type plugin_class: class 203 """ 204 plugin_name = plugin_class.name 205 206 # TODO: remove plugin's instance if it exists 207 208 # remove the plugin_class from self._plugin_classes 209 if plugin_name in self._plugin_classes: 210 del self._plugin_classes[plugin_name] 211 self.info("Unloaded the plugin '%s'" % plugin_name)
212
213 - def get_plugin_with_name(self, name):
214 """ 215 Look for the plugin with given name, if it's not found, 216 instantiate it and return it. If no plugin with such name is 217 found, raise an exception PluginNotFound. 218 219 @param name: name of the Plugin to look for 220 @type name: string 221 @rtype: L{elisa.core.plugin.Plugin} 222 @raise PluginNotFound: if the plugin could not be found 223 """ 224 # first try in instantiated plugins list 225 plugin = self._plugin_instances.get(name) 226 227 # try to instantiate a new Plugin class 228 if plugin == None: 229 plugin_class = self._plugin_classes.get(name) 230 231 if plugin_class != None: 232 plugin = plugin_class() 233 self._plugin_instances[name] = plugin 234 else: 235 raise PluginNotFound(name) 236 237 return plugin
238
239 - def load_plugins(self):
240 """ 241 Find and register all Plugin classes found by setuptools and 242 in the plugins directory. 243 """ 244 # load plugins registered via various means 245 self._load_eggs() 246 self._load_packages() 247 self._load_modules() 248 self._load_by_config() 249 250 self._check_plugin_dependencies()
251
253 plugin_classes = self._plugin_classes.copy() 254 for plugin_name, plugin_class in plugin_classes.iteritems(): 255 try: 256 self.check_interplugin_dependencies(plugin_class) 257 except UnMetDependency, exception: 258 msg = "plugin %r is missing the following" \ 259 " dependencies: %s" \ 260 % (plugin_name, exception.error_message) 261 self.warning(msg) 262 continue 263 264 # for each component, check eventual inter-component deps 265 components = plugin_class.components.copy() 266 for component_name, informations in components.iteritems(): 267 try: 268 self.check_intercomponent_dependencies(plugin_class, 269 component_name) 270 except UnMetDependency, exception: 271 msg = "component %r is missing the following" \ 272 " dependencies: %s" \ 273 % (component_name, exception.error_message) 274 self.warning(msg) 275 continue
276
277 - def check_interplugin_dependencies(self, plugin_class):
278 """ 279 Check that all the plugins that L{plugin_class} depends on are loaded. 280 281 @param plugin_class: Plugin's class 282 @type plugin_class: class 283 284 @raises: L{elisa.core.component.UnMetDependency} if at least one 285 dependency is not satisfied; the list of unmet dependencies 286 is passed. 287 """ 288 unmet_dependencies = [] 289 290 for dependency in plugin_class.plugin_dependencies: 291 if dependency not in self._plugin_classes.keys(): 292 unmet_dependencies.append(dependency) 293 294 if len(unmet_dependencies) > 0: 295 raise UnMetDependency(plugin_class, unmet_dependencies)
296
297 - def check_intercomponent_dependencies(self, plugin, component_name):
298 """ 299 Check that all the components that L{component} depends on are 300 available. 301 302 @param plugin: plugin containing the tested component 303 @type plugin: Plugin 304 @param component_name: component which dependencies are to be checked 305 @type component_name: string 306 307 @raises: L{elisa.core.component.UnMetDependency} if at least one 308 dependency is not satisfied; the list of unmet dependencies 309 is passed. 310 """ 311 informations = plugin.components[component_name] 312 dependencies = informations.get('component_dependencies',[]) 313 unmet_dependencies = [] 314 315 for dependency in dependencies: 316 dependency_plugin, dependency_component = dependency.split(':') 317 dependency_plugin_class = self._plugin_classes.get(dependency_plugin) 318 if not dependency_plugin_class or \ 319 dependency_component not in dependency_plugin_class.components: 320 unmet_dependencies.append(dependency) 321 322 if len(unmet_dependencies) > 0: 323 raise UnMetDependency(component_name, unmet_dependencies)
324
325 - def _load_eggs(self):
326 entrypoints = list(pkg_resources.iter_entry_points('elisa.plugins')) 327 entrypoints.sort(key=lambda e: e.name) 328 329 # load plugins registered via setuptools 330 for entrypoint in entrypoints: 331 try: 332 py_object = entrypoint.load() 333 except: 334 self.warning("Error loading plugin %r" % entrypoint.module_name) 335 common.application.handle_traceback() 336 continue 337 338 full_path = inspect.getsourcefile(py_object) 339 plugin_dir = os.path.dirname(full_path) 340 341 if type(py_object) == types.ModuleType: 342 module_name = entrypoint.module_name 343 dirname = os.path.basename(os.path.split(full_path)[0]) 344 plugin_class = new.classobj(dirname.title(), (Plugin,),{}) 345 plugin_class.__module__ = module_name 346 347 # TODO: should that be configurable? 348 plugin_conf = 'plugin.conf' 349 350 config_path = plugin_class.get_resource_file(plugin_conf) 351 if not os.path.exists(config_path): 352 self.warning("Plugin config file not found for %r plugin", 353 entrypoint.name) 354 continue 355 plugin_class.config_file = config_path 356 else: 357 plugin_class = py_object 358 359 plugin_class.directory = plugin_dir 360 361 assert issubclass(plugin_class, Plugin), \ 362 '%r is not a valid Plugin!' % plugin_class 363 364 #plugin_class.enabled = entrypoint.name in plugin_names 365 self.register_plugin(plugin_class)
366
367 - def _load_packages(self):
368 # TODO: load plugin packages 369 pass
370
371 - def _load_modules(self):
372 # load .py module files plugins 373 modules = [] 374 for plugins_dir in self._plugin_directories: 375 try: 376 for filename in path.path(plugins_dir).walkfiles('*.py', 377 errors='ignore'): 378 parent = filename.parent 379 380 if parent not in sys.path: 381 sys.path.append(parent) 382 383 module_filename = os.path.basename(filename) 384 module_name, _ = os.path.splitext(module_filename) 385 386 if type(module_filename) == unicode: 387 module_filename = module_filename.encode('utf-8') 388 try: 389 module = __import__(module_name, {}, {}, 390 [module_filename,]) 391 except ImportError, error: 392 self.warning("Could not import module %s" % filename) 393 common.application.handle_traceback() 394 continue 395 else: 396 modules.append(module) 397 398 sys.path.remove(parent) 399 except OSError, error: 400 self.warning(error) 401 402 for module in modules: 403 for symbol_name in dir(module): 404 symbol = getattr(module, symbol_name) 405 if inspect.isclass(symbol) and symbol != Plugin and \ 406 issubclass(symbol, Plugin): 407 #symbol.enabled = symbol.name in plugin_names 408 self.register_plugin(symbol)
409
410 - def _load_by_config(self):
411 for plugins_dir in self._plugin_directories: 412 try: 413 for config_file in path.path(plugins_dir).walkfiles('*.conf', 414 errors='ignore'): 415 plugin_config = config.Config(config_file) 416 general = plugin_config.get_section('general', 417 default={}) 418 i18n = general.get('i18n', None) 419 420 name = general.get('name') 421 if name and name not in self._plugin_classes: 422 plugin_class = new.classobj(name.title(), 423 (Plugin,),{}) 424 plugin_class.config_file = config_file 425 plugin_class.i18n = i18n 426 plugin_class.directory = os.path.dirname(config_file) 427 self.register_plugin(plugin_class) 428 except OSError, error: 429 self.warning(error)
430
431 - def unload_plugins(self):
432 plugin_classes = self._plugin_classes.values()[:] 433 for plugin_class in plugin_classes: 434 self.unregister_plugin(plugin_class) 435 gc.collect()
436
437 - def create_component(self, component_path):
438 """ Use the PluginRegistry to to create a Component. 439 440 component_path format should be like this:: 441 442 plugin_name:component_name[:instance_id] 443 444 The plugin_name is the name of the plugin sub-class to 445 load. See L{elisa.core.plugin.Plugin.name}. 446 447 The component_name is the name of the Component sub-class to 448 load. See L{elisa.core.component.Component.name}. 449 450 @param component_path: information to locate the Component to load 451 @type component_path: string 452 @rtype: L{elisa.core.component.Component} 453 @raise InitializeFailure: When the component failed to initialize 454 @raise ComponentNotFound: When the component could not be found in any plugin 455 @raise UnMetDependency: When the component misses a dependency 456 """ 457 component = None 458 459 self.debug("Trying to create %r component" % component_path) 460 461 infos = self._get_component_infos(component_path) 462 463 ComponentClass, plugin, component_path_tuple = infos 464 465 if ComponentClass: 466 ComponentClass.name = component_path_tuple[1] 467 468 # bind the Component to its plugin early as it's needed by 469 # check_dependencies() 470 if not ComponentClass.plugin: 471 ComponentClass.plugin = plugin 472 473 instance_id = component_path_tuple[2] 474 self.log("Instantiating Component %r from %r", 475 ComponentClass.name, component_path_tuple[0]) 476 component = ComponentClass() 477 478 # component.name = component_name 479 component.path = component_path_tuple[3] 480 component.id = instance_id 481 component.load_config(self._app_config) 482 res = component.initialize() 483 if isinstance(res, defer.Deferred): 484 # NOTE: do this here instead of using maybeDeferred because 485 # callers don't expect a deferred. A lot of tests are calling 486 # create_component so this is the least invasive way of doing 487 # this. 488 def component_initialize_done(result): 489 self.info("Component %s loaded" % component.name) 490 491 return component
492 493 self.info("Component %s still loading" % component.name) 494 res.addCallback(component_initialize_done) 495 496 return res 497 498 self.info("Component %s loaded" % component.name) 499 else: 500 raise ComponentNotFound(component_path) 501 502 return component
503
504 - def _get_component_infos(self, component_path):
505 """ 506 Get information about a component given its path. This method 507 doesn't instantiate Components, it just looks for Component 508 classes. 509 510 @param component_path: information to locate the Component to load 511 @type component_path: string 512 @rtype: (L{elisa.core.component.Component}, L{elisa.core.plugin.Plugin}, tuple) 513 """ 514 ComponentClass = None 515 plugin = None 516 517 component_path_tuple = self._split_component_path(component_path) 518 519 if component_path_tuple: 520 521 plugin_name, component_name = component_path_tuple[0:2] 522 523 plugin = self.get_plugin_with_name(plugin_name) 524 self.debug("Found plugin %r, searching for Component %r", 525 plugin_name, component_name) 526 527 # lazily check plugin's python deps 528 plugin.check_dependencies() 529 530 ComponentClass = self._import_component(plugin, component_name) 531 532 return (ComponentClass, plugin, component_path_tuple)
533
534 - def _import_component(self, plugin, component_name):
535 """ 536 """ 537 Class = None 538 component = plugin.components.get(component_name,{}) 539 path = component.get('path') 540 if path: 541 542 if isinstance(path, basestring): 543 # relative.path:ComponentClass 544 545 # lazily check the dependencies of the Component before trying 546 # to import it. 547 plugin.check_component_dependencies(component_name) 548 549 mod_class = path.split(':') 550 plugin_path = plugin.directory 551 old_path = sys.path[:] 552 sys.path.insert(0, os.path.abspath(os.path.dirname(plugin_path))) 553 package_name = os.path.basename(plugin_path) 554 if package_name: 555 full_path = "%s.%s" % (package_name,mod_class[0]) 556 else: 557 full_path = mod_class[0] 558 try: 559 module = __import__(full_path, 560 {}, {}, [mod_class[1]]) 561 Class = getattr(module, mod_class[1]) 562 except (ImportError, AttributeError), error: 563 self.warning("Could not import component %s.%s", 564 plugin.name, path) 565 sys.path = old_path 566 common.application.handle_traceback() 567 else: 568 self.debug("Imported %r from %s", Class, plugin_path) 569 sys.path = old_path 570 571 else: 572 Class = path 573 574 return Class
575
576 - def get_component_class(self, component_path):
577 """ Retrieve the class of a Component given its path. 578 579 @param component_path: information to locate the Component 580 @type component_path: string 581 @rtype: L{elisa.core.component.Component} class 582 """ 583 self.debug("Retrieving component class of %r", component_path) 584 infos = self._get_component_infos(component_path) 585 ComponentClass, plugin, path_tuple = infos 586 587 return ComponentClass
588 589
590 - def _split_component_path(self, component_path):
591 """split the component path: 592 plugin_name:component_name[:instance_id] 593 594 add default instance_id (0) if missing 595 596 @param component_path: information to locate the Component to load 597 @type component_path: string 598 @returns: tuple of 4 elements : 599 (plugin_name, component_name, instance_id, adjusted_component_path) 600 return empty tuple if the intance is not valid 601 @rtype: tuple 602 """ 603 component_path_tuple = () 604 if component_path: 605 plugin_name = None 606 parts = component_path.split(':') 607 608 if len(parts) < 2: 609 #FIXME: raise exception ? 610 self.warning("Syntax error in %r. Components must be "\ 611 "specified with plugin_name:component_name"\ 612 " syntax" % component_path) 613 elif len(parts) == 2: 614 plugin_name, component_name = parts 615 instance_id = 0 616 #component_path += ':0' 617 elif len(parts) == 3: 618 plugin_name, component_name, instance_id = parts 619 try: 620 instance_id = int(instance_id) 621 except: 622 self.warning("Syntax error in %r. Components must "\ 623 "have a numerical instance_id", component_path) 624 plugin_name = None 625 626 if plugin_name: 627 component_path_tuple = (plugin_name, component_name, 628 instance_id, component_path) 629 else: 630 #FIXME: raise exception ? 631 self.warning("Empty component_path (%r), nothing to create then", 632 component_path) 633 634 return component_path_tuple
635