From d68bc2ba076461484ba4c30671ef437c48e349d5 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 26 Aug 2010 13:49:36 -0400 Subject: [PATCH] Finished the update of ElementBase with docs and unit tests. Corrected bugs in equality comparisons between stanzas. --- sleekxmpp/xmlstream/stanzabase.py | 137 ++++++++++++++++++++++++++- tests/test_elementbase.py | 152 +++++++++++++++++++++++++++++- 2 files changed, 282 insertions(+), 7 deletions(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 965f13f..c0e74ff 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 0eddd30..6b0c076 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): """) - 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, """ + + + + """) + self.failUnless(len(stanza) == 1, + "Incorrect stanza size with 1 substanza.") + + stanza.append(substanza2) + self.checkStanza(TestStanza, stanza, """ + + + + + """) + self.failUnless(len(stanza) == 2, + "Incorrect stanza size with 2 substanzas.") + + # Test popping substanzas + stanza.pop(0) + self.checkStanza(TestStanza, stanza, """ + + + + """) + + # 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)