diff --git a/docs/index.rst b/docs/index.rst index 1e7bbe6..9d6c496 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -213,6 +213,46 @@ 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 + + # 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) + Using a Message Queue --------------------- 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..ab3a59b --- /dev/null +++ b/socketio/namespace.py @@ -0,0 +1,91 @@ +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 + + # 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) + """ + + 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_event_handler(self, event_name): + """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'): + _event_name = getattr(attr, '_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): + """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 diff --git a/socketio/server.py b/socketio/server.py index 227ff80..976dada 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 @@ -150,6 +155,37 @@ class Server(object): return set_handler set_handler(handler) + def register_namespace(self, name, namespace_class): + """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 + 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) + 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. @@ -424,8 +460,9 @@ 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 = self.get_event_handler(event, namespace) + 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 372f61a..08547b4 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,22 @@ 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, 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_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() self.assertRaises(ValueError, s.on, 'two-words')