From fe40b30e35f709158a36d2eaabb12445c7519286 Mon Sep 17 00:00:00 2001 From: Robert Schindler Date: Sun, 21 Aug 2016 14:41:30 +0200 Subject: [PATCH 01/12] Added class-based namespaces. --- socketio/__init__.py | 5 ++-- socketio/namespace.py | 56 +++++++++++++++++++++++++++++++++++++++++++ socketio/server.py | 14 +++++++++++ tests/test_server.py | 11 +++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 socketio/namespace.py diff --git a/socketio/__init__.py b/socketio/__init__.py index be5882f..ff68424 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -1,9 +1,10 @@ from .middleware import Middleware +from .namespace import Namespace from .base_manager import BaseManager from .pubsub_manager import PubSubManager from .kombu_manager import KombuManager from .redis_manager import RedisManager from .server import Server -__all__ = [Middleware, Server, BaseManager, PubSubManager, KombuManager, - RedisManager] +__all__ = [Middleware, Namespace, Server, BaseManager, PubSubManager, + KombuManager, RedisManager] diff --git a/socketio/namespace.py b/socketio/namespace.py new file mode 100644 index 0000000..5a2b49d --- /dev/null +++ b/socketio/namespace.py @@ -0,0 +1,56 @@ +import types + + +class Namespace(object): + """A container for a set of event handlers for a specific namespace. + + A method of this class named ``on_xxx`` is considered as the event handler + for the event ``'xxx'`` in the namespace this class is registered to. + + There are also the following methods available that insert the current + namespace automatically when none is given before they call their matching + method of the ``Server`` instance: + ``emit``, ``send``, ``enter_room``, ``leave_room``, ``close_room``, + ``rooms``, ``disconnect`` + + Example: + + from socketio import Namespace, Server + class ChatNamespace(Namespace): + def on_msg(self, sid, msg): + # self.server references to the socketio.Server object + data = "[%s]: %s" \ + % (self.server.environ[sid].get("REMOTE_ADDR"), msg) + # Note that we don't pass namespace="/chat" to the emit method. + # It is done automatically for us. + self.emit("msg", data, skip_sid=sid) + return "received your message: %s" % msg + sio = socketio.Server() + sio.register_namespace("/chat", ChatNamespace) + """ + + def __init__(self, name, server): + self.name = name + self.server = server + + # wrap methods of Server object + def get_wrapped_method(func_name): + def wrapped_func(self, *args, **kwargs): + """If namespace is None, it is automatically set to this + object's one before the original method is called. + """ + if kwargs.get('namespace') is None: + kwargs['namespace'] = self.name + return getattr(self.server, func_name)(*args, **kwargs) + return types.MethodType(wrapped_func, self) + for func_name in ('emit', 'send', 'enter_room', 'leave_room', + 'close_room', 'rooms', 'disconnect'): + setattr(self, func_name, get_wrapped_method(func_name)) + + def _get_handlers(self): + """Returns a dict of event names and handlers this namespace provides.""" + handlers = {} + for attr_name in dir(self): + if attr_name.startswith('on_'): + handlers[attr_name[3:]] = getattr(self, attr_name) + return handlers diff --git a/socketio/server.py b/socketio/server.py index 227ff80..e927a7f 100644 --- a/socketio/server.py +++ b/socketio/server.py @@ -150,6 +150,20 @@ class Server(object): return set_handler set_handler(handler) + def register_namespace(self, name, namespace_class): + """Register all handlers of the given ``namespace_class`` under the + namespace named by ``name``. + + :param name: The namespace's name. It can be any string. + :param namespace_class: The sub class of ``Namespace`` to register + handlers of. Don't pass an instance instead. + + See documentation of ``Namespace`` class for an example. + """ + namespace = namespace_class(name, self) + for event, handler in six.iteritems(namespace._get_handlers()): + self.on(event, handler=handler, namespace=name) + def emit(self, event, data=None, room=None, skip_sid=None, namespace=None, callback=None): """Emit a custom event to one or more connected clients. diff --git a/tests/test_server.py b/tests/test_server.py index 372f61a..8c10a06 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -8,6 +8,7 @@ if six.PY3: else: import mock +from socketio import namespace from socketio import packet from socketio import server @@ -45,6 +46,16 @@ class TestServer(unittest.TestCase): self.assertEqual(s.handlers['/']['disconnect'], bar) self.assertEqual(s.handlers['/foo']['disconnect'], bar) + def test_register_namespace(self, eio): + class NS(namespace.Namespace): + def on_foo(self): + self.emit("bar") + + s = server.Server() + s.register_namespace('/ns', NS) + + self.assertIsNotNone(s.handlers['/ns'].get('foo')) + def test_on_bad_event_name(self, eio): s = server.Server() self.assertRaises(ValueError, s.on, 'two-words') From 4ec1f4083e3f8592776f2484c56727ae0f680939 Mon Sep 17 00:00:00 2001 From: Robert Schindler Date: Sun, 21 Aug 2016 14:57:26 +0200 Subject: [PATCH 02/12] Updated docs for class-based namespaces. --- docs/index.rst | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 1e7bbe6..6c3562f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -213,6 +213,41 @@ methods in the :class:`socketio.Server` class. When the ``namespace`` argument is omitted, set to ``None`` or to ``'/'``, the default namespace, representing the physical connection, is used. +Class-based namespaces +---------------------- + +Event handlers for a specific namespace can be grouped together in a +sub class of the ``Namespace`` class. + +A method of this class named ``on_xxx`` is considered as the event handler +for the event ``'xxx'`` in the namespace this class is registered to. + +There are also the following methods available that insert the current +namespace automatically when none is given before they call their matching +method of the ``Server`` instance: + + ``emit``, ``send``, ``enter_room``, ``leave_room``, ``close_room``, + ``rooms``, ``disconnect`` + +Example: + +:: + + from socketio import Namespace, Server + + class ChatNamespace(Namespace): + def on_msg(self, sid, msg): + # self.server references to the socketio.Server object + data = "[%s]: %s" \ + % (self.server.environ[sid].get("REMOTE_ADDR"), msg) + # Note that we don't pass namespace="/chat" to the emit method. + # It is done automatically for us. + self.emit("msg", data, skip_sid=sid) + return "received your message: %s" % msg + + sio = socketio.Server() + sio.register_namespace("/chat", ChatNamespace) + Using a Message Queue --------------------- From a38fdbc28bfc7d4a1824475775b9fadc3da51290 Mon Sep 17 00:00:00 2001 From: Robert Schindler Date: Sun, 21 Aug 2016 15:08:55 +0200 Subject: [PATCH 03/12] Unified indentation --- socketio/namespace.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/socketio/namespace.py b/socketio/namespace.py index 5a2b49d..af4920b 100644 --- a/socketio/namespace.py +++ b/socketio/namespace.py @@ -48,9 +48,10 @@ class Namespace(object): setattr(self, func_name, get_wrapped_method(func_name)) def _get_handlers(self): - """Returns a dict of event names and handlers this namespace provides.""" - handlers = {} - for attr_name in dir(self): - if attr_name.startswith('on_'): - handlers[attr_name[3:]] = getattr(self, attr_name) - return handlers + """Returns a dict of event names and handlers this namespace + provides.""" + handlers = {} + for attr_name in dir(self): + if attr_name.startswith('on_'): + handlers[attr_name[3:]] = getattr(self, attr_name) + return handlers From 517641603e062581db514e04346b739478416977 Mon Sep 17 00:00:00 2001 From: Robert Schindler Date: Mon, 22 Aug 2016 09:08:31 +0200 Subject: [PATCH 04/12] Added support for event names with spaces in class-based namespaces. --- docs/index.rst | 5 +++++ socketio/namespace.py | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 6c3562f..9d6c496 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -245,6 +245,11 @@ Example: self.emit("msg", data, skip_sid=sid) return "received your message: %s" % msg + # Here we set the event name explicitly by decorator. + @Namespace.event_name("event name with spaces") + def foo(self, sid): + # ... + sio = socketio.Server() sio.register_namespace("/chat", ChatNamespace) diff --git a/socketio/namespace.py b/socketio/namespace.py index af4920b..c797a14 100644 --- a/socketio/namespace.py +++ b/socketio/namespace.py @@ -16,6 +16,7 @@ class Namespace(object): Example: from socketio import Namespace, Server + class ChatNamespace(Namespace): def on_msg(self, sid, msg): # self.server references to the socketio.Server object @@ -25,6 +26,12 @@ class Namespace(object): # It is done automatically for us. self.emit("msg", data, skip_sid=sid) return "received your message: %s" % msg + + # Here we set the event name explicitly by decorator. + @Namespace.event_name("event name with spaces") + def foo(self, sid): + # ... + sio = socketio.Server() sio.register_namespace("/chat", ChatNamespace) """ @@ -52,6 +59,30 @@ class Namespace(object): provides.""" handlers = {} for attr_name in dir(self): - if attr_name.startswith('on_'): - handlers[attr_name[3:]] = getattr(self, attr_name) + attr = getattr(self, attr_name) + if hasattr(attr, '_event_name'): + event_name = getattr(attr, '_event_name') + elif attr_name.startswith('on_'): + event_name = attr_name[3:] + else: + continue + handlers[event_name] = attr return handlers + + @staticmethod + def event_name(name): + """Decorator to overwrite event names: + + @Namespace.event_name("event name with spaces") + def foo(self, sid, data): + return "received: %s" % data + + Note that this must be the last decorator applied on an event handler + (last applied means listed first) in order to work. + """ + def wrapper(handler): + def wrapped_handler(*args, **kwargs): + return handler(*args, **kwargs) + wrapped_handler._event_name = name + return wrapped_handler + return wrapper From 460aed1bdbbd3cc94cc469e058895a8c8dd42db5 Mon Sep 17 00:00:00 2001 From: Robert Schindler Date: Mon, 22 Aug 2016 10:49:11 +0200 Subject: [PATCH 05/12] register_namespace() now returns Namespace instance. --- socketio/server.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/socketio/server.py b/socketio/server.py index e927a7f..90c407c 100644 --- a/socketio/server.py +++ b/socketio/server.py @@ -158,11 +158,14 @@ class Server(object): :param namespace_class: The sub class of ``Namespace`` to register handlers of. Don't pass an instance instead. + This function returns the instance of ``namespace_class`` created. + See documentation of ``Namespace`` class for an example. """ namespace = namespace_class(name, self) for event, handler in six.iteritems(namespace._get_handlers()): self.on(event, handler=handler, namespace=name) + return namespace def emit(self, event, data=None, room=None, skip_sid=None, namespace=None, callback=None): From 8ab72d0eabc40b67b14e3630ebae86e2e0af96ed Mon Sep 17 00:00:00 2001 From: Robert Schindler Date: Mon, 22 Aug 2016 11:18:19 +0200 Subject: [PATCH 06/12] Implemented dynamic event -> handler mapping in Namespace class. --- socketio/namespace.py | 15 +++++++-------- socketio/server.py | 22 ++++++++++++++++------ tests/test_server.py | 9 +++++++-- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/socketio/namespace.py b/socketio/namespace.py index c797a14..fa10cf4 100644 --- a/socketio/namespace.py +++ b/socketio/namespace.py @@ -54,20 +54,19 @@ class Namespace(object): 'close_room', 'rooms', 'disconnect'): setattr(self, func_name, get_wrapped_method(func_name)) - def _get_handlers(self): - """Returns a dict of event names and handlers this namespace - provides.""" - handlers = {} + def get_event_handler(self, event_name): + """Returns the event handler for requested event or ``None``.""" for attr_name in dir(self): attr = getattr(self, attr_name) if hasattr(attr, '_event_name'): - event_name = getattr(attr, '_event_name') + _event_name = getattr(attr, '_event_name') elif attr_name.startswith('on_'): - event_name = attr_name[3:] + _event_name = attr_name[3:] else: continue - handlers[event_name] = attr - return handlers + if _event_name == event_name: + return attr + return None @staticmethod def event_name(name): diff --git a/socketio/server.py b/socketio/server.py index 90c407c..bce6aa8 100644 --- a/socketio/server.py +++ b/socketio/server.py @@ -4,6 +4,7 @@ import engineio import six from . import base_manager +from . import namespace as sio_namespace from . import packet @@ -141,6 +142,10 @@ class Server(object): namespace = namespace or '/' def set_handler(handler): + if isinstance(self.handlers.get(namespace), + sio_namespace.Namespace): + raise ValueError('A Namespace object has been registered ' + 'for this namespace.') if namespace not in self.handlers: self.handlers[namespace] = {} self.handlers[namespace][event] = handler @@ -151,8 +156,8 @@ class Server(object): set_handler(handler) def register_namespace(self, name, namespace_class): - """Register all handlers of the given ``namespace_class`` under the - namespace named by ``name``. + """Register the given ``namespace_class`` under the namespace named + by ``name``. :param name: The namespace's name. It can be any string. :param namespace_class: The sub class of ``Namespace`` to register @@ -163,8 +168,7 @@ class Server(object): See documentation of ``Namespace`` class for an example. """ namespace = namespace_class(name, self) - for event, handler in six.iteritems(namespace._get_handlers()): - self.on(event, handler=handler, namespace=name) + self.handlers[name] = namespace return namespace def emit(self, event, data=None, room=None, skip_sid=None, namespace=None, @@ -441,8 +445,14 @@ class Server(object): def _trigger_event(self, event, namespace, *args): """Invoke an application event handler.""" - if namespace in self.handlers and event in self.handlers[namespace]: - return self.handlers[namespace][event](*args) + handler = None + ns = self.handlers.get(namespace) + if isinstance(ns, sio_namespace.Namespace): + handler = ns.get_event_handler(event) + elif isinstance(ns, dict): + handler = ns.get(event) + if handler is not None: + return handler(*args) def _handle_eio_connect(self, sid, environ): """Handle the Engine.IO connection event.""" diff --git a/tests/test_server.py b/tests/test_server.py index 8c10a06..1939cd5 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -48,13 +48,18 @@ class TestServer(unittest.TestCase): def test_register_namespace(self, eio): class NS(namespace.Namespace): - def on_foo(self): + def on_foo(self, sid): self.emit("bar") + @namespace.Namespace.event_name('foo bar') + def abc(self, sid): + self.emit("foo bar") s = server.Server() s.register_namespace('/ns', NS) - self.assertIsNotNone(s.handlers['/ns'].get('foo')) + self.assertIsNotNone(s.handlers['/ns'].get_event_handler('foo')) + self.assertIsNotNone(s.handlers['/ns'].get_event_handler('foo bar')) + self.assertIsNone(s.handlers['/ns'].get_event_handler('abc')) def test_on_bad_event_name(self, eio): s = server.Server() From 5f51c7ffa8f0b7d8d1fb91a37c2e1c0805859681 Mon Sep 17 00:00:00 2001 From: Robert Schindler Date: Mon, 22 Aug 2016 11:59:23 +0200 Subject: [PATCH 07/12] Unified get_event_handler(event_name) so that it can be overloaded on both server and namespace level. --- socketio/namespace.py | 6 +++++- socketio/server.py | 22 ++++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/socketio/namespace.py b/socketio/namespace.py index fa10cf4..ab3a59b 100644 --- a/socketio/namespace.py +++ b/socketio/namespace.py @@ -55,7 +55,11 @@ class Namespace(object): setattr(self, func_name, get_wrapped_method(func_name)) def get_event_handler(self, event_name): - """Returns the event handler for requested event or ``None``.""" + """Returns the event handler for given ``event`` in this namespace or + ``None``, if none exists. + + :param event: The event name the handler is required for. + """ for attr_name in dir(self): attr = getattr(self, attr_name) if hasattr(attr, '_event_name'): diff --git a/socketio/server.py b/socketio/server.py index bce6aa8..976dada 100644 --- a/socketio/server.py +++ b/socketio/server.py @@ -171,6 +171,21 @@ class Server(object): self.handlers[name] = namespace return namespace + def get_event_handler(self, event, namespace): + """Returns the event handler for given ``event`` and ``namespace`` or + ``None``, if none exists. + + :param event: The event name the handler is required for. + :param namespace: The Socket.IO namespace for the event. + """ + handler = None + ns = self.handlers.get(namespace) + if isinstance(ns, sio_namespace.Namespace): + handler = ns.get_event_handler(event) + elif isinstance(ns, dict): + handler = ns.get(event) + return handler + def emit(self, event, data=None, room=None, skip_sid=None, namespace=None, callback=None): """Emit a custom event to one or more connected clients. @@ -445,12 +460,7 @@ class Server(object): def _trigger_event(self, event, namespace, *args): """Invoke an application event handler.""" - handler = None - ns = self.handlers.get(namespace) - if isinstance(ns, sio_namespace.Namespace): - handler = ns.get_event_handler(event) - elif isinstance(ns, dict): - handler = ns.get(event) + handler = self.get_event_handler(event, namespace) if handler is not None: return handler(*args) From 75b2dfa0f9a3cbc8cdba2c0e508a149ec860f400 Mon Sep 17 00:00:00 2001 From: Robert Schindler Date: Mon, 22 Aug 2016 12:06:24 +0200 Subject: [PATCH 08/12] Inserted blank line for travis-ci to pass. --- tests/test_server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_server.py b/tests/test_server.py index 1939cd5..08547b4 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -50,6 +50,7 @@ class TestServer(unittest.TestCase): class NS(namespace.Namespace): def on_foo(self, sid): self.emit("bar") + @namespace.Namespace.event_name('foo bar') def abc(self, sid): self.emit("foo bar") From cb168cf1128387987fc2ba8e517f77dbaf0c577b Mon Sep 17 00:00:00 2001 From: Robert Schindler Date: Mon, 22 Aug 2016 14:01:44 +0200 Subject: [PATCH 09/12] Implemented event handler middlewares + docs for them. --- docs/index.rst | 90 ++++++++++++++++++++++++++++++++++++++++++- socketio/namespace.py | 24 ++++++++---- socketio/server.py | 14 +++++-- socketio/util.py | 61 +++++++++++++++++++++++++++++ tests/test_server.py | 8 ++-- 5 files changed, 181 insertions(+), 16 deletions(-) create mode 100644 socketio/util.py diff --git a/docs/index.rst b/docs/index.rst index 9d6c496..835b2b0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -213,6 +213,86 @@ methods in the :class:`socketio.Server` class. When the ``namespace`` argument is omitted, set to ``None`` or to ``'/'``, the default namespace, representing the physical connection, is used. +Event handler middlewares +------------------------- + +Event handler middlewares are objects with the following methods or a +subset of them: + +* ``before_event(*args)`` is called before the event handler is executed + with the event name, the namespace and the list of arguments the event + handler will be called with. It may alter the arguments in that list + eventually. +* ``after_event(*args)`` is called after the event handler with the + event name, the namespace and the list of values the event handler + returned. It may alter that values eventually. + +If one of these methods returns something else than ``None``, execution +is stopped at that point and the returned value is treated as if it was +returned by the event handler. + +Event handler middlewares can be chained. The ``before_event`` methods +will be executed in the same order the middlewares were added, the +``after_event`` methods are called in reversed order. + +Note that you can also use classes (objects of type ``type``) instead +of instances as event handler middlewares. If you do so, they are +instantiated with no arguments every time they are used to process +an event. + +Example: + +:: + + from socketio import server + + class MyMiddleware(object): + def before_event(self, event, namespace, args): + print("before_event called with:", args) + if len(args) >= 2 or args[1] != "secret": + return "Ha, you don't know the password!" + + def after_event(self, event, namespace, args): + args[0] = "I faked the response!" + + sio = socketio.server.Server() + + @sio.on("/foo") + def foo(sid, data): + print("foo executed") + return "foo executed" + + sio.middlewares.append(MyMiddleware()) + +In this example, the middleware would be applied to every event +handler. There is also a decorator available to add it to specific +handlers only. Given the middleware class from above, it would be used +as follows: + +:: + + from socketio import util + + sio = socketio.server.Server() + + @sio.on("/foo") + @util.apply_middleware(MyMiddleware) + def foo(sid, data): + print("foo executed") + return "foo executed" + +Middlewares added by the decorator are treated as if they were added +*after* the server-wide or namespace-wide middlewares. Naturally, +decorators are applied from bottom to top. Hence the following will +first add ``MW1`` and then ``MW2``. + +:: + + @util.apply_middleware(MW2) + @util.apply_middleware(MW1) + def foo(sid): + # ... + Class-based namespaces ---------------------- @@ -251,7 +331,15 @@ Example: # ... sio = socketio.Server() - sio.register_namespace("/chat", ChatNamespace) + ns = sio.register_namespace("/chat", ChatNamespace) + # ns now holds the instantiated ChatNamespace object + +``Namespace`` objects do also support middlewares that are applied on +all event handlers in that namespace: + +:: + + ns.middlewares.append(MyMiddleware()) Using a Message Queue --------------------- diff --git a/socketio/namespace.py b/socketio/namespace.py index ab3a59b..56ec962 100644 --- a/socketio/namespace.py +++ b/socketio/namespace.py @@ -1,5 +1,7 @@ import types +from . import util + class Namespace(object): """A container for a set of event handlers for a specific namespace. @@ -10,6 +12,7 @@ class Namespace(object): There are also the following methods available that insert the current namespace automatically when none is given before they call their matching method of the ``Server`` instance: + ``emit``, ``send``, ``enter_room``, ``leave_room``, ``close_room``, ``rooms``, ``disconnect`` @@ -39,6 +42,7 @@ class Namespace(object): def __init__(self, name, server): self.name = name self.server = server + self.middlewares = [] # wrap methods of Server object def get_wrapped_method(func_name): @@ -54,7 +58,7 @@ class Namespace(object): 'close_room', 'rooms', 'disconnect'): setattr(self, func_name, get_wrapped_method(func_name)) - def get_event_handler(self, event_name): + def _get_event_handler(self, event_name): """Returns the event handler for given ``event`` in this namespace or ``None``, if none exists. @@ -62,15 +66,17 @@ class Namespace(object): """ for attr_name in dir(self): attr = getattr(self, attr_name) - if hasattr(attr, '_event_name'): - _event_name = getattr(attr, '_event_name') + if hasattr(attr, '_sio_event_name'): + _event_name = getattr(attr, '_sio_event_name') elif attr_name.startswith('on_'): _event_name = attr_name[3:] else: continue if _event_name == event_name: - return attr - return None + extra_middlewares = getattr(attr, '_sio_middlewares', []) + return util._apply_middlewares( + self.middlewares + extra_middlewares, event_name, + self.name, attr) @staticmethod def event_name(name): @@ -80,12 +86,14 @@ class Namespace(object): def foo(self, sid, data): return "received: %s" % data - Note that this must be the last decorator applied on an event handler - (last applied means listed first) in order to work. + Note that you must not add third-party decorators after the ones + provided by this library because you'll otherwise loose metadata + that this decorators create. You can add them before instead. """ def wrapper(handler): def wrapped_handler(*args, **kwargs): return handler(*args, **kwargs) - wrapped_handler._event_name = name + util._copy_sio_properties(handler, wrapped_handler) + wrapped_handler._sio_event_name = name return wrapped_handler return wrapper diff --git a/socketio/server.py b/socketio/server.py index 976dada..f1a0c3f 100644 --- a/socketio/server.py +++ b/socketio/server.py @@ -6,6 +6,7 @@ import six from . import base_manager from . import namespace as sio_namespace from . import packet +from . import util class Server(object): @@ -78,6 +79,7 @@ class Server(object): self.environ = {} self.handlers = {} + self.middlewares = [] self._binary_packet = [] @@ -171,7 +173,7 @@ class Server(object): self.handlers[name] = namespace return namespace - def get_event_handler(self, event, namespace): + def _get_event_handler(self, event, namespace): """Returns the event handler for given ``event`` and ``namespace`` or ``None``, if none exists. @@ -181,10 +183,14 @@ class Server(object): handler = None ns = self.handlers.get(namespace) if isinstance(ns, sio_namespace.Namespace): - handler = ns.get_event_handler(event) + handler = ns._get_event_handler(event) elif isinstance(ns, dict): handler = ns.get(event) - return handler + if handler is not None: + extra_middlewares = getattr(handler, '_sio_middlewares', []) + return util._apply_middlewares( + self.middlewares + extra_middlewares, event, namespace, + handler) def emit(self, event, data=None, room=None, skip_sid=None, namespace=None, callback=None): @@ -460,7 +466,7 @@ class Server(object): def _trigger_event(self, event, namespace, *args): """Invoke an application event handler.""" - handler = self.get_event_handler(event, namespace) + handler = self._get_event_handler(event, namespace) if handler is not None: return handler(*args) diff --git a/socketio/util.py b/socketio/util.py new file mode 100644 index 0000000..5b1f1b1 --- /dev/null +++ b/socketio/util.py @@ -0,0 +1,61 @@ +def _copy_sio_properties(from_func, to_func): + """Copies all properties starting with ``'_sio'`` from one function to + another.""" + for key in dir(from_func): + if key.startswith('_sio'): + setattr(to_func, key, getattr(from_func, key)) + +def _apply_middlewares(middlewares, event, namespace, handler): + """Wraps the given handler with a wrapper that executes middlewares + before and after the real event handler.""" + if not middlewares: + return handler + + def wrapped(*args): + _middlewares = [] + for middleware in middlewares: + if isinstance(middleware, type): + _middlewares.append(middleware()) + else: + _middlewares.append(middleware) + + for middleware in _middlewares: + if hasattr(middleware, 'before_event'): + result = middleware.before_event(event, namespace, args) + if result is not None: + return result + + result = handler(*args) + if result is None: + data = [] + elif isinstance(result, tuple): + data = list(result) + else: + data = [result] + + for middleware in reversed(_middlewares): + if hasattr(middleware, 'after_event'): + result = middleware.after_event(event, namespace, data) + if result is not None: + return result + + return tuple(data) + + return wrapped + +def apply_middleware(middleware): + """Returns a decorator for event handlers that adds the given + middleware to the handler decorated with it. + + :param middleware: The middleware to add + + Note that you must not add third-party decorators after the ones + provided by this library because you'll otherwise loose metadata + that this decorators create. You can add them before instead. + """ + def wrapper(handler): + if not hasattr(handler, '_sio_middlewares'): + handler._sio_middlewares = [] + handler._sio_middlewares.append(middleware) + return handler + return wrapper diff --git a/tests/test_server.py b/tests/test_server.py index 08547b4..b11b530 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -58,9 +58,9 @@ class TestServer(unittest.TestCase): s = server.Server() s.register_namespace('/ns', NS) - self.assertIsNotNone(s.handlers['/ns'].get_event_handler('foo')) - self.assertIsNotNone(s.handlers['/ns'].get_event_handler('foo bar')) - self.assertIsNone(s.handlers['/ns'].get_event_handler('abc')) + self.assertIsNotNone(s.handlers['/ns']._get_event_handler('foo')) + self.assertIsNotNone(s.handlers['/ns']._get_event_handler('foo bar')) + self.assertIsNone(s.handlers['/ns']._get_event_handler('abc')) def test_on_bad_event_name(self, eio): s = server.Server() @@ -215,6 +215,7 @@ class TestServer(unittest.TestCase): mgr = mock.MagicMock() s = server.Server(client_manager=mgr) handler = mock.MagicMock(return_value=False) + del handler._sio_middlewares s.on('connect', handler) s._handle_eio_connect('123', 'environ') handler.assert_called_once_with('123', 'environ') @@ -226,6 +227,7 @@ class TestServer(unittest.TestCase): mgr = mock.MagicMock() s = server.Server(client_manager=mgr) handler = mock.MagicMock(return_value=False) + del handler._sio_middlewares s.on('connect', handler, namespace='/foo') s._handle_eio_connect('123', 'environ') s._handle_eio_message('123', '0/foo') From 7c398c6b24f2fcf6c54dae6b4b069d59c6edb3ab Mon Sep 17 00:00:00 2001 From: Robert Schindler Date: Mon, 22 Aug 2016 15:16:00 +0200 Subject: [PATCH 10/12] Code style fixes --- socketio/namespace.py | 4 ++-- socketio/util.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/socketio/namespace.py b/socketio/namespace.py index 56ec962..c778d1e 100644 --- a/socketio/namespace.py +++ b/socketio/namespace.py @@ -75,8 +75,8 @@ class Namespace(object): if _event_name == event_name: extra_middlewares = getattr(attr, '_sio_middlewares', []) return util._apply_middlewares( - self.middlewares + extra_middlewares, event_name, - self.name, attr) + self.middlewares + extra_middlewares, event_name, + self.name, attr) @staticmethod def event_name(name): diff --git a/socketio/util.py b/socketio/util.py index 5b1f1b1..fd89ac7 100644 --- a/socketio/util.py +++ b/socketio/util.py @@ -5,6 +5,7 @@ def _copy_sio_properties(from_func, to_func): if key.startswith('_sio'): setattr(to_func, key, getattr(from_func, key)) + def _apply_middlewares(middlewares, event, namespace, handler): """Wraps the given handler with a wrapper that executes middlewares before and after the real event handler.""" @@ -43,6 +44,7 @@ def _apply_middlewares(middlewares, event, namespace, handler): return wrapped + def apply_middleware(middleware): """Returns a decorator for event handlers that adds the given middleware to the handler decorated with it. From 046638e7b43296f5626d3889e6aec49d7603100d Mon Sep 17 00:00:00 2001 From: Robert Schindler Date: Mon, 22 Aug 2016 19:24:13 +0200 Subject: [PATCH 11/12] - Fixed a bug at applying middlewares. - Simplified decorators. - Added test for event handler middlewares. --- docs/index.rst | 4 ++ socketio/__init__.py | 3 +- socketio/namespace.py | 22 +++++------ socketio/server.py | 74 ++++++++++++++++++++++++---------- socketio/util.py | 78 +++++++++++++++--------------------- tests/test_server.py | 92 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 192 insertions(+), 81 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 835b2b0..4aa2fde 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -226,6 +226,10 @@ subset of them: * ``after_event(*args)`` is called after the event handler with the event name, the namespace and the list of values the event handler returned. It may alter that values eventually. +* ``ignore_event`` is called before the middleware is applied to an + event handler with the event name and namespace as arguments. If its + return value resolves to ``True`` the middleware is not applied to that + particular event handler. If one of these methods returns something else than ``None``, execution is stopped at that point and the returned value is treated as if it was diff --git a/socketio/__init__.py b/socketio/__init__.py index ff68424..514583f 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -5,6 +5,7 @@ from .pubsub_manager import PubSubManager from .kombu_manager import KombuManager from .redis_manager import RedisManager from .server import Server +from .util import apply_middleware __all__ = [Middleware, Namespace, Server, BaseManager, PubSubManager, - KombuManager, RedisManager] + KombuManager, RedisManager, apply_middleware] diff --git a/socketio/namespace.py b/socketio/namespace.py index c778d1e..8fe4a20 100644 --- a/socketio/namespace.py +++ b/socketio/namespace.py @@ -36,7 +36,8 @@ class Namespace(object): # ... sio = socketio.Server() - sio.register_namespace("/chat", ChatNamespace) + ns = sio.register_namespace("/chat", ChatNamespace) + # ns now holds the instantiated ChatNamespace object """ def __init__(self, name, server): @@ -73,10 +74,7 @@ class Namespace(object): else: continue if _event_name == event_name: - extra_middlewares = getattr(attr, '_sio_middlewares', []) - return util._apply_middlewares( - self.middlewares + extra_middlewares, event_name, - self.name, attr) + return attr @staticmethod def event_name(name): @@ -86,14 +84,12 @@ class Namespace(object): def foo(self, sid, data): return "received: %s" % data - Note that you must not add third-party decorators after the ones - provided by this library because you'll otherwise loose metadata - that this decorators create. You can add them before instead. + Ensure that you only add well-behaving decorators after this one + (meaning such that preserve attributes) because you'll loose them + otherwise. """ + @util._simple_decorator def wrapper(handler): - def wrapped_handler(*args, **kwargs): - return handler(*args, **kwargs) - util._copy_sio_properties(handler, wrapped_handler) - wrapped_handler._sio_event_name = name - return wrapped_handler + handler._sio_event_name = name + return handler return wrapper diff --git a/socketio/server.py b/socketio/server.py index f1a0c3f..2215199 100644 --- a/socketio/server.py +++ b/socketio/server.py @@ -6,7 +6,6 @@ import six from . import base_manager from . import namespace as sio_namespace from . import packet -from . import util class Server(object): @@ -173,25 +172,6 @@ class Server(object): self.handlers[name] = namespace return namespace - def _get_event_handler(self, event, namespace): - """Returns the event handler for given ``event`` and ``namespace`` or - ``None``, if none exists. - - :param event: The event name the handler is required for. - :param namespace: The Socket.IO namespace for the event. - """ - handler = None - ns = self.handlers.get(namespace) - if isinstance(ns, sio_namespace.Namespace): - handler = ns._get_event_handler(event) - elif isinstance(ns, dict): - handler = ns.get(event) - if handler is not None: - extra_middlewares = getattr(handler, '_sio_middlewares', []) - return util._apply_middlewares( - self.middlewares + extra_middlewares, event, namespace, - handler) - def emit(self, event, data=None, room=None, skip_sid=None, namespace=None, callback=None): """Emit a custom event to one or more connected clients. @@ -466,10 +446,62 @@ class Server(object): def _trigger_event(self, event, namespace, *args): """Invoke an application event handler.""" - handler = self._get_event_handler(event, namespace) + handler = None + middlewares = list(self.middlewares) + ns = self.handlers.get(namespace) + if isinstance(ns, sio_namespace.Namespace): + middlewares.extend(ns.middlewares) + handler = ns._get_event_handler(event) + elif isinstance(ns, dict): + handler = ns.get(event) if handler is not None: + middlewares.extend(getattr(handler, '_sio_middlewares', [])) + handler = self._apply_middlewares(middlewares, event, namespace, + handler) return handler(*args) + @staticmethod + def _apply_middlewares(middlewares, event, namespace, handler): + """Wraps the given handler with a wrapper that executes middlewares + before and after the real event handler.""" + + _middlewares = [] + for middleware in middlewares: + if isinstance(middleware, type): + middleware = middleware() + if not hasattr(middleware, 'ignore_event') or \ + not middleware.ignore_event(event, namespace): + _middlewares.append(middleware) + if not _middlewares: + return handler + + def wrapped(*args): + args = list(args) + + for middleware in _middlewares: + if hasattr(middleware, 'before_event'): + result = middleware.before_event(event, namespace, args) + if result is not None: + return result + + result = handler(*args) + if result is None: + data = [] + elif isinstance(result, tuple): + data = list(result) + else: + data = [result] + + for middleware in reversed(_middlewares): + if hasattr(middleware, 'after_event'): + result = middleware.after_event(event, namespace, data) + if result is not None: + return result + + return tuple(data) + + return wrapped + def _handle_eio_connect(self, sid, environ): """Handle the Engine.IO connection event.""" self.environ[sid] = environ diff --git a/socketio/util.py b/socketio/util.py index fd89ac7..5fd885c 100644 --- a/socketio/util.py +++ b/socketio/util.py @@ -1,48 +1,33 @@ -def _copy_sio_properties(from_func, to_func): - """Copies all properties starting with ``'_sio'`` from one function to - another.""" - for key in dir(from_func): - if key.startswith('_sio'): - setattr(to_func, key, getattr(from_func, key)) - - -def _apply_middlewares(middlewares, event, namespace, handler): - """Wraps the given handler with a wrapper that executes middlewares - before and after the real event handler.""" - if not middlewares: - return handler - - def wrapped(*args): - _middlewares = [] - for middleware in middlewares: - if isinstance(middleware, type): - _middlewares.append(middleware()) - else: - _middlewares.append(middleware) - - for middleware in _middlewares: - if hasattr(middleware, 'before_event'): - result = middleware.before_event(event, namespace, args) - if result is not None: - return result - - result = handler(*args) - if result is None: - data = [] - elif isinstance(result, tuple): - data = list(result) - else: - data = [result] - - for middleware in reversed(_middlewares): - if hasattr(middleware, 'after_event'): - result = middleware.after_event(event, namespace, data) - if result is not None: - return result +def _simple_decorator(decorator): + """This decorator can be used to turn simple functions + into well-behaved decorators, so long as the decorators + are fairly simple. If a decorator expects a function and + returns a function (no descriptors), and if it doesn't + modify function attributes or docstring, then it is + eligible to use this. Simply apply @_simple_decorator to + your decorator and it will automatically preserve the + docstring and function attributes of functions to which + it is applied. + + Also preserves all properties starting with ``'_sio'``. + """ + def copy_attrs(a, b): + """Copies attributes from a to b.""" + for attr_name in ('__name__', '__doc__'): + if hasattr(a, attr_name): + setattr(b, attr_name, getattr(a, attr_name)) + if hasattr(a, '__dict__') and hasattr(b, '__dict__'): + b.__dict__.update(a.__dict__) - return tuple(data) + def new_decorator(f): + g = decorator(f) + copy_attrs(f, g) + return g - return wrapped + # Now a few lines needed to make _simple_decorator itself + # be a well-behaved decorator. + copy_attrs(decorator, new_decorator) + return new_decorator def apply_middleware(middleware): @@ -51,10 +36,11 @@ def apply_middleware(middleware): :param middleware: The middleware to add - Note that you must not add third-party decorators after the ones - provided by this library because you'll otherwise loose metadata - that this decorators create. You can add them before instead. + Ensure that you only add well-behaving decorators after this one + (meaning such that preserve attributes) because you'll loose them + otherwise. """ + @_simple_decorator def wrapper(handler): if not hasattr(handler, '_sio_middlewares'): handler._sio_middlewares = [] diff --git a/tests/test_server.py b/tests/test_server.py index b11b530..1a137d8 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -11,6 +11,7 @@ else: from socketio import namespace from socketio import packet from socketio import server +from socketio import util @mock.patch('engineio.Server') @@ -62,6 +63,97 @@ class TestServer(unittest.TestCase): self.assertIsNotNone(s.handlers['/ns']._get_event_handler('foo bar')) self.assertIsNone(s.handlers['/ns']._get_event_handler('abc')) + def test_middleware(self, eio): + class MW: + def __init__(self): + self.ignore_event = mock.MagicMock(side_effect=100*[False]) + self.before_event = mock.MagicMock(side_effect=100*[None]) + self.after_event = mock.MagicMock(side_effect=100*[None]) + + mw1 = MW() + mw2 = MW() + mw3 = MW() + mw4 = MW() + mw4.ignore_event = 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]) + + class NS(namespace.Namespace): + def on_foo(self, sid): + pass + + @namespace.Namespace.event_name('foo bar') + @util.apply_middleware(mw4) + def some_name(self, sid): + pass + + s = server.Server() + s.middlewares.append(mw1) + + @s.on('abc') + @util.apply_middleware(mw2) + def abc(sid): + pass + + ns = s.register_namespace('/ns', NS) + ns.middlewares.append(mw3) + + # only mw1 and mw3 should run completely + s._trigger_event('foo', '/ns', '123') + self.assertEqual(mw1.before_event.call_count, 1) + self.assertEqual(mw1.after_event.call_count, 1) + self.assertEqual(mw2.before_event.call_count, 0) + self.assertEqual(mw2.after_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, 0) + self.assertEqual(mw4.after_event.call_count, 0) + + # only mw1 and mw3 should run completely, mw4 is enabled but ignored + 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(mw4.after_event.call_count, 0) + + # again, this time mw4 before_event should be triggered + 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(mw4.after_event.call_count, 0) + + # again, this time mw4 before + after_event should be triggered + # but after_event should abort execution + s._trigger_event('foo bar', '/ns', '123') + self.assertEqual(mw1.before_event.call_count, 4) + 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, 4) + self.assertEqual(mw3.after_event.call_count, 2) + self.assertEqual(mw4.before_event.call_count, 2) + self.assertEqual(mw4.after_event.call_count, 1) + + # only mw1 and mw2 should run completely + s._trigger_event('abc', '/', '123') + self.assertEqual(mw1.before_event.call_count, 5) + self.assertEqual(mw1.after_event.call_count, 3) + 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, 2) + self.assertEqual(mw4.before_event.call_count, 2) + self.assertEqual(mw4.after_event.call_count, 1) + def test_on_bad_event_name(self, eio): s = server.Server() self.assertRaises(ValueError, s.on, 'two-words') From edf49db6e9261499603e7fab49790c00204298bf Mon Sep 17 00:00:00 2001 From: Robert Schindler Date: Mon, 22 Aug 2016 21:44:01 +0200 Subject: [PATCH 12/12] Code style fixes --- tests/test_server.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index 1a137d8..3c17573 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -66,17 +66,17 @@ class TestServer(unittest.TestCase): def test_middleware(self, eio): class MW: def __init__(self): - self.ignore_event = mock.MagicMock(side_effect=100*[False]) - self.before_event = mock.MagicMock(side_effect=100*[None]) - self.after_event = mock.MagicMock(side_effect=100*[None]) + self.ignore_event = mock.MagicMock(side_effect=100 * [False]) + self.before_event = mock.MagicMock(side_effect=100 * [None]) + self.after_event = mock.MagicMock(side_effect=100 * [None]) mw1 = MW() mw2 = MW() mw3 = MW() mw4 = MW() - mw4.ignore_event = 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]) + mw4.ignore_event = 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]) class NS(namespace.Namespace): def on_foo(self, sid):