summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--INSTALL9
-rw-r--r--example.py54
-rw-r--r--examples/config.xml10
-rwxr-xr-xexamples/config_component.py190
-rwxr-xr-xexamples/echo_client.py129
-rw-r--r--sleekxmpp/__init__.py36
-rw-r--r--sleekxmpp/basexmpp.py2
-rw-r--r--sleekxmpp/component_example.py41
-rw-r--r--sleekxmpp/xmlstream/filesocket.py36
-rw-r--r--sleekxmpp/xmlstream/handler/base.py93
-rw-r--r--sleekxmpp/xmlstream/handler/callback.py104
-rw-r--r--sleekxmpp/xmlstream/handler/waiter.py116
-rw-r--r--sleekxmpp/xmlstream/handler/xmlcallback.py32
-rw-r--r--sleekxmpp/xmlstream/handler/xmlwaiter.py28
-rw-r--r--sleekxmpp/xmlstream/matcher/base.py30
-rw-r--r--sleekxmpp/xmlstream/matcher/id.py29
-rw-r--r--sleekxmpp/xmlstream/matcher/many.py39
-rw-r--r--sleekxmpp/xmlstream/matcher/stanzapath.py34
-rw-r--r--sleekxmpp/xmlstream/matcher/xmlmask.py204
-rw-r--r--sleekxmpp/xmlstream/matcher/xpath.py99
-rw-r--r--sleekxmpp/xmlstream/stanzabase.py187
-rw-r--r--sleekxmpp/xmlstream/xmlstream.py13
-rw-r--r--tests/sleektest.py13
-rw-r--r--tests/test_elementbase.py43
-rw-r--r--tests/test_handlers.py112
-rw-r--r--tests/test_stanzabase.py79
26 files changed, 1411 insertions, 351 deletions
diff --git a/INSTALL b/INSTALL
index f081a35a..82f87123 100644
--- a/INSTALL
+++ b/INSTALL
@@ -1,11 +1,12 @@
Pre-requisites:
-Python 3.1 or 2.6
+- Python 3.1 or 2.6
Install:
-python3 setup.py install
+> python3 setup.py install
Root install:
-sudo python3 setup.py install
+> sudo python3 setup.py install
To test:
-python example.py -v -j [USER@example.com] -p [PASSWORD]
+> cd examples
+> python echo_client.py -v -j [USER@example.com] -p [PASSWORD]
diff --git a/example.py b/example.py
deleted file mode 100644
index 4eb88b3b..00000000
--- a/example.py
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/usr/bin/env python
-# coding=utf8
-
-import sleekxmpp
-import logging
-from optparse import OptionParser
-import time
-
-import sys
-
-if sys.version_info < (3,0):
- reload(sys)
- sys.setdefaultencoding('utf8')
-
-
-class Example(sleekxmpp.ClientXMPP):
-
- def __init__(self, jid, password):
- sleekxmpp.ClientXMPP.__init__(self, jid, password)
- self.add_event_handler("session_start", self.start)
- self.add_event_handler("message", self.message)
-
- def start(self, event):
- self.getRoster()
- self.sendPresence()
-
- def message(self, msg):
- msg.reply("Thanks for sending\n%(body)s" % msg).send()
-
-if __name__ == '__main__':
- #parse command line arguements
- optp = OptionParser()
- optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO)
- optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
- optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)
- optp.add_option("-j","--jid", dest="jid", help="JID to use")
- optp.add_option("-p","--password", dest="password", help="password to use")
- opts,args = optp.parse_args()
-
- logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
- xmpp = Example(opts.jid, opts.password)
- xmpp.registerPlugin('xep_0030')
- xmpp.registerPlugin('xep_0004')
- xmpp.registerPlugin('xep_0060')
- xmpp.registerPlugin('xep_0199')
-
- # use this if you don't have pydns, and want to
- # talk to GoogleTalk (e.g.)
-# if xmpp.connect(('talk.google.com', 5222)):
- if xmpp.connect():
- xmpp.process(threaded=False)
- print("done")
- else:
- print("Unable to connect.")
diff --git a/examples/config.xml b/examples/config.xml
new file mode 100644
index 00000000..4ca3a3d6
--- /dev/null
+++ b/examples/config.xml
@@ -0,0 +1,10 @@
+<config xmlns="sleekxmpp:config">
+ <jid>component.localhost</jid>
+ <secret>ssshh</secret>
+ <server>localhost</server>
+ <port>8888</port>
+
+ <query xmlns="jabber:iq:roster">
+ <item jid="user@example.com" subscription="both" />
+ </query>
+</config>
diff --git a/examples/config_component.py b/examples/config_component.py
new file mode 100755
index 00000000..cbb8e628
--- /dev/null
+++ b/examples/config_component.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import sys
+import logging
+import time
+from optparse import OptionParser
+
+import sleekxmpp
+from sleekxmpp.componentxmpp import ComponentXMPP
+from sleekxmpp.stanza.roster import Roster
+from sleekxmpp.xmlstream import ElementBase
+from sleekxmpp.xmlstream.stanzabase import ET, registerStanzaPlugin
+
+# Python versions before 3.0 do not use UTF-8 encoding
+# by default. To ensure that Unicode is handled properly
+# throughout SleekXMPP, we will set the default encoding
+# ourselves to UTF-8.
+if sys.version_info < (3, 0):
+ reload(sys)
+ sys.setdefaultencoding('utf8')
+
+
+class Config(ElementBase):
+
+ """
+ In order to make loading and manipulating an XML config
+ file easier, we will create a custom stanza object for
+ our config XML file contents. See the documentation
+ on stanza objects for more information on how to create
+ and use stanza objects and stanza plugins.
+
+ We will reuse the IQ roster query stanza to store roster
+ information since it already exists.
+
+ Example config XML:
+ <config xmlns="sleekxmpp:config">
+ <jid>component.localhost</jid>
+ <secret>ssshh</secret>
+ <server>localhost</server>
+ <port>8888</port>
+
+ <query xmlns="jabber:iq:roster">
+ <item jid="user@example.com" subscription="both" />
+ </query>
+ </config>
+ """
+
+ name = "config"
+ namespace = "sleekxmpp:config"
+ interfaces = set(('jid', 'secret', 'server', 'port'))
+ sub_interfaces = interfaces
+
+
+registerStanzaPlugin(Config, Roster)
+
+
+class ConfigComponent(ComponentXMPP):
+
+ """
+ A simple SleekXMPP component that uses an external XML
+ file to store its configuration data. To make testing
+ that the component works, it will also echo messages sent
+ to it.
+ """
+
+ def __init__(self, config):
+ """
+ Create a ConfigComponent.
+
+ Arguments:
+ config -- The XML contents of the config file.
+ config_file -- The XML config file object itself.
+ """
+ ComponentXMPP.__init__(self, config['jid'],
+ config['secret'],
+ config['server'],
+ config['port'])
+
+ # Store the roster information.
+ self.roster = config['roster']['items']
+
+ # The session_start event will be triggered when
+ # the component establishes its connection with the
+ # server and the XML streams are ready for use. We
+ # want to listen for this event so that we we can
+ # broadcast any needed initial presence stanzas.
+ self.add_event_handler("session_start", self.start)
+
+ # The message event is triggered whenever a message
+ # stanza is received. Be aware that that includes
+ # MUC messages and error messages.
+ self.add_event_handler("message", self.message)
+
+ def start(self, event):
+ """
+ Process the session_start event.
+
+ The typical action for the session_start event in a component
+ is to broadcast presence stanzas to all subscribers to the
+ component. Note that the component does not have a roster
+ provided by the XMPP server. In this case, we have possibly
+ saved a roster in the component's configuration file.
+
+ Since the component may use any number of JIDs, you should
+ also include the JID that is sending the presence.
+
+ Arguments:
+ event -- An empty dictionary. The session_start
+ event does not provide any additional
+ data.
+ """
+ for jid in self.roster:
+ if self.roster[jid]['subscription'] != 'none':
+ self.sendPresence(pfrom=self.jid, pto=jid)
+
+ def message(self, msg):
+ """
+ Process incoming message stanzas. Be aware that this also
+ includes MUC messages and error messages. It is usually
+ a good idea to check the messages's type before processing
+ or sending replies.
+
+ Since a component may send messages from any number of JIDs,
+ it is best to always include a from JID.
+
+ Arguments:
+ msg -- The received message stanza. See the documentation
+ for stanza objects and the Message stanza to see
+ how it may be used.
+ """
+ # The reply method will use the messages 'to' JID as the
+ # outgoing reply's 'from' JID.
+ msg.reply("Thanks for sending\n%(body)s" % msg).send()
+
+
+if __name__ == '__main__':
+ # Setup the command line arguments.
+ optp = OptionParser()
+
+ # Output verbosity options.
+ optp.add_option('-q', '--quiet', help='set logging to ERROR',
+ action='store_const', dest='loglevel',
+ const=logging.ERROR, default=logging.INFO)
+ optp.add_option('-d', '--debug', help='set logging to DEBUG',
+ action='store_const', dest='loglevel',
+ const=logging.DEBUG, default=logging.INFO)
+ optp.add_option('-v', '--verbose', help='set logging to COMM',
+ action='store_const', dest='loglevel',
+ const=5, default=logging.INFO)
+
+ # Component name and secret options.
+ optp.add_option("-c", "--config", help="path to config file",
+ dest="config", default="config.xml")
+
+ opts, args = optp.parse_args()
+
+ # Setup logging.
+ logging.basicConfig(level=opts.loglevel,
+ format='%(levelname)-8s %(message)s')
+
+ # Load configuration data.
+ config_file = open(opts.config, 'r+')
+ config_data = "\n".join([line for line in config_file])
+ config = Config(xml=ET.fromstring(config_data))
+ config_file.close()
+
+ # Setup the ConfigComponent and register plugins. Note that while plugins
+ # may have interdependencies, the order in which you register them does
+ # not matter.
+ xmpp = ConfigComponent(config)
+ xmpp.registerPlugin('xep_0030') # Service Discovery
+ xmpp.registerPlugin('xep_0004') # Data Forms
+ xmpp.registerPlugin('xep_0060') # PubSub
+ xmpp.registerPlugin('xep_0199') # XMPP Ping
+
+ # Connect to the XMPP server and start processing XMPP stanzas.
+ if xmpp.connect():
+ xmpp.process(threaded=False)
+ print("Done")
+ else:
+ print("Unable to connect.")
diff --git a/examples/echo_client.py b/examples/echo_client.py
new file mode 100755
index 00000000..99967d5f
--- /dev/null
+++ b/examples/echo_client.py
@@ -0,0 +1,129 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import sys
+import logging
+import time
+from optparse import OptionParser
+
+import sleekxmpp
+
+# Python versions before 3.0 do not use UTF-8 encoding
+# by default. To ensure that Unicode is handled properly
+# throughout SleekXMPP, we will set the default encoding
+# ourselves to UTF-8.
+if sys.version_info < (3, 0):
+ reload(sys)
+ sys.setdefaultencoding('utf8')
+
+
+class EchoBot(sleekxmpp.ClientXMPP):
+
+ """
+ A simple SleekXMPP bot that will echo messages it
+ receives, along with a short thank you message.
+ """
+
+ def __init__(self, jid, password):
+ sleekxmpp.ClientXMPP.__init__(self, jid, password)
+
+ # The session_start event will be triggered when
+ # the bot establishes its connection with the server
+ # and the XML streams are ready for use. We want to
+ # listen for this event so that we we can intialize
+ # our roster.
+ self.add_event_handler("session_start", self.start)
+
+ # The message event is triggered whenever a message
+ # stanza is received. Be aware that that includes
+ # MUC messages and error messages.
+ self.add_event_handler("message", self.message)
+
+ def start(self, event):
+ """
+ Process the session_start event.
+
+ Typical actions for the session_start event are
+ requesting the roster and broadcasting an intial
+ presence stanza.
+
+ Arguments:
+ event -- An empty dictionary. The session_start
+ event does not provide any additional
+ data.
+ """
+ self.getRoster()
+ self.sendPresence()
+
+ def message(self, msg):
+ """
+ Process incoming message stanzas. Be aware that this also
+ includes MUC messages and error messages. It is usually
+ a good idea to check the messages's type before processing
+ or sending replies.
+
+ Arguments:
+ msg -- The received message stanza. See the documentation
+ for stanza objects and the Message stanza to see
+ how it may be used.
+ """
+ msg.reply("Thanks for sending\n%(body)s" % msg).send()
+
+
+if __name__ == '__main__':
+ # Setup the command line arguments.
+ optp = OptionParser()
+
+ # Output verbosity options.
+ optp.add_option('-q', '--quiet', help='set logging to ERROR',
+ action='store_const', dest='loglevel',
+ const=logging.ERROR, default=logging.INFO)
+ optp.add_option('-d', '--debug', help='set logging to DEBUG',
+ action='store_const', dest='loglevel',
+ const=logging.DEBUG, default=logging.INFO)
+ optp.add_option('-v', '--verbose', help='set logging to COMM',
+ action='store_const', dest='loglevel',
+ const=5, default=logging.INFO)
+
+ # JID and password options.
+ optp.add_option("-j", "--jid", dest="jid",
+ help="JID to use")
+ optp.add_option("-p", "--password", dest="password",
+ help="password to use")
+
+ opts, args = optp.parse_args()
+
+ # Setup logging.
+ logging.basicConfig(level=opts.loglevel,
+ format='%(levelname)-8s %(message)s')
+
+ # Setup the EchoBot and register plugins. Note that while plugins may
+ # have interdependencies, the order in which you register them does
+ # not matter.
+ xmpp = EchoBot(opts.jid, opts.password)
+ xmpp.registerPlugin('xep_0030') # Service Discovery
+ xmpp.registerPlugin('xep_0004') # Data Forms
+ xmpp.registerPlugin('xep_0060') # PubSub
+ xmpp.registerPlugin('xep_0199') # XMPP Ping
+
+ # Connect to the XMPP server and start processing XMPP stanzas.
+ if xmpp.connect():
+ # If you do not have the pydns library installed, you will need
+ # to manually specify the name of the server if it does not match
+ # the one in the JID. For example, to use Google Talk you would
+ # need to use:
+ #
+ # if xmpp.connect(('talk.google.com', 5222)):
+ # ...
+ xmpp.process(threaded=False)
+ print("Done")
+ else:
+ print("Unable to connect.")
diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py
index d2f5765f..afb7d9d4 100644
--- a/sleekxmpp/__init__.py
+++ b/sleekxmpp/__init__.py
@@ -37,7 +37,7 @@ except ImportError:
#class PresenceStanzaType(object):
-#
+#
# def fromXML(self, xml):
# self.ptype = xml.get('type')
@@ -69,24 +69,24 @@ class ClientXMPP(basexmpp, XMLStream):
self.bound = False
self.bindfail = False
self.is_component = False
- self.registerHandler(Callback('Stream Features', MatchXPath('{http://etherx.jabber.org/streams}features'), self._handleStreamFeatures, thread=True))
- self.registerHandler(Callback('Roster Update', MatchXPath('{%s}iq/{jabber:iq:roster}query' % self.default_ns), self._handleRoster, thread=True))
+ self.registerHandler(Callback('Stream Features', MatchXPath('{http://etherx.jabber.org/streams}features'), self._handleStreamFeatures))
+ self.registerHandler(Callback('Roster Update', MatchXPath('{%s}iq/{jabber:iq:roster}query' % self.default_ns), self._handleRoster))
#self.registerHandler(Callback('Roster Update', MatchXMLMask("<presence xmlns='%s' type='subscribe' />" % self.default_ns), self._handlePresenceSubscribe, thread=True))
self.registerFeature("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", self.handler_starttls, True)
self.registerFeature("<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_sasl_auth, True)
self.registerFeature("<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' />", self.handler_bind_resource)
self.registerFeature("<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />", self.handler_start_session)
-
+
#self.registerStanzaExtension('PresenceStanza', PresenceStanzaType)
#self.register_plugins()
-
+
def __getitem__(self, key):
if key in self.plugin:
return self.plugin[key]
else:
logging.warning("""Plugin "%s" is not loaded.""" % key)
return False
-
+
def get(self, key, default):
return self.plugin.get(key, default)
@@ -104,7 +104,7 @@ class ClientXMPP(basexmpp, XMLStream):
logging.debug("No appropriate SRV record found. Using JID server name.")
else:
# pick a random answer, weighted by priority
- # there are less verbose ways of doing this (random.choice() with answer * priority), but I chose this way anyway
+ # there are less verbose ways of doing this (random.choice() with answer * priority), but I chose this way anyway
# suggestions are welcome
addresses = {}
intmax = 0
@@ -128,18 +128,18 @@ class ClientXMPP(basexmpp, XMLStream):
logging.warning("Failed to connect")
self.event("disconnected")
return result
-
+
# overriding reconnect and disconnect so that we can get some events
# should events be part of or required by xmlstream? Maybe that would be cleaner
def reconnect(self):
logging.info("Reconnecting")
self.event("disconnected")
XMLStream.reconnect(self)
-
+
def disconnect(self, init=True, close=False, reconnect=False):
self.event("disconnected")
XMLStream.disconnect(self, reconnect)
-
+
def registerFeature(self, mask, pointer, breaker = False):
"""Register a stream feature."""
self.registered_features.append((MatchXMLMask(mask), pointer, breaker))
@@ -157,12 +157,12 @@ class ClientXMPP(basexmpp, XMLStream):
iq['type'] = 'set'
iq['roster']['items'] = {jid: {'subscription': 'remove'}}
return iq.send()['type'] == 'result'
-
+
def getRoster(self):
"""Request the roster be sent."""
iq = self.Iq().setStanzaValues({'type': 'get'}).enable('roster').send()
self._handleRoster(iq, request=True)
-
+
def _handleStreamFeatures(self, features):
self.features = []
for sub in features.xml:
@@ -173,7 +173,7 @@ class ClientXMPP(basexmpp, XMLStream):
#if self.maskcmp(subelement, feature[0], True):
if feature[1](subelement) and feature[2]: #if breaker, don't continue
return True
-
+
def handler_starttls(self, xml):
if not self.authenticated and self.ssl_support:
self.add_handler("<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", self.handler_tls_start, name='TLS Proceed', instream=True)
@@ -187,7 +187,7 @@ class ClientXMPP(basexmpp, XMLStream):
logging.debug("Starting TLS")
if self.startTLS():
raise RestartStream()
-
+
def handler_sasl_auth(self, xml):
if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features:
return False
@@ -209,7 +209,7 @@ class ClientXMPP(basexmpp, XMLStream):
#if 'sasl:DIGEST-MD5' in self.features:
# self._auth_digestmd5()
return True
-
+
def handler_auth_success(self, xml):
self.authenticated = True
self.features = []
@@ -219,7 +219,7 @@ class ClientXMPP(basexmpp, XMLStream):
logging.info("Authentication failed.")
self.disconnect()
self.event("failed_auth")
-
+
def handler_bind_resource(self, xml):
logging.debug("Requesting resource: %s" % self.resource)
xml.clear()
@@ -238,7 +238,7 @@ class ClientXMPP(basexmpp, XMLStream):
logging.debug("Established Session")
self.sessionstarted = True
self.event("session_start")
-
+
def handler_start_session(self, xml):
if self.authenticated and self.bound:
iq = self.makeIqSet(xml)
@@ -249,7 +249,7 @@ class ClientXMPP(basexmpp, XMLStream):
else:
#bind probably hasn't happened yet
self.bindfail = True
-
+
def _handleRoster(self, iq, request=False):
if iq['type'] == 'set' or (iq['type'] == 'result' and request):
for jid in iq['roster']['items']:
diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py
index b7b605b0..f83fc062 100644
--- a/sleekxmpp/basexmpp.py
+++ b/sleekxmpp/basexmpp.py
@@ -123,7 +123,7 @@ class basexmpp(object):
# threaded is no longer needed, but leaving it for backwards compatibility for now
if name is None:
name = 'add_handler_%s' % self.getNewId()
- self.registerHandler(XMLCallback(name, MatchXMLMask(mask), pointer, threaded, disposable, instream))
+ self.registerHandler(XMLCallback(name, MatchXMLMask(mask), pointer, once=disposable, instream=instream))
def getId(self):
return "%x".upper() % self.id
diff --git a/sleekxmpp/component_example.py b/sleekxmpp/component_example.py
deleted file mode 100644
index f24216c2..00000000
--- a/sleekxmpp/component_example.py
+++ /dev/null
@@ -1,41 +0,0 @@
-import sleekxmpp.componentxmpp
-import logging
-from optparse import OptionParser
-import time
-
-class Example(sleekxmpp.componentxmpp.ComponentXMPP):
-
- def __init__(self, jid, password):
- sleekxmpp.componentxmpp.ComponentXMPP.__init__(self, jid, password, 'vm1', 5230)
- self.add_event_handler("session_start", self.start)
- self.add_event_handler("message", self.message)
-
- def start(self, event):
- #self.getRoster()
- #self.sendPresence(pto='admin@tigase.netflint.net/sarkozy')
- #self.sendPresence(pto='tigase.netflint.net')
- pass
-
- def message(self, event):
- self.sendMessage("%s/%s" % (event['jid'], event['resource']), "Thanks for sending me, \"%s\"." % event['message'], mtype=event['type'])
-
-if __name__ == '__main__':
- #parse command line arguements
- optp = OptionParser()
- optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO)
- optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
- optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)
- optp.add_option("-c","--config", dest="configfile", default="config.xml", help="set config file to use")
- opts,args = optp.parse_args()
-
- logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
- xmpp = Example('component.vm1', 'secreteating')
- xmpp.registerPlugin('xep_0004')
- xmpp.registerPlugin('xep_0030')
- xmpp.registerPlugin('xep_0060')
- xmpp.registerPlugin('xep_0199')
- if xmpp.connect():
- xmpp.process(threaded=False)
- print("done")
- else:
- print("Unable to connect.")
diff --git a/sleekxmpp/xmlstream/filesocket.py b/sleekxmpp/xmlstream/filesocket.py
index 07b395dc..441ff875 100644
--- a/sleekxmpp/xmlstream/filesocket.py
+++ b/sleekxmpp/xmlstream/filesocket.py
@@ -5,21 +5,37 @@
See the file LICENSE for copying permission.
"""
+
from socket import _fileobject
import socket
-class filesocket(_fileobject):
- def read(self, size=4096):
- data = self._sock.recv(size)
- if data is not None:
- return data
+class FileSocket(_fileobject):
+
+ """
+ Create a file object wrapper for a socket to work around
+ issues present in Python 2.6 when using sockets as file objects.
+
+ The parser for xml.etree.cElementTree requires a file, but we will
+ be reading from the XMPP connection socket instead.
+ """
+
+ def read(self, size=4096):
+ """Read data from the socket as if it were a file."""
+ data = self._sock.recv(size)
+ if data is not None:
+ return data
+
class Socket26(socket._socketobject):
- def makefile(self, mode='r', bufsize=-1):
- """makefile([mode[, bufsize]]) -> file object
- Return a regular file object corresponding to the socket. The mode
- and bufsize arguments are as for the built-in open() function."""
- return filesocket(self._sock, mode, bufsize)
+ """
+ A custom socket implementation that uses our own FileSocket class
+ to work around issues in Python 2.6 when using sockets as files.
+ """
+ def makefile(self, mode='r', bufsize=-1):
+ """makefile([mode[, bufsize]]) -> file object
+ Return a regular file object corresponding to the socket. The mode
+ and bufsize arguments are as for the built-in open() function."""
+ return FileSocket(self._sock, mode, bufsize)
diff --git a/sleekxmpp/xmlstream/handler/base.py b/sleekxmpp/xmlstream/handler/base.py
index 720846d6..3ae82a89 100644
--- a/sleekxmpp/xmlstream/handler/base.py
+++ b/sleekxmpp/xmlstream/handler/base.py
@@ -6,23 +6,82 @@
See the file LICENSE for copying permission.
"""
+
class BaseHandler(object):
+ """
+ Base class for stream handlers. Stream handlers are matched with
+ incoming stanzas so that the stanza may be processed in some way.
+ Stanzas may be matched with multiple handlers.
+
+ Handler execution may take place in two phases. The first is during
+ the stream processing itself. The second is after stream processing
+ and during SleekXMPP's main event loop. The prerun method is used
+ for execution during stream processing, and the run method is used
+ during the main event loop.
+
+ Attributes:
+ name -- The name of the handler.
+ stream -- The stream this handler is assigned to.
+
+ Methods:
+ match -- Compare a stanza with the handler's matcher.
+ prerun -- Handler execution during stream processing.
+ run -- Handler execution during the main event loop.
+ checkDelete -- Indicate if the handler may be removed from use.
+ """
+
+ def __init__(self, name, matcher, stream=None):
+ """
+ Create a new stream handler.
+
+ Arguments:
+ name -- The name of the handler.
+ matcher -- A matcher object from xmlstream.matcher that will be
+ used to determine if a stanza should be accepted by
+ this handler.
+ stream -- The XMLStream instance the handler should monitor.
+ """
+ self.name = name
+ self.stream = stream
+ self._destroy = False
+ self._payload = None
+ self._matcher = matcher
+ if stream is not None:
+ stream.registerHandler(self)
+
+ def match(self, xml):
+ """
+ Compare a stanza or XML object with the handler's matcher.
+
+ Arguments
+ xml -- An XML or stanza object.
+ """
+ return self._matcher.match(xml)
+
+ def prerun(self, payload):
+ """
+ Prepare the handler for execution while the XML stream is being
+ processed.
+
+ Arguments:
+ payload -- A stanza object.
+ """
+ self._payload = payload
+
+ def run(self, payload):
+ """
+ Execute the handler after XML stream processing and during the
+ main event loop.
+
+ Arguments:
+ payload -- A stanza object.
+ """
+ self._payload = payload
- def __init__(self, name, matcher):
- self.name = name
- self._destroy = False
- self._payload = None
- self._matcher = matcher
-
- def match(self, xml):
- return self._matcher.match(xml)
-
- def prerun(self, payload):
- self._payload = payload
-
- def run(self, payload):
- self._payload = payload
-
- def checkDelete(self):
- return self._destroy
+ def checkDelete(self):
+ """
+ Check if the handler should be removed from the list of stream
+ handlers.
+ """
+ return self._destroy
diff --git a/sleekxmpp/xmlstream/handler/callback.py b/sleekxmpp/xmlstream/handler/callback.py
index 889b0aa7..f0a72853 100644
--- a/sleekxmpp/xmlstream/handler/callback.py
+++ b/sleekxmpp/xmlstream/handler/callback.py
@@ -5,30 +5,80 @@
See the file LICENSE for copying permission.
"""
-from . import base
-import logging
-
-class Callback(base.BaseHandler):
-
- def __init__(self, name, matcher, pointer, thread=False, once=False, instream=False):
- base.BaseHandler.__init__(self, name, matcher)
- self._pointer = pointer
- self._thread = thread
- self._once = once
- self._instream = instream
-
- def prerun(self, payload):
- base.BaseHandler.prerun(self, payload)
- if self._instream:
- self.run(payload, True)
-
- def run(self, payload, instream=False):
- if not self._instream or instream:
- base.BaseHandler.run(self, payload)
- #if self._thread:
- # x = threading.Thread(name="Callback_%s" % self.name, target=self._pointer, args=(payload,))
- # x.start()
- #else:
- self._pointer(payload)
- if self._once:
- self._destroy = True
+
+from sleekxmpp.xmlstream.handler.base import BaseHandler
+
+
+class Callback(BaseHandler):
+
+ """
+ The Callback handler will execute a callback function with
+ matched stanzas.
+
+ The handler may execute the callback either during stream
+ processing or during the main event loop.
+
+ Callback functions are all executed in the same thread, so be
+ aware if you are executing functions that will block for extended
+ periods of time. Typically, you should signal your own events using the
+ SleekXMPP object's event() method to pass the stanza off to a threaded
+ event handler for further processing.
+
+ Methods:
+ prerun -- Overrides BaseHandler.prerun
+ run -- Overrides BaseHandler.run
+ """
+
+ def __init__(self, name, matcher, pointer, thread=False,
+ once=False, instream=False, stream=None):
+ """
+ Create a new callback handler.
+
+ Arguments:
+ name -- The name of the handler.
+ matcher -- A matcher object for matching stanza objects.
+ pointer -- The function to execute during callback.
+ thread -- DEPRECATED. Remains only for backwards compatibility.
+ once -- Indicates if the handler should be used only
+ once. Defaults to False.
+ instream -- Indicates if the callback should be executed
+ during stream processing instead of in the
+ main event loop.
+ stream -- The XMLStream instance this handler should monitor.
+ """
+ BaseHandler.__init__(self, name, matcher, stream)
+ self._pointer = pointer
+ self._once = once
+ self._instream = instream
+
+ def prerun(self, payload):
+ """
+ Execute the callback during stream processing, if
+ the callback was created with instream=True.
+
+ Overrides BaseHandler.prerun
+
+ Arguments:
+ payload -- The matched stanza object.
+ """
+ BaseHandler.prerun(self, payload)
+ if self._instream:
+ self.run(payload, True)
+
+ def run(self, payload, instream=False):
+ """
+ Execute the callback function with the matched stanza payload.
+
+ Overrides BaseHandler.run
+
+ Arguments:
+ payload -- The matched stanza object.
+ instream -- Force the handler to execute during
+ stream processing. Used only by prerun.
+ Defaults to False.
+ """
+ if not self._instream or instream:
+ BaseHandler.run(self, payload)
+ self._pointer(payload)
+ if self._once:
+ self._destroy = True
diff --git a/sleekxmpp/xmlstream/handler/waiter.py b/sleekxmpp/xmlstream/handler/waiter.py
index 7c4330a4..1e101ed3 100644
--- a/sleekxmpp/xmlstream/handler/waiter.py
+++ b/sleekxmpp/xmlstream/handler/waiter.py
@@ -5,32 +5,94 @@
See the file LICENSE for copying permission.
"""
-from . import base
+
+import logging
try:
- import queue
+ import queue
except ImportError:
- import Queue as queue
-import logging
-from .. stanzabase import StanzaBase
-
-class Waiter(base.BaseHandler):
-
- def __init__(self, name, matcher):
- base.BaseHandler.__init__(self, name, matcher)
- self._payload = queue.Queue()
-
- def prerun(self, payload):
- self._payload.put(payload)
-
- def run(self, payload):
- pass
-
- def wait(self, timeout=60):
- try:
- return self._payload.get(True, timeout)
- except queue.Empty:
- logging.warning("Timed out waiting for %s" % self.name)
- return False
-
- def checkDelete(self):
- return True
+ import Queue as queue
+
+from sleekxmpp.xmlstream import StanzaBase, RESPONSE_TIMEOUT
+from sleekxmpp.xmlstream.handler.base import BaseHandler
+
+
+class Waiter(BaseHandler):
+
+ """
+ The Waiter handler allows an event handler to block
+ until a particular stanza has been received. The handler
+ will either be given the matched stanza, or False if the
+ waiter has timed out.
+
+ Methods:
+ checkDelete -- Overrides BaseHandler.checkDelete
+ prerun -- Overrides BaseHandler.prerun
+ run -- Overrides BaseHandler.run
+ wait -- Wait for a stanza to arrive and return it to
+ an event handler.
+ """
+
+ def __init__(self, name, matcher, stream=None):
+ """
+ Create a new Waiter.
+
+ Arguments:
+ name -- The name of the waiter.
+ matcher -- A matcher object to detect the desired stanza.
+ stream -- Optional XMLStream instance to monitor.
+ """
+ BaseHandler.__init__(self, name, matcher, stream=stream)
+ self._payload = queue.Queue()
+
+ def prerun(self, payload):
+ """
+ Store the matched stanza.
+
+ Overrides BaseHandler.prerun
+
+ Arguments:
+ payload -- The matched stanza object.
+ """
+ self._payload.put(payload)
+
+ def run(self, payload):
+ """
+ Do not process this handler during the main event loop.
+
+ Overrides BaseHandler.run
+
+ Arguments:
+ payload -- The matched stanza object.
+ """
+ pass
+
+ def wait(self, timeout=RESPONSE_TIMEOUT):
+ """
+ Block an event handler while waiting for a stanza to arrive.
+
+ Be aware that this will impact performance if called from a
+ non-threaded event handler.
+
+ Will return either the received stanza, or False if the waiter
+ timed out.
+
+ Arguments:
+ timeout -- The number of seconds to wait for the stanza to
+ arrive. Defaults to the global default timeout
+ value sleekxmpp.xmlstream.RESPONSE_TIMEOUT.
+ """
+ try:
+ stanza = self._payload.get(True, timeout)
+ except queue.Empty:
+ stanza = False
+ logging.warning("Timed out waiting for %s" % self.name)
+ self.stream.removeHandler(self.name)
+ return stanza
+
+ def checkDelete(self):
+ """
+ Always remove waiters after use.
+
+ Overrides BaseHandler.checkDelete
+ """
+ return True
diff --git a/sleekxmpp/xmlstream/handler/xmlcallback.py b/sleekxmpp/xmlstream/handler/xmlcallback.py
index 67879dfe..11607ffb 100644
--- a/sleekxmpp/xmlstream/handler/xmlcallback.py
+++ b/sleekxmpp/xmlstream/handler/xmlcallback.py
@@ -5,10 +5,32 @@
See the file LICENSE for copying permission.
"""
-import threading
-from . callback import Callback
+
+from sleekxmpp.xmlstream.handler import Callback
+
class XMLCallback(Callback):
-
- def run(self, payload, instream=False):
- Callback.run(self, payload.xml, instream)
+
+ """
+ The XMLCallback class is identical to the normal Callback class,
+ except that XML contents of matched stanzas will be processed instead
+ of the stanza objects themselves.
+
+ Methods:
+ run -- Overrides Callback.run
+ """
+
+ def run(self, payload, instream=False):
+ """
+ Execute the callback function with the matched stanza's
+ XML contents, instead of the stanza itself.
+
+ Overrides BaseHandler.run
+
+ Arguments:
+ payload -- The matched stanza object.
+ instream -- Force the handler to execute during
+ stream processing. Used only by prerun.
+ Defaults to False.
+ """
+ Callback.run(self, payload.xml, instream)
diff --git a/sleekxmpp/xmlstream/handler/xmlwaiter.py b/sleekxmpp/xmlstream/handler/xmlwaiter.py
index cf90751d..5201caf3 100644
--- a/sleekxmpp/xmlstream/handler/xmlwaiter.py
+++ b/sleekxmpp/xmlstream/handler/xmlwaiter.py
@@ -5,9 +5,29 @@
See the file LICENSE for copying permission.
"""
-from . waiter import Waiter
+
+from sleekxmpp.xmlstream.handler import Waiter
+
class XMLWaiter(Waiter):
-
- def prerun(self, payload):
- Waiter.prerun(self, payload.xml)
+
+ """
+ The XMLWaiter class is identical to the normal Waiter class
+ except that it returns the XML contents of the stanza instead
+ of the full stanza object itself.
+
+ Methods:
+ prerun -- Overrides Waiter.prerun
+ """
+
+ def prerun(self, payload):
+ """
+ Store the XML contents of the stanza to return to the
+ waiting event handler.
+
+ Overrides Waiter.prerun
+
+ Arguments:
+ payload -- The matched stanza object.
+ """
+ Waiter.prerun(self, payload.xml)
diff --git a/sleekxmpp/xmlstream/matcher/base.py b/sleekxmpp/xmlstream/matcher/base.py
index 51da0942..701ab32f 100644
--- a/sleekxmpp/xmlstream/matcher/base.py
+++ b/sleekxmpp/xmlstream/matcher/base.py
@@ -5,10 +5,30 @@
See the file LICENSE for copying permission.
"""
+
+
class MatcherBase(object):
- def __init__(self, criteria):
- self._criteria = criteria
-
- def match(self, xml):
- return False
+ """
+ Base class for stanza matchers. Stanza matchers are used to pick
+ stanzas out of the XML stream and pass them to the appropriate
+ stream handlers.
+ """
+
+ def __init__(self, criteria):
+ """
+ Create a new stanza matcher.
+
+ Arguments:
+ criteria -- Object to compare some aspect of a stanza
+ against.
+ """
+ self._criteria = criteria
+
+ def match(self, xml):
+ """
+ Check if a stanza matches the stored criteria.
+
+ Meant to be overridden.
+ """
+ return False
diff --git a/sleekxmpp/xmlstream/matcher/id.py b/sleekxmpp/xmlstream/matcher/id.py
index 43972c23..0c8ce2d8 100644
--- a/sleekxmpp/xmlstream/matcher/id.py
+++ b/sleekxmpp/xmlstream/matcher/id.py
@@ -5,9 +5,28 @@
See the file LICENSE for copying permission.
"""
-from . import base
-class MatcherId(base.MatcherBase):
-
- def match(self, xml):
- return xml['id'] == self._criteria
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+class MatcherId(MatcherBase):
+
+ """
+ The ID matcher selects stanzas that have the same stanza 'id'
+ interface value as the desired ID.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ """
+
+ def match(self, xml):
+ """
+ Compare the given stanza's 'id' attribute to the stored
+ id value.
+
+ Overrides MatcherBase.match.
+
+ Arguments:
+ xml -- The stanza to compare against.
+ """
+ return xml['id'] == self._criteria
diff --git a/sleekxmpp/xmlstream/matcher/many.py b/sleekxmpp/xmlstream/matcher/many.py
index ff0c4e4d..f470ec9c 100644
--- a/sleekxmpp/xmlstream/matcher/many.py
+++ b/sleekxmpp/xmlstream/matcher/many.py
@@ -5,13 +5,36 @@
See the file LICENSE for copying permission.
"""
-from . import base
-from xml.etree import cElementTree
-class MatchMany(base.MatcherBase):
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
- def match(self, xml):
- for m in self._criteria:
- if m.match(xml):
- return True
- return False
+
+class MatchMany(MatcherBase):
+
+ """
+ The MatchMany matcher may compare a stanza against multiple
+ criteria. It is essentially an OR relation combining multiple
+ matchers.
+
+ Each of the criteria must implement a match() method.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ """
+
+ def match(self, xml):
+ """
+ Match a stanza against multiple criteria. The match is successful
+ if one of the criteria matches.
+
+ Each of the criteria must implement a match() method.
+
+ Overrides MatcherBase.match.
+
+ Arguments:
+ xml -- The stanza object to compare against.
+ """
+ for m in self._criteria:
+ if m.match(xml):
+ return True
+ return False
diff --git a/sleekxmpp/xmlstream/matcher/stanzapath.py b/sleekxmpp/xmlstream/matcher/stanzapath.py
index e315445d..f8ff283d 100644
--- a/sleekxmpp/xmlstream/matcher/stanzapath.py
+++ b/sleekxmpp/xmlstream/matcher/stanzapath.py
@@ -5,10 +5,34 @@
See the file LICENSE for copying permission.
"""
-from . import base
-from xml.etree import cElementTree
-class StanzaPath(base.MatcherBase):
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
- def match(self, stanza):
- return stanza.match(self._criteria)
+
+class StanzaPath(MatcherBase):
+
+ """
+ The StanzaPath matcher selects stanzas that match a given "stanza path",
+ which is similar to a normal XPath except that it uses the interfaces and
+ plugins of the stanza instead of the actual, underlying XML.
+
+ In most cases, the stanza path and XPath should be identical, but be
+ aware that differences may occur.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ """
+
+ def match(self, stanza):
+ """
+ Compare a stanza against a "stanza path". A stanza path is similar to
+ an XPath expression, but uses the stanza's interfaces and plugins
+ instead of the underlying XML. For most cases, the stanza path and
+ XPath should be identical, but be aware that differences may occur.
+
+ Overrides MatcherBase.match.
+
+ Arguments:
+ stanza -- The stanza object to compare against.
+ """
+ return stanza.match(self._criteria)
diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py
index 89fd6422..2967a2af 100644
--- a/sleekxmpp/xmlstream/matcher/xmlmask.py
+++ b/sleekxmpp/xmlstream/matcher/xmlmask.py
@@ -5,63 +5,151 @@
See the file LICENSE for copying permission.
"""
-from . import base
-from xml.etree import cElementTree
+
from xml.parsers.expat import ExpatError
-ignore_ns = False
-
-class MatchXMLMask(base.MatcherBase):
-
- def __init__(self, criteria):
- base.MatcherBase.__init__(self, criteria)
- if type(criteria) == type(''):
- self._criteria = cElementTree.fromstring(self._criteria)
- self.default_ns = 'jabber:client'
-
- def setDefaultNS(self, ns):
- self.default_ns = ns
-
- def match(self, xml):
- if hasattr(xml, 'xml'):
- xml = xml.xml
- return self.maskcmp(xml, self._criteria, True)
-
- def maskcmp(self, source, maskobj, use_ns=False, default_ns='__no_ns__'):
- """maskcmp(xmlobj, maskobj):
- Compare etree xml object to etree xml object mask"""
- use_ns = not ignore_ns
- #TODO require namespaces
- if source == None: #if element not found (happens during recursive check below)
- return False
- if not hasattr(maskobj, 'attrib'): #if the mask is a string, make it an xml obj
- try:
- maskobj = cElementTree.fromstring(maskobj)
- except ExpatError:
- logging.log(logging.WARNING, "Expat error: %s\nIn parsing: %s" % ('', maskobj))
- if not use_ns and source.tag.split('}', 1)[-1] != maskobj.tag.split('}', 1)[-1]: # strip off ns and compare
- return False
- if use_ns and (source.tag != maskobj.tag and "{%s}%s" % (self.default_ns, maskobj.tag) != source.tag ):
- return False
- if maskobj.text and source.text != maskobj.text:
- return False
- for attr_name in maskobj.attrib: #compare attributes
- if source.attrib.get(attr_name, "__None__") != maskobj.attrib[attr_name]:
- return False
- #for subelement in maskobj.getiterator()[1:]: #recursively compare subelements
- for subelement in maskobj: #recursively compare subelements
- if use_ns:
- if not self.maskcmp(source.find(subelement.tag), subelement, use_ns):
- return False
- else:
- if not self.maskcmp(self.getChildIgnoreNS(source, subelement.tag), subelement, use_ns):
- return False
- return True
-
- def getChildIgnoreNS(self, xml, tag):
- tag = tag.split('}')[-1]
- try:
- idx = [c.tag.split('}')[-1] for c in xml.getchildren()].index(tag)
- except ValueError:
- return None
- return xml.getchildren()[idx]
+from sleekxmpp.xmlstream.stanzabase import ET
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+# Flag indicating if the builtin XPath matcher should be used, which
+# uses namespaces, or a custom matcher that ignores namespaces.
+# Changing this will affect ALL XMLMask matchers.
+IGNORE_NS = False
+
+
+class MatchXMLMask(MatcherBase):
+
+ """
+ The XMLMask matcher selects stanzas whose XML matches a given
+ XML pattern, or mask. For example, message stanzas with body elements
+ could be matched using the mask:
+
+ <message xmlns="jabber:client"><body /></message>
+
+ Use of XMLMask is discouraged, and XPath or StanzaPath should be used
+ instead.
+
+ The use of namespaces in the mask comparison is controlled by
+ IGNORE_NS. Setting IGNORE_NS to True will disable namespace based matching
+ for ALL XMLMask matchers.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ setDefaultNS -- Set the default namespace for the mask.
+ """
+
+ def __init__(self, criteria):
+ """
+ Create a new XMLMask matcher.
+
+ Arguments:
+ criteria -- Either an XML object or XML string to use as a mask.
+ """
+ MatcherBase.__init__(self, criteria)
+ if isinstance(criteria, str):
+ self._criteria = ET.fromstring(self._criteria)
+ self.default_ns = 'jabber:client'
+
+ def setDefaultNS(self, ns):
+ """
+ Set the default namespace to use during comparisons.
+
+ Arguments:
+ ns -- The new namespace to use as the default.
+ """
+ self.default_ns = ns
+
+ def match(self, xml):
+ """
+ Compare a stanza object or XML object against the stored XML mask.
+
+ Overrides MatcherBase.match.
+
+ Arguments:
+ xml -- The stanza object or XML object to compare against.
+ """
+ if hasattr(xml, 'xml'):
+ xml = xml.xml
+ return self._mask_cmp(xml, self._criteria, True)
+
+ def _mask_cmp(self, source, mask, use_ns=False, default_ns='__no_ns__'):
+ """
+ Compare an XML object against an XML mask.
+
+ Arguments:
+ source -- The XML object to compare against the mask.
+ mask -- The XML object serving as the mask.
+ use_ns -- Indicates if namespaces should be respected during
+ the comparison.
+ default_ns -- The default namespace to apply to elements that
+ do not have a specified namespace.
+ Defaults to "__no_ns__".
+ """
+ use_ns = not IGNORE_NS
+
+ if source is None:
+ # If the element was not found. May happend during recursive calls.
+ return False
+
+ # Convert the mask to an XML object if it is a string.
+ if not hasattr(mask, 'attrib'):
+ try:
+ mask = ET.fromstring(mask)
+ except ExpatError:
+ logging.log(logging.WARNING,
+ "Expat error: %s\nIn parsing: %s" % ('', mask))
+
+ if not use_ns:
+ # Compare the element without using namespaces.
+ source_tag = source.tag.split('}', 1)[-1]
+ mask_tag = mask.tag.split('}', 1)[-1]
+ if source_tag != mask_tag:
+ return False
+ else:
+ # Compare the element using namespaces
+ mask_ns_tag = "{%s}%s" % (self.default_ns, mask.tag)
+ if source.tag not in [mask.tag, mask_ns_tag]:
+ return False
+
+ # If the mask includes text, compare it.
+ if mask.text and source.text != mask.text:
+ return False
+
+ # Compare attributes. The stanza must include the attributes
+ # defined by the mask, but may include others.
+ for name, value in mask.attrib.items():
+ if source.attrib.get(name, "__None__") != value:
+ return False
+
+ # Recursively check subelements.
+ for subelement in mask:
+ if use_ns:
+ if not self._mask_cmp(source.find(subelement.tag),
+ subelement, use_ns):
+ return False
+ else:
+ if not self._mask_cmp(self._get_child(source, subelement.tag),
+ subelement, use_ns):
+ return False
+
+ # Everything matches.
+ return True
+
+ def _get_child(self, xml, tag):
+ """
+ Return a child element given its tag, ignoring namespace values.
+
+ Returns None if the child was not found.
+
+ Arguments:
+ xml -- The XML object to search for the given child tag.
+ tag -- The name of the subelement to find.
+ """
+ tag = tag.split('}')[-1]
+ try:
+ children = [c.tag.split('}')[-1] for c in xml.getchildren()]
+ index = children.index(tag)
+ except ValueError:
+ return None
+ return xml.getchildren()[index]
diff --git a/sleekxmpp/xmlstream/matcher/xpath.py b/sleekxmpp/xmlstream/matcher/xpath.py
index 7f3d20be..669c9f16 100644
--- a/sleekxmpp/xmlstream/matcher/xpath.py
+++ b/sleekxmpp/xmlstream/matcher/xpath.py
@@ -5,30 +5,75 @@
See the file LICENSE for copying permission.
"""
-from . import base
-from xml.etree import cElementTree
-
-ignore_ns = False
-
-class MatchXPath(base.MatcherBase):
-
- def match(self, xml):
- if hasattr(xml, 'xml'):
- xml = xml.xml
- x = cElementTree.Element('x')
- x.append(xml)
- if not ignore_ns:
- if x.find(self._criteria) is not None:
- return True
- return False
- else:
- criteria = [c.split('}')[-1] for c in self._criteria.split('/')]
- xml = x
- for tag in criteria:
- children = [c.tag.split('}')[-1] for c in xml.getchildren()]
- try:
- idx = children.index(tag)
- except ValueError:
- return False
- xml = xml.getchildren()[idx]
- return True
+
+from sleekxmpp.xmlstream.stanzabase import ET
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+# Flag indicating if the builtin XPath matcher should be used, which
+# uses namespaces, or a custom matcher that ignores namespaces.
+# Changing this will affect ALL XPath matchers.
+IGNORE_NS = False
+
+
+class MatchXPath(MatcherBase):
+
+ """
+ The XPath matcher selects stanzas whose XML contents matches a given
+ XPath expression.
+
+ Note that using this matcher may not produce expected behavior when using
+ attribute selectors. For Python 2.6 and 3.1, the ElementTree find method
+ does not support the use of attribute selectors. If you need to support
+ Python 2.6 or 3.1, it might be more useful to use a StanzaPath matcher.
+
+ If the value of IGNORE_NS is set to true, then XPath expressions will
+ be matched without using namespaces.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ """
+
+ def match(self, xml):
+ """
+ Compare a stanza's XML contents to an XPath expression.
+
+ If the value of IGNORE_NS is set to true, then XPath expressions
+ will be matched without using namespaces.
+
+ Note that in Python 2.6 and 3.1 the ElementTree find method does
+ not support attribute selectors in the XPath expression.
+
+ Arguments:
+ xml -- The stanza object to compare against.
+ """
+ if hasattr(xml, 'xml'):
+ xml = xml.xml
+ x = ET.Element('x')
+ x.append(xml)
+
+ if not IGNORE_NS:
+ # Use builtin, namespace respecting, XPath matcher.
+ if x.find(self._criteria) is not None:
+ return True
+ return False
+ else:
+ # Remove namespaces from the XPath expression.
+ criteria = []
+ for ns_block in self._criteria.split('{'):
+ criteria.extend(ns_block.split('}')[-1].split('/'))
+
+ # Walk the XPath expression.
+ xml = x
+ for tag in criteria:
+ if not tag:
+ # Skip empty tag name artifacts from the cleanup phase.
+ continue
+
+ children = [c.tag.split('}')[-1] for c in xml.getchildren()]
+ try:
+ index = children.index(tag)
+ except ValueError:
+ return False
+ xml = xml.getchildren()[index]
+ return True
diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py
index 8814df78..f8242005 100644
--- a/sleekxmpp/xmlstream/stanzabase.py
+++ b/sleekxmpp/xmlstream/stanzabase.py
@@ -586,15 +586,16 @@ class ElementBase(object):
string or a list of element names with attribute checks.
"""
if isinstance(xpath, str):
- xpath = xpath.split('/')
+ xpath = self._fix_ns(xpath, split=True, propagate_ns=False)
+
# Extract the tag name and attribute checks for the first XPath node.
components = xpath[0].split('@')
tag = components[0]
attributes = components[1:]
- if tag not in (self.name, "{%s}%s" % (self.namespace, self.name),
- self.plugins, self.plugin_attrib):
+ if tag not in (self.name, "{%s}%s" % (self.namespace, self.name)) and \
+ tag not in self.plugins and tag not in self.plugin_attrib:
# The requested tag is not in this stanza, so no match.
return False
@@ -613,6 +614,12 @@ class ElementBase(object):
if self[name] != value:
return False
+ # Check sub interfaces.
+ if len(xpath) > 1:
+ next_tag = xpath[1]
+ if next_tag in self.sub_interfaces and self[next_tag]:
+ return True
+
# Attempt to continue matching the XPath using the stanza's plugins.
if not matched_substanzas and len(xpath) > 1:
# Convert {namespace}tag@attribs to just tag
@@ -754,30 +761,45 @@ class ElementBase(object):
"""
return self
- def _fix_ns(self, xpath, split=False):
+ def _fix_ns(self, xpath, split=False, propagate_ns=True):
"""
Apply the stanza's namespace to elements in an XPath expression.
Arguments:
- xpath -- The XPath expression to fix with namespaces.
- split -- Indicates if the fixed XPath should be left as a
- list of element names with namespaces. Defaults to
- False, which returns a flat string path.
+ xpath -- The XPath expression to fix with namespaces.
+ split -- Indicates if the fixed XPath should be left as a
+ list of element names with namespaces. Defaults to
+ False, which returns a flat string path.
+ propagate_ns -- Overrides propagating parent element namespaces
+ to child elements. Useful if you wish to simply
+ split an XPath that has non-specified namespaces,
+ and child and parent namespaces are known not to
+ always match. Defaults to True.
"""
fixed = []
+ # Split the XPath into a series of blocks, where a block
+ # is started by an element with a namespace.
ns_blocks = xpath.split('{')
for ns_block in ns_blocks:
if '}' in ns_block:
+ # Apply the found namespace to following elements
+ # that do not have namespaces.
namespace = ns_block.split('}')[0]
elements = ns_block.split('}')[1].split('/')
else:
+ # Apply the stanza's namespace to the following
+ # elements since no namespace was provided.
namespace = self.namespace
elements = ns_block.split('/')
for element in elements:
if element:
- fixed.append('{%s}%s' % (namespace,
- element))
+ # Skip empty entry artifacts from splitting.
+ if propagate_ns:
+ tag = '{%s}%s' % (namespace, element)
+ else:
+ tag = element
+ fixed.append(tag)
if split:
return fixed
return '/'.join(fixed)
@@ -886,6 +908,48 @@ class ElementBase(object):
class StanzaBase(ElementBase):
+
+ """
+ StanzaBase provides the foundation for all other stanza objects used by
+ SleekXMPP, and defines a basic set of interfaces common to nearly
+ all stanzas. These interfaces are the 'id', 'type', 'to', and 'from'
+ attributes. An additional interface, 'payload', is available to access
+ the XML contents of the stanza. Most stanza objects will provided more
+ specific interfaces, however.
+
+ Stanza Interface:
+ from -- A JID object representing the sender's JID.
+ id -- An optional id value that can be used to associate stanzas
+ with their replies.
+ payload -- The XML contents of the stanza.
+ to -- A JID object representing the recipient's JID.
+ type -- The type of stanza, typically will be 'normal', 'error',
+ 'get', or 'set', etc.
+
+ Attributes:
+ stream -- The XMLStream instance that will handle sending this stanza.
+ tag -- The namespaced version of the stanza's name.
+
+ Methods:
+ setType -- Set the type of the stanza.
+ getTo -- Return the stanza recipients JID.
+ setTo -- Set the stanza recipient's JID.
+ getFrom -- Return the stanza sender's JID.
+ setFrom -- Set the stanza sender's JID.
+ getPayload -- Return the stanza's XML contents.
+ setPayload -- Append to the stanza's XML contents.
+ delPayload -- Remove the stanza's XML contents.
+ clear -- Reset the stanza's XML contents.
+ reply -- Reset the stanza and modify the 'to' and 'from'
+ attributes to prepare for sending a reply.
+ error -- Set the stanza's type to 'error'.
+ unhandled -- Callback for when the stanza is not handled by a
+ stream handler.
+ exception -- Callback for if an exception is raised while
+ handling the stanza.
+ send -- Send the stanza using the stanza's stream.
+ """
+
name = 'stanza'
namespace = 'jabber:client'
interfaces = set(('type', 'to', 'from', 'id', 'payload'))
@@ -894,6 +958,17 @@ class StanzaBase(ElementBase):
def __init__(self, stream=None, xml=None, stype=None,
sto=None, sfrom=None, sid=None):
+ """
+ Create a new stanza.
+
+ Arguments:
+ stream -- Optional XMLStream responsible for sending this stanza.
+ xml -- Optional XML contents to initialize stanza values.
+ stype -- Optional stanza type value.
+ sto -- Optional string or JID object of the recipient's JID.
+ sfrom -- Optional string or JID object of the sender's JID.
+ sid -- Optional ID value for the stanza.
+ """
self.stream = stream
if stream is not None:
self.namespace = stream.default_ns
@@ -907,22 +982,73 @@ class StanzaBase(ElementBase):
self.tag = "{%s}%s" % (self.namespace, self.name)
def setType(self, value):
+ """
+ Set the stanza's 'type' attribute.
+
+ Only type values contained in StanzaBase.types are accepted.
+
+ Arguments:
+ value -- One of the values contained in StanzaBase.types
+ """
if value in self.types:
self.xml.attrib['type'] = value
return self
+ def getTo(self):
+ """Return the value of the stanza's 'to' attribute."""
+ return JID(self._getAttr('to'))
+
+ def setTo(self, value):
+ """
+ Set the 'to' attribute of the stanza.
+
+ Arguments:
+ value -- A string or JID object representing the recipient's JID.
+ """
+ return self._setAttr('to', str(value))
+
+ def getFrom(self):
+ """Return the value of the stanza's 'from' attribute."""
+ return JID(self._getAttr('from'))
+
+ def setFrom(self, value):
+ """
+ Set the 'from' attribute of the stanza.
+
+ Arguments:
+ from -- A string or JID object representing the sender's JID.
+ """
+ return self._setAttr('from', str(value))
+
def getPayload(self):
+ """Return a list of XML objects contained in the stanza."""
return self.xml.getchildren()
def setPayload(self, value):
- self.xml.append(value)
+ """
+ Add XML content to the stanza.
+
+ Arguments:
+ value -- Either an XML or a stanza object, or a list
+ of XML or stanza objects.
+ """
+ if not isinstance(value, list):
+ value = [value]
+ for val in value:
+ self.append(val)
return self
def delPayload(self):
+ """Remove the XML contents of the stanza."""
self.clear()
return self
def clear(self):
+ """
+ Remove all XML element contents and plugins.
+
+ Any attribute values will be preserved.
+ """
for child in self.xml.getchildren():
self.xml.remove(child)
for plugin in list(self.plugins.keys()):
@@ -930,6 +1056,12 @@ class StanzaBase(ElementBase):
return self
def reply(self):
+ """
+ Reset the stanza and swap its 'from' and 'to' attributes to prepare
+ for sending a reply stanza.
+
+ For client streams, the 'from' attribute is removed.
+ """
# if it's a component, use from
if self.stream and hasattr(self.stream, "is_component") and \
self.stream.is_component:
@@ -941,35 +1073,42 @@ class StanzaBase(ElementBase):
return self
def error(self):
+ """Set the stanza's type to 'error'."""
self['type'] = 'error'
return self
- def getTo(self):
- return JID(self._getAttr('to'))
-
- def setTo(self, value):
- return self._setAttr('to', str(value))
-
- def getFrom(self):
- return JID(self._getAttr('from'))
-
- def setFrom(self, value):
- return self._setAttr('from', str(value))
-
def unhandled(self):
+ """
+ Called when no handlers have been registered to process this
+ stanza.
+
+ Meant to be overridden.
+ """
pass
def exception(self, e):
+ """
+ Handle exceptions raised during stanza processing.
+
+ Meant to be overridden.
+ """
logging.exception('Error handling {%s}%s stanza' % (self.namespace,
self.name))
def send(self):
+ """Queue the stanza to be sent on the XML stream."""
self.stream.sendRaw(self.__str__())
def __copy__(self):
- return self.__class__(xml=copy.deepcopy(self.xml), stream=self.stream)
+ """
+ Return a copy of the stanza object that does not share the
+ same underlying XML object, but does share the same XML stream.
+ """
+ return self.__class__(xml=copy.deepcopy(self.xml),
+ stream=self.stream)
def __str__(self):
+ """Serialize the stanza's XML to a string."""
return tostring(self.xml, xmlns='',
stanza_ns=self.namespace,
stream=self.stream)
diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py
index bf39bb33..28aee2b4 100644
--- a/sleekxmpp/xmlstream/xmlstream.py
+++ b/sleekxmpp/xmlstream/xmlstream.py
@@ -139,8 +139,7 @@ class XMLStream(object):
self.socket = ssl.wrap_socket(self.socket, ssl_version=ssl.PROTOCOL_TLSv1, do_handshake_on_connect=False)
self.socket.do_handshake()
if sys.version_info < (3,0):
- from . filesocket import filesocket
- self.filesocket = filesocket(self.socket)
+ self.filesocket = filesocket.FileSocket(self.socket)
else:
self.filesocket = self.socket.makefile('rb', 0)
return True
@@ -358,8 +357,10 @@ class XMLStream(object):
return False
def registerHandler(self, handler, before=None, after=None):
- "Add handler with matcher class and parameters."
- self.__handlers.append(handler)
+ "Add handler with matcher class and parameters."
+ if handler.stream is None:
+ self.__handlers.append(handler)
+ handler.stream = self
def removeHandler(self, name):
"Removes the handler."
@@ -367,8 +368,10 @@ class XMLStream(object):
for handler in self.__handlers:
if handler.name == name:
self.__handlers.pop(idx)
- return
+ return True
idx += 1
+ return False
+
def registerStanza(self, stanza_class):
"Adds stanza. If root stanzas build stanzas sent in events while non-root stanzas build substanza objects."
diff --git a/tests/sleektest.py b/tests/sleektest.py
index 801253d3..66535bcb 100644
--- a/tests/sleektest.py
+++ b/tests/sleektest.py
@@ -504,7 +504,18 @@ class SleekTest(unittest.TestCase):
if xml.attrib != other.attrib:
return False
- # Step 3: Recursively check children
+ # Step 3: Check text
+ if xml.text is None:
+ xml.text = ""
+ if other.text is None:
+ other.text = ""
+ xml.text = xml.text.strip()
+ other.text = other.text.strip()
+
+ if xml.text != other.text:
+ return False
+
+ # Step 4: Recursively check children
for child in xml:
child2s = other.findall("%s" % child.tag)
if child2s is None:
diff --git a/tests/test_elementbase.py b/tests/test_elementbase.py
index 6b0c076b..dfd37b53 100644
--- a/tests/test_elementbase.py
+++ b/tests/test_elementbase.py
@@ -3,6 +3,21 @@ from sleekxmpp.xmlstream.stanzabase import ElementBase
class TestElementBase(SleekTest):
+ def testFixNs(self):
+ """Test fixing namespaces in an XPath expression."""
+
+ e = ElementBase()
+ ns = "http://jabber.org/protocol/disco#items"
+ result = e._fix_ns("{%s}foo/bar/{abc}baz/{%s}more" % (ns, ns))
+
+ expected = "/".join(["{%s}foo" % ns,
+ "{%s}bar" % ns,
+ "{abc}baz",
+ "{%s}more" % ns])
+ self.failUnless(expected == result,
+ "Incorrect namespace fixing result: %s" % str(result))
+
+
def testExtendedName(self):
"""Test element names of the form tag1/tag2/tag3."""
@@ -332,7 +347,7 @@ class TestElementBase(SleekTest):
</wrapper>
</foo>
""")
- stanza._setSubText('bar', text='', keep=True)
+ stanza._setSubText('wrapper/bar', text='', keep=True)
self.checkStanza(TestStanza, stanza, """
<foo xmlns="foo">
<wrapper>
@@ -343,7 +358,7 @@ class TestElementBase(SleekTest):
""", use_values=False)
stanza['bar'] = 'a'
- stanza._setSubText('bar', text='')
+ stanza._setSubText('wrapper/bar', text='')
self.checkStanza(TestStanza, stanza, """
<foo xmlns="foo">
<wrapper>
@@ -439,12 +454,19 @@ class TestElementBase(SleekTest):
class TestStanza(ElementBase):
name = "foo"
namespace = "foo"
- interfaces = set(('bar','baz'))
+ interfaces = set(('bar','baz', 'qux'))
+ sub_interfaces = set(('qux',))
subitem = (TestSubStanza,)
+ def setQux(self, value):
+ self._setSubText('qux', text=value)
+
+ def getQux(self):
+ return self._getSubText('qux')
+
class TestStanzaPlugin(ElementBase):
name = "plugin"
- namespace = "bar"
+ namespace = "http://test/slash/bar"
interfaces = set(('attrib',))
registerStanzaPlugin(TestStanza, TestStanzaPlugin)
@@ -464,11 +486,22 @@ class TestElementBase(SleekTest):
self.failUnless(stanza.match("foo@bar=a@baz=b"),
"Stanza did not match its own name with multiple attributes.")
+ stanza['qux'] = 'c'
+ self.failUnless(stanza.match("foo/qux"),
+ "Stanza did not match with subelements.")
+
+ stanza['qux'] = ''
+ self.failUnless(stanza.match("foo/qux") == False,
+ "Stanza matched missing subinterface element.")
+
+ self.failUnless(stanza.match("foo/bar") == False,
+ "Stanza matched nonexistent element.")
+
stanza['plugin']['attrib'] = 'c'
self.failUnless(stanza.match("foo/plugin@attrib=c"),
"Stanza did not match with plugin and attribute.")
- self.failUnless(stanza.match("foo/{bar}plugin"),
+ self.failUnless(stanza.match("foo/{http://test/slash/bar}plugin"),
"Stanza did not match with namespaced plugin.")
substanza = TestSubStanza()
diff --git a/tests/test_handlers.py b/tests/test_handlers.py
new file mode 100644
index 00000000..c6262c61
--- /dev/null
+++ b/tests/test_handlers.py
@@ -0,0 +1,112 @@
+from . sleektest import *
+import sleekxmpp
+from sleekxmpp.xmlstream.handler import *
+from sleekxmpp.xmlstream.matcher import *
+
+class TestHandlers(SleekTest):
+ """
+ Test that we can simulate and test a stanza stream.
+ """
+
+ def setUp(self):
+ self.streamStart()
+
+ def tearDown(self):
+ self.streamClose()
+
+ def testCallback(self):
+ """Test using stream callback handlers."""
+
+ def callback_handler(stanza):
+ self.xmpp.sendRaw("""
+ <message>
+ <body>Success!</body>
+ </message>
+ """)
+
+ callback = Callback('Test Callback',
+ MatchXPath('{test}tester'),
+ callback_handler)
+
+ self.xmpp.registerHandler(callback)
+
+ self.streamRecv("""<tester xmlns="test" />""")
+
+ msg = self.Message()
+ msg['body'] = 'Success!'
+ self.streamSendMessage(msg)
+
+ def testWaiter(self):
+ """Test using stream waiter handler."""
+
+ def waiter_handler(stanza):
+ iq = self.xmpp.Iq()
+ iq['id'] = 'test'
+ iq['type'] = 'set'
+ iq['query'] = 'test'
+ reply = iq.send(block=True)
+ if reply:
+ self.xmpp.sendRaw("""
+ <message>
+ <body>Successful: %s</body>
+ </message>
+ """ % reply['query'])
+
+ self.xmpp.add_event_handler('message', waiter_handler, threaded=True)
+
+ # Send message to trigger waiter_handler
+ self.streamRecv("""
+ <message>
+ <body>Testing</body>
+ </message>
+ """)
+
+ # Check that Iq was sent by waiter_handler
+ iq = self.Iq()
+ iq['id'] = 'test'
+ iq['type'] = 'set'
+ iq['query'] = 'test'
+ self.streamSendIq(iq)
+
+ # Send the reply Iq
+ self.streamRecv("""
+ <iq id="test" type="result">
+ <query xmlns="test" />
+ </iq>
+ """)
+
+ # Check that waiter_handler received the reply
+ msg = self.Message()
+ msg['body'] = 'Successful: test'
+ self.streamSendMessage(msg)
+
+ def testWaiterTimeout(self):
+ """Test that waiter handler is removed after timeout."""
+
+ def waiter_handler(stanza):
+ iq = self.xmpp.Iq()
+ iq['id'] = 'test2'
+ iq['type'] = 'set'
+ iq['query'] = 'test2'
+ reply = iq.send(block=True, timeout=0)
+
+ self.xmpp.add_event_handler('message', waiter_handler, threaded=True)
+
+ # Start test by triggerig waiter_handler
+ self.streamRecv("""<message><body>Start Test</body></message>""")
+
+ # Check that Iq was sent to trigger start of timeout period
+ iq = self.Iq()
+ iq['id'] = 'test2'
+ iq['type'] = 'set'
+ iq['query'] = 'test2'
+ self.streamSendIq(iq)
+
+ # Check that the waiter is no longer registered
+ waiter_exists = self.xmpp.removeHandler('IqWait_test2')
+
+ self.failUnless(waiter_exists == False,
+ "Waiter handler was not removed.")
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestHandlers)
diff --git a/tests/test_stanzabase.py b/tests/test_stanzabase.py
new file mode 100644
index 00000000..682068d9
--- /dev/null
+++ b/tests/test_stanzabase.py
@@ -0,0 +1,79 @@
+from . sleektest import *
+import sleekxmpp
+from sleekxmpp.xmlstream.stanzabase import ET, StanzaBase
+
+class TestStanzaBase(SleekTest):
+
+ def testTo(self):
+ """Test the 'to' interface of StanzaBase."""
+ stanza = StanzaBase()
+ stanza['to'] = 'user@example.com'
+ self.failUnless(str(stanza['to']) == 'user@example.com',
+ "Setting and retrieving stanza 'to' attribute did not work.")
+
+ def testFrom(self):
+ """Test the 'from' interface of StanzaBase."""
+ stanza = StanzaBase()
+ stanza['from'] = 'user@example.com'
+ self.failUnless(str(stanza['from']) == 'user@example.com',
+ "Setting and retrieving stanza 'from' attribute did not work.")
+
+ def testPayload(self):
+ """Test the 'payload' interface of StanzaBase."""
+ stanza = StanzaBase()
+ self.failUnless(stanza['payload'] == [],
+ "Empty stanza does not have an empty payload.")
+
+ stanza['payload'] = ET.Element("{foo}foo")
+ self.failUnless(len(stanza['payload']) == 1,
+ "Stanza contents and payload do not match.")
+
+ stanza['payload'] = ET.Element('{bar}bar')
+ self.failUnless(len(stanza['payload']) == 2,
+ "Stanza payload was not appended.")
+
+ del stanza['payload']
+ self.failUnless(stanza['payload'] == [],
+ "Stanza payload not cleared after deletion.")
+
+ stanza['payload'] = [ET.Element('{foo}foo'),
+ ET.Element('{bar}bar')]
+ self.failUnless(len(stanza['payload']) == 2,
+ "Adding multiple elements to stanza's payload did not work.")
+
+ def testClear(self):
+ """Test clearing a stanza."""
+ stanza = StanzaBase()
+ stanza['to'] = 'user@example.com'
+ stanza['payload'] = ET.Element("{foo}foo")
+ stanza.clear()
+
+ self.failUnless(stanza['payload'] == [],
+ "Stanza payload was not cleared after calling .clear()")
+ self.failUnless(str(stanza['to']) == "user@example.com",
+ "Stanza attributes were not preserved after calling .clear()")
+
+ def testReply(self):
+ """Test creating a reply stanza."""
+ stanza = StanzaBase()
+ stanza['to'] = "recipient@example.com"
+ stanza['from'] = "sender@example.com"
+ stanza['payload'] = ET.Element("{foo}foo")
+
+ stanza.reply()
+
+ self.failUnless(str(stanza['to'] == "sender@example.com"),
+ "Stanza reply did not change 'to' attribute.")
+ self.failUnless(stanza['payload'] == [],
+ "Stanza reply did not empty stanza payload.")
+
+ def testError(self):
+ """Test marking a stanza as an error."""
+ stanza = StanzaBase()
+ stanza['type'] = 'get'
+ stanza.error()
+ self.failUnless(stanza['type'] == 'error',
+ "Stanza type is not 'error' after calling error()")
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestStanzaBase)