mirror of
https://github.com/correl/SleekXMPP.git
synced 2024-11-27 19:19:54 +00:00
Finished the update of ElementBase with docs and unit tests.
Corrected bugs in equality comparisons between stanzas.
This commit is contained in:
parent
10298a6eab
commit
d68bc2ba07
2 changed files with 282 additions and 7 deletions
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue