Browse Source

Implemented event handler middlewares + docs for them.

pull/43/merge^2
Robert Schindler 9 years ago
parent
commit
cb168cf112
  1. 90
      docs/index.rst
  2. 24
      socketio/namespace.py
  3. 14
      socketio/server.py
  4. 61
      socketio/util.py
  5. 8
      tests/test_server.py

90
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 When the ``namespace`` argument is omitted, set to ``None`` or to ``'/'``, the
default namespace, representing the physical connection, is used. 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 Class-based namespaces
---------------------- ----------------------
@ -251,7 +331,15 @@ Example:
# ... # ...
sio = socketio.Server() 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 Using a Message Queue
--------------------- ---------------------

24
socketio/namespace.py

@ -1,5 +1,7 @@
import types import types
from . import util
class Namespace(object): class Namespace(object):
"""A container for a set of event handlers for a specific namespace. """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 There are also the following methods available that insert the current
namespace automatically when none is given before they call their matching namespace automatically when none is given before they call their matching
method of the ``Server`` instance: method of the ``Server`` instance:
``emit``, ``send``, ``enter_room``, ``leave_room``, ``close_room``, ``emit``, ``send``, ``enter_room``, ``leave_room``, ``close_room``,
``rooms``, ``disconnect`` ``rooms``, ``disconnect``
@ -39,6 +42,7 @@ class Namespace(object):
def __init__(self, name, server): def __init__(self, name, server):
self.name = name self.name = name
self.server = server self.server = server
self.middlewares = []
# wrap methods of Server object # wrap methods of Server object
def get_wrapped_method(func_name): def get_wrapped_method(func_name):
@ -54,7 +58,7 @@ class Namespace(object):
'close_room', 'rooms', 'disconnect'): 'close_room', 'rooms', 'disconnect'):
setattr(self, func_name, get_wrapped_method(func_name)) 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 """Returns the event handler for given ``event`` in this namespace or
``None``, if none exists. ``None``, if none exists.
@ -62,15 +66,17 @@ class Namespace(object):
""" """
for attr_name in dir(self): for attr_name in dir(self):
attr = getattr(self, attr_name) attr = getattr(self, attr_name)
if hasattr(attr, '_event_name'): if hasattr(attr, '_sio_event_name'):
_event_name = getattr(attr, '_event_name') _event_name = getattr(attr, '_sio_event_name')
elif attr_name.startswith('on_'): elif attr_name.startswith('on_'):
_event_name = attr_name[3:] _event_name = attr_name[3:]
else: else:
continue continue
if _event_name == event_name: if _event_name == event_name:
return attr extra_middlewares = getattr(attr, '_sio_middlewares', [])
return None return util._apply_middlewares(
self.middlewares + extra_middlewares, event_name,
self.name, attr)
@staticmethod @staticmethod
def event_name(name): def event_name(name):
@ -80,12 +86,14 @@ class Namespace(object):
def foo(self, sid, data): def foo(self, sid, data):
return "received: %s" % data return "received: %s" % data
Note that this must be the last decorator applied on an event handler Note that you must not add third-party decorators after the ones
(last applied means listed first) in order to work. provided by this library because you'll otherwise loose metadata
that this decorators create. You can add them before instead.
""" """
def wrapper(handler): def wrapper(handler):
def wrapped_handler(*args, **kwargs): def wrapped_handler(*args, **kwargs):
return 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 wrapped_handler
return wrapper return wrapper

14
socketio/server.py

@ -6,6 +6,7 @@ import six
from . import base_manager from . import base_manager
from . import namespace as sio_namespace from . import namespace as sio_namespace
from . import packet from . import packet
from . import util
class Server(object): class Server(object):
@ -78,6 +79,7 @@ class Server(object):
self.environ = {} self.environ = {}
self.handlers = {} self.handlers = {}
self.middlewares = []
self._binary_packet = [] self._binary_packet = []
@ -171,7 +173,7 @@ class Server(object):
self.handlers[name] = namespace self.handlers[name] = namespace
return 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 """Returns the event handler for given ``event`` and ``namespace`` or
``None``, if none exists. ``None``, if none exists.
@ -181,10 +183,14 @@ class Server(object):
handler = None handler = None
ns = self.handlers.get(namespace) ns = self.handlers.get(namespace)
if isinstance(ns, sio_namespace.Namespace): if isinstance(ns, sio_namespace.Namespace):
handler = ns.get_event_handler(event) handler = ns._get_event_handler(event)
elif isinstance(ns, dict): elif isinstance(ns, dict):
handler = ns.get(event) 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, def emit(self, event, data=None, room=None, skip_sid=None, namespace=None,
callback=None): callback=None):
@ -460,7 +466,7 @@ class Server(object):
def _trigger_event(self, event, namespace, *args): def _trigger_event(self, event, namespace, *args):
"""Invoke an application event handler.""" """Invoke an application event handler."""
handler = self.get_event_handler(event, namespace) handler = self._get_event_handler(event, namespace)
if handler is not None: if handler is not None:
return handler(*args) return handler(*args)

61
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

8
tests/test_server.py

@ -58,9 +58,9 @@ class TestServer(unittest.TestCase):
s = server.Server() s = server.Server()
s.register_namespace('/ns', NS) s.register_namespace('/ns', NS)
self.assertIsNotNone(s.handlers['/ns'].get_event_handler('foo')) self.assertIsNotNone(s.handlers['/ns']._get_event_handler('foo'))
self.assertIsNotNone(s.handlers['/ns'].get_event_handler('foo bar')) self.assertIsNotNone(s.handlers['/ns']._get_event_handler('foo bar'))
self.assertIsNone(s.handlers['/ns'].get_event_handler('abc')) self.assertIsNone(s.handlers['/ns']._get_event_handler('abc'))
def test_on_bad_event_name(self, eio): def test_on_bad_event_name(self, eio):
s = server.Server() s = server.Server()
@ -215,6 +215,7 @@ class TestServer(unittest.TestCase):
mgr = mock.MagicMock() mgr = mock.MagicMock()
s = server.Server(client_manager=mgr) s = server.Server(client_manager=mgr)
handler = mock.MagicMock(return_value=False) handler = mock.MagicMock(return_value=False)
del handler._sio_middlewares
s.on('connect', handler) s.on('connect', handler)
s._handle_eio_connect('123', 'environ') s._handle_eio_connect('123', 'environ')
handler.assert_called_once_with('123', 'environ') handler.assert_called_once_with('123', 'environ')
@ -226,6 +227,7 @@ class TestServer(unittest.TestCase):
mgr = mock.MagicMock() mgr = mock.MagicMock()
s = server.Server(client_manager=mgr) s = server.Server(client_manager=mgr)
handler = mock.MagicMock(return_value=False) handler = mock.MagicMock(return_value=False)
del handler._sio_middlewares
s.on('connect', handler, namespace='/foo') s.on('connect', handler, namespace='/foo')
s._handle_eio_connect('123', 'environ') s._handle_eio_connect('123', 'environ')
s._handle_eio_message('123', '0/foo') s._handle_eio_message('123', '0/foo')

Loading…
Cancel
Save