changeset 71:a564fc84d5d0

twisted: added Twisted module for Python 3.8 compatibility: Current Twisted version (19.10.0) is not Python 3.8 compatible, so a twisted directory has been added to fix `twisted.words.xish.domish` and be able to use it with Python 3.8. To be removed once Python 3.8 compatible version of Twisted is published.
author Goffi <goffi@goffi.org>
date Mon, 18 Nov 2019 20:48:45 +0100
parents 922b2f0efa60
children 29b383e14ebf
files sat_tmp/twisted/LICENSE sat_tmp/twisted/README sat_tmp/twisted/__init__.py sat_tmp/twisted/domish.py sat_tmp/wokkel/__init__.py
diffstat 5 files changed, 1018 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sat_tmp/twisted/LICENSE	Mon Nov 18 20:48:45 2019 +0100
@@ -0,0 +1,73 @@
+Copyright (c) 2001-2019
+Allen Short
+Amber Hawkie Brown
+Andrew Bennetts
+Andy Gayton
+Antoine Pitrou
+Apple Computer, Inc.
+Ashwini Oruganti
+Benjamin Bruheim
+Bob Ippolito
+Canonical Limited
+Christopher Armstrong
+Ciena Corporation
+David Reid
+Divmod Inc.
+Donovan Preston
+Eric Mangold
+Eyal Lotem
+Google Inc.
+Hybrid Logic Ltd.
+Hynek Schlawack
+Itamar Turner-Trauring
+James Knight
+Jason A. Mobarak
+Jean-Paul Calderone
+Jessica McKellar
+Jonathan D. Simms
+Jonathan Jacobs
+Jonathan Lange
+Julian Berman
+Jürgen Hermann
+Kevin Horn
+Kevin Turner
+Laurens Van Houtven
+Mary Gardiner
+Massachusetts Institute of Technology
+Matthew Lefkowitz
+Moshe Zadka
+Paul Swartz
+Pavel Pergamenshchik
+Rackspace, US Inc.
+Ralph Meijer
+Richard Wall
+Sean Riley
+Software Freedom Conservancy
+Tavendo GmbH
+Thijs Triemstra
+Thomas Herve
+Timothy Allen
+Tom Most
+Tom Prince
+Travis B. Hartwell
+
+and others that have contributed code to the public domain.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sat_tmp/twisted/README	Mon Nov 18 20:48:45 2019 +0100
@@ -0,0 +1,10 @@
+This directory includes modules from Twisted with fixes not yet released.
+
+Note that the LICENSE in this directory apply only to this directory, other files follow
+license specified in their respective directories, or by default in root directory's
+COPYING file.
+
+The files here (except __init__.py) come from Twisted repository
+(https://github.com/twisted/twisted) and are copyrighted to their respective authors.
+
+"__init__.py" is part of sat_tmp and licensed under the same license (see root COPYING).
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sat_tmp/twisted/__init__.py	Mon Nov 18 20:48:45 2019 +0100
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+#
+# Series of Twisted patches used by SàT
+# Copyright (C) 2015-2019 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+from twisted import version as v
+from logging import Logger
+
+log = Logger(__name__)
+
+
+def install():
+    """Monkey patch Twisted to have improvments implemented here"""
+    # Python 3.8 support for domish
+    if (v.major, v.minor) > (19, 10):
+        log.warning(
+            f"New release of Twisted available ({v.short()}), Python 3.8 patch may not "
+            f"be needed anymore")
+
+    from . import domish
+    sys.modules['twisted.words.xish.domish'] = domish
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sat_tmp/twisted/domish.py	Mon Nov 18 20:48:45 2019 +0100
@@ -0,0 +1,899 @@
+# -*- test-case-name: twisted.words.test.test_domish -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+DOM-like XML processing support.
+
+This module provides support for parsing XML into DOM-like object structures
+and serializing such structures to an XML string representation, optimized
+for use in streaming XML applications.
+"""
+
+from __future__ import absolute_import, division
+
+from zope.interface import implementer, Interface, Attribute
+
+from twisted.python.compat import (_PY3, StringType, _coercedUnicode,
+                                   iteritems, itervalues, unicode)
+
+def _splitPrefix(name):
+    """ Internal method for splitting a prefixed Element name into its
+        respective parts """
+    ntok = name.split(":", 1)
+    if len(ntok) == 2:
+        return ntok
+    else:
+        return (None, ntok[0])
+
+# Global map of prefixes that always get injected
+# into the serializers prefix map (note, that doesn't
+# mean they're always _USED_)
+G_PREFIXES = { "http://www.w3.org/XML/1998/namespace":"xml" }
+
+class _ListSerializer:
+    """ Internal class which serializes an Element tree into a buffer """
+    def __init__(self, prefixes=None, prefixesInScope=None):
+        self.writelist = []
+        self.prefixes = {}
+        if prefixes:
+            self.prefixes.update(prefixes)
+        self.prefixes.update(G_PREFIXES)
+        self.prefixStack = [G_PREFIXES.values()] + (prefixesInScope or [])
+        self.prefixCounter = 0
+
+    def getValue(self):
+        return u"".join(self.writelist)
+
+    def getPrefix(self, uri):
+        if uri not in self.prefixes:
+            self.prefixes[uri] = "xn%d" % (self.prefixCounter)
+            self.prefixCounter = self.prefixCounter + 1
+        return self.prefixes[uri]
+
+    def prefixInScope(self, prefix):
+        stack = self.prefixStack
+        for i in range(-1, (len(self.prefixStack)+1) * -1, -1):
+            if prefix in stack[i]:
+                return True
+        return False
+
+    def serialize(self, elem, closeElement=1, defaultUri=''):
+        # Optimization shortcuts
+        write = self.writelist.append
+
+        # Shortcut, check to see if elem is actually a chunk o' serialized XML
+        if isinstance(elem, SerializedXML):
+            write(elem)
+            return
+
+        # Shortcut, check to see if elem is actually a string (aka Cdata)
+        if isinstance(elem, StringType):
+            write(escapeToXml(elem))
+            return
+
+        # Further optimizations
+        name = elem.name
+        uri = elem.uri
+        defaultUri, currentDefaultUri = elem.defaultUri, defaultUri
+
+        for p, u in iteritems(elem.localPrefixes):
+            self.prefixes[u] = p
+        self.prefixStack.append(list(elem.localPrefixes.keys()))
+
+        # Inherit the default namespace
+        if defaultUri is None:
+            defaultUri = currentDefaultUri
+
+        if uri is None:
+            uri = defaultUri
+
+        prefix = None
+        if uri != defaultUri or uri in self.prefixes:
+            prefix = self.getPrefix(uri)
+            inScope = self.prefixInScope(prefix)
+
+        # Create the starttag
+
+        if not prefix:
+            write("<%s" % (name))
+        else:
+            write("<%s:%s" % (prefix, name))
+
+            if not inScope:
+                write(" xmlns:%s='%s'" % (prefix, uri))
+                self.prefixStack[-1].append(prefix)
+                inScope = True
+
+        if defaultUri != currentDefaultUri and \
+           (uri != defaultUri or not prefix or not inScope):
+            write(" xmlns='%s'" % (defaultUri))
+
+        for p, u in iteritems(elem.localPrefixes):
+            write(" xmlns:%s='%s'" % (p, u))
+
+        # Serialize attributes
+        for k,v in elem.attributes.items():
+            # If the attribute name is a tuple, it's a qualified attribute
+            if isinstance(k, tuple):
+                attr_uri, attr_name = k
+                attr_prefix = self.getPrefix(attr_uri)
+
+                if not self.prefixInScope(attr_prefix):
+                    write(" xmlns:%s='%s'" % (attr_prefix, attr_uri))
+                    self.prefixStack[-1].append(attr_prefix)
+
+                write(" %s:%s='%s'" % (attr_prefix, attr_name,
+                                       escapeToXml(v, 1)))
+            else:
+                write((" %s='%s'" % ( k, escapeToXml(v, 1))))
+
+        # Shortcut out if this is only going to return
+        # the element (i.e. no children)
+        if closeElement == 0:
+            write(">")
+            return
+
+        # Serialize children
+        if len(elem.children) > 0:
+            write(">")
+            for c in elem.children:
+                self.serialize(c, defaultUri=defaultUri)
+            # Add closing tag
+            if not prefix:
+                write("</%s>" % (name))
+            else:
+                write("</%s:%s>" % (prefix, name))
+        else:
+            write("/>")
+
+        self.prefixStack.pop()
+
+
+SerializerClass = _ListSerializer
+
+def escapeToXml(text, isattrib = 0):
+    """ Escape text to proper XML form, per section 2.3 in the XML specification.
+
+    @type text: C{str}
+    @param text: Text to escape
+
+    @type isattrib: C{bool}
+    @param isattrib: Triggers escaping of characters necessary for use as
+                     attribute values
+    """
+    text = text.replace("&", "&amp;")
+    text = text.replace("<", "&lt;")
+    text = text.replace(">", "&gt;")
+    if isattrib == 1:
+        text = text.replace("'", "&apos;")
+        text = text.replace("\"", "&quot;")
+    return text
+
+def unescapeFromXml(text):
+    text = text.replace("&lt;", "<")
+    text = text.replace("&gt;", ">")
+    text = text.replace("&apos;", "'")
+    text = text.replace("&quot;", "\"")
+    text = text.replace("&amp;", "&")
+    return text
+
+def generateOnlyInterface(list, int):
+    """ Filters items in a list by class
+    """
+    for n in list:
+        if int.providedBy(n):
+            yield n
+
+def generateElementsQNamed(list, name, uri):
+    """ Filters Element items in a list with matching name and URI. """
+    for n in list:
+        if IElement.providedBy(n) and n.name == name and n.uri == uri:
+            yield n
+
+def generateElementsNamed(list, name):
+    """ Filters Element items in a list with matching name, regardless of URI.
+    """
+    for n in list:
+        if IElement.providedBy(n) and n.name == name:
+            yield n
+
+
+class SerializedXML(unicode):
+    """ Marker class for pre-serialized XML in the DOM. """
+    pass
+
+
+class Namespace:
+    """ Convenience object for tracking namespace declarations. """
+    def __init__(self, uri):
+        self._uri = uri
+    def __getattr__(self, n):
+        return (self._uri, n)
+    def __getitem__(self, n):
+        return (self._uri, n)
+
+class IElement(Interface):
+    """
+    Interface to XML element nodes.
+
+    See L{Element} for a detailed example of its general use.
+
+    Warning: this Interface is not yet complete!
+    """
+
+    uri = Attribute(""" Element's namespace URI """)
+    name = Attribute(""" Element's local name """)
+    defaultUri = Attribute(""" Default namespace URI of child elements """)
+    attributes = Attribute(""" Dictionary of element attributes """)
+    children = Attribute(""" List of child nodes """)
+    parent = Attribute(""" Reference to element's parent element """)
+    localPrefixes = Attribute(""" Dictionary of local prefixes """)
+
+    def toXml(prefixes=None, closeElement=1, defaultUri='',
+              prefixesInScope=None):
+        """ Serializes object to a (partial) XML document
+
+        @param prefixes: dictionary that maps namespace URIs to suggested
+                         prefix names.
+        @type prefixes: L{dict}
+
+        @param closeElement: flag that determines whether to include the
+            closing tag of the element in the serialized string. A value of
+            C{0} only generates the element's start tag. A value of C{1} yields
+            a complete serialization.
+        @type closeElement: L{int}
+
+        @param defaultUri: Initial default namespace URI. This is most useful
+            for partial rendering, where the logical parent element (of which
+            the starttag was already serialized) declares a default namespace
+            that should be inherited.
+        @type defaultUri: L{unicode}
+
+        @param prefixesInScope: list of prefixes that are assumed to be
+            declared by ancestors.
+        @type prefixesInScope: C{list}
+
+        @return: (partial) serialized XML
+        @rtype: C{unicode}
+        """
+
+    def addElement(name, defaultUri=None, content=None):
+        """
+        Create an element and add as child.
+
+        The new element is added to this element as a child, and will have
+        this element as its parent.
+
+        @param name: element name. This can be either a L{unicode} object that
+            contains the local name, or a tuple of (uri, local_name) for a
+            fully qualified name. In the former case, the namespace URI is
+            inherited from this element.
+        @type name: L{unicode} or L{tuple} of (L{unicode}, L{unicode})
+
+        @param defaultUri: default namespace URI for child elements. If
+            L{None}, this is inherited from this element.
+        @type defaultUri: L{unicode}
+
+        @param content: text contained by the new element.
+        @type content: L{unicode}
+
+        @return: the created element
+        @rtype: object providing L{IElement}
+        """
+
+    def addChild(node):
+        """
+        Adds a node as child of this element.
+
+        The C{node} will be added to the list of childs of this element, and
+        will have this element set as its parent when C{node} provides
+        L{IElement}. If C{node} is a L{unicode} and the current last child is
+        character data (L{unicode}), the text from C{node} is appended to the
+        existing last child.
+
+        @param node: the child node.
+        @type node: L{unicode} or object implementing L{IElement}
+        """
+
+    def addContent(text):
+        """
+        Adds character data to this element.
+
+        If the current last child of this element is a string, the text will
+        be appended to that string. Otherwise, the text will be added as a new
+        child.
+
+        @param text: The character data to be added to this element.
+        @type text: L{unicode}
+        """
+
+
+@implementer(IElement)
+class Element(object):
+    """ Represents an XML element node.
+
+    An Element contains a series of attributes (name/value pairs), content
+    (character data), and other child Element objects. When building a document
+    with markup (such as HTML or XML), use this object as the starting point.
+
+    Element objects fully support XML Namespaces. The fully qualified name of
+    the XML Element it represents is stored in the C{uri} and C{name}
+    attributes, where C{uri} holds the namespace URI. There is also a default
+    namespace, for child elements. This is stored in the C{defaultUri}
+    attribute. Note that C{''} means the empty namespace.
+
+    Serialization of Elements through C{toXml()} will use these attributes
+    for generating proper serialized XML. When both C{uri} and C{defaultUri}
+    are not None in the Element and all of its descendents, serialization
+    proceeds as expected:
+
+      >>> from twisted.words.xish import domish
+      >>> root = domish.Element(('myns', 'root'))
+      >>> root.addElement('child', content='test')
+      <twisted.words.xish.domish.Element object at 0x83002ac>
+      >>> root.toXml()
+      u"<root xmlns='myns'><child>test</child></root>"
+
+    For partial serialization, needed for streaming XML, a special value for
+    namespace URIs can be used: L{None}.
+
+    Using L{None} as the value for C{uri} means: this element is in whatever
+    namespace inherited by the closest logical ancestor when the complete XML
+    document has been serialized. The serialized start tag will have a
+    non-prefixed name, and no xmlns declaration will be generated.
+
+    Similarly, L{None} for C{defaultUri} means: the default namespace for my
+    child elements is inherited from the logical ancestors of this element,
+    when the complete XML document has been serialized.
+
+    To illustrate, an example from a Jabber stream. Assume the start tag of the
+    root element of the stream has already been serialized, along with several
+    complete child elements, and sent off, looking like this::
+
+      <stream:stream xmlns:stream='http://etherx.jabber.org/streams'
+                     xmlns='jabber:client' to='example.com'>
+        ...
+
+    Now suppose we want to send a complete element represented by an
+    object C{message} created like:
+
+      >>> message = domish.Element((None, 'message'))
+      >>> message['to'] = 'user@example.com'
+      >>> message.addElement('body', content='Hi!')
+      <twisted.words.xish.domish.Element object at 0x8276e8c>
+      >>> message.toXml()
+      u"<message to='user@example.com'><body>Hi!</body></message>"
+
+    As, you can see, this XML snippet has no xmlns declaration. When sent
+    off, it inherits the C{jabber:client} namespace from the root element.
+    Note that this renders the same as using C{''} instead of L{None}:
+
+      >>> presence = domish.Element(('', 'presence'))
+      >>> presence.toXml()
+      u"<presence/>"
+
+    However, if this object has a parent defined, the difference becomes
+    clear:
+
+      >>> child = message.addElement(('http://example.com/', 'envelope'))
+      >>> child.addChild(presence)
+      <twisted.words.xish.domish.Element object at 0x8276fac>
+      >>> message.toXml()
+      u"<message to='user@example.com'><body>Hi!</body><envelope xmlns='http://example.com/'><presence xmlns=''/></envelope></message>"
+
+    As, you can see, the <presence/> element is now in the empty namespace, not
+    in the default namespace of the parent or the streams'.
+
+    @type uri: C{unicode} or None
+    @ivar uri: URI of this Element's name
+
+    @type name: C{unicode}
+    @ivar name: Name of this Element
+
+    @type defaultUri: C{unicode} or None
+    @ivar defaultUri: URI this Element exists within
+
+    @type children: C{list}
+    @ivar children: List of child Elements and content
+
+    @type parent: L{Element}
+    @ivar parent: Reference to the parent Element, if any.
+
+    @type attributes: L{dict}
+    @ivar attributes: Dictionary of attributes associated with this Element.
+
+    @type localPrefixes: L{dict}
+    @ivar localPrefixes: Dictionary of namespace declarations on this
+                         element. The key is the prefix to bind the
+                         namespace uri to.
+    """
+
+    _idCounter = 0
+
+    def __init__(self, qname, defaultUri=None, attribs=None,
+                       localPrefixes=None):
+        """
+        @param qname: Tuple of (uri, name)
+        @param defaultUri: The default URI of the element; defaults to the URI
+                           specified in C{qname}
+        @param attribs: Dictionary of attributes
+        @param localPrefixes: Dictionary of namespace declarations on this
+                              element. The key is the prefix to bind the
+                              namespace uri to.
+        """
+        self.localPrefixes = localPrefixes or {}
+        self.uri, self.name = qname
+        if defaultUri is None and \
+           self.uri not in itervalues(self.localPrefixes):
+            self.defaultUri = self.uri
+        else:
+            self.defaultUri = defaultUri
+        self.attributes = attribs or {}
+        self.children = []
+        self.parent = None
+
+    def __getattr__(self, key):
+        # Check child list for first Element with a name matching the key
+        for n in self.children:
+            if IElement.providedBy(n) and n.name == key:
+                return n
+
+        # Tweak the behaviour so that it's more friendly about not
+        # finding elements -- we need to document this somewhere :)
+        if key.startswith('_'):
+            raise AttributeError(key)
+        else:
+            return None
+
+    def __getitem__(self, key):
+        return self.attributes[self._dqa(key)]
+
+    def __delitem__(self, key):
+        del self.attributes[self._dqa(key)];
+
+    def __setitem__(self, key, value):
+        self.attributes[self._dqa(key)] = value
+
+    def __unicode__(self):
+        """
+        Retrieve the first CData (content) node
+        """
+        for n in self.children:
+            if isinstance(n, StringType):
+                return n
+        return u""
+
+    def __bytes__(self):
+        """
+        Retrieve the first character data node as UTF-8 bytes.
+        """
+        return unicode(self).encode('utf-8')
+
+    if _PY3:
+        __str__ = __unicode__
+    else:
+        __str__ = __bytes__
+
+    def _dqa(self, attr):
+        """ Dequalify an attribute key as needed """
+        if isinstance(attr, tuple) and not attr[0]:
+            return attr[1]
+        else:
+            return attr
+
+    def getAttribute(self, attribname, default = None):
+        """ Retrieve the value of attribname, if it exists """
+        return self.attributes.get(attribname, default)
+
+    def hasAttribute(self, attrib):
+        """ Determine if the specified attribute exists """
+        return self._dqa(attrib) in self.attributes
+
+    def compareAttribute(self, attrib, value):
+        """ Safely compare the value of an attribute against a provided value.
+
+        L{None}-safe.
+        """
+        return self.attributes.get(self._dqa(attrib), None) == value
+
+    def swapAttributeValues(self, left, right):
+        """ Swap the values of two attribute. """
+        d = self.attributes
+        l = d[left]
+        d[left] = d[right]
+        d[right] = l
+
+    def addChild(self, node):
+        """ Add a child to this Element. """
+        if IElement.providedBy(node):
+            node.parent = self
+        self.children.append(node)
+        return node
+
+    def addContent(self, text):
+        """ Add some text data to this Element. """
+        text = _coercedUnicode(text)
+        c = self.children
+        if len(c) > 0 and isinstance(c[-1], unicode):
+            c[-1] = c[-1] + text
+        else:
+            c.append(text)
+        return c[-1]
+
+    def addElement(self, name, defaultUri = None, content = None):
+        if isinstance(name, tuple):
+            if defaultUri is None:
+                defaultUri = name[0]
+            child = Element(name, defaultUri)
+        else:
+            if defaultUri is None:
+                defaultUri = self.defaultUri
+            child = Element((defaultUri, name), defaultUri)
+
+        self.addChild(child)
+
+        if content:
+            child.addContent(content)
+
+        return child
+
+    def addRawXml(self, rawxmlstring):
+        """ Add a pre-serialized chunk o' XML as a child of this Element. """
+        self.children.append(SerializedXML(rawxmlstring))
+
+    def addUniqueId(self):
+        """ Add a unique (across a given Python session) id attribute to this
+            Element.
+        """
+        self.attributes["id"] = "H_%d" % Element._idCounter
+        Element._idCounter = Element._idCounter + 1
+
+
+    def elements(self, uri=None, name=None):
+        """
+        Iterate across all children of this Element that are Elements.
+
+        Returns a generator over the child elements. If both the C{uri} and
+        C{name} parameters are set, the returned generator will only yield
+        on elements matching the qualified name.
+
+        @param uri: Optional element URI.
+        @type uri: C{unicode}
+        @param name: Optional element name.
+        @type name: C{unicode}
+        @return: Iterator that yields objects implementing L{IElement}.
+        """
+        if name is None:
+            return generateOnlyInterface(self.children, IElement)
+        else:
+            return generateElementsQNamed(self.children, name, uri)
+
+
+    def toXml(self, prefixes=None, closeElement=1, defaultUri='',
+                    prefixesInScope=None):
+        """ Serialize this Element and all children to a string. """
+        s = SerializerClass(prefixes=prefixes, prefixesInScope=prefixesInScope)
+        s.serialize(self, closeElement=closeElement, defaultUri=defaultUri)
+        return s.getValue()
+
+    def firstChildElement(self):
+        for c in self.children:
+            if IElement.providedBy(c):
+                return c
+        return None
+
+
+class ParserError(Exception):
+    """ Exception thrown when a parsing error occurs """
+    pass
+
+def elementStream():
+    """ Preferred method to construct an ElementStream
+
+    Uses Expat-based stream if available, and falls back to Sux if necessary.
+    """
+    try:
+        es = ExpatElementStream()
+        return es
+    except ImportError:
+        if SuxElementStream is None:
+            raise Exception("No parsers available :(")
+        es = SuxElementStream()
+        return es
+
+try:
+    from twisted.web import sux
+except:
+    SuxElementStream = None
+else:
+    class SuxElementStream(sux.XMLParser):
+        def __init__(self):
+            self.connectionMade()
+            self.DocumentStartEvent = None
+            self.ElementEvent = None
+            self.DocumentEndEvent = None
+            self.currElem = None
+            self.rootElem = None
+            self.documentStarted = False
+            self.defaultNsStack = []
+            self.prefixStack = []
+
+        def parse(self, buffer):
+            try:
+                self.dataReceived(buffer)
+            except sux.ParseError as e:
+                raise ParserError(str(e))
+
+
+        def findUri(self, prefix):
+            # Walk prefix stack backwards, looking for the uri
+            # matching the specified prefix
+            stack = self.prefixStack
+            for i in range(-1, (len(self.prefixStack)+1) * -1, -1):
+                if prefix in stack[i]:
+                    return stack[i][prefix]
+            return None
+
+        def gotTagStart(self, name, attributes):
+            defaultUri = None
+            localPrefixes = {}
+            attribs = {}
+            uri = None
+
+            # Pass 1 - Identify namespace decls
+            for k, v in list(attributes.items()):
+                if k.startswith("xmlns"):
+                    x, p = _splitPrefix(k)
+                    if (x is None): # I.e.  default declaration
+                        defaultUri = v
+                    else:
+                        localPrefixes[p] = v
+                    del attributes[k]
+
+            # Push namespace decls onto prefix stack
+            self.prefixStack.append(localPrefixes)
+
+            # Determine default namespace for this element; if there
+            # is one
+            if defaultUri is None:
+                if len(self.defaultNsStack) > 0:
+                    defaultUri = self.defaultNsStack[-1]
+                else:
+                    defaultUri = ''
+
+            # Fix up name
+            prefix, name = _splitPrefix(name)
+            if prefix is None: # This element is in the default namespace
+                uri = defaultUri
+            else:
+                # Find the URI for the prefix
+                uri = self.findUri(prefix)
+
+            # Pass 2 - Fix up and escape attributes
+            for k, v in attributes.items():
+                p, n = _splitPrefix(k)
+                if p is None:
+                    attribs[n] = v
+                else:
+                    attribs[(self.findUri(p)), n] = unescapeFromXml(v)
+
+            # Construct the actual Element object
+            e = Element((uri, name), defaultUri, attribs, localPrefixes)
+
+            # Save current default namespace
+            self.defaultNsStack.append(defaultUri)
+
+            # Document already started
+            if self.documentStarted:
+                # Starting a new packet
+                if self.currElem is None:
+                    self.currElem = e
+                # Adding to existing element
+                else:
+                    self.currElem = self.currElem.addChild(e)
+            # New document
+            else:
+                self.rootElem = e
+                self.documentStarted = True
+                self.DocumentStartEvent(e)
+
+        def gotText(self, data):
+            if self.currElem != None:
+                if isinstance(data, bytes):
+                    data = data.decode('ascii')
+                self.currElem.addContent(data)
+
+        def gotCData(self, data):
+            if self.currElem != None:
+                if isinstance(data, bytes):
+                    data = data.decode('ascii')
+                self.currElem.addContent(data)
+
+        def gotComment(self, data):
+            # Ignore comments for the moment
+            pass
+
+        entities = { "amp" : "&",
+                     "lt"  : "<",
+                     "gt"  : ">",
+                     "apos": "'",
+                     "quot": "\"" }
+
+        def gotEntityReference(self, entityRef):
+            # If this is an entity we know about, add it as content
+            # to the current element
+            if entityRef in SuxElementStream.entities:
+                data = SuxElementStream.entities[entityRef]
+                if isinstance(data, bytes):
+                    data = data.decode('ascii')
+                self.currElem.addContent(data)
+
+        def gotTagEnd(self, name):
+            # Ensure the document hasn't already ended
+            if self.rootElem is None:
+                # XXX: Write more legible explanation
+                raise ParserError("Element closed after end of document.")
+
+            # Fix up name
+            prefix, name = _splitPrefix(name)
+            if prefix is None:
+                uri = self.defaultNsStack[-1]
+            else:
+                uri = self.findUri(prefix)
+
+            # End of document
+            if self.currElem is None:
+                # Ensure element name and uri matches
+                if self.rootElem.name != name or self.rootElem.uri != uri:
+                    raise ParserError("Mismatched root elements")
+                self.DocumentEndEvent()
+                self.rootElem = None
+
+            # Other elements
+            else:
+                # Ensure the tag being closed matches the name of the current
+                # element
+                if self.currElem.name != name or self.currElem.uri != uri:
+                    # XXX: Write more legible explanation
+                    raise ParserError("Malformed element close")
+
+                # Pop prefix and default NS stack
+                self.prefixStack.pop()
+                self.defaultNsStack.pop()
+
+                # Check for parent null parent of current elem;
+                # that's the top of the stack
+                if self.currElem.parent is None:
+                    self.currElem.parent = self.rootElem
+                    self.ElementEvent(self.currElem)
+                    self.currElem = None
+
+                # Anything else is just some element wrapping up
+                else:
+                    self.currElem = self.currElem.parent
+
+
+class ExpatElementStream:
+    def __init__(self):
+        import pyexpat
+        self.DocumentStartEvent = None
+        self.ElementEvent = None
+        self.DocumentEndEvent = None
+        self.error = pyexpat.error
+        self.parser = pyexpat.ParserCreate("UTF-8", " ")
+        self.parser.StartElementHandler = self._onStartElement
+        self.parser.EndElementHandler = self._onEndElement
+        self.parser.CharacterDataHandler = self._onCdata
+        self.parser.StartNamespaceDeclHandler = self._onStartNamespace
+        self.parser.EndNamespaceDeclHandler = self._onEndNamespace
+        self.currElem = None
+        self.defaultNsStack = ['']
+        self.documentStarted = 0
+        self.localPrefixes = {}
+
+    def parse(self, buffer):
+        try:
+            self.parser.Parse(buffer)
+        except self.error as e:
+            raise ParserError(str(e))
+
+    def _onStartElement(self, name, attrs):
+        # Generate a qname tuple from the provided name.  See
+        # http://docs.python.org/library/pyexpat.html#xml.parsers.expat.ParserCreate
+        # for an explanation of the formatting of name.
+        qname = name.rsplit(" ", 1)
+        if len(qname) == 1:
+            qname = ('', name)
+
+        # Process attributes
+        new_attrs = {}
+        to_delete = []
+        for k, v in attrs.items():
+            if " " in k:
+                aqname = k.rsplit(" ", 1)
+                new_attrs[(aqname[0], aqname[1])] = v
+                to_delete.append(k)
+
+        attrs.update(new_attrs)
+
+        for k in to_delete:
+            del attrs[k]
+
+        # Construct the new element
+        e = Element(qname, self.defaultNsStack[-1], attrs, self.localPrefixes)
+        self.localPrefixes = {}
+
+        # Document already started
+        if self.documentStarted == 1:
+            if self.currElem != None:
+                self.currElem.children.append(e)
+                e.parent = self.currElem
+            self.currElem = e
+
+        # New document
+        else:
+            self.documentStarted = 1
+            self.DocumentStartEvent(e)
+
+    def _onEndElement(self, _):
+        # Check for null current elem; end of doc
+        if self.currElem is None:
+            self.DocumentEndEvent()
+
+        # Check for parent that is None; that's
+        # the top of the stack
+        elif self.currElem.parent is None:
+            self.ElementEvent(self.currElem)
+            self.currElem = None
+
+        # Anything else is just some element in the current
+        # packet wrapping up
+        else:
+            self.currElem = self.currElem.parent
+
+    def _onCdata(self, data):
+        if self.currElem != None:
+            self.currElem.addContent(data)
+
+    def _onStartNamespace(self, prefix, uri):
+        # If this is the default namespace, put
+        # it on the stack
+        if prefix is None:
+            self.defaultNsStack.append(uri)
+        else:
+            self.localPrefixes[prefix] = uri
+
+    def _onEndNamespace(self, prefix):
+        # Remove last element on the stack
+        if prefix is None:
+            self.defaultNsStack.pop()
+
+## class FileParser(ElementStream):
+##     def __init__(self):
+##         ElementStream.__init__(self)
+##         self.DocumentStartEvent = self.docStart
+##         self.ElementEvent = self.elem
+##         self.DocumentEndEvent = self.docEnd
+##         self.done = 0
+
+##     def docStart(self, elem):
+##         self.document = elem
+
+##     def elem(self, elem):
+##         self.document.addChild(elem)
+
+##     def docEnd(self):
+##         self.done = 1
+
+##     def parse(self, filename):
+##         with open(filename) as f:
+##             for l in f.readlines():
+##                 self.parser.Parse(l)
+##         assert self.done == 1
+##         return self.document
+
+## def parseFile(filename):
+##     return FileParser().parse(filename)
+
+
--- a/sat_tmp/wokkel/__init__.py	Mon Nov 18 19:14:24 2019 +0100
+++ b/sat_tmp/wokkel/__init__.py	Mon Nov 18 20:48:45 2019 +0100
@@ -1,5 +1,4 @@
-# -*- coding: utf-8 -*-
-# -*- test-case-name: wokkel.test.test_rsm -*-
+#!/usr/bin/env python3
 #
 # Series of Wokkel patches used by SàT
 # Copyright (C) 2015-2019 Jérôme Poisson (goffi@goffi.org)