source: ralphm-patches/async-observer.patch @ 72:727b4d29c48e

Last change on this file since 72:727b4d29c48e was 72:727b4d29c48e, checked in by Ralph Meijer <ralphm@…>, 8 years ago

Major reworking of avatars, session manager and stanza handlers.

File size: 13.4 KB
RevLine 
[72]1# HG changeset patch
2# Parent 2076bac14221bf6aa7dc9337590b18604b100f32
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
16@@ -379,6 +381,89 @@
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):
28+        ...    def connectionMade(self):
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
49+    stanza.
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+
98+        element.handled = result
99+    return observe
100+
101+
102+
103 class IQHandlerMixin(object):
104     """
105     XMPP subprotocol mixin for handle incoming IQ stanzas.
106@@ -425,6 +510,7 @@
107 
108     iqHandlers = None
109 
110+    @asyncObserver
111     def handleRequest(self, iq):
112         """
113         Find a handler and wrap the call for sending a response stanza.
114@@ -435,25 +521,13 @@
115             if result:
116                 if IElement.providedBy(result):
117                     response.addChild(result)
118-                else:
119-                    for element in result:
120-                        response.addChild(element)
121 
122             return response
123 
124-        def checkNotImplemented(failure):
125+        def trapNotImplemented(failure):
126             failure.trap(NotImplementedError)
127             raise error.StanzaError('feature-not-implemented')
128 
129-        def fromStanzaError(failure, iq):
130-            failure.trap(error.StanzaError)
131-            return failure.value.toResponse(iq)
132-
133-        def fromOtherError(failure, iq):
134-            log.msg("Unhandled error in iq handler:", isError=True)
135-            log.err(failure)
136-            return error.StanzaError('internal-server-error').toResponse(iq)
137-
138         handler = None
139         for queryString, method in self.iqHandlers.iteritems():
140             if xpath.internQuery(queryString).matches(iq):
141@@ -461,14 +535,10 @@
142 
143         if handler:
144             d = defer.maybeDeferred(handler, iq)
145+            d.addCallback(toResult, iq)
146+            d.addCallback(self.send)
147         else:
148             d = defer.fail(NotImplementedError())
149 
150-        d.addCallback(toResult, iq)
151-        d.addErrback(checkNotImplemented)
152-        d.addErrback(fromStanzaError, iq)
153-        d.addErrback(fromOtherError, iq)
154-
155-        d.addCallback(self.send)
156-
157-        iq.handled = True
158+        d.addErrback(trapNotImplemented)
159+        return d
160diff --git a/wokkel/test/test_subprotocols.py b/wokkel/test/test_subprotocols.py
161--- a/wokkel/test/test_subprotocols.py
162+++ b/wokkel/test/test_subprotocols.py
163@@ -790,13 +790,175 @@
164         self.xmlstream.send(obj)
165 
166 
167+
168+class AsyncObserverTest(unittest.TestCase):
169+    """
170+    Tests for L{wokkel.subprotocols.asyncObserver}.
171+    """
172+
173+    def setUp(self):
174+        self.output = []
175+
176+
177+    def send(self, element):
178+        self.output.append(element)
179+
180+
181+    def test_handled(self):
182+        """
183+        If the element is marked as handled, it is ignored.
184+        """
185+        called = []
186+
187+        @subprotocols.asyncObserver
188+        def observer(self, element):
189+            called.append(element)
190+
191+        element = domish.Element((None, u'message'))
192+        element.handled = True
193+
194+        observer(self, element)
195+        self.assertFalse(called)
196+        self.assertTrue(element.handled)
197+        self.assertFalse(self.output)
198+
199+
200+    def test_syncFalse(self):
201+        """
202+        If the observer returns False, the element is not marked as handled.
203+        """
204+        @subprotocols.asyncObserver
205+        def observer(self, element):
206+            return False
207+
208+        element = domish.Element((None, u'message'))
209+
210+        observer(self, element)
211+        self.assertFalse(element.handled)
212+        self.assertFalse(self.output)
213+
214+
215+    def test_syncTrue(self):
216+        """
217+        If the observer returns True, the element is marked as handled.
218+        """
219+        @subprotocols.asyncObserver
220+        def observer(self, element):
221+            return True
222+
223+        element = domish.Element((None, u'message'))
224+
225+        observer(self, element)
226+        self.assertTrue(element.handled)
227+        self.assertFalse(self.output)
228+
229+
230+    def test_syncNotImplemented(self):
231+        """
232+        NotImplementedError causes a service-unavailable response.
233+        """
234+        @subprotocols.asyncObserver
235+        def observer(self, element):
236+            raise NotImplementedError()
237+
238+        element = domish.Element((None, u'message'))
239+
240+        observer(self, element)
241+        self.assertTrue(element.handled)
242+        self.assertEqual(1, len(self.output))
243+        exc = error.exceptionFromStanza(self.output[-1])
244+        self.assertEqual(u'service-unavailable', exc.condition)
245+
246+
247+    def test_syncStanzaError(self):
248+        """
249+        A StanzaError is sent back as is.
250+        """
251+        @subprotocols.asyncObserver
252+        def observer(self, element):
253+            raise error.StanzaError(u'forbidden')
254+
255+        element = domish.Element((None, u'message'))
256+
257+        observer(self, element)
258+        self.assertTrue(element.handled)
259+        self.assertEqual(1, len(self.output))
260+        exc = error.exceptionFromStanza(self.output[-1])
261+        self.assertEqual(u'forbidden', exc.condition)
262+
263+
264+    def test_syncOtherError(self):
265+        """
266+        Other exceptions are logged and cause an internal-service response.
267+        """
268+        class Error(Exception):
269+            pass
270+
271+        @subprotocols.asyncObserver
272+        def observer(self, element):
273+            raise Error(u"oops")
274+
275+        element = domish.Element((None, u'message'))
276+
277+        observer(self, element)
278+        self.assertTrue(element.handled)
279+        self.assertEqual(1, len(self.output))
280+        exc = error.exceptionFromStanza(self.output[-1])
281+        self.assertEqual(u'internal-server-error', exc.condition)
282+        self.assertEqual(1, len(self.flushLoggedErrors()))
283+
284+
285+    def test_asyncError(self):
286+        """
287+        Other exceptions are logged and cause an internal-service response.
288+        """
289+        class Error(Exception):
290+            pass
291+
292+        @subprotocols.asyncObserver
293+        def observer(self, element):
294+            return defer.fail(Error("oops"))
295+
296+        element = domish.Element((None, u'message'))
297+
298+        observer(self, element)
299+        self.assertTrue(element.handled)
300+        self.assertEqual(1, len(self.output))
301+        exc = error.exceptionFromStanza(self.output[-1])
302+        self.assertEqual(u'internal-server-error', exc.condition)
303+        self.assertEqual(1, len(self.flushLoggedErrors()))
304+
305+
306+    def test_errorMessage(self):
307+        """
308+        If the element is an error message, observer exceptions are just logged.
309+        """
310+        class Error(Exception):
311+            pass
312+
313+        @subprotocols.asyncObserver
314+        def observer(self, element):
315+            raise Error("oops")
316+
317+        element = domish.Element((None, u'message'))
318+        element[u'type'] = u'error'
319+
320+        observer(self, element)
321+        self.assertTrue(element.handled)
322+        self.assertEqual(0, len(self.output))
323+        self.assertEqual(1, len(self.flushLoggedErrors()))
324+
325+
326+
327 class IQHandlerTest(unittest.TestCase):
328+    """
329+    Tests for L{subprotocols.IQHandler}.
330+    """
331 
332     def test_match(self):
333         """
334-        Test that the matching handler gets called.
335+        The matching handler gets called.
336         """
337-
338         class Handler(DummyIQHandler):
339             called = False
340 
341@@ -810,11 +972,11 @@
342         handler.handleRequest(iq)
343         self.assertTrue(handler.called)
344 
345+
346     def test_noMatch(self):
347         """
348-        Test that the matching handler gets called.
349+        If the element does not match the handler is not called.
350         """
351-
352         class Handler(DummyIQHandler):
353             called = False
354 
355@@ -828,11 +990,11 @@
356         handler.handleRequest(iq)
357         self.assertFalse(handler.called)
358 
359+
360     def test_success(self):
361         """
362-        Test response when the request is handled successfully.
363+        If None is returned, an empty result iq is returned.
364         """
365-
366         class Handler(DummyIQHandler):
367             def onGet(self, iq):
368                 return None
369@@ -847,11 +1009,11 @@
370         self.assertEquals('iq', response.name)
371         self.assertEquals('result', response['type'])
372 
373+
374     def test_successPayload(self):
375         """
376-        Test response when the request is handled successfully with payload.
377+        If an Element is returned it is added as the payload of the result iq.
378         """
379-
380         class Handler(DummyIQHandler):
381             payload = domish.Element(('testns', 'foo'))
382 
383@@ -870,11 +1032,11 @@
384         payload = response.elements().next()
385         self.assertEqual(handler.payload, payload)
386 
387+
388     def test_successDeferred(self):
389         """
390-        Test response when where the handler was a deferred.
391+        A deferred result is used when fired.
392         """
393-
394         class Handler(DummyIQHandler):
395             def onGet(self, iq):
396                 return defer.succeed(None)
397@@ -889,11 +1051,11 @@
398         self.assertEquals('iq', response.name)
399         self.assertEquals('result', response['type'])
400 
401+
402     def test_failure(self):
403         """
404-        Test response when the request is handled unsuccessfully.
405+        A raised StanzaError causes an error response.
406         """
407-
408         class Handler(DummyIQHandler):
409             def onGet(self, iq):
410                 raise error.StanzaError('forbidden')
411@@ -910,11 +1072,11 @@
412         e = error.exceptionFromStanza(response)
413         self.assertEquals('forbidden', e.condition)
414 
415+
416     def test_failureUnknown(self):
417         """
418-        Test response when the request handler raises a non-stanza-error.
419+        Any Exception cause an internal-server-error response.
420         """
421-
422         class TestError(Exception):
423             pass
424 
425@@ -935,11 +1097,11 @@
426         self.assertEquals('internal-server-error', e.condition)
427         self.assertEquals(1, len(self.flushLoggedErrors(TestError)))
428 
429+
430     def test_notImplemented(self):
431         """
432-        Test response when the request is recognised but not implemented.
433+        A NotImplementedError causes a feature-not-implemented response.
434         """
435-
436         class Handler(DummyIQHandler):
437             def onGet(self, iq):
438                 raise NotImplementedError()
439@@ -956,11 +1118,11 @@
440         e = error.exceptionFromStanza(response)
441         self.assertEquals('feature-not-implemented', e.condition)
442 
443+
444     def test_noHandler(self):
445         """
446-        Test when the request is not recognised.
447+        A missing handler causes a feature-not-implemented response.
448         """
449-
450         iq = domish.Element((None, 'iq'))
451         iq['type'] = 'set'
452         iq['id'] = 'r1'
Note: See TracBrowser for help on using the repository browser.