diff --git a/docs/index.rst b/docs/index.rst index 6218b05..026bef1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -252,6 +252,9 @@ instance includes versions of several of the methods in the :class:`socketio.Server` class that default to the proper namespace when the ``namespace`` argument is not given. +Note: if an event has a handler in a class-based namespace, and also a +decorator-based function handler, the standalone function handler is invoked. + Event handler interceptors -------------------------- @@ -307,9 +310,9 @@ Example: :: - from socketio import interceptor, server + import socketio - class MyInterceptor(interceptor.Interceptor): + class MyInterceptor(socketio.Interceptor): def ignore_for(self, event, namespace): # This interceptor won't be applied to connect and disconnect # event handlers. @@ -326,7 +329,7 @@ Example: def after_event(self, event, namespace, args): args[0] = "I faked the response!" - sio = socketio.server.Server() + sio = socketio.Server() @sio.on("/foo") def foo(sid, data): @@ -342,7 +345,10 @@ handlers. Interceptors can be added to a ``Namespace`` object as well by inserting them into its ``interceptors`` ``list`` attribute. They are applied after the server-wide interceptors to every event handler defined in that -``Namespace`` object. +``Namespace`` object. By adding interceptors to a namespace's +``ignore_middleware`` ``list`` attribute, you can remove interceptors +that have been added server-wide for that particular namespace. The +order in wich interceptors are added to the ignore list doesn't matter. There is also a decorator available to add a interceptor to a specific handler only. Given the interceptor class from above, it would be used @@ -350,30 +356,28 @@ as follows: :: - from socketio import util - - sio = socketio.server.Server() - @sio.on("/foo") - @util.apply_interceptor(MyInterceptor) + @socketio.apply_interceptor(MyInterceptor) def foo(sid, data): print("foo executed") return "foo executed" -Middlewares added by the decorator are treated as if they were added +Interceptors added by the decorator are treated as if they were added *after* the server-wide and namespace-wide interceptors. Naturally, decorators are applied from bottom to top. Hence the following will first add ``MW1`` and then ``MW2``. :: - @util.apply_interceptor(MW2) - @util.apply_interceptor(MW1) + @socketio.apply_interceptor(MW2) + @socketio.apply_interceptor(MW1) def foo(sid): # ... -Note: if an event has a handler in a class-based namespace, and also a -decorator-based function handler, the standalone function handler is invoked. +A ``socketio.ignore_middleware`` decorator is available as well. By +decorating an event handler with it, you can remove interceptors that +have been added server-wide or namespace-wide for that particular event +handler. The order this decorator is applied in doesn't matter. Using a Message Queue --------------------- diff --git a/socketio/__init__.py b/socketio/__init__.py index 540de25..b4b0362 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -6,7 +6,8 @@ from .pubsub_manager import PubSubManager from .kombu_manager import KombuManager from .redis_manager import RedisManager from .server import Server -from .util import apply_interceptor +from .util import apply_interceptor, ignore_interceptor __all__ = [Interceptor, Middleware, Namespace, Server, BaseManager, - PubSubManager, KombuManager, RedisManager, apply_interceptor] + PubSubManager, KombuManager, RedisManager, + apply_interceptor, ignore_interceptor] diff --git a/socketio/namespace.py b/socketio/namespace.py index 3df6029..ce691e2 100644 --- a/socketio/namespace.py +++ b/socketio/namespace.py @@ -14,6 +14,7 @@ class Namespace(object): self.name = namespace or '/' self.server = None self.interceptors = [] + self.ignore_interceptors = [] def _set_server(self, server): self.server = server diff --git a/socketio/server.py b/socketio/server.py index 37a2754..bbd08f4 100644 --- a/socketio/server.py +++ b/socketio/server.py @@ -452,9 +452,17 @@ class Server(object): handler = self.handlers[namespace][event] elif namespace in self.namespace_handlers: ns = self.namespace_handlers[namespace] - interceptors.extend(ns.interceptors) handler = ns._get_event_handler(event) + if handler is not None: + for interceptor in ns.ignore_interceptors: + while interceptor in interceptors: + interceptors.remove(interceptor) + interceptors.extend(ns.interceptors) if handler is not None: + for interceptor in getattr( + handler, '_sio_ignore_interceptors', []): + while interceptor in interceptors: + interceptors.remove(interceptor) interceptors.extend(getattr(handler, '_sio_interceptors', [])) handler = self._apply_interceptors(interceptors, event, namespace, handler) @@ -504,11 +512,11 @@ class Server(object): try: # Apply before_event methods. for interceptor in _interceptors: - interceptors_processed += 1 result = interceptor.before_event( event, namespace, args) if result is not None: break + interceptors_processed += 1 if result is None: # call the real event handler result = handler(*args) diff --git a/socketio/util.py b/socketio/util.py index 1d35a78..249caad 100644 --- a/socketio/util.py +++ b/socketio/util.py @@ -14,3 +14,21 @@ def apply_interceptor(interceptor): handler._sio_interceptors.append(interceptor) return handler return wrapper + + +def ignore_interceptor(interceptor): + """Returns a decorator for event handlers that ignores the given + interceptor for the handler decorated with it. + + :param interceptor: The interceptor to ignore + + Ensure that you only add well-behaving decorators after this one + (meaning such that preserve attributes) because you'll loose them + otherwise. + """ + def wrapper(handler): + if not hasattr(handler, '_sio_ignore_interceptors'): + handler._sio_ignore_interceptors = [] + handler._sio_ignore_interceptors.append(interceptor) + return handler + return wrapper diff --git a/tests/test_server.py b/tests/test_server.py index 2d72d13..b10a8cd 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -62,7 +62,6 @@ class TestServer(unittest.TestCase): mw2 = MW(s) mw3 = MW(s) mw4 = MW(s) - mw4.ignore_for = mock.MagicMock(side_effect=[True] + 100 * [False]) mw4.before_event = mock.MagicMock(side_effect=['x'] + 100 * [None]) mw4.after_event = mock.MagicMock(side_effect=['x'] + 100 * [None]) @@ -72,6 +71,7 @@ class TestServer(unittest.TestCase): @namespace.Namespace.event_name('foo bar') @util.apply_interceptor(mw4) + @util.ignore_interceptor(mw3) def some_name(self, sid): pass @@ -97,37 +97,39 @@ class TestServer(unittest.TestCase): self.assertEqual(mw4.before_event.call_count, 0) self.assertEqual(mw4.after_event.call_count, 0) - # only mw1 and mw3 should run completely, mw4 is enabled but ignored + # only mw1 and mw4 should run, mw3 is ignored and mw4 + # before_event returns "x" and starts after_event processing. s._trigger_event('foo bar', '/ns', '123') self.assertEqual(mw1.before_event.call_count, 2) self.assertEqual(mw1.after_event.call_count, 2) self.assertEqual(mw2.before_event.call_count, 0) self.assertEqual(mw2.after_event.call_count, 0) - self.assertEqual(mw3.before_event.call_count, 2) - self.assertEqual(mw3.after_event.call_count, 2) - self.assertEqual(mw4.before_event.call_count, 0) + self.assertEqual(mw3.before_event.call_count, 1) + self.assertEqual(mw3.after_event.call_count, 1) + self.assertEqual(mw4.before_event.call_count, 1) self.assertEqual(mw4.after_event.call_count, 0) - # again, this time mw4 before + after should be triggered as well + # again, this time mw4 after should be triggered as well and abort + # ing "x" s._trigger_event('foo bar', '/ns', '123') self.assertEqual(mw1.before_event.call_count, 3) self.assertEqual(mw1.after_event.call_count, 2) self.assertEqual(mw2.before_event.call_count, 0) self.assertEqual(mw2.after_event.call_count, 0) - self.assertEqual(mw3.before_event.call_count, 3) - self.assertEqual(mw3.after_event.call_count, 2) - self.assertEqual(mw4.before_event.call_count, 1) + self.assertEqual(mw3.before_event.call_count, 1) + self.assertEqual(mw3.after_event.call_count, 1) + self.assertEqual(mw4.before_event.call_count, 2) self.assertEqual(mw4.after_event.call_count, 1) - # again, this time mw4 before + after_event should be triggered + # again, this time mw1 and mw4 should run completely s._trigger_event('foo bar', '/ns', '123') self.assertEqual(mw1.before_event.call_count, 4) self.assertEqual(mw1.after_event.call_count, 3) self.assertEqual(mw2.before_event.call_count, 0) self.assertEqual(mw2.after_event.call_count, 0) - self.assertEqual(mw3.before_event.call_count, 4) - self.assertEqual(mw3.after_event.call_count, 3) - self.assertEqual(mw4.before_event.call_count, 2) + self.assertEqual(mw3.before_event.call_count, 1) + self.assertEqual(mw3.after_event.call_count, 1) + self.assertEqual(mw4.before_event.call_count, 3) self.assertEqual(mw4.after_event.call_count, 2) # only mw1 and mw2 should run completely @@ -136,9 +138,9 @@ class TestServer(unittest.TestCase): self.assertEqual(mw1.after_event.call_count, 4) self.assertEqual(mw2.before_event.call_count, 1) self.assertEqual(mw2.after_event.call_count, 1) - self.assertEqual(mw3.before_event.call_count, 4) - self.assertEqual(mw3.after_event.call_count, 3) - self.assertEqual(mw4.before_event.call_count, 2) + self.assertEqual(mw3.before_event.call_count, 1) + self.assertEqual(mw3.after_event.call_count, 1) + self.assertEqual(mw4.before_event.call_count, 3) self.assertEqual(mw4.after_event.call_count, 2) def test_on_bad_event_name(self, eio):