source: ralphm-patches/async-observer.patch

Last change on this file was 79:0752a1cca356, checked in by Ralph Meijer <ralphm@…>, 6 years ago

Split out stanza module and response tracking patches, fix other patches.

File size: 14.6 KB
RevLine 
[72]1# HG changeset patch
[79]2# Parent 56607e5ddb53e3fafbecb41118bd220dea9310c7
[72]3
4diff --git a/wokkel/subprotocols.py b/wokkel/subprotocols.py
5--- a/wokkel/subprotocols.py
6+++ b/wokkel/subprotocols.py
7@@ -10,6 +10,8 @@
8 __all__ = ['XMPPHandler', 'XMPPHandlerCollection', 'StreamManager',
9            'IQHandlerMixin']
10 
11+from functools import wraps
12+
13 from zope.interface import implements
14 
15 from twisted.internet import defer
[79]16@@ -379,6 +381,89 @@
[72]17 
18 
19 
20+def asyncObserver(observer):
21+    """
22+    Decorator for asynchronous stanza observers.
23+
24+    This decorator makes it easier to do proper error handling for stanza
25+    observers and supports deferred results:
26+
27+        >>> class MyHandler(XMPPHandler):
[73]28+        ...    def connectionInitialized(self):
[72]29+        ...       self.xmlstream.addObserver('/message', self.onMessage)
30+        ...    @asyncObserver
31+        ...    def onMessage(self, element):
32+        ...        return False
33+        ...    @asyncObserver
34+        ...    def onMessage(self, element):
35+        ...        return True
36+        ...    @asyncObserver
37+        ...    def onMessage(self, element):
38+        ...        raise NotImplementedError
39+        ...    @asyncObserver
40+        ...    def onMessage(self, element):
41+        ...        return defer.fail(StanzaError('bad-request'))
42+
43+
44+    If the stanza had its C{handled} attribute set to C{True}, it will be
45+    ignored and the observer will not be called.
46+
47+    The return value of the wrapped observer is used to set the C{handled}
48+    attribute, so that handlers may choose to ignore processing the same
[79]49+    stanza.
[72]50+
51+    If an exception is raised, or the deferred has its errback called, the
52+    exception is checked for being a L{error.StanzaError}. If so, an error
53+    response is sent. A L{NotImplementedError} will cause an error response
54+    with the condition C{service-unavailable}. Any other exception will cause a
55+    error response of C{internal-server-error} to be sent.
56+
57+    The return value of the deferred is not used.
58+    """
59+    @wraps(observer)
60+    def observe(self, element):
61+        def checkStanzaType(failure):
62+            if element.getAttribute('type') in ('result', 'error'):
63+                log.err(failure, 'Cannot return error in response to a '
64+                                 'response stanza.')
65+            else:
66+                return failure
67+
68+        def trapNotImplemented(failure):
69+            failure.trap(NotImplementedError)
70+            raise error.StanzaError('service-unavailable')
71+
72+        def trapOtherError(failure):
73+            if failure.check(error.StanzaError):
74+                return failure
75+            else:
76+                log.err(failure, "Unhandled error in observer")
77+                raise error.StanzaError('internal-server-error')
78+
79+        def trapStanzaError(failure):
80+            failure.trap(error.StanzaError)
81+            self.send(failure.value.toResponse(element))
82+
83+        if element.handled:
84+            return
85+
86+        try:
87+            result = observer(self, element)
88+        except Exception:
89+            result = defer.fail()
90+
91+        if isinstance(result, defer.Deferred):
92+            result.addErrback(checkStanzaType)
93+            result.addErrback(trapNotImplemented)
94+            result.addErrback(trapOtherError)
95+            result.addErrback(trapStanzaError)
96+            result.addErrback(log.err)
97+
[79]98+        element.handled = result
[72]99+    return observe
100+
101+
102+
103 class IQHandlerMixin(object):
104     """
105     XMPP subprotocol mixin for handle incoming IQ stanzas.
[79]106@@ -401,15 +486,15 @@
[73]107 
108     A typical way to use this mixin, is to set up L{xpath} observers on the
109     C{xmlstream} to call handleRequest, for example in an overridden
110-    L{XMPPHandler.connectionMade}. It is likely a good idea to only listen for
111-    incoming iq get and/org iq set requests, and not for any iq, to prevent
112-    hijacking incoming responses to outgoing iq requests. An example:
113+    L{XMPPHandler.connectionInitialized}. It is likely a good idea to only
114+    listen for incoming iq get and/org iq set requests, and not for any iq, to
115+    prevent hijacking incoming responses to outgoing iq requests. An example:
116 
117         >>> QUERY_ROSTER = "/query[@xmlns='jabber:iq:roster']"
118         >>> class MyHandler(XMPPHandler, IQHandlerMixin):
119         ...    iqHandlers = {"/iq[@type='get']" + QUERY_ROSTER: 'onRosterGet',
120         ...                  "/iq[@type='set']" + QUERY_ROSTER: 'onRosterSet'}
121-        ...    def connectionMade(self):
122+        ...    def connectionInitialized(self):
123         ...        self.xmlstream.addObserver(
124         ...          "/iq[@type='get' or @type='set']" + QUERY_ROSTER,
125         ...          self.handleRequest)
[79]126@@ -425,6 +510,7 @@
[72]127 
128     iqHandlers = None
129 
130+    @asyncObserver
131     def handleRequest(self, iq):
132         """
133         Find a handler and wrap the call for sending a response stanza.
[79]134@@ -435,25 +521,13 @@
[72]135             if result:
136                 if IElement.providedBy(result):
137                     response.addChild(result)
138-                else:
139-                    for element in result:
140-                        response.addChild(element)
141 
142             return response
143 
144-        def checkNotImplemented(failure):
145+        def trapNotImplemented(failure):
146             failure.trap(NotImplementedError)
147             raise error.StanzaError('feature-not-implemented')
148 
149-        def fromStanzaError(failure, iq):
150-            failure.trap(error.StanzaError)
151-            return failure.value.toResponse(iq)
152-
153-        def fromOtherError(failure, iq):
154-            log.msg("Unhandled error in iq handler:", isError=True)
155-            log.err(failure)
156-            return error.StanzaError('internal-server-error').toResponse(iq)
157-
158         handler = None
159         for queryString, method in self.iqHandlers.iteritems():
160             if xpath.internQuery(queryString).matches(iq):
[79]161@@ -461,14 +535,10 @@
[72]162 
163         if handler:
164             d = defer.maybeDeferred(handler, iq)
165+            d.addCallback(toResult, iq)
166+            d.addCallback(self.send)
167         else:
168             d = defer.fail(NotImplementedError())
169 
170-        d.addCallback(toResult, iq)
171-        d.addErrback(checkNotImplemented)
172-        d.addErrback(fromStanzaError, iq)
173-        d.addErrback(fromOtherError, iq)
174-
175-        d.addCallback(self.send)
176-
177-        iq.handled = True
178+        d.addErrback(trapNotImplemented)
179+        return d
180diff --git a/wokkel/test/test_subprotocols.py b/wokkel/test/test_subprotocols.py
181--- a/wokkel/test/test_subprotocols.py
182+++ b/wokkel/test/test_subprotocols.py
[79]183@@ -790,13 +790,175 @@
[72]184         self.xmlstream.send(obj)
185 
186 
187+
188+class AsyncObserverTest(unittest.TestCase):
189+    """
190+    Tests for L{wokkel.subprotocols.asyncObserver}.
191+    """
192+
193+    def setUp(self):
194+        self.output = []
195+
196+
197+    def send(self, element):
198+        self.output.append(element)
199+
200+
201+    def test_handled(self):
202+        """
203+        If the element is marked as handled, it is ignored.
204+        """
205+        called = []
206+
207+        @subprotocols.asyncObserver
208+        def observer(self, element):
209+            called.append(element)
210+
211+        element = domish.Element((None, u'message'))
212+        element.handled = True
213+
214+        observer(self, element)
215+        self.assertFalse(called)
216+        self.assertTrue(element.handled)
217+        self.assertFalse(self.output)
218+
219+
220+    def test_syncFalse(self):
221+        """
222+        If the observer returns False, the element is not marked as handled.
223+        """
224+        @subprotocols.asyncObserver
225+        def observer(self, element):
226+            return False
227+
228+        element = domish.Element((None, u'message'))
229+
230+        observer(self, element)
231+        self.assertFalse(element.handled)
232+        self.assertFalse(self.output)
233+
234+
235+    def test_syncTrue(self):
236+        """
237+        If the observer returns True, the element is marked as handled.
238+        """
239+        @subprotocols.asyncObserver
240+        def observer(self, element):
241+            return True
242+
243+        element = domish.Element((None, u'message'))
244+
245+        observer(self, element)
246+        self.assertTrue(element.handled)
247+        self.assertFalse(self.output)
248+
249+
250+    def test_syncNotImplemented(self):
251+        """
252+        NotImplementedError causes a service-unavailable response.
253+        """
254+        @subprotocols.asyncObserver
255+        def observer(self, element):
256+            raise NotImplementedError()
257+
258+        element = domish.Element((None, u'message'))
259+
260+        observer(self, element)
261+        self.assertTrue(element.handled)
[76]262+        self.assertEquals(1, len(self.output))
[72]263+        exc = error.exceptionFromStanza(self.output[-1])
[76]264+        self.assertEquals(u'service-unavailable', exc.condition)
[72]265+
266+
267+    def test_syncStanzaError(self):
268+        """
269+        A StanzaError is sent back as is.
270+        """
271+        @subprotocols.asyncObserver
272+        def observer(self, element):
273+            raise error.StanzaError(u'forbidden')
274+
275+        element = domish.Element((None, u'message'))
276+
277+        observer(self, element)
278+        self.assertTrue(element.handled)
[76]279+        self.assertEquals(1, len(self.output))
[72]280+        exc = error.exceptionFromStanza(self.output[-1])
[76]281+        self.assertEquals(u'forbidden', exc.condition)
[72]282+
283+
284+    def test_syncOtherError(self):
285+        """
286+        Other exceptions are logged and cause an internal-service response.
287+        """
288+        class Error(Exception):
289+            pass
290+
291+        @subprotocols.asyncObserver
292+        def observer(self, element):
293+            raise Error(u"oops")
294+
295+        element = domish.Element((None, u'message'))
296+
297+        observer(self, element)
298+        self.assertTrue(element.handled)
[76]299+        self.assertEquals(1, len(self.output))
[72]300+        exc = error.exceptionFromStanza(self.output[-1])
[76]301+        self.assertEquals(u'internal-server-error', exc.condition)
302+        self.assertEquals(1, len(self.flushLoggedErrors()))
[72]303+
304+
305+    def test_asyncError(self):
306+        """
307+        Other exceptions are logged and cause an internal-service response.
308+        """
309+        class Error(Exception):
310+            pass
311+
312+        @subprotocols.asyncObserver
313+        def observer(self, element):
314+            return defer.fail(Error("oops"))
315+
316+        element = domish.Element((None, u'message'))
317+
318+        observer(self, element)
319+        self.assertTrue(element.handled)
[76]320+        self.assertEquals(1, len(self.output))
[72]321+        exc = error.exceptionFromStanza(self.output[-1])
[76]322+        self.assertEquals(u'internal-server-error', exc.condition)
323+        self.assertEquals(1, len(self.flushLoggedErrors()))
[72]324+
325+
326+    def test_errorMessage(self):
327+        """
328+        If the element is an error message, observer exceptions are just logged.
329+        """
330+        class Error(Exception):
331+            pass
332+
333+        @subprotocols.asyncObserver
334+        def observer(self, element):
335+            raise Error("oops")
336+
337+        element = domish.Element((None, u'message'))
338+        element[u'type'] = u'error'
339+
340+        observer(self, element)
341+        self.assertTrue(element.handled)
[76]342+        self.assertEquals(0, len(self.output))
343+        self.assertEquals(1, len(self.flushLoggedErrors()))
[72]344+
345+
346+
347 class IQHandlerTest(unittest.TestCase):
348+    """
349+    Tests for L{subprotocols.IQHandler}.
350+    """
351 
352     def test_match(self):
353         """
354-        Test that the matching handler gets called.
355+        The matching handler gets called.
356         """
357-
358         class Handler(DummyIQHandler):
359             called = False
360 
[79]361@@ -810,11 +972,11 @@
[72]362         handler.handleRequest(iq)
363         self.assertTrue(handler.called)
364 
365+
366     def test_noMatch(self):
367         """
368-        Test that the matching handler gets called.
369+        If the element does not match the handler is not called.
370         """
371-
372         class Handler(DummyIQHandler):
373             called = False
374 
[79]375@@ -828,11 +990,11 @@
[72]376         handler.handleRequest(iq)
377         self.assertFalse(handler.called)
378 
379+
380     def test_success(self):
381         """
382-        Test response when the request is handled successfully.
383+        If None is returned, an empty result iq is returned.
384         """
385-
386         class Handler(DummyIQHandler):
387             def onGet(self, iq):
388                 return None
[79]389@@ -847,11 +1009,11 @@
[72]390         self.assertEquals('iq', response.name)
391         self.assertEquals('result', response['type'])
392 
393+
394     def test_successPayload(self):
395         """
396-        Test response when the request is handled successfully with payload.
397+        If an Element is returned it is added as the payload of the result iq.
398         """
399-
400         class Handler(DummyIQHandler):
401             payload = domish.Element(('testns', 'foo'))
402 
[79]403@@ -870,11 +1032,11 @@
[72]404         payload = response.elements().next()
405         self.assertEqual(handler.payload, payload)
406 
407+
408     def test_successDeferred(self):
409         """
410-        Test response when where the handler was a deferred.
411+        A deferred result is used when fired.
412         """
413-
414         class Handler(DummyIQHandler):
415             def onGet(self, iq):
416                 return defer.succeed(None)
[79]417@@ -889,11 +1051,11 @@
[72]418         self.assertEquals('iq', response.name)
419         self.assertEquals('result', response['type'])
420 
421+
422     def test_failure(self):
423         """
424-        Test response when the request is handled unsuccessfully.
425+        A raised StanzaError causes an error response.
426         """
427-
428         class Handler(DummyIQHandler):
429             def onGet(self, iq):
430                 raise error.StanzaError('forbidden')
[79]431@@ -910,11 +1072,11 @@
[72]432         e = error.exceptionFromStanza(response)
433         self.assertEquals('forbidden', e.condition)
434 
435+
436     def test_failureUnknown(self):
437         """
438-        Test response when the request handler raises a non-stanza-error.
439+        Any Exception cause an internal-server-error response.
440         """
441-
442         class TestError(Exception):
443             pass
444 
[79]445@@ -935,11 +1097,11 @@
[72]446         self.assertEquals('internal-server-error', e.condition)
447         self.assertEquals(1, len(self.flushLoggedErrors(TestError)))
448 
449+
450     def test_notImplemented(self):
451         """
452-        Test response when the request is recognised but not implemented.
453+        A NotImplementedError causes a feature-not-implemented response.
454         """
455-
456         class Handler(DummyIQHandler):
457             def onGet(self, iq):
458                 raise NotImplementedError()
[79]459@@ -956,11 +1118,11 @@
[72]460         e = error.exceptionFromStanza(response)
461         self.assertEquals('feature-not-implemented', e.condition)
462 
463+
464     def test_noHandler(self):
465         """
466-        Test when the request is not recognised.
467+        A missing handler causes a feature-not-implemented response.
468         """
469-
470         iq = domish.Element((None, 'iq'))
471         iq['type'] = 'set'
472         iq['id'] = 'r1'
Note: See TracBrowser for help on using the repository browser.