From 7b34a39da7da69a3a22b7d974c7574cb09f0d0a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20=27Necoro=27=20Neumann?= Date: Thu, 3 Jul 2008 01:29:37 +0200 Subject: New plugin system - first hack --- portato/plugin_old.py | 530 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 530 insertions(+) create mode 100644 portato/plugin_old.py (limited to 'portato/plugin_old.py') diff --git a/portato/plugin_old.py b/portato/plugin_old.py new file mode 100644 index 0000000..5926922 --- /dev/null +++ b/portato/plugin_old.py @@ -0,0 +1,530 @@ +# -*- coding: utf-8 -*- +# +# File: portato/plugin.py +# This file is part of the Portato-Project, a graphical portage-frontend. +# +# Copyright (C) 2007 René 'Necoro' Neumann +# This is free software. You may redistribute copies of it under the terms of +# the GNU General Public License version 2. +# There is NO WARRANTY, to the extent permitted by law. +# +# Written by René 'Necoro' Neumann + +"""A module containing the management of the plugin system.""" + +from __future__ import absolute_import + +import os, os.path +from xml.dom.minidom import parse +from lxml import etree + +from .constants import PLUGIN_DIR, XSD_LOCATION +from .helper import debug, info, warning, error, flatten + +class PluginImportException (ImportError): + pass + +class Options (object): + """The -element.""" + + __options = ("disabled", "blocking") + + def __init__ (self, options = None): + + self.disabled = False + self.blocking = False + + if options: + self.parse(options) + + def parse (self, options): + for opt in options: + nodes = opt.childNodes + type = str(nodes[0].nodeValue.strip()) + if type in self.__options: + self.set(type, True) + + def get (self, name): + return self.__getattribute__(name) + + def set (self, name, value): + return self.__setattr__(name, value) + +class Menu: + """A single -element.""" + def __init__ (self, plugin, label, call): + """Constructor. + + @param plugin: the plugin this menu belongs to + @type plugin: Plugin + @param label: the label to show + @type label: string + @param call: the function to call relative to the import statement + @type call: string + + @raises PluginImportException: if the plugin's import could not be imported""" + + self.label = label + self.plugin = plugin + + if self.plugin.needs_import(): # get import + imp = self.plugin.get_import() + try: + mod = __import__(imp, globals(), locals(), [call]) + except ImportError: + raise PluginImportException, imp + + try: + self.call = eval("mod."+call) # build function + except AttributeError: + raise PluginImportException, imp + else: + try: + self.call = eval(call) + except AttributeError: + raise PluginImportException, imp + +class Connect: + """A single -element.""" + + def __init__ (self, hook, type, depend_plugin): + """Constructor. + + @param hook: the parent Hook + @type hook: Hook + @param type: the type of the connect ("before", "after", "override") + @type type: string + @param depend_plugin: a plugin we are dependant on + @type depend_plugin: string or None""" + + self.type = type + self.hook = hook + self.depend_plugin = depend_plugin + + def is_before_type (self): + return self.type == "before" + + def is_after_type (self): + return self.type == "after" + + def is_override_type (self): + return self.type == "override" + +class Hook: + """A single -element.""" + + def __init__ (self, plugin, hook, call): + """Constructor. + + @param plugin: the parent Plugin + @type plugin: Plugin + @param hook: the hook to add to + @type hook: string + @param call: the call to make + @type call: string""" + + self.plugin = plugin + self.hook = hook + self.call = call + self.connects = [] + + def parse_connects (self, connects): + """This gets a list of -elements and parses them. + + @param connects: the list of 's + @type connects: NodeList""" + + if not connects: # no connects - assume "before" connect + self.connects.append(Connect(self, "before", None)) + + for c in connects: + type = c.getAttribute("type") + if type == '': + type = "before" + + # get dep_plugin if available + dep_plugin = None + if c.hasChildNodes(): + nodes = c.childNodes + dep_plugin = nodes[0].nodeValue.strip() + + connect = Connect(self, type, dep_plugin) + self.connects.append(connect) + +class Plugin: + """A complete plugin.""" + + (STAT_DISABLED, STAT_TEMP_ENABLED, STAT_ENABLED, STAT_TEMP_DISABLED) = range(4) + + def __init__ (self, file, name, author): + """Constructor. + + @param file: the file name of the plugin.xml + @type file: string + @param name: the name of the plugin + @type name: Node + @param author: the author of the plugin + @type author: Node""" + + self.file = file + self.name = name.firstChild.nodeValue.strip() + self.author = author.firstChild.nodeValue.strip() + self._import = None + self.hooks = [] + self.menus = [] + self.options = Options() + + self.status = self.STAT_ENABLED + + def parse_hooks (self, hooks): + """Gets an -elements and parses it. + + @param hooks: the hooks node + @type hooks: NodeList""" + + if hooks: + for h in hooks[0].getElementsByTagName("hook"): + hook = Hook(self, str(h.getAttribute("type")), str(h.getAttribute("call"))) + hook.parse_connects(h.getElementsByTagName("connect")) + self.hooks.append(hook) + + def parse_menus (self, menus): + """Get a list of -elements and parses them. + + @param menus: the menu nodelist + @type menus: NodeList""" + + if menus: + for item in menus[0].getElementsByTagName("item"): + menu = Menu(self, item.firstChild.nodeValue.strip(), str(item.getAttribute("call"))) + self.menus.append(menu) + + def parse_options (self, options): + if options: + for o in options: + self.options.parse(o.getElementsByTagName("option")) + + self.status = self.STAT_DISABLED if self.options.get("disabled") else self.STAT_ENABLED + + def set_import (self, imports): + """This gets a list of imports and parses them - setting the import needed to call the plugin. + + @param imports: list of imports + @type imports: NodeList + + @raises PluginImportException: if the plugin's import could not be imported""" + + if imports: + self._import = str(imports[0].firstChild.nodeValue.strip()) + + try: # try loading + mod = __import__(self._import) + del mod + except ImportError: + raise PluginImportException, self._import + + def needs_import (self): + """Returns True if an import is required prior to calling the plugin. + @rtype: bool""" + return self._import is not None + + def get_import (self): + """Returns the module to import. + @rtype: string""" + return self._import + + def get_option(self, name): + return self.options.get(name) + + def set_option (self, name, value): + return self.options.set(name, value) + + def is_enabled (self): + return (self.status in (self.STAT_ENABLED, self.STAT_TEMP_ENABLED)) + +class PluginQueue: + """Class managing and loading the plugins.""" + + def __init__ (self, frontend, load = True): + """Constructor. + + @param frontend: the frontend used + @type frontend: string + @param load: if False nothing is loaded + @type load: bool""" + + self.frontend = frontend + self.list = [] + self.hooks = {} + if load: + self._load() + + def get_plugins (self, list_disabled = True): + return [x for x in self.list if (x.is_enabled() or list_disabled)] + + def get_plugin_data (self, list_disabled = False): + return [(x.name, x.author) for x in self.list if (x.is_enabled() or list_disabled)] + + def get_plugin_menus (self, list_disabled = False): + return flatten([x.menus for x in self.list if (x.is_enabled() or list_disabled)]) + + def hook (self, hook, *hargs, **hkwargs): + """This is a method taking care of calling the plugins. + + B{Example}:: + + @pluginQueue.hook("some_hook", data) + def function (a, b, c): + orig_call(b,c,data) + + def function (a, b, c): + hook = pluginQueue.hook("some_hook", data) + hook(orig_call)(b,c,data) + + @param hook: the name of the hook + @type hook: string""" + + def call (cmd): + """Convienience function for calling a connect. + @param cmd: the actual Connect + @type cmd: Connect""" + + imp = "" + if cmd.hook.plugin.needs_import(): # get import + imp = cmd.hook.plugin.get_import() + try: + mod = __import__(imp, globals(), locals(), [cmd.hook.call]) + except ImportError: + error(_("%s cannot be imported."), imp) + return + + try: + f = eval("mod."+cmd.hook.call) # build function + except AttributeError: + error(_("%s cannot be imported."), cmd.hook.call) + else: + try: + f = eval(cmd.hook.call) + except AttributeError: + error(_("%s cannot be imported."), cmd.hook.call) + + return f(*hargs, **hkwargs) # call function + + def hook_decorator (func): + """This is the real decorator.""" + + if hook in self.hooks: + list = self.hooks[hook] + else: + list = ([],[],[]) + + # remove disabled + _list = ([],[],[]) + for i in range(len(list)): + for cmd in list[i]: + if cmd.hook.plugin.is_enabled(): + _list[i].append(cmd) + + list = _list + + def wrapper (*args, **kwargs): + + ret = None + + # before + for cmd in list[0]: + debug(_("Accessing hook '%(hook)s' of plugin '%(plugin)s' (before)."), {"hook" : hook, "plugin": cmd.hook.plugin.name}) + call(cmd) + + if list[1]: # override + info(_("Overriding hook '%(hook)s' with plugin '%(plugin)s'."), {"hook": hook, "plugin": list[1][0].hook.plugin.name}) + ret = call(list[1][0]) + else: # normal + ret = func(*args, **kwargs) + + # after + for cmd in list[2]: + debug(_("Accessing hook '%(hook)s' of plugin '%(plugin)s' (after)."), {"hook":hook, "plugin": cmd.hook.plugin.name}) + call(cmd) + + return ret + + return wrapper + + return hook_decorator + + def _load (self): + """Load the plugins.""" + plugins = filter(lambda x: x.endswith(".xml"), os.listdir(PLUGIN_DIR)) + plugins = map(lambda x: os.path.join(PLUGIN_DIR, x), plugins) + schema = etree.XMLSchema(file = XSD_LOCATION) + + for p in plugins: + + try: + schema.assertValid(etree.parse(p)) + except etree.XMLSyntaxError: + error(_("Loading plugin '%s' failed. Invalid XML syntax."), p) + continue + except etree.DocumentInvalid: + error(_("Loading plugin '%s' failed. Plugin does not comply with schema."), p) + continue + + doc = parse(p) + + try: + list = doc.getElementsByTagName("plugin") + elem = list[0] + + frontendOK = None + frontends = elem.getElementsByTagName("frontends") + if frontends: + nodes = frontends[0].childNodes + for f in nodes[0].nodeValue.strip().split(): + if f == self.frontend: + frontendOK = True # one positive is enough + break + elif frontendOK is None: # do not make negative if we already have a positive + frontendOK = False + + if frontendOK is None or frontendOK == True: + plugin = Plugin(p, elem.getElementsByTagName("name")[0], elem.getElementsByTagName("author")[0]) + plugin.parse_hooks(elem.getElementsByTagName("hooks")) + plugin.set_import(elem.getElementsByTagName("import")) + plugin.parse_menus(elem.getElementsByTagName("menu")) + plugin.parse_options(elem.getElementsByTagName("options")) + + self.list.append(plugin) + info(_("Plugin '%s' loaded."), p) + + except PluginImportException, e: + error(_("Loading plugin '%(plugin)s' failed: Could not import %(import)s"), {"plugin": p, "import": e[0]}) + finally: + doc.unlink() + + self._organize() + + def _organize (self): + """Creates the lists of connects in a way, that all dependencies are fullfilled.""" + unresolved_before = {} + unresolved_after = {} + star_before = {} # should be _before_ all other + star_after = {} # should be _after_ all other + + for plugin in self.list: # plugins + for hook in plugin.hooks: # hooks in plugin + if not hook.hook in self.hooks: + self.hooks[hook.hook] = ([], [], []) + + for connect in hook.connects: # connects in hook + + # type="before" + if connect.is_before_type(): + if connect.depend_plugin is None: # no dependency -> straight add + self.hooks[hook.hook][0].append(connect) + elif connect.depend_plugin == "*": + self.hooks[hook.hook][0][0:0] = [connect] + elif connect.depend_plugin == "-*": + if not hook.hook in star_before: + star_before[hook.hook] = [] + + star_before[hook.hook].append(connect) + else: + named = [x.plugin.name for x in self.hooks[hook.hook][0]] + if connect.depend_plugin in named: + self.hooks[hook.hook][0].insert(named.index(connect.depend_plugin), connect) + else: + if not hook.hook in unresolved_before: + unresolved_before[hook.hook] = [] + + unresolved_before[hook.hook].append(connect) + + # type = "after" + elif connect.is_after_type(): + if connect.depend_plugin is None: # no dependency -> straight add + self.hooks[hook.hook][2].append(connect) + elif connect.depend_plugin == "*": + if not hook.hook in star_after: + star_after[hook.hook] = [] + + star_after[hook.hook].append(connect) + elif connect.depend_plugin == "-*": + self.hooks[hook.hook][2][0:0] = [connect] + else: + named = [x.hook.plugin.name for x in self.hooks[hook.hook][2]] + if connect.depend_plugin in named: + self.hooks[hook.hook][2].insert(named.index(connect.depend_plugin)+1, connect) + else: + if not hook.hook in unresolved_after: + unresolved_after[hook.hook] = [] + + unresolved_after[hook.hook].append(connect) + + # type = "override" + elif connect.is_override_type(): + if self.hooks[hook.hook][1]: + warning(_("For hook '%(hook)s' an override is already defined by plugin '%(plugin)s'!"), {"hook": hook.hook, "plugin": self.hooks[hook.hook][1][0]}) + + self.hooks[hook.hook][1][:1] = [connect] + continue + + self._resolve_unresolved(unresolved_before, unresolved_after) + + for hook in star_before: + self.hooks[hook][0].extend(star_before[hook]) # append the list + + for hook in star_after: + self.hooks[hook][2].extend(star_after[hook]) # append the list + + + def _resolve_unresolved (self, before, after): + def resolve(hook, list, idx, add): + if not list: + return + + changed = False + for connect in list[:]: + named = [x.plugin.name for x in self.hooks[hook][idx]] + if connect.depend_plugin in named: + changed = True + self.hooks[hook][idx].insert(named.index(connect.depend_plugin)+add, connect) + list.remove(connect) + + if changed: + resolve(hook, list, idx, add) + + for l in list: + warning("Command for hook '%(hook)s' in plugin '%(plugin)s' could not be added due to missing dependant: '%(dep)s'!", {"hook": hook, "plugin": l.hook.plugin.name, "dep": l.depend_plugin}) + + for hook in before: + resolve(hook, before[hook], 0, 0) + + for hook in after: + resolve(hook, after[hook], 2, 1) + + +__plugins = None + +def load_plugins(frontend): + """Loads the plugins for a given frontend. + @param frontend: The frontend. See L{constants.FRONTENDS} for the correct list of values. + @type frontend: string""" + + global __plugins + if __plugins is None or __plugins.frontend != frontend: + __plugins = PluginQueue(frontend) + +def get_plugin_queue(): + """Returns the actual L{PluginQueue}. If it is C{None}, they are not being loaded yet. + + @returns: the actual L{PluginQueue} or C{None} + @rtype: PluginQueue""" + return __plugins + +def hook(hook, *args, **kwargs): + if __plugins is None: + def pseudo_decorator(f): + return f + return pseudo_decorator + else: + return __plugins.hook(hook, *args, **kwargs) -- cgit v1.2.3