diff options
-rw-r--r-- | plugin.xsd | 89 | ||||
-rw-r--r-- | plugins/etc_proposals.xml | 28 | ||||
-rw-r--r-- | plugins/noroot.xml | 18 | ||||
-rw-r--r-- | plugins/resume_loop.xml | 25 | ||||
-rw-r--r-- | plugins/shutdown.xml | 19 | ||||
-rwxr-xr-x | portato.py | 13 | ||||
-rw-r--r-- | portato/constants.py | 19 | ||||
-rw-r--r-- | portato/plugin.py | 161 | ||||
-rw-r--r-- | setup.py | 4 |
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> @@ -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() @@ -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: |