# -*- coding: utf-8 -*- ## This file is part of Gajim. ## ## Gajim is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published ## by the Free Software Foundation; version 3 only. ## ## Gajim is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Gajim. If not, see . ## ''' Plug-in management related classes. :author: Mateusz Biliński :since: 30th May 2008 :copyright: Copyright (2008) Mateusz Biliński :license: GPL ''' __all__ = ['PluginManager'] import os import sys import fnmatch import common.gajim as gajim from plugins.helpers import log, log_calls, Singleton from plugins.plugin import GajimPlugin class PluginManager(object): ''' Main plug-in management class. Currently: - scans for plugins - activates them - handles GUI extension points, when called by GUI objects after plugin is activated (by dispatching info about call to handlers in plugins) :todo: add more info about how GUI extension points work :todo: add list of available GUI extension points :todo: implement mechanism to dynamically load plugins where GUI extension points have been already called (i.e. when plugin is activated after GUI object creation). [DONE?] :todo: implement mechanism to dynamically deactive plugins (call plugin's deactivation handler) [DONE?] :todo: when plug-in is deactivated all GUI extension points are removed from `PluginManager.gui_extension_points_handlers`. But when object that invoked GUI extension point is abandoned by Gajim, eg. closed ChatControl object, the reference to called GUI extension points is still in `PluginManager.gui_extension_points`. These should be removed, so that object can be destroyed by Python. Possible solution: add call to clean up method in classes 'destructors' (classes that register GUI extension points) ''' __metaclass__ = Singleton #@log_calls('PluginManager') def __init__(self): self.plugins = [] ''' Detected plugin classes. Each class object in list is `GajimPlugin` subclass. :type: [] of class objects ''' self.active_plugins = [] ''' Instance objects of active plugins. These are object instances of classes held `plugins`, but only those that were activated. :type: [] of `GajimPlugin` based objects ''' self.gui_extension_points = {} ''' Registered GUI extension points. ''' self.gui_extension_points_handlers = {} ''' Registered handlers of GUI extension points. ''' for path in gajim.PLUGINS_DIRS: self.add_plugins(PluginManager.scan_dir_for_plugins(path)) log.debug('plugins: %s'%(self.plugins)) self._activate_all_plugins_from_global_config() log.debug('active: %s'%(self.active_plugins)) @log_calls('PluginManager') def _plugin_has_entry_in_global_config(self, plugin): if gajim.config.get_per('plugins', plugin.short_name) is None: return False else: return True @log_calls('PluginManager') def _create_plugin_entry_in_global_config(self, plugin): gajim.config.add_per('plugins', plugin.short_name) @log_calls('PluginManager') def add_plugin(self, plugin_class): ''' :todo: what about adding plug-ins that are already added? Module reload and adding class from reloaded module or ignoring adding plug-in? ''' plugin = plugin_class() if not self._plugin_has_entry_in_global_config(plugin): self._create_plugin_entry_in_global_config(plugin) self.plugins.append(plugin) plugin.active = False @log_calls('PluginManager') def add_plugins(self, plugin_classes): for plugin_class in plugin_classes: self.add_plugin(plugin_class) @log_calls('PluginManager') def gui_extension_point(self, gui_extpoint_name, *args): ''' Invokes all handlers (from plugins) for particular GUI extension point. :param gui_extpoint_name: name of GUI extension point. :type gui_extpoint_name: unicode :param args: parameters to be passed to extension point handlers (typically and object that invokes `gui_extension_point`; however, this can be practically anything) :type args: tuple :todo: GUI extension points must be documented well - names with parameters that will be passed to handlers (in plugins). Such documentation must be obeyed both in core and in plugins. This is a loosely coupled approach and is pretty natural in Python. :bug: what if only some handlers are successfully connected? we should revert all those connections that where successfully made. Maybe call 'self._deactivate_plugin()' or sth similar. Looking closer - we only rewrite tuples here. Real check should be made in method that invokes gui_extpoints handlers. ''' self._add_gui_extension_point_call_to_list(gui_extpoint_name, *args) self._execute_all_handlers_of_gui_extension_point(gui_extpoint_name, *args) @log_calls('PluginManager') def _add_gui_extension_point_call_to_list(self, gui_extpoint_name, *args): self.gui_extension_points.setdefault(gui_extpoint_name, []).append(args) @log_calls('PluginManager') def _execute_all_handlers_of_gui_extension_point(self, gui_extpoint_name, *args): if gui_extpoint_name in self.gui_extension_points_handlers: for handlers in self.gui_extension_points_handlers[gui_extpoint_name]: handlers[0](*args) @log_calls('PluginManager') def activate_plugin(self, plugin): ''' :param plugin: plugin to be activated :type plugin: class object of `GajimPlugin` subclass :todo: success checks should be implemented using exceptions. Such control should also be implemented in deactivation. ''' success = False if not plugin.active: self._add_gui_extension_points_handlers_from_plugin(plugin) self._handle_all_gui_extension_points_with_plugin(plugin) success = True if success: self.active_plugins.append(plugin) plugin.activate() self._set_plugin_active_in_global_config(plugin) plugin.active = True return success def deactivate_plugin(self, plugin): # remove GUI extension points handlers (provided by plug-in) from # handlers list for gui_extpoint_name, gui_extpoint_handlers in \ plugin.gui_extension_points.iteritems(): self.gui_extension_points_handlers[gui_extpoint_name].remove(gui_extpoint_handlers) # detaching plug-in from handler GUI extension points (calling # cleaning up method that must be provided by plug-in developer # for each handled GUI extension point) for gui_extpoint_name, gui_extpoint_handlers in \ plugin.gui_extension_points.iteritems(): if gui_extpoint_name in self.gui_extension_points: for gui_extension_point_args in self.gui_extension_points[gui_extpoint_name]: handler = gui_extpoint_handlers[1] if handler: handler(*gui_extension_point_args) # removing plug-in from active plug-ins list plugin.deactivate() self.active_plugins.remove(plugin) self._set_plugin_active_in_global_config(plugin, False) plugin.active = False def _deactivate_all_plugins(self): for plugin_object in self.active_plugins: self.deactivate_plugin(plugin_object) @log_calls('PluginManager') def _add_gui_extension_points_handlers_from_plugin(self, plugin): for gui_extpoint_name, gui_extpoint_handlers in \ plugin.gui_extension_points.iteritems(): self.gui_extension_points_handlers.setdefault(gui_extpoint_name, []).append( gui_extpoint_handlers) @log_calls('PluginManager') def _handle_all_gui_extension_points_with_plugin(self, plugin): for gui_extpoint_name, gui_extpoint_handlers in \ plugin.gui_extension_points.iteritems(): if gui_extpoint_name in self.gui_extension_points: for gui_extension_point_args in self.gui_extension_points[gui_extpoint_name]: handler = gui_extpoint_handlers[0] if handler: handler(*gui_extension_point_args) @log_calls('PluginManager') def _activate_all_plugins(self): ''' Activates all plugins in `plugins`. Activated plugins are appended to `active_plugins` list. ''' #self.active_plugins = [] for plugin in self.plugins: self.activate_plugin(plugin) def _activate_all_plugins_from_global_config(self): for plugin in self.plugins: if self._plugin_is_active_in_global_config(plugin): self.activate_plugin(plugin) def _plugin_is_active_in_global_config(self, plugin): return gajim.config.get_per('plugins', plugin.short_name, 'active') def _set_plugin_active_in_global_config(self, plugin, active=True): gajim.config.set_per('plugins', plugin.short_name, 'active', active) @staticmethod @log_calls('PluginManager') def scan_dir_for_plugins(path): ''' Scans given directory for plugin classes. :param path: directory to scan for plugins :type path: unicode :return: list of found plugin classes (subclasses of `GajimPlugin` :rtype: [] of class objects :note: currently it only searches for plugin classes in '\*.py' files present in given direcotory `path` (no recursion here) :todo: add scanning packages :todo: add scanning zipped modules ''' plugins_found = [] if os.path.isdir(path): dir_list = os.listdir(path) #log.debug(dir_list) sys.path.insert(0, path) #log.debug(sys.path) for elem_name in dir_list: log.debug('- "%s"'%(elem_name)) file_path = os.path.join(path, elem_name) log.debug(' "%s"'%(file_path)) module = None if os.path.isfile(file_path) and fnmatch.fnmatch(file_path,'*.py'): module_name = os.path.splitext(elem_name)[0] log.debug('Possible module detected.') try: module = __import__(module_name) log.debug('Module imported.') except ValueError, value_error: log.debug('Module not imported successfully. ValueError: %s'%(value_error)) except ImportError, import_error: log.debug('Module not imported successfully. ImportError: %s'%(import_error)) elif os.path.isdir(file_path): module_name = elem_name file_path += os.path.sep log.debug('Possible package detected.') try: module = __import__(module_name) log.debug('Package imported.') except ValueError, value_error: log.debug('Package not imported successfully. ValueError: %s'%(value_error)) except ImportError, import_error: log.debug('Package not imported successfully. ImportError: %s'%(import_error)) if module: log.debug('Attributes processing started') for module_attr_name in [attr_name for attr_name in dir(module) if not (attr_name.startswith('__') or attr_name.endswith('__'))]: module_attr = getattr(module, module_attr_name) log.debug('%s : %s'%(module_attr_name, module_attr)) try: if issubclass(module_attr, GajimPlugin) and \ not module_attr is GajimPlugin: log.debug('is subclass of GajimPlugin') #log.debug('file_path: %s\nabspath: %s\ndirname: %s'%(file_path, os.path.abspath(file_path), os.path.dirname(os.path.abspath(file_path)))) #log.debug('file_path: %s\ndirname: %s\nabspath: %s'%(file_path, os.path.dirname(file_path), os.path.abspath(os.path.dirname(file_path)))) module_attr.__path__ = os.path.abspath(os.path.dirname(file_path)) plugins_found.append(module_attr) except TypeError, type_error: log.debug('module_attr: %s, error : %s'%( module_name+'.'+module_attr_name, type_error)) log.debug(module) return plugins_found