From 0885d993b40c92d86a61e3952c9df6c00578bfce Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Tue, 24 Jun 2008 10:54:57 +0200 Subject: Removed frontend specification from notify plugin --- plugins/notify.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/notify.xml b/plugins/notify.xml index 8de08c8..9a89987 100644 --- a/plugins/notify.xml +++ b/plugins/notify.xml @@ -3,7 +3,6 @@ René 'Necoro' Neumann Notify - gtk portato.plugins.notify -- cgit v1.2.3-54-g00ecf From 05d555bdd35332c1b2b45c9261a3c2ace3888591 Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Tue, 24 Jun 2008 10:55:27 +0200 Subject: Removed frontend tag; added dependencies tag --- plugin.xsd | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/plugin.xsd b/plugin.xsd index 38cb872..985b105 100644 --- a/plugin.xsd +++ b/plugin.xsd @@ -6,7 +6,13 @@ - + + + + + + + @@ -78,12 +84,4 @@ - - - - - - - - -- cgit v1.2.3-54-g00ecf From 014b668a1bd5b1bc768587b80d18c7038f748f8a Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Mon, 30 Jun 2008 20:02:52 +0200 Subject: Added stuff to plugin.py --- portato/plugin.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/portato/plugin.py b/portato/plugin.py index 5926922..ae89a1c 100644 --- a/portato/plugin.py +++ b/portato/plugin.py @@ -18,12 +18,16 @@ import os, os.path from xml.dom.minidom import parse from lxml import etree +from .backend import system from .constants import PLUGIN_DIR, XSD_LOCATION from .helper import debug, info, warning, error, flatten class PluginImportException (ImportError): pass +class UnmatchedDepsException (Exception): + pass + class Options (object): """The -element.""" @@ -72,7 +76,10 @@ class Menu: try: mod = __import__(imp, globals(), locals(), [call]) except ImportError: - raise PluginImportException, imp + if self.plugin.unmatched_deps: + raise UnmatchedDepsException + else: + raise PluginImportException, imp try: self.call = eval("mod."+call) # build function @@ -172,6 +179,8 @@ class Plugin: self._import = None self.hooks = [] self.menus = [] + self.deps = [] + self.unmatched_deps = [] self.options = Options() self.status = self.STAT_ENABLED @@ -205,6 +214,15 @@ class Plugin: self.options.parse(o.getElementsByTagName("option")) self.status = self.STAT_DISABLED if self.options.get("disabled") else self.STAT_ENABLED + + def parse_deps (self, deps): + if deps: + for d in deps.firstChild.getElementsByTagName("dependency"): + self.deps.append(d.firstChild.nodeValue.strip()) + + for d in self.deps: + if not system.find_packages(d, "installed", with_version = False): + self.unmatched_deps.append(d) def set_import (self, imports): """This gets a list of imports and parses them - setting the import needed to call the plugin. @@ -221,7 +239,10 @@ class Plugin: mod = __import__(self._import) del mod except ImportError: - raise PluginImportException, self._import + if self.unmatched_deps: + raise UnmatchedDepsException + else: + raise PluginImportException, self._import def needs_import (self): """Returns True if an import is required prior to calling the plugin. @@ -389,6 +410,7 @@ class PluginQueue: if frontendOK is None or frontendOK == True: plugin = Plugin(p, elem.getElementsByTagName("name")[0], elem.getElementsByTagName("author")[0]) + plugin.parse_deps(elem.getElementsByTagName("dependencies")) plugin.parse_hooks(elem.getElementsByTagName("hooks")) plugin.set_import(elem.getElementsByTagName("import")) plugin.parse_menus(elem.getElementsByTagName("menu")) -- cgit v1.2.3-54-g00ecf From 9b22030eda3b9af70ae052611cf485a0d7ae063b Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Mon, 30 Jun 2008 20:12:55 +0200 Subject: First draft of the new plugin window --- portato/gui/templates/PluginWindow.glade | 180 +++++++++++++++++++++++++++++-- 1 file changed, 172 insertions(+), 8 deletions(-) diff --git a/portato/gui/templates/PluginWindow.glade b/portato/gui/templates/PluginWindow.glade index 1de5fc0..28b46e6 100644 --- a/portato/gui/templates/PluginWindow.glade +++ b/portato/gui/templates/PluginWindow.glade @@ -1,6 +1,6 @@ - + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -18,17 +18,181 @@ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 - + True - False - GTK_POLICY_NEVER - GTK_POLICY_NEVER + True + 120 + True - + True - True - False + False + GTK_POLICY_NEVER + GTK_POLICY_NEVER + + + True + True + False + + + + True + False + + + + + True + 4 + 2 + + + True + label + + + 2 + GTK_FILL + + + + + True + label + + + 1 + 2 + GTK_FILL + + + + + True + label + + + 1 + 2 + 1 + 2 + GTK_FILL + + + + + True + 5 + True + GTK_BUTTONBOX_EDGE + + + True + True + Enabled + 0 + True + + + True + + + + + True + True + Temporarily enabled + 0 + True + radiobutton1 + + + 1 + True + + + + + True + True + Temporarily disabled + 0 + True + radiobutton1 + + + 2 + True + + + + + True + True + Disabled + 0 + True + radiobutton1 + + + 3 + True + + + + + 2 + 3 + 4 + GTK_FILL + + + + + True + label + + + 2 + 3 + GTK_FILL + + + + + True + True + + + True + True + True + + + + + True + expander + + + label_item + + + + + 1 + 2 + 2 + 3 + + + + + True + False + -- cgit v1.2.3-54-g00ecf From 09e693a8061d0b96c3aac8aae6ae12d6272b2c3e Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Mon, 30 Jun 2008 21:06:17 +0200 Subject: Now the new design is able to do the same as the old one --- portato/gui/templates/PluginWindow.glade | 265 +++++++++++++++---------------- portato/gui/windows/plugin.py | 84 ++++++---- portato/plugin.py | 1 + 3 files changed, 182 insertions(+), 168 deletions(-) diff --git a/portato/gui/templates/PluginWindow.glade b/portato/gui/templates/PluginWindow.glade index 28b46e6..01db971 100644 --- a/portato/gui/templates/PluginWindow.glade +++ b/portato/gui/templates/PluginWindow.glade @@ -1,11 +1,12 @@ - + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 Plugins + False True GTK_WIN_POS_CENTER_ON_PARENT True @@ -18,183 +19,177 @@ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 - + True - True - 120 - True + False + GTK_POLICY_NEVER + GTK_POLICY_NEVER - + True - False - GTK_POLICY_NEVER - GTK_POLICY_NEVER - - - True - True - False - - + False + True + False - - True - False - + + + + + True + 4 + 2 + 5 - + True - 4 - 2 + 5 + True + GTK_BUTTONBOX_EDGE - + True - label + True + Enabled + 0 + True + - - 2 - GTK_FILL - - + True - label + True + Temporarily enabled + 0 + enabledRB - 1 - 2 - GTK_FILL + 1 - + True - label + True + Temporarily disabled + 0 + enabledRB + - 1 - 2 - 1 - 2 - GTK_FILL + 2 - + True - 5 - True - GTK_BUTTONBOX_EDGE - - - True - True - Enabled - 0 - True - - - True - - - - - True - True - Temporarily enabled - 0 - True - radiobutton1 - - - 1 - True - - - - - True - True - Temporarily disabled - 0 - True - radiobutton1 - - - 2 - True - - - - - True - True - Disabled - 0 - True - radiobutton1 - - - 3 - True - - + True + Disabled + 0 + enabledRB + - 2 - 3 - 4 - GTK_FILL + False + False + 3 + + + 2 + 3 + 4 + GTK_FILL + + + + + True + 0 + label + True + + + 1 + 2 + 1 + 2 + GTK_FILL + + + + + True + <b>Author:</b> + True + True + + + 1 + 2 + GTK_FILL + + + + + True + label + + + 2 + GTK_FILL + + + + + True + True - + True - label + True + True - - 2 - 3 - GTK_FILL - - + True - True - - - True - True - True - - - - - True - expander - - - label_item - - + Needed dependencies + True - 1 - 2 - 2 - 3 + label_item - True - False + 2 + 3 + + + + + True + True + True + _Install dependencies + True + 0 + + + 1 + 2 + 2 + 3 + + False + 1 + @@ -232,7 +227,7 @@ False - 1 + 2 diff --git a/portato/gui/windows/plugin.py b/portato/gui/windows/plugin.py index fb9446e..6e8fdab 100644 --- a/portato/gui/windows/plugin.py +++ b/portato/gui/windows/plugin.py @@ -34,48 +34,40 @@ class PluginWindow (AbstractDialog): self.plugins = plugins self.changedPlugins = {} - view = self.tree.get_widget("pluginList") - self.store = gtk.ListStore(str,str,str) + self.buttons = map(self.tree.get_widget, ("disabledRB", "tempEnabledRB", "enabledRB", "tempDisabledRB")) + map(lambda b: b.set_mode(False), self.buttons) + + self.descrLabel = self.tree.get_widget("descrLabel") + self.authorLabel = self.tree.get_widget("authorLabel") + + self.depExpander = self.tree.get_widget("depExpander") + self.installBtn = self.tree.get_widget("installBtn") - view.set_model(self.store) + self.view = self.tree.get_widget("pluginList") + self.store = gtk.ListStore(str) - cell = gtk.CellRendererText() - col = gtk.TreeViewColumn(_("Plugin"), cell, markup = 0) - view.append_column(col) + self.view.set_model(self.store) - col = gtk.TreeViewColumn(_("Authors"), cell, text = 1) - view.append_column(col) - - ccell = gtk.CellRendererCombo() - ccell.set_property("model", self.statsStore) - ccell.set_property("text-column", 0) - ccell.set_property("has-entry", False) - ccell.set_property("editable", True) - ccell.connect("edited", self.cb_status_changed) - col = gtk.TreeViewColumn(_("Status"), ccell, markup = 2) - view.append_column(col) + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn("Plugin", cell, markup = 0) + self.view.append_column(col) - for p in ((""+p.name+"", p.author, _(self.statsStore[p.status][0])) for p in plugins): - self.store.append(p) + for p in plugins: + self.store.append(["%s" % p.name]) - self.window.show_all() + self.view.get_selection().connect("changed", self.cb_list_selection) - def cb_status_changed (self, cell, path, new_text): - path = int(path) - - self.store[path][2] = "%s" % new_text + self.window.show_all() - # convert string to constant - const = None - for num, val in enumerate(self.statsStore): - if val[0] == new_text: - const = num - break + def cb_state_toggled (self, rb): + + plugin = self.get_actual() - assert (const is not None) + if plugin: + state = self.buttons.index(rb) - self.changedPlugins.update({self.plugins[path] : const}) - debug("new changed plugins: %s => %d", self.plugins[path].name, const) + self.changedPlugins[plugin] = state + debug("new changed plugins: %s => %d", plugin.name, state) def cb_ok_clicked (self, btn): for plugin, val in self.changedPlugins.iteritems(): @@ -83,3 +75,29 @@ class PluginWindow (AbstractDialog): self.close() return True + + def cb_list_selection (self, selection): + plugin = self.get_actual() + + if plugin: + if not plugin.description: + self.descrLabel.hide() + else: + self.descrLabel.set_label(plugin.description) + self.descrLabel.show() + + self.authorLabel.set_label(plugin.author) + + status = self.changedPlugins.get(plugin, plugin.status) + self.buttons[status].set_active(True) + + self.installBtn.hide() + self.depExpander.hide() + + def get_actual (self): + store, it = self.view.get_selection().get_selected() + + if it: + return self.plugins[int(store.get_path(it)[0])] + else: + return None diff --git a/portato/plugin.py b/portato/plugin.py index ae89a1c..cbadfd1 100644 --- a/portato/plugin.py +++ b/portato/plugin.py @@ -176,6 +176,7 @@ class Plugin: self.file = file self.name = name.firstChild.nodeValue.strip() self.author = author.firstChild.nodeValue.strip() + self.description = "" self._import = None self.hooks = [] self.menus = [] -- cgit v1.2.3-54-g00ecf From 7b34a39da7da69a3a22b7d974c7574cb09f0d0a2 Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Thu, 3 Jul 2008 01:29:37 +0200 Subject: New plugin system - first hack --- portato/plugin.py | 573 ++++++++++++++------------------------------------ portato/plugin_old.py | 530 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 683 insertions(+), 420 deletions(-) create mode 100644 portato/plugin_old.py diff --git a/portato/plugin.py b/portato/plugin.py index 5926922..6160d5e 100644 --- a/portato/plugin.py +++ b/portato/plugin.py @@ -3,349 +3,145 @@ # File: portato/plugin.py # This file is part of the Portato-Project, a graphical portage-frontend. # -# Copyright (C) 2007 René 'Necoro' Neumann +# Copyright (C) 2008 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) +from collections import defaultdict +from functools import wraps - def set (self, name, value): - return self.__setattr__(name, value) +from .helper import debug, warning, info -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""" +class Menu (object): + __slots__ = ("label", "call") + def __init__ (self, label, call): 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. + self.call = call - @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""" +class Call (object): + __slots__ = ("plugin", "hook", "call", "type", "dep") + def __init__ (self, plugin, hook, call, type = "before", dep = None): 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""" + self.type = type + self.dep = dep - 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) +class Hook (object): + __slots__ = ("before", "override", "after") - def parse_menus (self, menus): - """Get a list of -elements and parses them. + def __init__ (self): + self.before = [] + self.override = None + self.after = [] - @param menus: the menu nodelist - @type menus: NodeList""" +class Plugin (object): - if menus: - for item in menus[0].getElementsByTagName("item"): - menu = Menu(self, item.firstChild.nodeValue.strip(), str(item.getAttribute("call"))) - self.menus.append(menu) + (STAT_DISABLED, STAT_TEMP_ENABLED, STAT_ENABLED, STAT_TEMP_DISABLED) = range(4) - def parse_options (self, options): - if options: - for o in options: - self.options.parse(o.getElementsByTagName("option")) + def __init__ (self): + self.__menus = [] + self.__calls = [] + self.state = STAT_ENABLED - 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""" + @property + def author (self): + return getattr(self, "__author__", "") - if imports: - self._import = str(imports[0].firstChild.nodeValue.strip()) + @property + def description (self): + if hasattr(self, "__description__"): + return self.__description__ + else: + return getattr(self, "__doc__", "") - try: # try loading - mod = __import__(self._import) - del mod - except ImportError: - raise PluginImportException, self._import + @property + def name (self): + return getattr(self, "__name__", self.__class__.__name__) - def needs_import (self): - """Returns True if an import is required prior to calling the plugin. - @rtype: bool""" - return self._import is not None + @property + def menus (self): + return iter(self.__menus) - def get_import (self): - """Returns the module to import. - @rtype: string""" - return self._import + @property + def calls (self): + return iter(self.__calls) - def get_option(self, name): - return self.options.get(name) + def add_menu (self, label, callable): + self.__menus.append(Menu(label, callable)) - def set_option (self, name, value): - return self.options.set(name, value) + def add_call (self, hook, callable, type = "before", dep = None): + self.__calls.append(Call(self, hook, callable, type, dep)) def is_enabled (self): return (self.status in (self.STAT_ENABLED, self.STAT_TEMP_ENABLED)) -class PluginQueue: +class PluginQueue (object): """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""" + def __init__ (self): + """ + Constructor. + """ - self.frontend = frontend - self.list = [] - self.hooks = {} - if load: - self._load() + self.plugins = [] + self.hooks = defaultdict(Hook) + self._load() def get_plugins (self, list_disabled = True): - return [x for x in self.list if (x.is_enabled() or list_disabled)] + return (x for x in self.plugins 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 _load (self): + """Load the plugins.""" + self._organize() - 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 add (self, plugin): + self.plugins.append(plugin) 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 = ([],[],[]) + h = self.hooks.hook + + active = Hook() # 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 + for type in ("before", "after"): + calls = getattr(h, type) + aCalls = getattr(active, type) + for call in calls: + if call.plugin.is_enabled(): + aCalls.append(call) + if h.override and h.override.plugin.is_enabled(): + active.override = h.override + + @wraps(func) 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) + for call in active.before: + debug("Accessing hook '%(hook)s' of plugin '%(plugin)s' (before).", {"hook" : hook, "plugin": call.plugin.name}) + call.call(*hargs, **hkwargs) - 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]) + if active.override: # override + info(_("Overriding hook '%(hook)s' with plugin '%(plugin)s'."), {"hook": hook, "plugin": active.override.plugin.name}) + ret = active.override.call(*hargs, **hkwargs) 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) + for call in active.after: + debug("Accessing hook '%(hook)s' of plugin '%(plugin)s' (after).", {"hook": hook, "plugin": call.plugin.name}) + call.call(*hargs, **hkwargs) return ret @@ -353,166 +149,99 @@ class PluginQueue: 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) + unresolved_before = defaultdict(list) + unresolved_after = defaultdict(list) + star_before = defaultdict(Hook) # should be _before_ all other + star_after = defaultdict(Hook) # should be _after_ all other + + for plugin in self.plugins: # plugins + for call in plugin.calls: # hooks in plugin + if call.type == "before": + if call.dep is None: # no dependency -> straight add + self.hooks[call.hook].before.append(call) + elif call.dep == "*": + self.hooks[call.hook].before.insert(0, call) + elif call.dep == "-*": + star_before[call.hook].append(call) + else: + named = [x.plugin.name for x in self.hooks[call.hook].before] + if call.dep in named: + self.hooks[call.hook].before.insert(named.index(call.dep), call) 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] + unresolved_before[call.hook].append(call) + + elif call.type == "after": + if call.dep is None: # no dependency -> straight add + self.hooks[call.hook].after.append(call) + elif call.dep == "*": + star_after[call.hook].append(call) + elif call.dep == "-*": + self.hooks[call.hook].after.insert(0, call) + else: + named = [x.plugin.name for x in self.hooks[call.hook].after] + if call.dep in named: + self.hooks[call.hook].after.insert(named.index(call.dep)+1, call) 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) + unresolved_after[call.hook].append(call) + + # type = "override" + elif call.type == "override": + if self.hooks[call.hook].override: + warning(_("For hook '%(hook)s' an override is already defined by plugin '%(plugin)s'!"), {"hook": call.hook, "plugin": self.hooks[call.hook].override.plugin.name}) + warning(_("It is now replaced by the one from plugin '%s'!"), call.plugin.name) - # 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.hooks[call.hook].override = call + 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, calls in star_before.iteritems(): + self.hooks[hook].before.extend(calls) # append the list - for hook in star_after: - self.hooks[hook][2].extend(star_after[hook]) # append the list + for hook, calls in star_after.iteritems(): + self.hooks[hook].after.extend(calls) # append the list def _resolve_unresolved (self, before, after): - def resolve(hook, list, idx, add): + def resolve(hook, list, type, 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) + callList = getattr(self.hooks[hook], type) + named = [x.plugin.name for x in callList] + + while list and named: + newNamed = [] # use newNamed, so in each iteration only the plugins inserted last are searched + for call in list[:]: + if call.dep in named: + callList.insert(named.index(call.dep)+add, call) + list.remove(call) + newNamed.append(call.plugin.name) - if changed: - resolve(hook, list, idx, add) + named = newNamed 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}) + warning(_("Command for hook '%(hook)s' in plugin '%(plugin)s' could not be added due to missing dependant: '%(dep)s'!"), {"hook": hook, "plugin": l.plugin.name, "dep": l.dep}) for hook in before: - resolve(hook, before[hook], 0, 0) + resolve(hook, before[hook], "before", 0) for hook in after: - resolve(hook, after[hook], 2, 1) + resolve(hook, after[hook], "after", 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""" - +def load_plugins(): + """ + Loads the plugins. + """ + global __plugins - if __plugins is None or __plugins.frontend != frontend: - __plugins = PluginQueue(frontend) + if __plugins is None: + __plugins = PluginQueue() def get_plugin_queue(): """Returns the actual L{PluginQueue}. If it is C{None}, they are not being loaded yet. @@ -528,3 +257,7 @@ def hook(hook, *args, **kwargs): return pseudo_decorator else: return __plugins.hook(hook, *args, **kwargs) + +def register (plugin): + if __plugins is not None: + __plugins.add(plugin) 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-54-g00ecf From a32293a8bbb0a90512d4f8e0fbc385257b29e72a Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Thu, 3 Jul 2008 17:59:53 +0200 Subject: Should show dependencies now --- portato/gui/templates/PluginWindow.glade | 160 +++++++++++++++---------------- portato/gui/windows/plugin.py | 50 +++++++++- 2 files changed, 128 insertions(+), 82 deletions(-) diff --git a/portato/gui/templates/PluginWindow.glade b/portato/gui/templates/PluginWindow.glade index 01db971..8ba7aa9 100644 --- a/portato/gui/templates/PluginWindow.glade +++ b/portato/gui/templates/PluginWindow.glade @@ -1,6 +1,6 @@ - + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -41,81 +41,55 @@ 2 5 - + True - 5 - True - GTK_BUTTONBOX_EDGE - - - True - True - Enabled - 0 - True - - - - - - True - True - Temporarily enabled - 0 - enabledRB - - - 1 - - + True + True + _Install dependencies + True + 0 + + + 1 + 2 + 2 + 3 + + + + + True + True - + True True - Temporarily disabled - 0 - enabledRB - + True - - 2 - - + True - True - Disabled - 0 - enabledRB - + Needed dependencies + True - False - False - 3 + label_item - 2 - 3 - 4 - GTK_FILL + 2 + 3 - + True - 0 label - True - 1 2 - 1 - 2 GTK_FILL @@ -133,56 +107,82 @@ - + True + 0 label + True + 1 2 + 1 + 2 GTK_FILL - + True - True + 5 + True + GTK_BUTTONBOX_EDGE - + True True - True + Enabled + 0 + True + - + True - Needed dependencies - True + True + Temporarily enabled + 0 + enabledRB - label_item + 1 + + + + + True + True + Temporarily disabled + 0 + enabledRB + + + + 2 + + + + + True + True + Disabled + 0 + enabledRB + + + + False + False + 3 - 2 - 3 - - - - - True - True - True - _Install dependencies - True - 0 - - - 1 2 - 2 - 3 + 3 + 4 + GTK_FILL diff --git a/portato/gui/windows/plugin.py b/portato/gui/windows/plugin.py index 6e8fdab..17c5326 100644 --- a/portato/gui/windows/plugin.py +++ b/portato/gui/windows/plugin.py @@ -42,6 +42,10 @@ class PluginWindow (AbstractDialog): self.depExpander = self.tree.get_widget("depExpander") self.installBtn = self.tree.get_widget("installBtn") + self.depList = self.tree.get_widget("depList") + self.build_dep_list() + + self.instIcon = self.window.render_icon(gtk.STOCK_YES, gtk.ICON_SIZE_MENU) self.view = self.tree.get_widget("pluginList") self.store = gtk.ListStore(str) @@ -59,6 +63,32 @@ class PluginWindow (AbstractDialog): self.window.show_all() + def build_dep_list (self): + store = gtk.TreeStore(gtk.gdk.Pixbuf, str) + + self.depList.set_model(store) + + col = gtk.TreeViewColumn() + + cell = gtk.CellRendererPixbuf() + col.pack_start(cell, False) + col.add_attribute(cell, "pixbuf", 0) + + cell = gtk.CellRendererText() + col.pack_start(cell, True) + col.add_attribute(cell, "text", 1) + + self.depList.append_column(col) + + def fill_dep_list (self, inst = [], ninst = []): + store = self.depList.get_model() + store.clear() + + for dep in inst: + store.append([self.instIcon, dep]) + for dep in ninst: + store.append([None, dep]) + def cb_state_toggled (self, rb): plugin = self.get_actual() @@ -91,8 +121,24 @@ class PluginWindow (AbstractDialog): status = self.changedPlugins.get(plugin, plugin.status) self.buttons[status].set_active(True) - self.installBtn.hide() - self.depExpander.hide() + if plugin.deps: + inst = [] + ninst = [] + + for dep in plugin.deps: + if system.find_packages(dep, pkgSet = "installed"): + inst.append(dep) + else: + ninst.append(dep) + + self.fill_dep_list(inst, ninst) + self.depExpander.show() + + self.installBtn.show() + self.installBtn.set_sensitive(bool(ninst)) + else: + self.installBtn.hide() + self.depExpander.hide() def get_actual (self): store, it = self.view.get_selection().get_selected() -- cgit v1.2.3-54-g00ecf From b33a8067895a127a7c2b4e0627df55550503b6f1 Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Thu, 3 Jul 2008 23:17:23 +0200 Subject: Ported completely --- portato/gui/windows/main.py | 19 +- portato/plugin.py | 53 ++++- portato/plugin_old.py | 530 -------------------------------------------- 3 files changed, 54 insertions(+), 548 deletions(-) delete mode 100644 portato/plugin_old.py diff --git a/portato/gui/windows/main.py b/portato/gui/windows/main.py index 364810d..b4e6353 100644 --- a/portato/gui/windows/main.py +++ b/portato/gui/windows/main.py @@ -607,13 +607,13 @@ class MainWindow (Window): # set plugins and plugin-menu splash(_("Loading Plugins")) - plugin.load_plugins("gtk") - menus = plugin.get_plugin_queue().get_plugin_menus() + plugin.load_plugins() + menus = [p.menus for p in plugin.get_plugin_queue().get_plugins()] if menus: self.tree.get_widget("pluginMenuItem").set_no_show_all(False) pluginMenu = self.tree.get_widget("pluginMenu") - for m in menus: + for m in itt.chain(*menus): item = gtk.MenuItem(m.label) item.connect("activate", m.call) pluginMenu.append(item) @@ -1082,13 +1082,8 @@ class MainWindow (Window): def save_plugin (p): def _save (): - stat_on = p.status >= p.STAT_ENABLED - hard_on = not p.get_option("disabled") - - if stat_on != hard_on: - return int(stat_on) - else: - return "" + return int(p.status >= p.STAT_ENABLED) + return _save # SESSION VERSION @@ -1553,8 +1548,8 @@ class MainWindow (Window): if queue is None: plugins = [] else: - plugins = queue.get_plugins() - + plugins = list(queue.get_plugins()) + PluginWindow(self.window, plugins) return True diff --git a/portato/plugin.py b/portato/plugin.py index 6160d5e..88e5c55 100644 --- a/portato/plugin.py +++ b/portato/plugin.py @@ -12,10 +12,17 @@ from __future__ import absolute_import +import os +import os.path as osp from collections import defaultdict from functools import wraps -from .helper import debug, warning, info +from .helper import debug, warning, info, error +from .constants import PLUGIN_DIR +from . import plugins as plugin_module + +class PluginLoadException (Exception): + pass class Menu (object): __slots__ = ("label", "call") @@ -49,7 +56,7 @@ class Plugin (object): def __init__ (self): self.__menus = [] self.__calls = [] - self.state = STAT_ENABLED + self.status = self.STAT_ENABLED @property def author (self): @@ -93,13 +100,37 @@ class PluginQueue (object): self.plugins = [] self.hooks = defaultdict(Hook) - self._load() def get_plugins (self, list_disabled = True): return (x for x in self.plugins if (x.is_enabled() or list_disabled)) - def _load (self): + def load (self): """Load the plugins.""" + + plugins = [] + for f in os.listdir(PLUGIN_DIR): + path = osp.join(PLUGIN_DIR, f) + if osp.isdir(path): + if osp.isfile(osp.join(path, "__init__.py")): + plugins.append(f) + else: + debug("'%s' is not a plugin: __init__.py missing", path) + else: + if f.endswith(".py"): + plugins.append(f[:-3]) + else: + debug("'%s' is not a plugin: not a .py file", path) + + plugin_module.__path__.insert(0, PLUGIN_DIR.rstrip("/")) + plugin_module.__builtins__["Plugin"] = Plugin + plugin_module.__builtins__["register"] = register + + for p in plugins: + try: + exec "from portato.plugins import %s" % p in {} + except PluginLoadException, e: + error(_("Loading plugin '%(plugin)s' failed: %(error)s"), {"plugin" : p, "error" : e.message}) + self._organize() def add (self, plugin): @@ -108,7 +139,7 @@ class PluginQueue (object): def hook (self, hook, *hargs, **hkwargs): def hook_decorator (func): - h = self.hooks.hook + h = self.hooks[hook] active = Hook() @@ -242,6 +273,8 @@ def load_plugins(): global __plugins if __plugins is None: __plugins = PluginQueue() + __plugins.load() + def get_plugin_queue(): """Returns the actual L{PluginQueue}. If it is C{None}, they are not being loaded yet. @@ -260,4 +293,12 @@ def hook(hook, *args, **kwargs): def register (plugin): if __plugins is not None: - __plugins.add(plugin) + if callable(plugin) and Plugin in plugin.__bases__: + p = plugin() # need an instance and not the class + elif isinstance(plugin, Plugin): + p = plugin + else: + raise PluginLoadException, "Is neither a subclass nor an instance of Plugin." + + info(_("Plugin '%s' loaded."), p.name) + __plugins.add(p) diff --git a/portato/plugin_old.py b/portato/plugin_old.py deleted file mode 100644 index 5926922..0000000 --- a/portato/plugin_old.py +++ /dev/null @@ -1,530 +0,0 @@ -# -*- 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-54-g00ecf From b862584d289e09a6c12d862521f6a867a6f7cffa Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Thu, 3 Jul 2008 23:18:11 +0200 Subject: Remove xsd and -x cmdline option --- plugin.xsd | 89 -------------------------------------------------------------- portato.py | 19 +------------- 2 files changed, 1 insertion(+), 107 deletions(-) delete mode 100644 plugin.xsd diff --git a/plugin.xsd b/plugin.xsd deleted file mode 100644 index 38cb872..0000000 --- a/plugin.xsd +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/portato.py b/portato.py index a4f871a..69fb792 100755 --- a/portato.py +++ b/portato.py @@ -36,30 +36,13 @@ def main (): parser.add_option("--shm", action = "store", nargs = 3, type="long", dest = "shm", help = SUPPRESS_HELP) - parser.add_option("-x", "--validate", action = "store", dest = "validate", metavar="PLUGIN", - help = _("validates the given plugin xml instead of launching Portato")) - parser.add_option("-F", "--no-fork", "-L", action = "store_true", dest = "nofork", default = False, help = _("do not fork off as root") + (" (%s)" % _("-L is deprecated"))) # run parser (options, args) = parser.parse_args() - if options.validate: # validate a plugin - from lxml import etree - try: - etree.XMLSchema(file = XSD_LOCATION).assertValid(etree.parse(options.validate)) - except etree.XMLSyntaxError, e: - print _("Validation failed. XML syntax error: %s.") % e[0] - sys.exit(3) - except etree.DocumentInvalid: - print _("Validation failed. Does not comply with schema.") - sys.exit(3) - else: - print _("Validation succeeded.") - return - - elif options.nofork or os.getuid() == 0: # start GUI + if options.nofork or os.getuid() == 0: # start GUI from portato.gui import run from portato.helper import info info("%s v. %s", _("Starting Portato"), VERSION) -- cgit v1.2.3-54-g00ecf From dd83f2011ffee4d8eea7b78f86631c7266cdfdc2 Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Thu, 3 Jul 2008 23:21:09 +0200 Subject: Ported Notify plugin --- plugins/notify.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ plugins/notify.xml | 14 -------------- portato/plugins/__init__.py | 7 ++++++- portato/plugins/notify.py | 22 ---------------------- 4 files changed, 51 insertions(+), 37 deletions(-) create mode 100644 plugins/notify.py delete mode 100644 plugins/notify.xml delete mode 100644 portato/plugins/notify.py diff --git a/plugins/notify.py b/plugins/notify.py new file mode 100644 index 0000000..bc1b2ea --- /dev/null +++ b/plugins/notify.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# +# File: plugins/notify.py +# This file is part of the Portato-Project, a graphical portage-frontend. +# +# Copyright (C) 2007-2008 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 + +import pynotify + +from portato import get_listener + +from portato.helper import warning, error, debug +from portato.constants import APP_ICON, APP + +class Notify (Plugin): + __author__ = "René 'Necoro' Neumann" + __description__ = "Show notifications when an emerge process finishes." + __dependency__ = ["dev-python/notify-python"] + + def __init__ (self): + Plugin.__init__(self) + self.add_call("after_emerge", self.notify) + + def notify (self, retcode, **kwargs): + if retcode is None: + warning("NOTIFY :: %s", _("Notify called while process is still running!")) + else: + icon = APP_ICON + if retcode == 0: + text = _("Emerge finished!") + descr = "" + urgency = pynotify.URGENCY_NORMAL + else: + text = _("Emerge failed!") + descr = _("Error Code: %d") % retcode + urgency = pynotify.URGENCY_CRITICAL + + get_listener().send_notify(base = text, descr = descr, icon = icon, urgency = urgency) + +register(Notify) diff --git a/plugins/notify.xml b/plugins/notify.xml deleted file mode 100644 index 8de08c8..0000000 --- a/plugins/notify.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - René 'Necoro' Neumann - Notify - gtk - - portato.plugins.notify - - - - - - diff --git a/portato/plugins/__init__.py b/portato/plugins/__init__.py index fe95dbc..5080cec 100644 --- a/portato/plugins/__init__.py +++ b/portato/plugins/__init__.py @@ -3,9 +3,14 @@ # File: portato/plugins/__init__.py # This file is part of the Portato-Project, a graphical portage-frontend. # -# Copyright (C) 2007 René 'Necoro' Neumann +# Copyright (C) 2007-2008 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 + +""" +This is a dummy module. The plugins loaded from here are in reality stored +in /usr/share/portato/plugins or alike. +""" diff --git a/portato/plugins/notify.py b/portato/plugins/notify.py deleted file mode 100644 index 78812b3..0000000 --- a/portato/plugins/notify.py +++ /dev/null @@ -1,22 +0,0 @@ -import pynotify - -from portato import get_listener - -from portato.helper import warning, error, debug -from portato.constants import APP_ICON, APP - -def notify (retcode, **kwargs): - if retcode is None: - warning("NOTIFY :: %s", _("Notify called while process is still running!")) - else: - icon = APP_ICON - if retcode == 0: - text = _("Emerge finished!") - descr = "" - urgency = pynotify.URGENCY_NORMAL - else: - text = _("Emerge failed!") - descr = _("Error Code: %d") % retcode - urgency = pynotify.URGENCY_CRITICAL - - get_listener().send_notify(base = text, descr = descr, icon = icon, urgency = urgency) -- cgit v1.2.3-54-g00ecf From 1540e5c8fbba5713e0176c8812f7473ea5e91037 Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Thu, 3 Jul 2008 23:25:24 +0200 Subject: Ported gpytage plugin --- plugins/gpytage.py | 27 +++++++++++++++++++++++++++ plugins/gpytage.xml | 13 ------------- portato/plugins/gpytage.py | 16 ---------------- 3 files changed, 27 insertions(+), 29 deletions(-) create mode 100644 plugins/gpytage.py delete mode 100644 plugins/gpytage.xml delete mode 100644 portato/plugins/gpytage.py diff --git a/plugins/gpytage.py b/plugins/gpytage.py new file mode 100644 index 0000000..33509e1 --- /dev/null +++ b/plugins/gpytage.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# File: plugins/gpytage.py +# This file is part of the Portato-Project, a graphical portage-frontend. +# +# Copyright (C) 2008 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 + +from subprocess import Popen + +class GPytage (Plugin): + __author__ = "René 'Necoro' Neumann" + __description__ = "Adds a menu entry to directly start gpytage, a config editor." + __dependency__ = ["app-portage/gpytage"] + + def __init__ (self): + Plugin.__init__(self) + self.add_menu("Config _Editor", self.menu) + + def menu (self, *args): + Popen(["/usr/bin/gpytage"]) + +register(GPytage) diff --git a/plugins/gpytage.xml b/plugins/gpytage.xml deleted file mode 100644 index b203ae0..0000000 --- a/plugins/gpytage.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - René 'Necoro' Neumann - GPytage - - portato.plugins.gpytage - - - Config _Editor - - - diff --git a/portato/plugins/gpytage.py b/portato/plugins/gpytage.py deleted file mode 100644 index 22cc7ef..0000000 --- a/portato/plugins/gpytage.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# -# File: portato/plugins/gpytage.py -# This file is part of the Portato-Project, a graphical portage-frontend. -# -# Copyright (C) 2008 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 - -from subprocess import Popen - -def gpytage(*args, **kwargs): - Popen(["/usr/bin/gpytage"]) -- cgit v1.2.3-54-g00ecf From e42090a8bd7dcc5f9e7c6e41b0f44698588e6586 Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Thu, 3 Jul 2008 23:33:21 +0200 Subject: Ported etc-proposals plugin --- plugins/etc_proposals.py | 43 ++++++++++++++++++++++++++++++++++++++++ plugins/etc_proposals.xml | 19 ------------------ portato/plugins/etc_proposals.py | 31 ----------------------------- 3 files changed, 43 insertions(+), 50 deletions(-) create mode 100644 plugins/etc_proposals.py delete mode 100644 plugins/etc_proposals.xml delete mode 100644 portato/plugins/etc_proposals.py diff --git a/plugins/etc_proposals.py b/plugins/etc_proposals.py new file mode 100644 index 0000000..f02cef5 --- /dev/null +++ b/plugins/etc_proposals.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# File: plugins/etc_proposals.py +# This file is part of the Portato-Project, a graphical portage-frontend. +# +# Copyright (C) 2007-2008 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 + +from portato.helper import error + +import os +from subprocess import Popen + +class EtcProposals (Plugin): + __author__ = "René 'Necoro' Neumann" + __description__ = "Adds support for etc-proposals, a graphical etc-update replacement." + __dependency__ = "app-portage/etc-proposals" + + def __init__ (self): + Plugin.__init__(self) + + self.prog = ["/usr/sbin/etc-proposals"] + self.add_call("after_emerge", self.hook, type = "after") + self.add_menu("Et_c-Proposals", self.menu) + + def launch (self, options = []): + if os.getuid() == 0: + Popen(self.prog+options) + else: + error("ETC_PROPOSALS :: %s",_("Cannot start etc-proposals. Not root!")) + + def hook (self, *args, **kwargs): + """Entry point for this plugin.""" + self.launch(["--fastexit"]) + + def menu (self, *args): + self.launch() + +register(EtcProposals) diff --git a/plugins/etc_proposals.xml b/plugins/etc_proposals.xml deleted file mode 100644 index 2caf341..0000000 --- a/plugins/etc_proposals.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - René 'Necoro' Neumann - Etc-proposals - - portato.plugins.etc_proposals - - - - - - - - - Et_c-Proposals - - - diff --git a/portato/plugins/etc_proposals.py b/portato/plugins/etc_proposals.py deleted file mode 100644 index d5f707f..0000000 --- a/portato/plugins/etc_proposals.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# -# File: portato/plugins/etc_proposals.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 - -from portato.helper import error - -import os -from subprocess import Popen - -PROG=["/usr/sbin/etc-proposals"] - -def launch (options = []): - if os.getuid() == 0: - Popen(PROG+options) - else: - error("ETC_PROPOSALS :: %s",_("Cannot start etc-proposals. Not root!")) - -def etc_prop (*args, **kwargs): - """Entry point for this plugin.""" - launch(["--fastexit"]) - -def etc_prop_menu (*args, **kwargs): - launch() -- cgit v1.2.3-54-g00ecf From 72649c40b6278dbc29a595517fa11948d4df0532 Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Thu, 3 Jul 2008 23:37:03 +0200 Subject: Updated setup.py to install the correct set of plugins --- setup.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index a8a62d6..5203dd7 100644 --- a/setup.py +++ b/setup.py @@ -17,14 +17,13 @@ from portato.constants import VERSION, DATA_DIR, ICON_DIR, PLUGIN_DIR, TEMPLATE_ def plugin_list (*args): """Creates a list of correct plugin pathes out of the arguments.""" - return [("plugins/%s.xml" % x) for x in args] + return [("plugins/%s.py" % x) for x in args] packages = ["portato", "portato.gui", "portato.gui.windows", "portato.plugins", "portato.backend", "portato.backend.portage"] data_files = [ (TEMPLATE_DIR, [os.path.join("portato/gui/templates",x) for x in os.listdir("portato/gui/templates") if x.endswith(".glade")]), (ICON_DIR, ["icons/portato-icon.png"]), -# (PLUGIN_DIR, plugin_list("dbus_init")), - (DATA_DIR, ["plugin.xsd"])] + (PLUGIN_DIR, plugin_list("gpytage", "notify", "etc_proposals"))] # do the distutils setup setup(name="Portato", -- cgit v1.2.3-54-g00ecf From 372c5781ae6723f65bb9e6cea2d719f916d8b265 Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Thu, 3 Jul 2008 23:50:00 +0200 Subject: Removed XSD_LOCATION -- as there is no more xsd ;) --- portato.py | 2 +- portato/constants.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/portato.py b/portato.py index 69fb792..cea4ad7 100755 --- a/portato.py +++ b/portato.py @@ -19,7 +19,7 @@ import gettext, locale from optparse import OptionParser, SUPPRESS_HELP from portato import get_listener -from portato.constants import VERSION, XSD_LOCATION, LOCALE_DIR, APP, SU_COMMAND +from portato.constants import VERSION, LOCALE_DIR, APP, SU_COMMAND def main (): # set gettext stuff diff --git a/portato/constants.py b/portato/constants.py index 32f0f9b..93e4240 100644 --- a/portato/constants.py +++ b/portato/constants.py @@ -45,8 +45,6 @@ These should be set during the installation. @type SETTINGS_DIR: string @var TEMPLATE_DIR: Directory containing the UI template files. @type TEMPLATE_DIR: string -@var XSD_LOCATION: Path of the plugin schema. -@type XSD_LOCATION: string """ import os from os.path import join as pjoin @@ -73,5 +71,3 @@ LOCALE_DIR = "i18n/" PLUGIN_DIR = pjoin(DATA_DIR, "plugins/") SETTINGS_DIR = pjoin(HOME, "."+APP) TEMPLATE_DIR = "portato/gui/templates/" - -XSD_LOCATION = pjoin(DATA_DIR, "plugin.xsd") -- cgit v1.2.3-54-g00ecf From dff240a07fc6150cf313db3f745f226f0203fe51 Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Thu, 3 Jul 2008 23:58:06 +0200 Subject: Some window refinement --- portato/gui/templates/PluginWindow.glade | 235 +++++++++++++++++-------------- 1 file changed, 126 insertions(+), 109 deletions(-) diff --git a/portato/gui/templates/PluginWindow.glade b/portato/gui/templates/PluginWindow.glade index 8ba7aa9..9e1fc43 100644 --- a/portato/gui/templates/PluginWindow.glade +++ b/portato/gui/templates/PluginWindow.glade @@ -1,6 +1,6 @@ - + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -35,159 +35,176 @@ - + True - 4 - 2 - 5 + 0 + GTK_SHADOW_OUT - + True - True - True - _Install dependencies - True - 0 - - - 1 - 2 - 2 - 3 - - - - - True - True + 4 + 2 + 10 - + True - True - True + 5 + True + GTK_BUTTONBOX_EDGE + + + True + True + Enabled + 0 + True + + + + + + True + True + Temporarily enabled + 0 + enabledRB + + + 1 + + + + + True + True + Temporarily disabled + 0 + enabledRB + + + + 2 + + + + + True + True + Disabled + 0 + enabledRB + + + + False + False + 3 + + + + 2 + 3 + 4 + GTK_FILL + - + True - Needed dependencies + 0 + label True - label_item + 1 + 2 + 1 + 2 + GTK_FILL - - - 2 - 3 - - - - - True - label - - - 2 - GTK_FILL - - - - - True - <b>Author:</b> - True - True - - - 1 - 2 - GTK_FILL - - - - - True - 0 - label - True - - - 1 - 2 - 1 - 2 - GTK_FILL - - - - - True - 5 - True - GTK_BUTTONBOX_EDGE - + True - True - Enabled - 0 - True - + <b>Author:</b> + True + True + + 1 + 2 + GTK_FILL + - + True - True - Temporarily enabled - 0 - enabledRB + label - 1 + 2 + GTK_FILL + 10 - + True True - Temporarily disabled - 0 - enabledRB - + + + True + True + False + True + + + + + True + Needed dependencies + True + + + label_item + + - 2 + 2 + 3 + 10 - + True True - Disabled + True + _Install dependencies + True 0 - enabledRB - - False - False - 3 + 1 + 2 + 2 + 3 + + 10 + + + - 2 - 3 - 4 - GTK_FILL + label_item - False 1 -- cgit v1.2.3-54-g00ecf From 8e07fd436cfbf02fbf43c9e221badb55acdb546a Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Fri, 4 Jul 2008 14:15:55 +0200 Subject: Some more dependency awareness --- plugins/etc_proposals.py | 4 +-- plugins/gpytage.py | 3 +-- plugins/notify.py | 12 ++++++--- portato/gui/windows/main.py | 3 +++ portato/plugin.py | 60 ++++++++++++++++++++++++++++++++++----------- 5 files changed, 59 insertions(+), 23 deletions(-) diff --git a/plugins/etc_proposals.py b/plugins/etc_proposals.py index 07f9a80..c32c8f3 100644 --- a/plugins/etc_proposals.py +++ b/plugins/etc_proposals.py @@ -20,9 +20,7 @@ class EtcProposals (Plugin): __description__ = "Adds support for etc-proposals, a graphical etc-update replacement." __dependency__ = ["app-portage/etc-proposals"] - def __init__ (self): - Plugin.__init__(self) - + def init (self): self.prog = ["/usr/sbin/etc-proposals"] self.add_call("after_emerge", self.hook, type = "after") self.add_menu("Et_c-Proposals", self.menu) diff --git a/plugins/gpytage.py b/plugins/gpytage.py index 33509e1..d8c2831 100644 --- a/plugins/gpytage.py +++ b/plugins/gpytage.py @@ -17,8 +17,7 @@ class GPytage (Plugin): __description__ = "Adds a menu entry to directly start gpytage, a config editor." __dependency__ = ["app-portage/gpytage"] - def __init__ (self): - Plugin.__init__(self) + def init (self): self.add_menu("Config _Editor", self.menu) def menu (self, *args): diff --git a/plugins/notify.py b/plugins/notify.py index bc1b2ea..6446812 100644 --- a/plugins/notify.py +++ b/plugins/notify.py @@ -10,7 +10,12 @@ # # Written by René 'Necoro' Neumann -import pynotify +disable = False + +try: + import pynotify +except ImportError: + disable = True from portato import get_listener @@ -22,8 +27,7 @@ class Notify (Plugin): __description__ = "Show notifications when an emerge process finishes." __dependency__ = ["dev-python/notify-python"] - def __init__ (self): - Plugin.__init__(self) + def init (self): self.add_call("after_emerge", self.notify) def notify (self, retcode, **kwargs): @@ -42,4 +46,4 @@ class Notify (Plugin): get_listener().send_notify(base = text, descr = descr, icon = icon, urgency = urgency) -register(Notify) +register(Notify, disable) diff --git a/portato/gui/windows/main.py b/portato/gui/windows/main.py index b4e6353..479274d 100644 --- a/portato/gui/windows/main.py +++ b/portato/gui/windows/main.py @@ -1082,6 +1082,9 @@ class MainWindow (Window): def save_plugin (p): def _save (): + if p.status == p.STAT_HARD_DISABLED: + return "" + return int(p.status >= p.STAT_ENABLED) return _save diff --git a/portato/plugin.py b/portato/plugin.py index 7b8a493..ada7a0f 100644 --- a/portato/plugin.py +++ b/portato/plugin.py @@ -19,6 +19,7 @@ from functools import wraps from .helper import debug, warning, info, error from .constants import PLUGIN_DIR +from .backend import system from . import plugins as plugin_module class PluginLoadException (Exception): @@ -52,11 +53,30 @@ class Hook (object): class Plugin (object): (STAT_DISABLED, STAT_TEMP_ENABLED, STAT_ENABLED, STAT_TEMP_DISABLED) = range(4) + STAT_HARD_DISABLED = -1 - def __init__ (self): + def __init__ (self, disable = False): self.__menus = [] self.__calls = [] - self.status = self.STAT_ENABLED + self._unresolved_deps = False + + if not disable: + self.status = self.STAT_ENABLED + else: + self.status = self.STAT_HARD_DISABLED + + def _init (self): + + for d in self.deps: + if not system.find_packages(d, pkgSet="installed", with_version = False): + self._unresolved_deps = True + break + + if self.status != self.STAT_HARD_DISABLED and not self._unresolved_deps: + self.init() + + def init (self): + pass @property def author (self): @@ -140,8 +160,28 @@ class PluginQueue (object): self._organize() - def add (self, plugin): - self.plugins.append(plugin) + def add (self, plugin, disable = False): + if callable(plugin) and Plugin in plugin.__bases__: + p = plugin(disable = disable) # need an instance and not the class + elif isinstance(plugin, Plugin): + p = plugin + if disable: + p.status = p.STAT_HARD_DISABLED + else: + raise PluginLoadException, "Is neither a subclass nor an instance of Plugin." + + p._init() + + self.plugins.append(p) + + if p.status == p.STAT_HARD_DISABLED: + msg = _("Plugin is disabled!") + elif p._unresolved_deps: + msg = _("Plugin has unresolved dependencies - disabled!") + else: + msg = "" + + info("%s %s", _("Plugin '%s' loaded.") % p.name, msg) def hook (self, hook, *hargs, **hkwargs): @@ -298,14 +338,6 @@ def hook(hook, *args, **kwargs): else: return __plugins.hook(hook, *args, **kwargs) -def register (plugin): +def register (plugin, disable = False): if __plugins is not None: - if callable(plugin) and Plugin in plugin.__bases__: - p = plugin() # need an instance and not the class - elif isinstance(plugin, Plugin): - p = plugin - else: - raise PluginLoadException, "Is neither a subclass nor an instance of Plugin." - - info(_("Plugin '%s' loaded."), p.name) - __plugins.add(p) + __plugins.add(plugin, disable) -- cgit v1.2.3-54-g00ecf From cf70f253a11871ba6db372eb4735335ec97129cd Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Fri, 4 Jul 2008 15:57:28 +0200 Subject: Added ability to install missing plugin deps --- portato/gui/templates/PluginWindow.glade | 172 +++++++++++++++---------------- portato/gui/windows/main.py | 2 +- portato/gui/windows/plugin.py | 47 +++++++-- 3 files changed, 126 insertions(+), 95 deletions(-) diff --git a/portato/gui/templates/PluginWindow.glade b/portato/gui/templates/PluginWindow.glade index 9e1fc43..f76193e 100644 --- a/portato/gui/templates/PluginWindow.glade +++ b/portato/gui/templates/PluginWindow.glade @@ -1,6 +1,6 @@ - + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -38,7 +38,6 @@ True 0 - GTK_SHADOW_OUT True @@ -46,82 +45,62 @@ 2 10 - + True - 5 - True - GTK_BUTTONBOX_EDGE - - - True - True - Enabled - 0 - True - - - - - - True - True - Temporarily enabled - 0 - enabledRB - - - 1 - - + True + True + _Install dependencies + True + 0 + + + + 1 + 2 + 2 + 3 + + 10 + + + + + True + True - + True True - Temporarily disabled - 0 - enabledRB - + False + True - - 2 - - + True - True - Disabled - 0 - enabledRB - + Needed dependencies + True - False - False - 3 + label_item - 2 - 3 - 4 - GTK_FILL + 2 + 3 + 10 - + True - 0 label - True - 1 2 - 1 - 2 GTK_FILL + 10 @@ -138,61 +117,82 @@ - + True + 0 label + True + 1 2 + 1 + 2 GTK_FILL - 10 - + True - True + 5 + True + GTK_BUTTONBOX_EDGE - + True True - False - True + Enabled + 0 + True + - + True - Needed dependencies - True + True + Temporarily enabled + 0 + enabledRB - label_item + 1 + + + + + True + True + Temporarily disabled + 0 + enabledRB + + + + 2 + + + + + True + True + Disabled + 0 + enabledRB + + + + False + False + 3 - 2 - 3 - 10 - - - - - True - True - True - _Install dependencies - True - 0 - - - 1 2 - 2 - 3 - - 10 + 3 + 4 + GTK_FILL diff --git a/portato/gui/windows/main.py b/portato/gui/windows/main.py index 479274d..265d4dd 100644 --- a/portato/gui/windows/main.py +++ b/portato/gui/windows/main.py @@ -1553,7 +1553,7 @@ class MainWindow (Window): else: plugins = list(queue.get_plugins()) - PluginWindow(self.window, plugins) + PluginWindow(self.window, plugins, self.queue) return True def cb_show_updates_clicked (self, *args): diff --git a/portato/gui/windows/plugin.py b/portato/gui/windows/plugin.py index 9760658..392654e 100644 --- a/portato/gui/windows/plugin.py +++ b/portato/gui/windows/plugin.py @@ -15,7 +15,9 @@ from __future__ import absolute_import import gtk from .basic import AbstractDialog +from ..dialogs import blocked_dialog, unmask_dialog from ...backend import system +from ...backend.exceptions import PackageNotFoundException, BlockedException from ...helper import debug class PluginWindow (AbstractDialog): @@ -25,7 +27,7 @@ class PluginWindow (AbstractDialog): for s in (_("Disabled"), _("Temporarily enabled"), _("Enabled"), _("Temporarily disabled")): statsStore.append([s]) - def __init__ (self, parent, plugins): + def __init__ (self, parent, plugins, queue = None): """Constructor. @param parent: the parent window @@ -33,7 +35,10 @@ class PluginWindow (AbstractDialog): AbstractDialog.__init__(self, parent) self.plugins = plugins + self.queue = queue self.changedPlugins = {} + self.inst = [] + self.ninst = [] self.buttons = map(self.tree.get_widget, ("disabledRB", "tempEnabledRB", "enabledRB", "tempDisabledRB")) map(lambda b: b.set_mode(False), self.buttons) @@ -46,6 +51,8 @@ class PluginWindow (AbstractDialog): self.depList = self.tree.get_widget("depList") self.build_dep_list() + self.buttonBox = self.tree.get_widget("buttonBox") + self.instIcon = self.window.render_icon(gtk.STOCK_YES, gtk.ICON_SIZE_MENU) self.view = self.tree.get_widget("pluginList") @@ -109,6 +116,8 @@ class PluginWindow (AbstractDialog): def cb_list_selection (self, selection): plugin = self.get_actual() + self.inst = [] + self.ninst = [] if plugin: if not plugin.description: @@ -123,23 +132,45 @@ class PluginWindow (AbstractDialog): self.buttons[status].set_active(True) if plugin.deps: - inst = [] - ninst = [] for dep in plugin.deps: - if system.find_packages(dep, pkgSet = "installed"): - inst.append(dep) + if system.find_packages(dep, pkgSet = "installed", with_version = False): + self.inst.append(dep) else: - ninst.append(dep) + self.ninst.append(dep) - self.fill_dep_list(inst, ninst) + self.fill_dep_list(self.inst, self.ninst) self.depExpander.show() self.installBtn.show() - self.installBtn.set_sensitive(bool(ninst)) + self.installBtn.set_sensitive(bool(self.ninst)) + else: self.installBtn.hide() self.depExpander.hide() + + self.buttonBox.set_sensitive(not plugin._unresolved_deps and plugin.status != plugin.STAT_HARD_DISABLED) + + def cb_install_clicked (self, *args): + if not self.queue: + return False + + for cpv in self.ninst: + + pkg = system.find_best_match(cpv, masked = False, only_cpv = True) + if not pkg: + pkg = system.find_best_match(cpv, masked = True, only_cpv = True) + + try: + try: + self.queue.append(pkg, type = "install") + except PackageNotFoundException, e: + if unmask_dialog(e[0]) == gtk.RESPONSE_YES: + self.queue.append(pkg, type = "install", unmask = True) + except BlockedException, e: + blocked_dialog(e[0], e[1]) + + return True def get_actual (self): store, it = self.view.get_selection().get_selected() -- cgit v1.2.3-54-g00ecf From 624d0dd5d4ea23553a83121218a9d445257e7427 Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Fri, 4 Jul 2008 17:37:01 +0200 Subject: Ignore errors on plugin loading --- portato/plugin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/portato/plugin.py b/portato/plugin.py index ada7a0f..b6376da 100644 --- a/portato/plugin.py +++ b/portato/plugin.py @@ -14,6 +14,7 @@ from __future__ import absolute_import import os import os.path as osp +import traceback from collections import defaultdict from functools import wraps @@ -145,6 +146,8 @@ class PluginQueue (object): else: if f.endswith(".py"): plugins.append(f[:-3]) + elif f.endswith(".pyc") or f.endswith(".pyo"): + pass # ignore .pyc and .pyo else: debug("'%s' is not a plugin: not a .py file", path) @@ -157,6 +160,9 @@ class PluginQueue (object): exec "from portato.plugins import %s" % p in {} except PluginLoadException, e: error(_("Loading plugin '%(plugin)s' failed: %(error)s"), {"plugin" : p, "error" : e.message}) + except: + tb = traceback.format_exc() + error(_("Loading plugin '%(plugin)s' failed: %(error)s"), {"plugin" : p, "error" : tb}) self._organize() -- cgit v1.2.3-54-g00ecf From 40f37a603157a14fe4198f10d63829c0f1d9d547 Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Fri, 4 Jul 2008 17:42:42 +0200 Subject: Removed lxml version in error string --- portato/gui/exception_handling.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/portato/gui/exception_handling.py b/portato/gui/exception_handling.py index eadf124..9e42783 100644 --- a/portato/gui/exception_handling.py +++ b/portato/gui/exception_handling.py @@ -100,15 +100,13 @@ def convert (version): def get_version_infos(): from ..constants import VERSION from ..backend import system - from lxml import etree return "\n".join(( "Portato version: %s" % VERSION, "Python version: %s" % sys.version, "Used backend: %s" % system.get_version(), "pygtk: %s (using GTK+: %s)" % (convert(gtk.pygtk_version), convert(gtk.gtk_version)), - "pygobject: %s (using GLib: %s)" % (convert(gobject.pygobject_version), convert(gobject.glib_version)), - "lxml: %s" % convert(etree.LXML_VERSION), + "pygobject: %s (using GLib: %s)" % (convert(gobject.pygobject_version), convert(gobject.glib_version)) "")) def get_trace(type, value, tb): -- cgit v1.2.3-54-g00ecf From 657ca163ea9fc4992c004b8b7daf33a72b6e571a Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Fri, 4 Jul 2008 17:57:34 +0200 Subject: Removed lxml version in error string --- portato/gui/exception_handling.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/portato/gui/exception_handling.py b/portato/gui/exception_handling.py index 9e42783..dae95ed 100644 --- a/portato/gui/exception_handling.py +++ b/portato/gui/exception_handling.py @@ -106,8 +106,7 @@ def get_version_infos(): "Python version: %s" % sys.version, "Used backend: %s" % system.get_version(), "pygtk: %s (using GTK+: %s)" % (convert(gtk.pygtk_version), convert(gtk.gtk_version)), - "pygobject: %s (using GLib: %s)" % (convert(gobject.pygobject_version), convert(gobject.glib_version)) - "")) + "pygobject: %s (using GLib: %s)" % (convert(gobject.pygobject_version), convert(gobject.glib_version)))) def get_trace(type, value, tb): trace = StringIO() -- cgit v1.2.3-54-g00ecf From 92017bd23d9b29810fb6a5a4987daa810fe5bb42 Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Fri, 4 Jul 2008 17:58:19 +0200 Subject: Ported remaining plugins --- plugins/exception.py | 20 +++++++++++ plugins/exception.xml | 13 ------- plugins/new_version.py | 80 ++++++++++++++++++++++++++++++++++++++++++ plugins/new_version.xml | 16 --------- portato/plugins/exception.py | 2 -- portato/plugins/new_version.py | 58 ------------------------------ 6 files changed, 100 insertions(+), 89 deletions(-) create mode 100644 plugins/exception.py delete mode 100644 plugins/exception.xml create mode 100644 plugins/new_version.py delete mode 100644 plugins/new_version.xml delete mode 100644 portato/plugins/exception.py delete mode 100644 portato/plugins/new_version.py diff --git a/plugins/exception.py b/plugins/exception.py new file mode 100644 index 0000000..e653853 --- /dev/null +++ b/plugins/exception.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# +# File: plugins/exception.py +# This file is part of the Portato-Project, a graphical portage-frontend. +# +# Copyright (C) 2008 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 + +def throw (*args, **kwargs): + raise Exception, "As requested, Sir!" + +p = Plugin() +p.__name__ = "ExceptionThrower" +p.__author__ = "René 'Necoro' Neumann" +p.add_menu("Throw exception", throw) +register(p) diff --git a/plugins/exception.xml b/plugins/exception.xml deleted file mode 100644 index 385e743..0000000 --- a/plugins/exception.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - René 'Necoro' Neumann - Exception Thrower - - portato.plugins.exception - - - Throw exception - - - diff --git a/plugins/new_version.py b/plugins/new_version.py new file mode 100644 index 0000000..f3479b4 --- /dev/null +++ b/plugins/new_version.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# +# File: plugins/new_version.py +# This file is part of the Portato-Project, a graphical portage-frontend. +# +# Copyright (C) 2008 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 + +try: + from bzrlib import plugin, branch +except ImportError: + plugin = branch = None +import gobject + +from portato.helper import debug, warning +from portato import get_listener +from portato.constants import VERSION, APP_ICON, APP +from portato.gui.utils import GtkThread + +class NewVersionFinder(Plugin): + """ + Checks for a new version of portato every 30 minutes and on startup. + """ + __author__ = "René 'Necoro' Neumann" + __dependency__ = ["dev-util/bzr"] + + def init (self): + self.add_call("main", self.run) + self.add_menu("Check for new _versions", self.menu) + + def find_version (self, rev): + try: + b = branch.Branch.open("lp:portato") + except Exception, e: + warning("NEW_VERSION :: Exception occured while accessing the remote branch: %s", str(e)) + return + + debug("NEW_VERSION :: Installed rev: %s - Current rev: %s", rev, b.revno()) + if int(rev) < int(b.revno()): + def callback(): + get_listener().send_notify(base = "New Portato Live Version Found", descr = "You have rev. %s, but the most recent revision is %s." % (rev, b.revno()), icon = APP_ICON) + return False + + gobject.idle_add(callback) + + def start_thread(self, rev): + t = GtkThread(target = self.find_version, name = "Version Updater Thread", args = (rev,)) + t.setDaemon(True) + t.start() + return True + + def menu (self, *args, **kwargs): + """ + Run the thread once. + """ + v = VERSION.split() + if len(v) != 3 or v[0] != "9999": + return None + + rev = v[-1] + + plugin.load_plugins() # to have lp: addresses parsed + + self.start_thread(rev) + return rev + + def run (self, *args, **kwargs): + """ + Run the thread once and add a 30 minutes timer. + """ + rev = self.menu() + + if rev is not None: + gobject.timeout_add(30*60*1000, self.start_thread, rev) # call it every 30 minutes + +register(NewVersionFinder, (branch is None)) diff --git a/plugins/new_version.xml b/plugins/new_version.xml deleted file mode 100644 index 2711054..0000000 --- a/plugins/new_version.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - René 'Necoro' Neumann - New Version Reminder - - portato.plugins.new_version - - - - - - - Check for new _versions - - diff --git a/portato/plugins/exception.py b/portato/plugins/exception.py deleted file mode 100644 index 64bdb77..0000000 --- a/portato/plugins/exception.py +++ /dev/null @@ -1,2 +0,0 @@ -def throw (*args, **kwargs): - raise Exception, "As requested, Sir!" diff --git a/portato/plugins/new_version.py b/portato/plugins/new_version.py deleted file mode 100644 index fe69292..0000000 --- a/portato/plugins/new_version.py +++ /dev/null @@ -1,58 +0,0 @@ -try: - from bzrlib import plugin, branch -except ImportError: - plugin = branch = None -import gobject - -from portato.helper import debug, warning -from portato import get_listener -from portato.constants import VERSION, APP_ICON, APP -from portato.gui.utils import GtkThread - -def find_version (rev): - try: - b = branch.Branch.open("lp:portato") - except Exception, e: - warning("NEW_VERSION :: Exception occured while accessing the remote branch: %s", str(e)) - return - - debug("NEW_VERSION :: Installed rev: %s - Current rev: %s", rev, b.revno()) - if int(rev) < int(b.revno()): - def callback(): - get_listener().send_notify(base = "New Portato Live Version Found", descr = "You have rev. %s, but the most recent revision is %s." % (rev, b.revno()), icon = APP_ICON) - return False - - gobject.idle_add(callback) - -def start_thread(rev): - t = GtkThread(target = find_version, name = "Version Updater Thread", args = (rev,)) - t.setDaemon(True) - t.start() - return True - -def run_menu (*args, **kwargs): - """ - Run the thread once. - """ - if not all((plugin, branch)): - return None - - v = VERSION.split() - if len(v) != 3 or v[0] != "9999": - return None - - rev = v[-1] - - plugin.load_plugins() # to have lp: addresses parsed - - start_thread(rev) - return rev - -def run (*args, **kwargs): - """ - Run the thread once and add a 30 minutes timer. - """ - rev = run_menu() - - if rev is not None: - gobject.timeout_add(30*60*1000, start_thread, rev) # call it every 30 minutes -- cgit v1.2.3-54-g00ecf From 67cdda0f63e0feb123dfe3b34ad44c50c51dd65f Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Tue, 8 Jul 2008 14:15:04 +0200 Subject: Documented the plugin module --- portato/config_parser.py | 24 ++--- portato/plugin.py | 237 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 229 insertions(+), 32 deletions(-) diff --git a/portato/config_parser.py b/portato/config_parser.py index 1383d69..6515d1b 100644 --- a/portato/config_parser.py +++ b/portato/config_parser.py @@ -285,8 +285,8 @@ class ConfigParser: :Exceptions: - KeyNotFoundException : Raised if the specified key could not be found. - SectionNotFoundException : Raised if the specified section could not be found. + - `KeyNotFoundException` : Raised if the specified key could not be found. + - `SectionNotFoundException` : Raised if the specified section could not be found. """ try: @@ -315,8 +315,8 @@ class ConfigParser: :Exceptions: - KeyNotFoundException : Raised if the specified key could not be found. - SectionNotFoundException : Raised if the specified section could not be found. + - `KeyNotFoundException` : Raised if the specified key could not be found. + - `SectionNotFoundException` : Raised if the specified section could not be found. """ section = section.upper() @@ -339,9 +339,9 @@ class ConfigParser: :Exceptions: - KeyNotFoundException : Raised if the specified key could not be found. - SectionNotFoundException : Raised if the specified section could not be found. - ValueError : Raised if the key accessed is not a boolean. + - `KeyNotFoundException` : Raised if the specified key could not be found. + - `SectionNotFoundException` : Raised if the specified section could not be found. + - `ValueError` : Raised if the key accessed is not a boolean. """ section = section.upper() @@ -369,8 +369,8 @@ class ConfigParser: :Exceptions: - KeyNotFoundException : Raised if the specified key could not be found. - SectionNotFoundException : Raised if the specified section could not be found. + - `KeyNotFoundException` : Raised if the specified key could not be found. + - `SectionNotFoundException` : Raised if the specified section could not be found. """ section = section.upper() @@ -394,9 +394,9 @@ class ConfigParser: :Exceptions: - KeyNotFoundException : Raised if the specified key could not be found. - SectionNotFoundException : Raised if the specified section could not be found. - ValueError : if the old/new value is not a boolean + - `KeyNotFoundException` : Raised if the specified key could not be found. + - `SectionNotFoundException` : Raised if the specified section could not be found. + - `ValueError` : if the old/new value is not a boolean """ section = section.upper() diff --git a/portato/plugin.py b/portato/plugin.py index b6376da..5994328 100644 --- a/portato/plugin.py +++ b/portato/plugin.py @@ -10,7 +10,12 @@ # # Written by René 'Necoro' Neumann +""" +A module managing the plugins for Portato. +""" + from __future__ import absolute_import +__docformat__ = "restructuredtext" import os import os.path as osp @@ -24,9 +29,23 @@ from .backend import system from . import plugins as plugin_module class PluginLoadException (Exception): + """ + Exception signaling a failed plugin loading. + """ pass class Menu (object): + """ + One single menu entry. + + :IVariables: + + label : string + The label of the entry. Can have underscores to define the shortcut. + + call + The function to call, if the entry is clicked. + """ __slots__ = ("label", "call") def __init__ (self, label, call): @@ -34,6 +53,36 @@ class Menu (object): self.call = call class Call (object): + """ + This class represents an object, which is attached to a specified hook. + + :IVariables: + + plugin : `Plugin` + The plugin where this call belongs to. + + hook : string + The name of the corresponding hook. + + call + The function to call. + + type : string + This is either ``before``, ``after`` or ``override`` and defines the type of the call: + + before + access before the original function + override + access *instead of* the original function. **USE THIS ONLY IF YOU KNOW WHAT YOU ARE DOING** + after + access after the original function has been called + + Default: ``before`` + + dep : string + This defines a plugin which should be executed after/before this one. + ``"*"`` means all and ``"-*"`` means none. + """ __slots__ = ("plugin", "hook", "call", "type", "dep") def __init__ (self, plugin, hook, call, type = "before", dep = None): @@ -44,6 +93,10 @@ class Call (object): self.dep = dep class Hook (object): + """ + Representing a hook with all the `Call` s for the different types. + """ + __slots__ = ("before", "override", "after") def __init__ (self): @@ -52,21 +105,49 @@ class Hook (object): self.after = [] class Plugin (object): + """ + This is the main plugin object. It is used where ever a plugin is wanted, and it is the one, which needs to be subclassed by plugin authors. + + :CVariables: + + STAT_DISABLED : status + Status: Disabled. + + STAT_TEMP_ENABLED : status + Status: Enabled for this session only. + + STAT_ENABLED : status + Status: Enabled. + + STAT_TEMP_DISABLED : status + Status: Disabled for this session only. + + STAT_HARD_DISABLED : status + Status: Forced disabled by program (i.e. because of errors in the plugin). + """ (STAT_DISABLED, STAT_TEMP_ENABLED, STAT_ENABLED, STAT_TEMP_DISABLED) = range(4) STAT_HARD_DISABLED = -1 def __init__ (self, disable = False): - self.__menus = [] - self.__calls = [] - self._unresolved_deps = False + """ + :param disable: Forcefully disable the plugin + :type disable: bool + """ + self.__menus = [] #: List of `Menu` + self.__calls = [] #: List of `Call` + self._unresolved_deps = False #: Does this plugin has unresolved dependencies? - if not disable: - self.status = self.STAT_ENABLED - else: + self.status = self.STAT_ENABLED #: The status of this plugin + + if disable: self.status = self.STAT_HARD_DISABLED def _init (self): + """ + Method called from outside to init the extension parts of this plugin. + If the current status is `STAT_HARD_DISABLED` or there are unresolved dependencies, the init process is not started. + """ for d in self.deps: if not system.find_packages(d, pkgSet="installed", with_version = False): @@ -77,14 +158,31 @@ class Plugin (object): self.init() def init (self): + """ + This method is called by `_init` and should be overriden by the plugin author. + + :precond: No unresolved deps and the status is not `STAT_HARD_DISABLED`. + """ pass @property def author (self): + """ + Returns the plugin's author. + The author is given by the ``__author__`` variable. + + :rtype: string + """ return getattr(self, "__author__", "") @property def description (self): + """ + Returns the description of this plugin. + It is given by either a ``__description__`` variable or by the normal class docstring. + + :rtype: string + """ if hasattr(self, "__description__"): return self.__description__ else: @@ -92,34 +190,83 @@ class Plugin (object): @property def name (self): + """ + The name of the plugin. If no ``__name__`` variable is given, the class name is taken. + + :rtype: string + """ return getattr(self, "__name__", self.__class__.__name__) @property def menus (self): + """ + Returns an iterator over the menus for this plugin. + + :rtype: iter<`Menu`> + """ return iter(self.__menus) @property def calls (self): + """ + Returns an iterator over the registered calls for this plugin. + + :rtype: iter<`Call`> + """ return iter(self.__calls) @property def deps (self): + """ + Returns an iterator of the dependencies or ``None`` if there are none. + The dependencies are given in the ``__dependency__`` variable. + + :rtype: None or iter + """ if hasattr(self, "__dependency__"): return iter(self.__dependency__) else: - return [] + return None + @property + def enabled (self): + """ + Returns ``True`` if the plugin is enabled. + + :rtype: boolean + :see: `status` + """ + return (self.status in (self.STAT_ENABLED, self.STAT_TEMP_ENABLED)) + def add_menu (self, label, callable): + """ + Adds a new menu item for this plugin. + + :see: `Menu` + """ self.__menus.append(Menu(label, callable)) def add_call (self, hook, callable, type = "before", dep = None): + """ + Adds a new call for this plugin. + + :see: `Call` + """ self.__calls.append(Call(self, hook, callable, type, dep)) - def is_enabled (self): - return (self.status in (self.STAT_ENABLED, self.STAT_TEMP_ENABLED)) class PluginQueue (object): - """Class managing and loading the plugins.""" + """ + Class managing and loading the plugins. + + :IVariables: + + plugins : `Plugin` [] + The list of managed plugins. + + hooks : string -> `Hook` + For each hook name map to a `Hook` object holding the corresponding `Call` objects. + """ def __init__ (self): """ @@ -130,11 +277,25 @@ class PluginQueue (object): self.hooks = defaultdict(Hook) def get_plugins (self, list_disabled = True): - return (x for x in self.plugins if (x.is_enabled() or list_disabled)) + """ + Returns the plugins. + + :param list_disabled: Also list disabled plugins. + :type list_disabled: boolean + + :rtype: iter<`Plugin`> + """ + return (x for x in self.plugins if (x.enabled or list_disabled)) def load (self): - """Load the plugins.""" + """ + Load the plugins. + + This method scans the `portato.constants.PLUGIN_DIR` for python modules and tries to load them. If the modules are real plugins, + they have called `register` and thus the plugins are added. + """ + # look them up plugins = [] for f in os.listdir(PLUGIN_DIR): path = osp.join(PLUGIN_DIR, f) @@ -151,11 +312,13 @@ class PluginQueue (object): else: debug("'%s' is not a plugin: not a .py file", path) - plugin_module.__path__.insert(0, PLUGIN_DIR.rstrip("/")) + # some magic ... + plugin_module.__path__.insert(0, PLUGIN_DIR.rstrip("/")) # make the plugins loadable as "portato.plugins.name" + # add Plugin and register to the builtins, so the plugins always have the correct version :) plugin_module.__builtins__["Plugin"] = Plugin plugin_module.__builtins__["register"] = register - for p in plugins: + for p in plugins: # import them try: exec "from portato.plugins import %s" % p in {} except PluginLoadException, e: @@ -167,6 +330,20 @@ class PluginQueue (object): self._organize() def add (self, plugin, disable = False): + """ + Adds a plugin to the internal list. + + :Parameters: + + plugin : `Plugin` + ``Plugin`` subclass or instance to add. If a class is passed, it is instantiated. + + disable : boolean + Disable the plugin. + + :raise PluginLoadException: passed plugin is not of class `Plugin` + """ + if callable(plugin) and Plugin in plugin.__bases__: p = plugin(disable = disable) # need an instance and not the class elif isinstance(plugin, Plugin): @@ -190,8 +367,18 @@ class PluginQueue (object): info("%s %s", _("Plugin '%s' loaded.") % p.name, msg) def hook (self, hook, *hargs, **hkwargs): + """ + The decorator to use in the program. + All parameters except ``hook`` are passed to plugins. + + :param hook: the name of the hook + :type hook: string + """ def hook_decorator (func): + """ + The real decorator. + """ h = self.hooks[hook] active = Hook() @@ -201,10 +388,10 @@ class PluginQueue (object): calls = getattr(h, type) aCalls = getattr(active, type) for call in calls: - if call.plugin.is_enabled(): + if call.plugin.enabled: aCalls.append(call) - if h.override and h.override.plugin.is_enabled(): + if h.override and h.override.plugin.enabled: active.override = h.override @wraps(func) @@ -234,7 +421,9 @@ class PluginQueue (object): return hook_decorator def _organize (self): - """Creates the lists of connects in a way, that all dependencies are fullfilled.""" + """ + Organizes the lists of `Call` in a way, that all dependencies are fullfilled. + """ unresolved_before = defaultdict(list) unresolved_after = defaultdict(list) star_before = defaultdict(Hook) # should be _before_ all other @@ -330,13 +519,16 @@ def load_plugins(): def get_plugin_queue(): - """Returns the actual L{PluginQueue}. If it is C{None}, they are not being loaded yet. + """ + Returns the actual `PluginQueue`. If it is ``None``, they are not being loaded yet. - @returns: the actual L{PluginQueue} or C{None} - @rtype: PluginQueue""" + :rtype: `PluginQueue` or ``None``""" return __plugins def hook(hook, *args, **kwargs): + """ + Shortcut to `PluginQueue.hook`. If no `PluginQueue` is loaded, this does nothing. + """ if __plugins is None: def pseudo_decorator(f): return f @@ -345,5 +537,10 @@ def hook(hook, *args, **kwargs): return __plugins.hook(hook, *args, **kwargs) def register (plugin, disable = False): + """ + Registers a plugin. + + :see: `PluginQueue.add` + """ if __plugins is not None: __plugins.add(plugin, disable) -- cgit v1.2.3-54-g00ecf From 7346ef212b6d4cb34182af7620df04e52c940fdb Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Tue, 8 Jul 2008 14:39:30 +0200 Subject: Very basic documentation --- doc/Howto_Write_Plugins | 73 +++++++++++-------------------------------------- 1 file changed, 16 insertions(+), 57 deletions(-) diff --git a/doc/Howto_Write_Plugins b/doc/Howto_Write_Plugins index 6f58de5..bb63120 100644 --- a/doc/Howto_Write_Plugins +++ b/doc/Howto_Write_Plugins @@ -1,67 +1,26 @@ HowTo Write A Plugin For Portato ================================= -(NOTE: The XML schema is likely to change in the future.) +Writing plugins for Portato is quite easy: (Nearly) all you have to do is to write a plain Python module :). -Writing a plugin is (quite) simple. You have to provide a XML file which tells portato how to communicate with the plugin and in most cases a Python module doing the actual work. This howto is not going to cover the writing of the plugin, but only the XML. +A plugin has two more builtins than a normal Python module: -General -------- + ``Plugin`` + This is a class representing a plugin. -So - how is a plugin is working in general? Portato defines a set of hooks (see the Hooks file for a complete list) to which your plugin can connect. For each hook you have three choices: To connect before, after or instead of the function being hooked. (Of course you can connect several times to one hook ...) The latter one should be used only if you really know what you are doing as it is likely that Portato won't work any longer after this. Also have in mind, that only one plugin can override. Thus: if several plugins want to override one hook, a randomly chosen one will get the allowance. + ``register`` + A function which you have to call to get the your plugin added to Portato. -For each of the "before" and "after" mode a queue exists, holding the plugins to execute. A plugin is allowed to state another plugin which should be executed after (in "before" mode) or before (in "after" mode) your plugin. The star * exists to note that this should be applied to ALL other plugins. (And "-*" does exactly the opposite.) Portato TRIES to satisfy your request... +In this module you need to have at least one class, which inherits from ``Plugin``. This class does all the handling you want your plugin to do. If you want, you can implement more classes - from Portato's view they are handled as different plugins. Thus: It is not the module hierarchy, but the classes that count. +Add the end you only call ``register`` once for each class and are done :). -When you now have chosen the connect(s) to chose you write an appropriate function (or better: a Python callable) which will be given in the XML-definition to be called by Portato with the hook's arguments. (Hint: All arguments will be passed as keyword parameters. So you can easily pick the ones you need and add a "**kwargs" argument, which will take the rest. As a nice side effect you do not have to reflect any API changes which will come with additional parameters :)). +Of course there are some things you should bare in mind: + + 1. Do not use the ``__init__`` method, but ``init``. + 2. Do not use a member which shadows one from the original class: + ``description``, ``author``, ``status``, ``menus``, ``name``, ``calls``, ``deps``, ``enabled``, ``add_menu``, ``add_call`` + 3. Of course you can *use* the members mentioned under point 2. -Finally: Add an import tag stating the module to import to execute the function(s) given - and you are done. +For the details, please see the source code at the moment or write your question to portato@necoro.net -If you are finished with your plugin, you can check if it complies with the XML Schema by doing: "portato -x $PLUGIN_FILE". - -Sample XML ----------- - - - - Joe Smith - Some small sample plugin - - plugins.sample.small - - - - - - - - - - -Notes: - -- If you want to specify a dependency plugin the connect tag has to be like: The other plugin we depend on . -- The "connect"-tag can be omitted. It then defaults to "". -- It is possible of course to have more than one "hook" tag. -- The options tag is optional. For a complete list of options, see below. - -Additional Tags ---------------- - -Menu: - It is possible, that your plugin gets an entry in the "Plugin"-menu. Therefore you have to provide a "menu" tag and one or more "item" tags: - - - - A small _Plugin - - - - Note, that the underscore in front of a character will make it be underlined in the menu and thus accessible by a shortcut. - -Options --------- - -disabled: - Disable the plugin by default, i.e. the user has to enable it, if he wants to use it. +.. vim: ft=rst -- cgit v1.2.3-54-g00ecf From b21023ecc7dd95339f83ae044ad642b3a863ecef Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Tue, 8 Jul 2008 14:52:57 +0200 Subject: Added the reload_portage plugin --- plugins/reload_portage.py | 27 +++++++++++++++++++++++++++ portato/plugin.py | 13 +++++++++---- 2 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 plugins/reload_portage.py diff --git a/plugins/reload_portage.py b/plugins/reload_portage.py new file mode 100644 index 0000000..280bd92 --- /dev/null +++ b/plugins/reload_portage.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# File: plugins/reload_portage.py +# This file is part of the Portato-Project, a graphical portage-frontend. +# +# Copyright (C) 2007-2008 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 + +from portato.backend import system + +class ReloadPortage (Plugin): + __author__ = "René 'Necoro' Neumann" + __description__ = """Reloads portage when an emerge process has finished. +This can take some time, but sometimes it is necessairy.""" + + def init(self): + self.add_call("after_emerge", self.hook, type = "after", dep = "EtcProposals") + self.status = self.STAT_DISABLED # disable by default + + def hook (self, *args, **kwargs): + system.reload_settings() + +register(ReloadPortage) diff --git a/portato/plugin.py b/portato/plugin.py index 5994328..0119909 100644 --- a/portato/plugin.py +++ b/portato/plugin.py @@ -186,7 +186,12 @@ class Plugin (object): if hasattr(self, "__description__"): return self.__description__ else: - return getattr(self, "__doc__", "") + doc = getattr(self, "__doc__", "") + + if not doc or doc == Plugin.__doc__: + return "" + else: + return doc @property def name (self): @@ -218,15 +223,15 @@ class Plugin (object): @property def deps (self): """ - Returns an iterator of the dependencies or ``None`` if there are none. + Returns an iterator of the dependencies or ``[]`` if there are none. The dependencies are given in the ``__dependency__`` variable. - :rtype: None or iter + :rtype: [] or iter """ if hasattr(self, "__dependency__"): return iter(self.__dependency__) else: - return None + return [] @property def enabled (self): -- cgit v1.2.3-54-g00ecf From 8b6db38a2d27fca00d0fe037e86eefc941d559e4 Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Tue, 8 Jul 2008 14:53:19 +0200 Subject: Added the reload_portage plugin --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5203dd7..57d5961 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ packages = ["portato", "portato.gui", "portato.gui.windows", "portato.plugins", data_files = [ (TEMPLATE_DIR, [os.path.join("portato/gui/templates",x) for x in os.listdir("portato/gui/templates") if x.endswith(".glade")]), (ICON_DIR, ["icons/portato-icon.png"]), - (PLUGIN_DIR, plugin_list("gpytage", "notify", "etc_proposals"))] + (PLUGIN_DIR, plugin_list("gpytage", "notify", "etc_proposals", "reload_portage"))] # do the distutils setup setup(name="Portato", -- cgit v1.2.3-54-g00ecf