summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--sleekxmpp/xmlstream/stanzabase.py137
-rw-r--r--tests/test_elementbase.py152
2 files changed, 282 insertions, 7 deletions
diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py
index 965f13f6..c0e74ff4 100644
--- a/sleekxmpp/xmlstream/stanzabase.py
+++ b/sleekxmpp/xmlstream/stanzabase.py
@@ -34,6 +34,116 @@ def registerStanzaPlugin(stanza, plugin):
class ElementBase(object):
+
+ """
+ The core of SleekXMPP's stanza XML manipulation and handling is provided
+ by ElementBase. ElementBase wraps XML cElementTree objects and enables
+ access to the XML contents through dictionary syntax, similar in style
+ to the Ruby XMPP library Blather's stanza implementation.
+
+ Stanzas are defined by their name, namespace, and interfaces. For
+ example, a simplistic Message stanza could be defined as:
+
+ >>> class Message(ElementBase):
+ ... name = "message"
+ ... namespace = "jabber:client"
+ ... interfaces = set(('to', 'from', 'type', 'body'))
+ ... sub_interfaces = set(('body',))
+
+ The resulting Message stanza's contents may be accessed as so:
+
+ >>> message['to'] = "user@example.com"
+ >>> message['body'] = "Hi!"
+
+ The interface values map to either custom access methods, stanza
+ XML attributes, or (if the interface is also in sub_interfaces) the
+ text contents of a stanza's subelement.
+
+ Custom access methods may be created by adding methods of the
+ form "getInterface", "setInterface", or "delInterface", where
+ "Interface" is the titlecase version of the interface name.
+
+ Stanzas may be extended through the use of plugins. A plugin
+ is simply a stanza that has a plugin_attrib value. For example:
+
+ >>> class MessagePlugin(ElementBase):
+ ... name = "custom_plugin"
+ ... namespace = "custom"
+ ... interfaces = set(('useful_thing', 'custom'))
+ ... plugin_attrib = "custom"
+
+ The plugin stanza class must be associated with its intended
+ container stanza by using registerStanzaPlugin as so:
+
+ >>> registerStanzaPlugin(Message, MessagePlugin)
+
+ The plugin may then be accessed as if it were built-in to the parent
+ stanza.
+
+ >>> message['custom']['useful_thing'] = 'foo'
+
+ If a plugin provides an interface that is the same as the plugin's
+ plugin_attrib value, then the plugin's interface may be accessed
+ directly from the parent stanza, as so:
+
+ >>> message['custom'] = 'bar' # Same as using message['custom']['custom']
+
+ Class Attributes:
+ name -- The name of the stanza's main element.
+ namespace -- The namespace of the stanza's main element.
+ interfaces -- A set of attribute and element names that may
+ be accessed using dictionary syntax.
+ sub_interfaces -- A subset of the set of interfaces which map
+ to subelements instead of attributes.
+ subitem -- A set of stanza classes which are allowed to
+ be added as substanzas.
+ types -- A set of generic type attribute values.
+ plugin_attrib -- The interface name that the stanza uses to be
+ accessed as a plugin from another stanza.
+ plugin_attrib_map -- A mapping of plugin attribute names with the
+ associated plugin stanza classes.
+ plugin_tag_map -- A mapping of plugin stanza tag names with
+ the associated plugin stanza classes.
+
+ Instance Attributes:
+ xml -- The stanza's XML contents.
+ parent -- The parent stanza of this stanza.
+ plugins -- A map of enabled plugin names with the
+ initialized plugin stanza objects.
+
+ Methods:
+ setup -- Initialize the stanza's XML contents.
+ enable -- Instantiate a stanza plugin. Alias for initPlugin.
+ initPlugin -- Instantiate a stanza plugin.
+ getStanzaValues -- Return a dictionary of stanza interfaces and
+ their values.
+ setStanzaValues -- Set stanza interface values given a dictionary of
+ interfaces and values.
+ __getitem__ -- Return the value of a stanza interface.
+ __setitem__ -- Set the value of a stanza interface.
+ __delitem__ -- Remove the value of a stanza interface.
+ _setAttr -- Set an attribute value of the main stanza element.
+ _delAttr -- Remove an attribute from the main stanza element.
+ _getAttr -- Return an attribute's value from the main
+ stanza element.
+ _getSubText -- Return the text contents of a subelement.
+ _setSubText -- Set the text contents of a subelement.
+ _delSub -- Remove a subelement.
+ match -- Compare the stanza against an XPath expression.
+ find -- Return subelement matching an XPath expression.
+ findall -- Return subelements matching an XPath expression.
+ get -- Return the value of a stanza interface, with an
+ optional default value.
+ keys -- Return the set of interface names accepted by
+ the stanza.
+ append -- Add XML content or a substanza to the stanza.
+ appendxml -- Add XML content to the stanza.
+ pop -- Remove a substanza.
+ next -- Return the next iterable substanza.
+ _fix_ns -- Apply the stanza's namespace to non-namespaced
+ elements in an XPath expression.
+ """
+
name = 'stanza'
plugin_attrib = 'plugin'
namespace = 'jabber:client'
@@ -567,7 +677,7 @@ class ElementBase(object):
out += [x for x in self.plugins]
if self.iterables:
out.append('substanzas')
- return tuple(out)
+ return out
def append(self, item):
"""
@@ -667,12 +777,35 @@ class ElementBase(object):
"""
if not isinstance(other, ElementBase):
return False
+
+ # Check that this stanza is a superset of the other stanza.
values = self.getStanzaValues()
- for key in other:
+ for key in other.keys():
if key not in values or values[key] != other[key]:
return False
+
+ # Check that the other stanza is a superset of this stanza.
+ values = other.getStanzaValues()
+ for key in self.keys():
+ if key not in values or values[key] != self[key]:
+ return False
+
+ # Both stanzas are supersets of each other, therefore they
+ # must be equal.
return True
+ def __ne__(self, other):
+ """
+ Compare the stanza object with another to test for inequality.
+
+ Stanzas are not equal if their interfaces return different values,
+ or if they are not both instances of ElementBase.
+
+ Arguments:
+ other -- The stanza object to compare against.
+ """
+ return not self.__eq__(other)
+
def __bool__(self):
"""
Stanza objects should be treated as True in boolean contexts.
diff --git a/tests/test_elementbase.py b/tests/test_elementbase.py
index 0eddd30b..6b0c076b 100644
--- a/tests/test_elementbase.py
+++ b/tests/test_elementbase.py
@@ -267,7 +267,7 @@ class TestElementBase(SleekTest):
self.failUnless(stanza._getAttr('bar', 'c') == 'c',
"Incorrect default value returned for an unset XML attribute.")
-
+
def testGetSubText(self):
"""Test retrieving the contents of a sub element."""
@@ -287,7 +287,7 @@ class TestElementBase(SleekTest):
return self._getSubText("wrapper/bar", default="not found")
stanza = TestStanza()
- self.failUnless(stanza['bar'] == 'not found',
+ self.failUnless(stanza['bar'] == 'not found',
"Default _getSubText value incorrect.")
stanza['bar'] = 'found'
@@ -298,7 +298,7 @@ class TestElementBase(SleekTest):
</wrapper>
</foo>
""")
- self.failUnless(stanza['bar'] == 'found',
+ self.failUnless(stanza['bar'] == 'found',
"_getSubText value incorrect: %s." % stanza['bar'])
def testSubElement(self):
@@ -450,7 +450,7 @@ class TestElementBase(SleekTest):
registerStanzaPlugin(TestStanza, TestStanzaPlugin)
stanza = TestStanza()
- self.failUnless(stanza.match("foo"),
+ self.failUnless(stanza.match("foo"),
"Stanza did not match its own tag name.")
self.failUnless(stanza.match("{foo}foo"),
@@ -479,6 +479,148 @@ class TestElementBase(SleekTest):
self.failUnless(stanza.match("foo/{baz}sub"),
"Stanza did not match with namespaced substanza.")
-
+
+ def testComparisons(self):
+ """Test comparing ElementBase objects."""
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+
+ stanza1 = TestStanza()
+ stanza1['bar'] = 'a'
+
+ self.failUnless(stanza1,
+ "Stanza object does not evaluate to True")
+
+ stanza2 = TestStanza()
+ stanza2['baz'] = 'b'
+
+ self.failUnless(stanza1 != stanza2,
+ "Different stanza objects incorrectly compared equal.")
+
+ stanza1['baz'] = 'b'
+ stanza2['bar'] = 'a'
+
+ self.failUnless(stanza1 == stanza2,
+ "Equal stanzas incorrectly compared inequal.")
+
+ def testKeys(self):
+ """Test extracting interface names from a stanza object."""
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+ plugin_attrib = 'qux'
+
+ registerStanzaPlugin(TestStanza, TestStanza)
+
+ stanza = TestStanza()
+
+ self.failUnless(set(stanza.keys()) == set(('bar', 'baz')),
+ "Returned set of interface keys does not match expected.")
+
+ stanza.enable('qux')
+
+ self.failUnless(set(stanza.keys()) == set(('bar', 'baz', 'qux')),
+ "Incorrect set of interface and plugin keys.")
+
+ def testGet(self):
+ """Test accessing stanza interfaces using get()."""
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+
+ stanza = TestStanza()
+ stanza['bar'] = 'a'
+
+ self.failUnless(stanza.get('bar') == 'a',
+ "Incorrect value returned by stanza.get")
+
+ self.failUnless(stanza.get('baz', 'b') == 'b',
+ "Incorrect default value returned by stanza.get")
+
+ def testSubStanzas(self):
+ """Test manipulating substanzas of a stanza object."""
+
+ class TestSubStanza(ElementBase):
+ name = "foobar"
+ namespace = "foo"
+ interfaces = set(('qux',))
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+ subitem = (TestSubStanza,)
+
+ stanza = TestStanza()
+ substanza1 = TestSubStanza()
+ substanza2 = TestSubStanza()
+ substanza1['qux'] = 'a'
+ substanza2['qux'] = 'b'
+
+ # Test appending substanzas
+ self.failUnless(len(stanza) == 0,
+ "Incorrect empty stanza size.")
+
+ stanza.append(substanza1)
+ self.checkStanza(TestStanza, stanza, """
+ <foo xmlns="foo">
+ <foobar qux="a" />
+ </foo>
+ """)
+ self.failUnless(len(stanza) == 1,
+ "Incorrect stanza size with 1 substanza.")
+
+ stanza.append(substanza2)
+ self.checkStanza(TestStanza, stanza, """
+ <foo xmlns="foo">
+ <foobar qux="a" />
+ <foobar qux="b" />
+ </foo>
+ """)
+ self.failUnless(len(stanza) == 2,
+ "Incorrect stanza size with 2 substanzas.")
+
+ # Test popping substanzas
+ stanza.pop(0)
+ self.checkStanza(TestStanza, stanza, """
+ <foo xmlns="foo">
+ <foobar qux="b" />
+ </foo>
+ """)
+
+ # Test iterating over substanzas
+ stanza.append(substanza1)
+ results = []
+ for substanza in stanza:
+ results.append(substanza['qux'])
+ self.failUnless(results == ['b', 'a'],
+ "Iteration over substanzas failed: %s." % str(results))
+
+ def testCopy(self):
+ """Test copying stanza objects."""
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+
+ stanza1 = TestStanza()
+ stanza1['bar'] = 'a'
+
+ stanza2 = stanza1.__copy__()
+
+ self.failUnless(stanza1 == stanza2,
+ "Copied stanzas are not equal to each other.")
+
+ stanza1['baz'] = 'b'
+ self.failUnless(stanza1 != stanza2,
+ "Divergent stanza copies incorrectly compared equal.")
suite = unittest.TestLoader().loadTestsFromTestCase(TestElementBase)