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

24
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

14
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)

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.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')

Loading…
Cancel
Save