Finished the update of ElementBase with docs and unit tests.

Corrected bugs in equality comparisons between stanzas.
This commit is contained in:
Lance Stout 2010-08-26 13:49:36 -04:00
parent 10298a6eab
commit d68bc2ba07
2 changed files with 282 additions and 7 deletions

View file

@ -34,6 +34,116 @@ def registerStanzaPlugin(stanza, plugin):
class ElementBase(object): 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' name = 'stanza'
plugin_attrib = 'plugin' plugin_attrib = 'plugin'
namespace = 'jabber:client' namespace = 'jabber:client'
@ -567,7 +677,7 @@ class ElementBase(object):
out += [x for x in self.plugins] out += [x for x in self.plugins]
if self.iterables: if self.iterables:
out.append('substanzas') out.append('substanzas')
return tuple(out) return out
def append(self, item): def append(self, item):
""" """
@ -667,12 +777,35 @@ class ElementBase(object):
""" """
if not isinstance(other, ElementBase): if not isinstance(other, ElementBase):
return False return False
# Check that this stanza is a superset of the other stanza.
values = self.getStanzaValues() values = self.getStanzaValues()
for key in other: for key in other.keys():
if key not in values or values[key] != other[key]: if key not in values or values[key] != other[key]:
return False 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 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): def __bool__(self):
""" """
Stanza objects should be treated as True in boolean contexts. Stanza objects should be treated as True in boolean contexts.

View file

@ -267,7 +267,7 @@ class TestElementBase(SleekTest):
self.failUnless(stanza._getAttr('bar', 'c') == 'c', self.failUnless(stanza._getAttr('bar', 'c') == 'c',
"Incorrect default value returned for an unset XML attribute.") "Incorrect default value returned for an unset XML attribute.")
def testGetSubText(self): def testGetSubText(self):
"""Test retrieving the contents of a sub element.""" """Test retrieving the contents of a sub element."""
@ -287,7 +287,7 @@ class TestElementBase(SleekTest):
return self._getSubText("wrapper/bar", default="not found") return self._getSubText("wrapper/bar", default="not found")
stanza = TestStanza() stanza = TestStanza()
self.failUnless(stanza['bar'] == 'not found', self.failUnless(stanza['bar'] == 'not found',
"Default _getSubText value incorrect.") "Default _getSubText value incorrect.")
stanza['bar'] = 'found' stanza['bar'] = 'found'
@ -298,7 +298,7 @@ class TestElementBase(SleekTest):
</wrapper> </wrapper>
</foo> </foo>
""") """)
self.failUnless(stanza['bar'] == 'found', self.failUnless(stanza['bar'] == 'found',
"_getSubText value incorrect: %s." % stanza['bar']) "_getSubText value incorrect: %s." % stanza['bar'])
def testSubElement(self): def testSubElement(self):
@ -450,7 +450,7 @@ class TestElementBase(SleekTest):
registerStanzaPlugin(TestStanza, TestStanzaPlugin) registerStanzaPlugin(TestStanza, TestStanzaPlugin)
stanza = TestStanza() stanza = TestStanza()
self.failUnless(stanza.match("foo"), self.failUnless(stanza.match("foo"),
"Stanza did not match its own tag name.") "Stanza did not match its own tag name.")
self.failUnless(stanza.match("{foo}foo"), self.failUnless(stanza.match("{foo}foo"),
@ -479,6 +479,148 @@ class TestElementBase(SleekTest):
self.failUnless(stanza.match("foo/{baz}sub"), self.failUnless(stanza.match("foo/{baz}sub"),
"Stanza did not match with namespaced substanza.") "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) suite = unittest.TestLoader().loadTestsFromTestCase(TestElementBase)