# Terminator by Chris Jones # GPL v2 only """plugin.py - Base plugin system Inspired by Armin Ronacher's post at http://lucumr.pocoo.org/2006/7/3/python-plugin-system Used with permission (the code in that post is to be considered BSD licenced, per the authors wishes) >>> registry = PluginRegistry() >>> isinstance(registry.instances, dict) True >>> registry.enable('TestPlugin') >>> registry.load_plugins() >>> plugins = registry.get_plugins_by_capability('test') >>> len(plugins) 1 >>> plugins[0] #doctest: +ELLIPSIS >>> registry.get_plugins_by_capability('this_should_not_ever_exist') [] >>> plugins[0].do_test() 'TestPluginWin' """ import sys import os from . import borg from .config import Config from .util import dbg, err, get_config_dir from .terminator import Terminator class Plugin(object): """Definition of our base plugin class""" capabilities = None def __init__(self): """Class initialiser.""" pass def unload(self): """Prepare to be unloaded""" pass class PluginRegistry(borg.Borg): """Definition of a class to store plugin instances""" available_plugins = None instances = None path = None done = None def __init__(self): """Class initialiser""" borg.Borg.__init__(self, self.__class__.__name__) self.prepare_attributes() def prepare_attributes(self): """Prepare our attributes""" if not self.instances: self.instances = {} if not self.path: self.path = [] (head, _tail) = os.path.split(borg.__file__) self.path.append(os.path.join(head, 'plugins')) self.path.append(os.path.join(get_config_dir(), 'plugins')) dbg('Plugin path: %s' % self.path) if not self.done: self.done = False if not self.available_plugins: self.available_plugins = {} def load_plugins(self, force=False): """Load all plugins present in the plugins/ directory in our module""" if self.done and (not force): dbg('Already loaded') return dbg('loading plugins, force:(%s)' % force) config = Config() for plugindir in self.path: sys.path.insert(0, plugindir) try: files = os.listdir(plugindir) except OSError: sys.path.remove(plugindir) continue for plugin in files: if plugin == '__init__.py': continue pluginpath = os.path.join(plugindir, plugin) if os.path.isfile(pluginpath) and plugin[-3:] == '.py': dbg('Importing plugin %s' % plugin) try: module = __import__(plugin[:-3], None, None, ['']) for item in getattr(module, 'AVAILABLE'): func = getattr(module, item) if item not in list(self.available_plugins.keys()): self.available_plugins[item] = func if item not in config['enabled_plugins']: dbg('plugin %s not enabled, skipping' % item) continue if item not in self.instances: self.instances[item] = func() elif force: #instead of multiple copies of loaded #plugin objects, unload where plugins #can clean up and then re-init so there #is one plugin object self.instances[item].unload() self.instances.pop(item, None) self.instances[item] = func() except Exception as ex: err('PluginRegistry::load_plugins: Importing plugin %s \ failed: %s' % (plugin, ex)) self.done = True def get_plugins_by_capability(self, capability): """Return a list of plugins with a particular capability""" result = [] dbg('searching %d plugins \ for %s' % (len(self.instances), capability)) for plugin in self.instances: if capability in self.instances[plugin].capabilities: result.append(self.instances[plugin]) return result def get_all_plugins(self): """Return all plugins""" return(self.instances) def get_available_plugins(self): """Return a list of all available plugins whether they are enabled or disabled""" return(list(self.available_plugins.keys())) def is_enabled(self, plugin): """Return a boolean value indicating whether a plugin is enabled or not""" return(plugin in self.instances) def enable(self, plugin): """Enable a plugin""" if plugin in self.instances: err("Cannot enable plugin %s, already enabled" % plugin) dbg("Enabling %s" % plugin) self.instances[plugin] = self.available_plugins[plugin]() def disable(self, plugin): """Disable a plugin""" dbg("Disabling %s" % plugin) self.instances[plugin].unload() del(self.instances[plugin]) # This is where we should define a base class for each type of plugin we # support # URLHandler - This adds a regex match to the Terminal widget and provides a # callback to turn that into a URL. class URLHandler(Plugin): """Base class for URL handlers""" capabilities = ['url_handler'] handler_name = None match = None nameopen = None namecopy = None def __init__(self): """Class initialiser""" Plugin.__init__(self) terminator = Terminator() for terminal in terminator.terminals: terminal.match_add(self.handler_name, self.match) def callback(self, url): """Callback to transform the enclosed URL""" raise NotImplementedError def unload(self): """Handle being removed""" if not self.handler_name: err('unload called without self.handler_name being set') return terminator = Terminator() for terminal in terminator.terminals: terminal.match_remove(self.handler_name) # MenuItem - This is able to execute code during the construction of the # context menu of a Terminal. class MenuItem(Plugin): """Base class for menu items""" capabilities = ['terminal_menu'] def callback(self, menuitems, menu, terminal): """Callback to transform the enclosed URL""" raise NotImplementedError