summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRené 'Necoro' Neumann <necoro@necoro.net>2008-07-08 15:00:20 +0200
committerRené 'Necoro' Neumann <necoro@necoro.net>2008-07-08 15:00:20 +0200
commit276451a383052ffdc67f561082825cc84aa83bd7 (patch)
treea148e31c8d201ecc61903eae18a495b9cfdf80be
parent0a8814713917548767f0ff823e34d412061b3ffe (diff)
parent8b6db38a2d27fca00d0fe037e86eefc941d559e4 (diff)
downloadportato-276451a383052ffdc67f561082825cc84aa83bd7.tar.gz
portato-276451a383052ffdc67f561082825cc84aa83bd7.tar.bz2
portato-276451a383052ffdc67f561082825cc84aa83bd7.zip
Merged in the new plugin system
-rw-r--r--doc/Howto_Write_Plugins73
-rw-r--r--plugin.xsd89
-rw-r--r--plugins/etc_proposals.py41
-rw-r--r--plugins/etc_proposals.xml19
-rw-r--r--plugins/exception.py (renamed from portato/plugins/gpytage.py)12
-rw-r--r--plugins/exception.xml13
-rw-r--r--plugins/gpytage.py26
-rw-r--r--plugins/gpytage.xml13
-rw-r--r--plugins/new_version.py80
-rw-r--r--plugins/new_version.xml16
-rw-r--r--plugins/notify.py49
-rw-r--r--plugins/notify.xml14
-rw-r--r--plugins/reload_portage.py27
-rwxr-xr-xportato.py21
-rw-r--r--portato/config_parser.py24
-rw-r--r--portato/constants.py4
-rw-r--r--portato/gui/exception_handling.py5
-rw-r--r--portato/gui/templates/PluginWindow.glade180
-rw-r--r--portato/gui/windows/main.py22
-rw-r--r--portato/gui/windows/plugin.py162
-rw-r--r--portato/plugin.py825
-rw-r--r--portato/plugins/__init__.py7
-rw-r--r--portato/plugins/etc_proposals.py31
-rw-r--r--portato/plugins/exception.py2
-rw-r--r--portato/plugins/new_version.py58
-rw-r--r--portato/plugins/notify.py22
-rw-r--r--setup.py5
27 files changed, 1010 insertions, 830 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
-----------
-
-<?xml version="1.0" encoding="UTF-8" ?>
-<plugin xmlns="http://portato.sourceforge.net/plugin" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://portato.sourceforge.net/plugin http://portato.sourceforge.net/plugin.xsd">
- <author>Joe Smith</author>
- <name>Some small sample plugin</name>
-
- <import>plugins.sample.small</import>
-
- <hooks>
- <hook type = "a_hook" call = "the_calling_function">
- <connect type="after" />
- </hook
- </hooks>
-
- <options>
- <option>disabled</option>
- </options
-</plugin>
-
-Notes:
-
-- If you want to specify a dependency plugin the connect tag has to be like: <connect type = "after"> The other plugin we depend on </connect>.
-- The "connect"-tag can be omitted. It then defaults to "<connect type='before' />".
-- 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:
-
- <menu>
- <item call = "the_calling_menu_function">
- A small _Plugin
- </item>
- </menu>
-
- 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
diff --git a/plugin.xsd b/plugin.xsd
deleted file mode 100644
index 38cb872..0000000
--- a/plugin.xsd
+++ /dev/null
@@ -1,89 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://portato.sourceforge.net/plugin" targetNamespace="http://portato.sourceforge.net/plugin" elementFormDefault="qualified">
- <xs:element name="plugin">
- <xs:complexType>
- <xs:all>
- <xs:element name="name" type="string" />
- <xs:element name="author" type="string" />
- <xs:element name="import" type="importString" minOccurs="0"/>
- <xs:element name="frontends" type="stringList" minOccurs="0" />
- <xs:element name="hooks" minOccurs="0">
- <xs:complexType>
- <xs:sequence>
- <xs:element name="hook" minOccurs="1" maxOccurs="unbounded">
- <xs:complexType>
- <xs:sequence>
- <xs:element name="connect" minOccurs="0" maxOccurs="unbounded">
- <xs:complexType>
- <xs:simpleContent>
- <xs:extension base="xs:string">
- <xs:attribute name="type" default="before">
- <xs:simpleType>
- <xs:restriction base="xs:string">
- <xs:enumeration value="before" />
- <xs:enumeration value="override" />
- <xs:enumeration value="after" />
- </xs:restriction>
- </xs:simpleType>
- </xs:attribute>
- </xs:extension>
- </xs:simpleContent>
- </xs:complexType>
- </xs:element>
- </xs:sequence>
- <xs:attribute name="type" type="string" use="required" />
- <xs:attribute name="call" type="functionCall" use="required" />
- </xs:complexType>
- </xs:element>
- </xs:sequence>
- </xs:complexType>
- </xs:element>
- <xs:element name="options" minOccurs="0">
- <xs:complexType>
- <xs:sequence>
- <xs:element name="option" minOccurs="1" maxOccurs="unbounded" type="string" />
- </xs:sequence>
- </xs:complexType>
- </xs:element>
- <xs:element name="menu" minOccurs="0">
- <xs:complexType>
- <xs:sequence>
- <xs:element name="item" minOccurs="1" maxOccurs="unbounded">
- <xs:complexType>
- <xs:simpleContent>
- <xs:extension base="string">
- <xs:attribute name="call" type="functionCall" use="required" />
- </xs:extension>
- </xs:simpleContent>
- </xs:complexType>
- </xs:element>
- </xs:sequence>
- </xs:complexType>
- </xs:element>
- </xs:all>
- </xs:complexType>
- </xs:element>
- <xs:simpleType name="importString">
- <xs:restriction base="xs:string">
- <xs:pattern value="([a-zA-Z_]+\.?)+" />
- </xs:restriction>
- </xs:simpleType>
- <xs:simpleType name="functionCall">
- <xs:restriction base="xs:string">
- <xs:pattern value="[a-zA-Z_][0-9a-zA-Z_]*" />
- </xs:restriction>
- </xs:simpleType>
- <xs:simpleType name="string">
- <xs:restriction base="xs:string">
- <xs:minLength value="1" />
- </xs:restriction>
- </xs:simpleType>
- <xs:simpleType name="_stringList">
- <xs:list itemType="string"/>
- </xs:simpleType>
- <xs:simpleType name="stringList">
- <xs:restriction base="_stringList">
- <xs:minLength value="1" />
- </xs:restriction>
- </xs:simpleType>
-</xs:schema>
diff --git a/plugins/etc_proposals.py b/plugins/etc_proposals.py
new file mode 100644
index 0000000..c32c8f3
--- /dev/null
+++ b/plugins/etc_proposals.py
@@ -0,0 +1,41 @@
+# -*- 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 <necoro@necoro.net>
+
+from portato.helper import error
+
+import os
+from subprocess import Popen
+
+class EtcProposals (Plugin):
+ __author__ = "René 'Necoro' Neumann"
+ __description__ = "Adds support for <b>etc-proposals</b>, a graphical etc-update replacement."
+ __dependency__ = ["app-portage/etc-proposals"]
+
+ 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)
+
+ 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 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<plugin xmlns="http://portato.sourceforge.net/plugin" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://portato.sourceforge.net/plugin http://portato.sourceforge.net/plugin.xsd">
-
- <author>René 'Necoro' Neumann</author>
- <name>Etc-proposals</name>
-
- <import>portato.plugins.etc_proposals</import>
-
- <hooks>
- <hook type = "after_emerge" call = "etc_prop">
- <connect type="after" />
- </hook>
- </hooks>
-
- <menu>
- <item call="etc_prop_menu">Et_c-Proposals</item>
- </menu>
-
-</plugin>
diff --git a/portato/plugins/gpytage.py b/plugins/exception.py
index 22cc7ef..e653853 100644
--- a/portato/plugins/gpytage.py
+++ b/plugins/exception.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
-# File: portato/plugins/gpytage.py
+# File: plugins/exception.py
# This file is part of the Portato-Project, a graphical portage-frontend.
#
# Copyright (C) 2008 René 'Necoro' Neumann
@@ -10,7 +10,11 @@
#
# Written by René 'Necoro' Neumann <necoro@necoro.net>
-from subprocess import Popen
+def throw (*args, **kwargs):
+ raise Exception, "As requested, Sir!"
-def gpytage(*args, **kwargs):
- Popen(["/usr/bin/gpytage"])
+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 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<plugin xmlns="http://portato.sourceforge.net/plugin" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://portato.sourceforge.net/plugin http://portato.sourceforge.net/plugin.xsd">
-
- <author>René 'Necoro' Neumann</author>
- <name>Exception Thrower</name>
-
- <import>portato.plugins.exception</import>
-
- <menu>
- <item call="throw">Throw exception</item>
- </menu>
-
-</plugin>
diff --git a/plugins/gpytage.py b/plugins/gpytage.py
new file mode 100644
index 0000000..d8c2831
--- /dev/null
+++ b/plugins/gpytage.py
@@ -0,0 +1,26 @@
+# -*- 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 <necoro@necoro.net>
+
+from subprocess import Popen
+
+class GPytage (Plugin):
+ __author__ = "René 'Necoro' Neumann"
+ __description__ = "Adds a menu entry to directly start <b>gpytage</b>, a config editor."
+ __dependency__ = ["app-portage/gpytage"]
+
+ def 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 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<plugin xmlns="http://portato.sourceforge.net/plugin" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://portato.sourceforge.net/plugin http://portato.sourceforge.net/plugin.xsd">
-
- <author>René 'Necoro' Neumann</author>
- <name>GPytage</name>
-
- <import>portato.plugins.gpytage</import>
-
- <menu>
- <item call="gpytage">Config _Editor</item>
- </menu>
-
-</plugin>
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 <necoro@necoro.net>
+
+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 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<plugin xmlns="http://portato.sourceforge.net/plugin" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://portato.sourceforge.net/plugin http://portato.sourceforge.net/plugin.xsd">
-
- <author>René 'Necoro' Neumann</author>
- <name>New Version Reminder</name>
-
- <import>portato.plugins.new_version</import>
-
- <hooks>
- <hook type="main" call="run" />
- </hooks>
-
- <menu>
- <item call="run_menu">Check for new _versions</item>
- </menu>
-</plugin>
diff --git a/plugins/notify.py b/plugins/notify.py
new file mode 100644
index 0000000..6446812
--- /dev/null
+++ b/plugins/notify.py
@@ -0,0 +1,49 @@
+# -*- 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 <necoro@necoro.net>
+
+disable = False
+
+try:
+ import pynotify
+except ImportError:
+ disable = True
+
+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):
+ 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, disable)
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 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<plugin xmlns="http://portato.sourceforge.net/plugin" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://portato.sourceforge.net/plugin http://portato.sourceforge.net/plugin.xsd">
-
- <author>René 'Necoro' Neumann</author>
- <name>Notify</name>
- <frontends>gtk</frontends>
-
- <import>portato.plugins.notify</import>
-
- <hooks>
- <hook type = "after_emerge" call = "notify" />
- </hooks>
-
-</plugin>
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 <necoro@necoro.net>
+
+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.py b/portato.py
index a4f871a..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
@@ -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)
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/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")
diff --git a/portato/gui/exception_handling.py b/portato/gui/exception_handling.py
index eadf124..dae95ed 100644
--- a/portato/gui/exception_handling.py
+++ b/portato/gui/exception_handling.py
@@ -100,16 +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):
trace = StringIO()
diff --git a/portato/gui/templates/PluginWindow.glade b/portato/gui/templates/PluginWindow.glade
index 1de5fc0..f76193e 100644
--- a/portato/gui/templates/PluginWindow.glade
+++ b/portato/gui/templates/PluginWindow.glade
@@ -1,11 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
-<!--Generated with glade3 3.4.1 on Fri Feb 29 00:03:30 2008 -->
+<!--Generated with glade3 3.4.5 on Fri Jul 4 15:24:27 2008 -->
<glade-interface>
<widget class="GtkWindow" id="PluginWindow">
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="border_width">5</property>
<property name="title" translatable="yes">Plugins</property>
+ <property name="resizable">False</property>
<property name="modal">True</property>
<property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
<property name="destroy_with_parent">True</property>
@@ -26,6 +27,7 @@
<child>
<widget class="GtkTreeView" id="pluginList">
<property name="visible">True</property>
+ <property name="headers_visible">False</property>
<property name="rules_hint">True</property>
<property name="enable_search">False</property>
</widget>
@@ -33,6 +35,180 @@
</widget>
</child>
<child>
+ <widget class="GtkFrame" id="frame1">
+ <property name="visible">True</property>
+ <property name="label_xalign">0</property>
+ <child>
+ <widget class="GtkTable" id="table1">
+ <property name="visible">True</property>
+ <property name="n_rows">4</property>
+ <property name="n_columns">2</property>
+ <property name="row_spacing">10</property>
+ <child>
+ <widget class="GtkButton" id="installBtn">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="label" translatable="yes">_Install dependencies</property>
+ <property name="use_underline">True</property>
+ <property name="response_id">0</property>
+ <signal name="clicked" handler="cb_install_clicked"/>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="y_options"></property>
+ <property name="x_padding">10</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkExpander" id="depExpander">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child>
+ <widget class="GtkTreeView" id="depList">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="headers_visible">False</property>
+ <property name="headers_clickable">True</property>
+ </widget>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="label4">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Needed dependencies</property>
+ <property name="single_line_mode">True</property>
+ </widget>
+ <packing>
+ <property name="type">label_item</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="top_attach">2</property>
+ <property name="bottom_attach">3</property>
+ <property name="x_padding">10</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="descrLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">label</property>
+ </widget>
+ <packing>
+ <property name="right_attach">2</property>
+ <property name="y_options">GTK_FILL</property>
+ <property name="y_padding">10</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">&lt;b&gt;Author:&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ <property name="single_line_mode">True</property>
+ </widget>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkLabel" id="authorLabel">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">label</property>
+ <property name="single_line_mode">True</property>
+ </widget>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">1</property>
+ <property name="bottom_attach">2</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkHButtonBox" id="buttonBox">
+ <property name="visible">True</property>
+ <property name="spacing">5</property>
+ <property name="homogeneous">True</property>
+ <property name="layout_style">GTK_BUTTONBOX_EDGE</property>
+ <child>
+ <widget class="GtkRadioButton" id="enabledRB">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="label" translatable="yes">Enabled</property>
+ <property name="response_id">0</property>
+ <property name="active">True</property>
+ <signal name="toggled" handler="cb_state_toggled"/>
+ </widget>
+ </child>
+ <child>
+ <widget class="GtkRadioButton" id="tempEnabledRB">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="label" translatable="yes">Temporarily enabled</property>
+ <property name="response_id">0</property>
+ <property name="group">enabledRB</property>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkRadioButton" id="tempDisabledRB">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="label" translatable="yes">Temporarily disabled</property>
+ <property name="response_id">0</property>
+ <property name="group">enabledRB</property>
+ <signal name="toggled" handler="cb_state_toggled"/>
+ </widget>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <widget class="GtkRadioButton" id="disabledRB">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="label" translatable="yes">Disabled</property>
+ <property name="response_id">0</property>
+ <property name="group">enabledRB</property>
+ <signal name="toggled" handler="cb_state_toggled"/>
+ </widget>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="right_attach">2</property>
+ <property name="top_attach">3</property>
+ <property name="bottom_attach">4</property>
+ <property name="y_options">GTK_FILL</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+ <child>
+ <placeholder/>
+ <packing>
+ <property name="type">label_item</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
<widget class="GtkHButtonBox" id="hbuttonbox2">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
@@ -68,7 +244,7 @@
</widget>
<packing>
<property name="expand">False</property>
- <property name="position">1</property>
+ <property name="position">2</property>
</packing>
</child>
</widget>
diff --git a/portato/gui/windows/main.py b/portato/gui/windows/main.py
index 364810d..265d4dd 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,11 @@ 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:
+ if p.status == p.STAT_HARD_DISABLED:
return ""
+
+ return int(p.status >= p.STAT_ENABLED)
+
return _save
# SESSION VERSION
@@ -1553,9 +1551,9 @@ class MainWindow (Window):
if queue is None:
plugins = []
else:
- plugins = queue.get_plugins()
-
- PluginWindow(self.window, plugins)
+ plugins = list(queue.get_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 fb9446e..392654e 100644
--- a/portato/gui/windows/plugin.py
+++ b/portato/gui/windows/plugin.py
@@ -15,6 +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):
@@ -24,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
@@ -32,50 +35,77 @@ class PluginWindow (AbstractDialog):
AbstractDialog.__init__(self, parent)
self.plugins = plugins
+ self.queue = queue
self.changedPlugins = {}
+ self.inst = []
+ self.ninst = []
- 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")
+ 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)
- 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 (("<b>"+p.name+"</b>", p.author, _(self.statsStore[p.status][0])) for p in plugins):
- self.store.append(p)
+ for p in plugins:
+ self.store.append(["<b>%s</b>" % p.name])
+
+ self.view.get_selection().connect("changed", self.cb_list_selection)
self.window.show_all()
- def cb_status_changed (self, cell, path, new_text):
- path = int(path)
-
- self.store[path][2] = "<b>%s</b>" % new_text
+ def build_dep_list (self):
+ store = gtk.ListStore(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])
- # 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 +113,69 @@ class PluginWindow (AbstractDialog):
self.close()
return True
+
+ def cb_list_selection (self, selection):
+ plugin = self.get_actual()
+ self.inst = []
+ self.ninst = []
+
+ if plugin:
+ if not plugin.description:
+ self.descrLabel.hide()
+ else:
+ self.descrLabel.set_markup(plugin.description)
+ self.descrLabel.show()
+
+ self.authorLabel.set_label(plugin.author)
+
+ status = self.changedPlugins.get(plugin, plugin.status)
+ self.buttons[status].set_active(True)
+
+ if plugin.deps:
+
+ for dep in plugin.deps:
+ if system.find_packages(dep, pkgSet = "installed", with_version = False):
+ self.inst.append(dep)
+ else:
+ self.ninst.append(dep)
+
+ self.fill_dep_list(self.inst, self.ninst)
+ self.depExpander.show()
+
+ self.installBtn.show()
+ 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()
+
+ 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 5926922..0119909 100644
--- a/portato/plugin.py
+++ b/portato/plugin.py
@@ -3,349 +3,421 @@
# 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 <necoro@necoro.net>
-"""A module containing the management of the plugin system."""
+"""
+A module managing the plugins for Portato.
+"""
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):
+__docformat__ = "restructuredtext"
+
+import os
+import os.path as osp
+import traceback
+from collections import defaultdict
+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):
+ """
+ Exception signaling a failed plugin loading.
+ """
pass
-class Options (object):
- """The <options>-element."""
+class Menu (object):
+ """
+ One single menu entry.
- __options = ("disabled", "blocking")
+ :IVariables:
- def __init__ (self, options = None):
+ label : string
+ The label of the entry. Can have underscores to define the shortcut.
- self.disabled = False
- self.blocking = False
+ call
+ The function to call, if the entry is clicked.
+ """
+ __slots__ = ("label", "call")
- 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 __init__ (self, label, call):
+ self.label = label
+ self.call = call
- def get (self, name):
- return self.__getattribute__(name)
+class Call (object):
+ """
+ This class represents an object, which is attached to a specified hook.
- def set (self, name, value):
- return self.__setattr__(name, value)
+ :IVariables:
-class Menu:
- """A single <menu>-element."""
- def __init__ (self, plugin, label, call):
- """Constructor.
+ plugin : `Plugin`
+ The plugin where this call belongs to.
- @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
+ hook : string
+ The name of the corresponding hook.
- @raises PluginImportException: if the plugin's import could not be imported"""
+ call
+ The function to call.
- self.label = label
- self.plugin = plugin
+ type : string
+ This is either ``before``, ``after`` or ``override`` and defines the type of the call:
- if self.plugin.needs_import(): # get import
- imp = self.plugin.get_import()
- try:
- mod = __import__(imp, globals(), locals(), [call])
- except ImportError:
- raise PluginImportException, imp
+ 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
- try:
- self.call = eval("mod."+call) # build function
- except AttributeError:
- raise PluginImportException, imp
- else:
- try:
- self.call = eval(call)
- except AttributeError:
- raise PluginImportException, imp
+ Default: ``before``
-class Connect:
- """A single <connect>-element."""
+ 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, hook, type, depend_plugin):
- """Constructor.
+ def __init__ (self, plugin, hook, call, type = "before", dep = None):
+ self.plugin = plugin
+ self.hook = hook
+ self.call = call
+ self.type = type
+ self.dep = dep
- @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"""
+class Hook (object):
+ """
+ Representing a hook with all the `Call` s for the different types.
+ """
+
+ __slots__ = ("before", "override", "after")
- self.type = type
- self.hook = hook
- self.depend_plugin = depend_plugin
+ def __init__ (self):
+ self.before = []
+ self.override = None
+ self.after = []
- def is_before_type (self):
- return self.type == "before"
+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.
- def is_after_type (self):
- return self.type == "after"
+ :CVariables:
- def is_override_type (self):
- return self.type == "override"
+ STAT_DISABLED : status
+ Status: Disabled.
-class Hook:
- """A single <hook>-element."""
+ STAT_TEMP_ENABLED : status
+ Status: Enabled for this session only.
- def __init__ (self, plugin, hook, call):
- """Constructor.
+ STAT_ENABLED : status
+ Status: Enabled.
- @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"""
+ STAT_TEMP_DISABLED : status
+ Status: Disabled for this session only.
- self.plugin = plugin
- self.hook = hook
- self.call = call
- self.connects = []
+ STAT_HARD_DISABLED : status
+ Status: Forced disabled by program (i.e. because of errors in the plugin).
+ """
- def parse_connects (self, connects):
- """This gets a list of <connect>-elements and parses them.
-
- @param connects: the list of <connect>'s
- @type connects: NodeList"""
-
- if not connects: # no connects - assume "before" connect
- self.connects.append(Connect(self, "before", None))
+ (STAT_DISABLED, STAT_TEMP_ENABLED, STAT_ENABLED, STAT_TEMP_DISABLED) = range(4)
+ STAT_HARD_DISABLED = -1
+
+ def __init__ (self, disable = 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?
+
+ self.status = self.STAT_ENABLED #: The status of this plugin
- 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)
+ 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):
+ self._unresolved_deps = True
+ break
-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"""
+ if self.status != self.STAT_HARD_DISABLED and not self._unresolved_deps:
+ 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:
+ doc = getattr(self, "__doc__", "")
- 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()
+ if not doc or doc == Plugin.__doc__:
+ return ""
+ else:
+ return doc
+
+ @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 ``[]`` if there are none.
+ The dependencies are given in the ``__dependency__`` variable.
+
+ :rtype: [] or iter<string>
+ """
+ if hasattr(self, "__dependency__"):
+ return iter(self.__dependency__)
+ else:
+ return []
- self.status = self.STAT_ENABLED
+ @property
+ def enabled (self):
+ """
+ Returns ``True`` if the plugin is enabled.
- def parse_hooks (self, hooks):
- """Gets an <hooks>-elements and parses it.
+ :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.
- @param hooks: the hooks node
- @type hooks: NodeList"""
+ :see: `Menu`
+ """
+ self.__menus.append(Menu(label, callable))
- 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 add_call (self, hook, callable, type = "before", dep = None):
+ """
+ Adds a new call for this plugin.
- def parse_menus (self, menus):
- """Get a list of <menu>-elements and parses them.
+ :see: `Call`
+ """
+ self.__calls.append(Call(self, hook, callable, type, dep))
- @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)
+class PluginQueue (object):
+ """
+ Class managing and loading the plugins.
+
+ :IVariables:
- def parse_options (self, options):
- if options:
- for o in options:
- self.options.parse(o.getElementsByTagName("option"))
+ plugins : `Plugin` []
+ The list of managed plugins.
- 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.
+ hooks : string -> `Hook`
+ For each hook name map to a `Hook` object holding the corresponding `Call` objects.
+ """
- @param imports: list of imports
- @type imports: NodeList
-
- @raises PluginImportException: if the plugin's import could not be imported"""
+ def __init__ (self):
+ """
+ Constructor.
+ """
- if imports:
- self._import = str(imports[0].firstChild.nodeValue.strip())
+ self.plugins = []
+ self.hooks = defaultdict(Hook)
- try: # try loading
- mod = __import__(self._import)
- del mod
- except ImportError:
- raise PluginImportException, self._import
+ def get_plugins (self, list_disabled = True):
+ """
+ Returns the plugins.
- def needs_import (self):
- """Returns True if an import is required prior to calling the plugin.
- @rtype: bool"""
- return self._import is not None
+ :param list_disabled: Also list disabled plugins.
+ :type list_disabled: boolean
- def get_import (self):
- """Returns the module to import.
- @rtype: string"""
- return self._import
+ :rtype: iter<`Plugin`>
+ """
+ return (x for x in self.plugins if (x.enabled or list_disabled))
- def get_option(self, name):
- return self.options.get(name)
+ def load (self):
+ """
+ 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)
+ 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])
+ 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)
+
+ # 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: # import them
+ 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})
+ except:
+ tb = traceback.format_exc()
+ error(_("Loading plugin '%(plugin)s' failed: %(error)s"), {"plugin" : p, "error" : tb})
- def set_option (self, name, value):
- return self.options.set(name, value)
+ self._organize()
- def is_enabled (self):
- return (self.status in (self.STAT_ENABLED, self.STAT_TEMP_ENABLED))
+ def add (self, plugin, disable = False):
+ """
+ Adds a plugin to the internal list.
-class PluginQueue:
- """Class managing and loading the plugins."""
+ :Parameters:
- 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"""
+ plugin : `Plugin`
+ ``Plugin`` subclass or instance to add. If a class is passed, it is instantiated.
- self.frontend = frontend
- self.list = []
- self.hooks = {}
- if load:
- self._load()
+ disable : boolean
+ Disable the plugin.
- def get_plugins (self, list_disabled = True):
- return [x for x in self.list if (x.is_enabled() or list_disabled)]
+ :raise PluginLoadException: passed plugin is not of class `Plugin`
+ """
- 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)]
+ 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."
- def get_plugin_menus (self, list_disabled = False):
- return flatten([x.menus for x in self.list if (x.is_enabled() or list_disabled)])
+ p._init()
- def hook (self, hook, *hargs, **hkwargs):
- """This is a method taking care of calling the plugins.
+ self.plugins.append(p)
- 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)
+ 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)
- @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)
+ def hook (self, hook, *hargs, **hkwargs):
+ """
+ The decorator to use in the program.
+ All parameters except ``hook`` are passed to plugins.
- return f(*hargs, **hkwargs) # call function
+ :param hook: the name of the hook
+ :type hook: string
+ """
def hook_decorator (func):
- """This is the real decorator."""
-
- if hook in self.hooks:
- list = self.hooks[hook]
- else:
- list = ([],[],[])
+ """
+ The real decorator.
+ """
+ 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.enabled:
+ aCalls.append(call)
+
+ if h.override and h.override.plugin.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,178 +425,127 @@ 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)
+ """
+ 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
+ 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]
- if changed:
- resolve(hook, list, idx, add)
+ 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)
+
+ 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()
+ __plugins.load()
+
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
return pseudo_decorator
else:
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)
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 <necoro@necoro.net>
+
+"""
+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/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 <necoro@necoro.net>
-
-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()
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
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)
diff --git a/setup.py b/setup.py
index a8a62d6..57d5961 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", "reload_portage"))]
# do the distutils setup
setup(name="Portato",