Browse Source

Implemented event handler middlewares + docs for them.

pull/43/head
Robert Schindler 9 years ago
parent
commit
1783778176
  1. 107
      docs/index.rst
  2. 3
      socketio/__init__.py
  3. 23
      socketio/namespace.py
  4. 70
      socketio/server.py
  5. 16
      socketio/util.py
  6. 100
      tests/test_server.py

107
docs/index.rst

@ -213,6 +213,103 @@ 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.
* ``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
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 ignore_event(self, event, namespace):
# This middleware won't be applied to connect and disconnect
# event handlers.
return event in ("connect", "disconnect")
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
executed by ``sio``, except for the ``'connect'`` and ``'disconnect'``
handlers.
Middlewares can be added to a ``Namespace`` object as well by inserting
them into its ``middlewares`` ``list`` attribute. They are applied
after the server-wide middlewares to every event handler defined in that
``Namespace`` object.
There is also a decorator available to add a middleware to a specific
handler 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 and 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 +348,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
---------------------

3
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]

23
socketio/namespace.py

@ -10,6 +10,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``
@ -33,12 +34,14 @@ 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):
self.name = name
self.server = server
self.middlewares = []
# wrap methods of Server object
def get_wrapped_method(func_name):
@ -54,7 +57,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 +65,14 @@ 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
@staticmethod
def event_name(name):
@ -80,12 +82,11 @@ 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.
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):
def wrapped_handler(*args, **kwargs):
return handler(*args, **kwargs)
wrapped_handler._event_name = name
return wrapped_handler
handler._sio_event_name = name
return handler
return wrapper

70
socketio/server.py

@ -78,6 +78,7 @@ class Server(object):
self.environ = {}
self.handlers = {}
self.middlewares = []
self._binary_packet = []
@ -171,21 +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)
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.
@ -460,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

16
socketio/util.py

@ -0,0 +1,16 @@
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
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_middlewares'):
handler._sio_middlewares = []
handler._sio_middlewares.append(middleware)
return handler
return wrapper

100
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')
@ -58,9 +59,100 @@ 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_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()
@ -215,6 +307,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 +319,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')

Loading…
Cancel
Save