Skip to content

Commit

Permalink
class-based namespaces
Browse files Browse the repository at this point in the history
  • Loading branch information
miguelgrinberg committed Aug 23, 2016
1 parent 6abd86f commit 7bc329a
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 1 deletion.
33 changes: 33 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,37 @@ 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
----------------------

As an alternative to the decorator-based event handlers, the event handlers
that belong to a namespace can be created as methods of a subclass of
:class:`socketio.Namespace`::

class MyCustomNamespace(socketio.Namespace):
def on_connect(sid, environ):
pass

def on_disconnect(sid):
pass

def on_my_event(sid, data):
self.emit('my_response', data)

sio.register_namespace(MyCustomNamespace('/test'))

When class-based namespaces are used, any events received by the server are
dispatched to a method named as the event name with the ``on_`` prefix. For
example, event ``my_event`` will be handled by a method named ``on_my_event``.
If an event is received for which there is no corresponding method defined in
the namespace class, then the event is ignored. All event names used in
class-based namespaces must used characters that are legal in method names.

As a convenience to methods defined in a class-based namespace, the namespace
instance includes versions of several of the methods in the
:class:`socketio.Server` class that default to the proper namespace when the
``namespace`` argument is not given.

Using a Message Queue
---------------------

Expand Down Expand Up @@ -457,6 +488,8 @@ API Reference
:members:
.. autoclass:: Server
:members:
.. autoclass:: Namespace
:members:
.. autoclass:: BaseManager
:members:
.. autoclass:: PubSubManager
Expand Down
3 changes: 2 additions & 1 deletion socketio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .kombu_manager import KombuManager
from .redis_manager import RedisManager
from .server import Server
from .namespace import Namespace

__all__ = [Middleware, Server, BaseManager, PubSubManager, KombuManager,
RedisManager]
RedisManager, Namespace]
96 changes: 96 additions & 0 deletions socketio/namespace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
class Namespace(object):
"""Base class for class-based namespaces.
A class-based namespace is a class that contains all the event handlers
for a Socket.IO namespace. The event handlers are methods of the class
with the prefix ``on_``, such as ``on_connect``, ``on_disconnect``,
``on_message``, ``on_json``, and so on.
:param namespace: The Socket.IO namespace to be used with all the event
handlers defined in this class. If this argument is
omitted, the default namespace is used.
"""
def __init__(self, namespace=None):
self.namespace = namespace or '/'
self.server = None

def set_server(self, server):
self.server = server

def trigger_event(self, event, *args):
handler_name = 'on_' + event
if hasattr(self, handler_name):
return getattr(self, handler_name)(*args)

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.
The only difference with the :func:`socketio.Server.emit` method is
that when the ``namespace`` argument is not given the namespace
associated with the class is used.
"""
return self.server.emit(event, data=data, room=room, skip_sid=skip_sid,
namespace=namespace or self.namespace,
callback=callback)

def send(self, data, room=None, skip_sid=None, namespace=None,
callback=None):
"""Send a message to one or more connected clients.
The only difference with the :func:`socketio.Server.send` method is
that when the ``namespace`` argument is not given the namespace
associated with the class is used.
"""
return self.server.send(data, room=room, skip_sid=skip_sid,
namespace=namespace or self.namespace,
callback=callback)

def enter_room(self, sid, room, namespace=None):
"""Enter a room.
The only difference with the :func:`socketio.Server.enter_room` method
is that when the ``namespace`` argument is not given the namespace
associated with the class is used.
"""
return self.server.enter_room(sid, room,
namespace=namespace or self.namespace)

def leave_room(self, sid, room, namespace=None):
"""Leave a room.
The only difference with the :func:`socketio.Server.leave_room` method
is that when the ``namespace`` argument is not given the namespace
associated with the class is used.
"""
return self.server.leave_room(sid, room,
namespace=namespace or self.namespace)

def close_room(self, room, namespace=None):
"""Close a room.
The only difference with the :func:`socketio.Server.close_room` method
is that when the ``namespace`` argument is not given the namespace
associated with the class is used.
"""
return self.server.close_room(room,
namespace=namespace or self.namespace)

def rooms(self, sid, namespace=None):
"""Return the rooms a client is in.
The only difference with the :func:`socketio.Server.rooms` method is
that when the ``namespace`` argument is not given the namespace
associated with the class is used.
"""
return self.server.rooms(sid, namespace=namespace or self.namespace)

def disconnect(self, sid, namespace=None):
"""Disconnect a client.
The only difference with the :func:`socketio.Server.disconnect` method
is that when the ``namespace`` argument is not given the namespace
associated with the class is used.
"""
return self.server.disconnect(sid,
namespace=namespace or self.namespace)
21 changes: 21 additions & 0 deletions socketio/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from . import base_manager
from . import packet
from . import namespace


class Server(object):
Expand Down Expand Up @@ -77,6 +78,7 @@ def __init__(self, client_manager=None, logger=False, binary=False,

self.environ = {}
self.handlers = {}
self.namespace_handlers = {}

self._binary_packet = []

Expand Down Expand Up @@ -150,6 +152,19 @@ def set_handler(handler):
return set_handler
set_handler(handler)

def register_namespace(self, namespace_handler):
"""Register a namespace handler object.
:param namespace_handler: A namespace subclass that handles all the
event traffic for a namespace. This method
accepts the class itself, or an instance.
"""
if not isinstance(namespace_handler, namespace.Namespace):
raise ValueError('Not a namespace instance')
namespace_handler.set_server(self)
self.namespace_handlers[namespace_handler.namespace] = \
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.
Expand Down Expand Up @@ -424,9 +439,15 @@ def _handle_ack(self, sid, namespace, id, data):

def _trigger_event(self, event, namespace, *args):
"""Invoke an application event handler."""
# first see if we have an explicit handler for the event
if namespace in self.handlers and event in self.handlers[namespace]:
return self.handlers[namespace][event](*args)

# or else, forward the event to a namepsace handler if one exists
elif namespace in self.namespace_handlers:
return self.namespace_handlers[namespace].trigger_event(
event, *args)

def _handle_eio_connect(self, sid, environ):
"""Handle the Engine.IO connection event."""
self.environ[sid] = environ
Expand Down
129 changes: 129 additions & 0 deletions tests/test_namespace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import unittest
import six
if six.PY3:
from unittest import mock
else:
import mock

from socketio import namespace


class TestNamespace(unittest.TestCase):
def test_connect_event(self):
result = {}

class MyNamespace(namespace.Namespace):
def on_connect(self, sid, environ):
result['result'] = (sid, environ)

ns = MyNamespace('/foo')
ns.set_server(mock.MagicMock())
ns.trigger_event('connect', 'sid', {'foo': 'bar'})
self.assertEqual(result['result'], ('sid', {'foo': 'bar'}))

def test_disconnect_event(self):
result = {}

class MyNamespace(namespace.Namespace):
def on_disconnect(self, sid):
result['result'] = sid

ns = MyNamespace('/foo')
ns.set_server(mock.MagicMock())
ns.trigger_event('disconnect', 'sid')
self.assertEqual(result['result'], 'sid')

def test_event(self):
result = {}

class MyNamespace(namespace.Namespace):
def on_custom_message(self, sid, data):
result['result'] = (sid, data)

ns = MyNamespace('/foo')
ns.set_server(mock.MagicMock())
ns.trigger_event('custom_message', 'sid', {'data': 'data'})
self.assertEqual(result['result'], ('sid', {'data': 'data'}))

def test_event_not_found(self):
result = {}

class MyNamespace(namespace.Namespace):
def on_custom_message(self, sid, data):
result['result'] = (sid, data)

ns = MyNamespace('/foo')
ns.set_server(mock.MagicMock())
ns.trigger_event('another_custom_message', 'sid', {'data': 'data'})
self.assertEqual(result, {})

def test_emit(self):
ns = namespace.Namespace('/foo')
ns.set_server(mock.MagicMock())
ns.emit('ev', data='data', room='room', skip_sid='skip',
callback='cb')
ns.server.emit.assert_called_with(
'ev', data='data', room='room', skip_sid='skip', namespace='/foo',
callback='cb')
ns.emit('ev', data='data', room='room', skip_sid='skip',
namespace='/bar', callback='cb')
ns.server.emit.assert_called_with(
'ev', data='data', room='room', skip_sid='skip', namespace='/bar',
callback='cb')

def test_send(self):
ns = namespace.Namespace('/foo')
ns.set_server(mock.MagicMock())
ns.send(data='data', room='room', skip_sid='skip', callback='cb')
ns.server.send.assert_called_with(
'data', room='room', skip_sid='skip', namespace='/foo',
callback='cb')
ns.send(data='data', room='room', skip_sid='skip', namespace='/bar',
callback='cb')
ns.server.send.assert_called_with(
'data', room='room', skip_sid='skip', namespace='/bar',
callback='cb')

def test_enter_room(self):
ns = namespace.Namespace('/foo')
ns.set_server(mock.MagicMock())
ns.enter_room('sid', 'room')
ns.server.enter_room.assert_called_with('sid', 'room',
namespace='/foo')
ns.enter_room('sid', 'room', namespace='/bar')
ns.server.enter_room.assert_called_with('sid', 'room',
namespace='/bar')

def test_leave_room(self):
ns = namespace.Namespace('/foo')
ns.set_server(mock.MagicMock())
ns.leave_room('sid', 'room')
ns.server.leave_room.assert_called_with('sid', 'room',
namespace='/foo')
ns.leave_room('sid', 'room', namespace='/bar')
ns.server.leave_room.assert_called_with('sid', 'room',
namespace='/bar')

def test_close_room(self):
ns = namespace.Namespace('/foo')
ns.set_server(mock.MagicMock())
ns.close_room('room')
ns.server.close_room.assert_called_with('room', namespace='/foo')
ns.close_room('room', namespace='/bar')
ns.server.close_room.assert_called_with('room', namespace='/bar')

def test_rooms(self):
ns = namespace.Namespace('/foo')
ns.set_server(mock.MagicMock())
ns.rooms('sid')
ns.server.rooms.assert_called_with('sid', namespace='/foo')
ns.rooms('sid', namespace='/bar')
ns.server.rooms.assert_called_with('sid', namespace='/bar')

def test_disconnect(self):
ns = namespace.Namespace('/foo')
ns.set_server(mock.MagicMock())
ns.disconnect('sid')
ns.server.disconnect.assert_called_with('sid', namespace='/foo')
ns.disconnect('sid', namespace='/bar')
ns.server.disconnect.assert_called_with('sid', namespace='/bar')
43 changes: 43 additions & 0 deletions tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from socketio import packet
from socketio import server
from socketio import namespace


@mock.patch('engineio.Server')
Expand Down Expand Up @@ -386,6 +387,48 @@ def test_disconnect_namespace(self, eio):
s.disconnect('123', namespace='/foo')
s.eio.send.assert_any_call('123', '1/foo', binary=False)

def test_namespace_handler(self, eio):
result = {}

class MyNamespace(namespace.Namespace):
def on_connect(self, sid, environ):
result['result'] = (sid, environ)

def on_disconnect(self, sid):
result['result'] = ('disconnect', sid)

def on_foo(self, sid, data):
result['result'] = (sid, data)

def on_bar(self, sid):
result['result'] = 'bar'

def on_baz(self, sid, data1, data2):
result['result'] = (data1, data2)

s = server.Server()
s.register_namespace(MyNamespace('/foo'))
s._handle_eio_connect('123', 'environ')
s._handle_eio_message('123', '0/foo')
self.assertEqual(result['result'], ('123', 'environ'))
s._handle_eio_message('123', '2/foo,["foo","a"]')
self.assertEqual(result['result'], ('123', 'a'))
s._handle_eio_message('123', '2/foo,["bar"]')
self.assertEqual(result['result'], 'bar')
s._handle_eio_message('123', '2/foo,["baz","a","b"]')
self.assertEqual(result['result'], ('a', 'b'))
s.disconnect('123', '/foo')
self.assertEqual(result['result'], ('disconnect', '123'))

def test_bad_namespace_handler(self, eio):
class Dummy(object):
pass

s = server.Server()
self.assertRaises(ValueError, s.register_namespace, 123)
self.assertRaises(ValueError, s.register_namespace, Dummy)
self.assertRaises(ValueError, s.register_namespace, Dummy())

def test_logger(self, eio):
s = server.Server(logger=False)
self.assertEqual(s.logger.getEffectiveLevel(), logging.ERROR)
Expand Down

0 comments on commit 7bc329a

Please sign in to comment.