summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--plugin.xsd89
-rw-r--r--plugins/etc_proposals.xml28
-rw-r--r--plugins/noroot.xml18
-rw-r--r--plugins/resume_loop.xml25
-rw-r--r--plugins/shutdown.xml19
-rwxr-xr-xportato.py13
-rw-r--r--portato/constants.py19
-rw-r--r--portato/plugin.py161
-rw-r--r--setup.py4
9 files changed, 214 insertions, 162 deletions
diff --git a/plugin.xsd b/plugin.xsd
new file mode 100644
index 0000000..7f9975b
--- /dev/null
+++ b/plugin.xsd
@@ -0,0 +1,89 @@
+<?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">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="hook" minOccurs="1" maxOccurs="unbounded">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="connect" minOccurs="1" 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.xml b/plugins/etc_proposals.xml
index 8686072..410ce4b 100644
--- a/plugins/etc_proposals.xml
+++ b/plugins/etc_proposals.xml
@@ -1,17 +1,19 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<plugin
- author="René 'Necoro' Neumann"
- name="Etc-proposals plugin">
+<?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 plugin</name>
+
<import>portato.plugins.etc_proposals</import>
- <hook
- hook = "after_emerge"
- call = "etc_prop">
- <connect type="after" />
- </hook>
+ <hooks>
+ <hook type = "after_emerge" call = "etc_prop">
+ <connect type="after" />
+ </hook>
+ </hooks>
+
+ <menu>
+ <item call="etc_prop_menu">Et_c-Proposals</item>
+ </menu>
- <menu
- label= "Et_c-Proposals"
- call = "etc_prop_menu"
- />
</plugin>
diff --git a/plugins/noroot.xml b/plugins/noroot.xml
index 7e744ef..850a039 100644
--- a/plugins/noroot.xml
+++ b/plugins/noroot.xml
@@ -1,12 +1,12 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<plugin
- author="René 'Necoro' Neumann"
- name="No Root">
+<?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>No Root</name>
<import>portato.plugins.noroot</import>
- <hook
- hook = "am_i_root"
- call = "i_am_root">
- <connect type="override" />
- </hook>
+ <hooks>
+ <hook type = "am_i_root" call = "i_am_root">
+ <connect type="override" />
+ </hook>
+ </hooks>
</plugin>
diff --git a/plugins/resume_loop.xml b/plugins/resume_loop.xml
index 572ccfa..14575cd 100644
--- a/plugins/resume_loop.xml
+++ b/plugins/resume_loop.xml
@@ -1,22 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
-<plugin
- author="René 'Necoro' Neumann"
- name="Emerge Resume Loop">
+<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>Emerge Resume Loop</name>
<import>portato.plugins.resume_loop</import>
- <hook
- hook = "emerge"
- call = "set_console">
- <connect/>
- </hook>
+ <hooks>
+ <hook type="emerge" call="set_console">
+ <connect />
+ </hook>
- <hook
- hook = "after_emerge"
- call = "resume_loop">
- <connect type="before">*</connect>
- </hook>
+ <hook type="after_emerge" call="resume_loop">
+ <connect type="before">*</connect>
+ </hook>
+ </hooks>
<options>
<option>disabled</option>
</options>
+
</plugin>
diff --git a/plugins/shutdown.xml b/plugins/shutdown.xml
index 586b57d..75323b1 100644
--- a/plugins/shutdown.xml
+++ b/plugins/shutdown.xml
@@ -1,17 +1,16 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<plugin
- author="René 'Necoro' Neumann"
- name="Shutdown">
+<?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>Shutdown</name>
<import>portato.plugins.shutdown</import>
- <hook
- hook = "after_emerge"
- call = "shutdown">
- <connect type="after">*</connect>
- </hook>
+ <hooks>
+ <hook type = "after_emerge" call = "shutdown">
+ <connect type="after">*</connect>
+ </hook>
+ </hooks>
<options>
<option>disabled</option>
</options>
-
</plugin>
diff --git a/portato.py b/portato.py
index e4210bb..5e8b30a 100755
--- a/portato.py
+++ b/portato.py
@@ -12,7 +12,7 @@
#
# Written by René 'Necoro' Neumann <necoro@necoro.net>
-from portato.constants import VERSION, FRONTENDS, STD_FRONTEND
+from portato.constants import VERSION, FRONTENDS, STD_FRONTEND, XSD_LOCATION
from optparse import OptionParser
import sys
@@ -37,6 +37,9 @@ def main ():
parser.add_option("-e", "--ebuild", action = "store", dest = "ebuild",
help = "opens the ebuild viewer instead of launching Portato")
+ parser.add_option("-x", "--validate", action = "store", dest = "validate", metavar="PLUGIN",
+ help = "validates the given plugin xml instead of launching Portato")
+
# run parser
(options, args) = parser.parse_args()
@@ -62,6 +65,14 @@ def main ():
if options.ebuild:
show_ebuild(options.ebuild)
+ elif options.validate:
+ from lxml import etree
+ if etree.XMLSchema(file = XSD_LOCATION).validate(etree.parse(options.validate)):
+ print "Passed validation."
+ return
+ else:
+ print "Verification failed."
+ sys.exit(3)
else:
run()
diff --git a/portato/constants.py b/portato/constants.py
index f8da545..c17ee24 100644
--- a/portato/constants.py
+++ b/portato/constants.py
@@ -14,6 +14,8 @@
Constants used through out the program. Mainly different pathes.
These should be set during the installation.
+@var VERSION: the current version
+@type VERSION: string
@var CONFIG_DIR: The configuration directory.
@type CONFIG_DIR: string
@var CONFIG_LOCATION: L{CONFIG_DIR} plus name of the config file.
@@ -22,8 +24,10 @@ These should be set during the installation.
@type DATA_DIR: string
@var PLUGIN_DIR: Directory containing the plugin xmls.
@type PLUGIN_DIR: string
-@var VERSION: the current version
-@type VERSION: string
+@var XSD_DIR: Directory containing the plugin-xml schema.
+@type XSD_DIR: string
+@var XSD_LOCATION: Path of the plugin schema.
+@type XSD_LOCATION: string
@var ICON_DIR: directory containing the icons
@type ICON_DIR: string
@var APP_ICON: the path of the application icon
@@ -33,16 +37,21 @@ These should be set during the installation.
@var STD_FRONTEND: the frontend uses as the default, i.e. if no other one is given on the cmdline
@type STD_FRONTEND: string
"""
+from os.path import join as pjoin
+
+VERSION = "9999"
CONFIG_DIR = "/etc/portato/"
-CONFIG_LOCATION = CONFIG_DIR+"portato.cfg"
+CONFIG_LOCATION = pjoin(CONFIG_DIR, "portato.cfg")
DATA_DIR = "portato/gui/templates/"
PLUGIN_DIR = "plugins/"
-VERSION = "9999"
+
+XSD_DIR = "./"
+XSD_LOCATION = pjoin(XSD_DIR, "plugin.xsd")
ICON_DIR = "icons/"
-APP_ICON = ICON_DIR+"/portato-icon.png"
+APP_ICON = pjoin(ICON_DIR, "portato-icon.png")
FRONTENDS = ["gtk" ,"qt"]
STD_FRONTEND = "gtk"
diff --git a/portato/plugin.py b/portato/plugin.py
index a833696..06ef135 100644
--- a/portato/plugin.py
+++ b/portato/plugin.py
@@ -14,11 +14,12 @@
import os, os.path
from xml.dom.minidom import parse
+from lxml import etree
-from constants import PLUGIN_DIR
+from constants import PLUGIN_DIR, XSD_LOCATION
from helper import *
-class ParseException (Exception):
+class PluginImportException (ImportError):
pass
class Options (object):
@@ -36,21 +37,10 @@ class Options (object):
def parse (self, options):
for opt in options:
- if opt.hasChildNodes():
- nodes = opt.childNodes
-
- if len(nodes) > 1:
- raise ParseException, "Malformed option"
-
- if nodes[0].nodeType != nodes[0].TEXT_NODE:
- raise ParseException, "Malformed option"
-
- type = str(nodes[0].nodeValue.strip())
-
- if type in self.__options:
- self.set(type, True)
- else:
- raise ParseException, "Malformed option"
+ 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)
@@ -70,13 +60,7 @@ class Menu:
@param call: the function to call relative to the import statement
@type call: string
- @raises ParseException: on parsing errors"""
-
- if not label:
- raise ParseException, "label attribute missing"
-
- if not call:
- raise ParseException, "call attribute missing"
+ @raises PluginImportException: if the plugin's import could not be imported"""
self.label = label
self.plugin = plugin
@@ -86,17 +70,17 @@ class Menu:
try:
mod = __import__(imp, globals(), locals(), [call])
except ImportError:
- raise ParseException, imp+" cannot be imported"
+ raise PluginImportException, imp
try:
self.call = eval("mod."+call) # build function
except AttributeError:
- raise ParseException, call+" cannot be imported"
+ raise PluginImportException, imp
else:
try:
self.call = eval(call)
except AttributeError:
- raise ParseException, call+" cannot be imported"
+ raise PluginImportException, imp
class Connect:
"""A single <connect>-element."""
@@ -109,12 +93,7 @@ class Connect:
@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
-
- @raises ParseException: on parsing errors"""
-
- if not type in ["before", "after", "override"]:
- raise ParseException, "Unknown connect type %s" % type
+ @type depend_plugin: string or None"""
self.type = type
self.hook = hook
@@ -140,16 +119,8 @@ class Hook:
@param hook: the hook to add to
@type hook: string
@param call: the call to make
- @type call: string
-
- @raises ParseException: on parsing errors"""
-
- if not hook:
- raise ParseException, "hook attribute missing"
+ @type call: string"""
- if not call:
- raise ParseException, "call attribute missing"
-
self.plugin = plugin
self.hook = hook
self.call = call
@@ -159,13 +130,8 @@ class Hook:
"""This gets a list of <connect>-elements and parses them.
@param connects: the list of <connect>'s
- @type connects: NodeList
-
- @raises ParseException: on parsing errors"""
+ @type connects: NodeList"""
- if not connects:
- raise ParseException, "No connect elements in hook"
-
for c in connects:
type = c.getAttribute("type")
if type == '':
@@ -175,12 +141,6 @@ class Hook:
dep_plugin = None
if c.hasChildNodes():
nodes = c.childNodes
- if len(nodes) > 1:
- raise ParseException, "Malformed connect"
-
- if nodes[0].nodeType != nodes[0].TEXT_NODE:
- raise ParseException, "Malformed connect"
-
dep_plugin = nodes[0].nodeValue.strip()
connect = Connect(self, type, dep_plugin)
@@ -195,42 +155,39 @@ class Plugin:
@param file: the file name of the plugin.xml
@type file: string
@param name: the name of the plugin
- @type name: string
+ @type name: Node
@param author: the author of the plugin
- @type author: string"""
+ @type author: Node"""
self.file = file
- self.name = name
- self.author = author
+ self.name = name.firstChild.nodeValue.strip()
+ self.author = author.firstChild.nodeValue.strip()
self._import = None
self.hooks = []
self.menus = []
self.options = Options()
def parse_hooks (self, hooks):
- """Gets a list of <hook>-elements and parses them.
+ """Gets an <hooks>-elements and parses it.
- @param hooks: the list of elements
- @type hooks: NodeList
-
- @raises ParseException: on parsing errors"""
+ @param hooks: the hooks node
+ @type hooks: Node"""
- for h in hooks:
- hook = Hook(self, str(h.getAttribute("hook")), str(h.getAttribute("call")))
+ for h in hooks.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):
- """Gets a list of <menu>-elements and parses them.
-
- @param menus: the list of elements
- @type menus: NodeList
+ """Get a list of <menu>-elements and parses them.
- @raises ParseException: on parsing errors"""
+ @param menus: the menu nodelist
+ @type menus: NodeList"""
- for m in menus:
- menu = Menu(self, str(m.getAttribute("label")), str(m.getAttribute("call")))
- self.menus.append(menu)
+ 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:
@@ -243,29 +200,16 @@ class Plugin:
@param imports: list of imports
@type imports: NodeList
- @raises ParseException: on parsing errors"""
-
- if len(imports) > 1:
- raise ParseException, "More than one import statement."
-
- if imports[0].hasChildNodes():
- nodes = imports[0].childNodes
-
- if len(nodes) > 1:
- raise ParseException, "Malformed import"
-
- if nodes[0].nodeType != nodes[0].TEXT_NODE:
- raise ParseException, "Malformed import"
+ @raises PluginImportException: if the plugin's import could not be imported"""
- self._import = str(nodes[0].nodeValue.strip())
+ if imports:
+ self._import = str(imports[0].firstChild.nodeValue.strip())
try: # try loading
mod = __import__(self._import)
del mod
except ImportError:
- raise ParseException, self._import+" cannot be imported"
- else:
- raise ParseException, "Malformed import"
+ raise PluginImportException, self._import
def needs_import (self):
"""Returns True if an import is required prior to calling the plugin.
@@ -401,46 +345,45 @@ class PluginQueue:
"""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:
+
+ if not schema.validate(etree.parse(p)):
+ error("Loading plugin '%s' failed. Plugin does not comply to schema.", p)
+ continue
+
doc = parse(p)
try:
try:
list = doc.getElementsByTagName("plugin")
- if len(list) != 1:
- raise ParseException, "Number of plugin elements unequal to 1."
-
elem = list[0]
frontendOK = None
- for f in elem.getElementsByTagName("frontend"):
- if f.hasChildNodes():
- nodes = f.childNodes
- if len(nodes) > 1:
- raise ParseException, "Malformed frontend"
-
- if nodes[0].nodeType != nodes[0].TEXT_NODE:
- raise ParseException, "Malformed frontend"
-
- fValue = nodes[0].nodeValue.strip()
- if fValue == self.frontend:
+ frontends = elem.getElementsByTagName("frontends")
+ if frontends:
+ nodes = f.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.getAttribute("name"), elem.getAttribute("author"))
- plugin.parse_hooks(elem.getElementsByTagName("hook"))
+ if frontendOK is None or frontendOK is True:
+ plugin = Plugin(p, elem.getElementsByTagName("name")[0], elem.getElementsByTagName("author")[0])
+ plugin.parse_hooks(elem.getElementsByTagName("hook")[0])
plugin.set_import(elem.getElementsByTagName("import"))
plugin.parse_menus(elem.getElementsByTagName("menu"))
plugin.parse_options(elem.getElementsByTagName("options"))
self.list.append(plugin)
- except ParseException, e:
- error("Malformed plugin \"%s\". Reason: %s", p, e[0])
+ except PluginImportException, e:
+ error("Loading plugin '%s' failed: Could not import %s", p, e[0])
+ else:
+ info("Plugin '%s' loaded.", p)
finally:
doc.unlink()
diff --git a/setup.py b/setup.py
index f744194..ca9ae4d 100644
--- a/setup.py
+++ b/setup.py
@@ -13,7 +13,7 @@
import os, os.path
from distutils.core import setup, Extension
-from portato.constants import VERSION, DATA_DIR, FRONTENDS, ICON_DIR, PLUGIN_DIR
+from portato.constants import VERSION, DATA_DIR, FRONTENDS, ICON_DIR, PLUGIN_DIR, XSD_DIR
def plugin_list (*args):
"""Creates a list of correct plugin pathes out of the arguments."""
@@ -26,7 +26,7 @@ def ui_file_list ():
packages = ["portato", "portato.gui", "portato.plugins", "portato.backend", "portato.backend.portage"]
ext_modules = []
-data_files = [(ICON_DIR, ["icons/portato-icon.png"]), (PLUGIN_DIR, plugin_list("shutdown", "resume_loop"))]
+data_files = [(ICON_DIR, ["icons/portato-icon.png"]), (PLUGIN_DIR, plugin_list("shutdown", "resume_loop")), (XSD_DIR, ["plugin.xsd"])]
cmdclass = {}
if "gtk" in FRONTENDS: