Finished the update of ElementBase with docs and unit tests.
Corrected bugs in equality comparisons between stanzas.
This commit is contained in:
		| @@ -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. | ||||||
|   | |||||||
| @@ -480,5 +480,147 @@ 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) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Lance Stout
					Lance Stout