Skip to content

Commit 7bc329a

Browse files
class-based namespaces
1 parent 6abd86f commit 7bc329a

File tree

6 files changed

+324
-1
lines changed

6 files changed

+324
-1
lines changed

docs/index.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,37 @@ methods in the :class:`socketio.Server` class.
213213
When the ``namespace`` argument is omitted, set to ``None`` or to ``'/'``, the
214214
default namespace, representing the physical connection, is used.
215215

216+
Class-Based Namespaces
217+
----------------------
218+
219+
As an alternative to the decorator-based event handlers, the event handlers
220+
that belong to a namespace can be created as methods of a subclass of
221+
:class:`socketio.Namespace`::
222+
223+
class MyCustomNamespace(socketio.Namespace):
224+
def on_connect(sid, environ):
225+
pass
226+
227+
def on_disconnect(sid):
228+
pass
229+
230+
def on_my_event(sid, data):
231+
self.emit('my_response', data)
232+
233+
sio.register_namespace(MyCustomNamespace('/test'))
234+
235+
When class-based namespaces are used, any events received by the server are
236+
dispatched to a method named as the event name with the ``on_`` prefix. For
237+
example, event ``my_event`` will be handled by a method named ``on_my_event``.
238+
If an event is received for which there is no corresponding method defined in
239+
the namespace class, then the event is ignored. All event names used in
240+
class-based namespaces must used characters that are legal in method names.
241+
242+
As a convenience to methods defined in a class-based namespace, the namespace
243+
instance includes versions of several of the methods in the
244+
:class:`socketio.Server` class that default to the proper namespace when the
245+
``namespace`` argument is not given.
246+
216247
Using a Message Queue
217248
---------------------
218249

@@ -457,6 +488,8 @@ API Reference
457488
:members:
458489
.. autoclass:: Server
459490
:members:
491+
.. autoclass:: Namespace
492+
:members:
460493
.. autoclass:: BaseManager
461494
:members:
462495
.. autoclass:: PubSubManager

socketio/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .kombu_manager import KombuManager
55
from .redis_manager import RedisManager
66
from .server import Server
7+
from .namespace import Namespace
78

89
__all__ = [Middleware, Server, BaseManager, PubSubManager, KombuManager,
9-
RedisManager]
10+
RedisManager, Namespace]

socketio/namespace.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
class Namespace(object):
2+
"""Base class for class-based namespaces.
3+
4+
A class-based namespace is a class that contains all the event handlers
5+
for a Socket.IO namespace. The event handlers are methods of the class
6+
with the prefix ``on_``, such as ``on_connect``, ``on_disconnect``,
7+
``on_message``, ``on_json``, and so on.
8+
9+
:param namespace: The Socket.IO namespace to be used with all the event
10+
handlers defined in this class. If this argument is
11+
omitted, the default namespace is used.
12+
"""
13+
def __init__(self, namespace=None):
14+
self.namespace = namespace or '/'
15+
self.server = None
16+
17+
def set_server(self, server):
18+
self.server = server
19+
20+
def trigger_event(self, event, *args):
21+
handler_name = 'on_' + event
22+
if hasattr(self, handler_name):
23+
return getattr(self, handler_name)(*args)
24+
25+
def emit(self, event, data=None, room=None, skip_sid=None, namespace=None,
26+
callback=None):
27+
"""Emit a custom event to one or more connected clients.
28+
29+
The only difference with the :func:`socketio.Server.emit` method is
30+
that when the ``namespace`` argument is not given the namespace
31+
associated with the class is used.
32+
"""
33+
return self.server.emit(event, data=data, room=room, skip_sid=skip_sid,
34+
namespace=namespace or self.namespace,
35+
callback=callback)
36+
37+
def send(self, data, room=None, skip_sid=None, namespace=None,
38+
callback=None):
39+
"""Send a message to one or more connected clients.
40+
41+
The only difference with the :func:`socketio.Server.send` method is
42+
that when the ``namespace`` argument is not given the namespace
43+
associated with the class is used.
44+
"""
45+
return self.server.send(data, room=room, skip_sid=skip_sid,
46+
namespace=namespace or self.namespace,
47+
callback=callback)
48+
49+
def enter_room(self, sid, room, namespace=None):
50+
"""Enter a room.
51+
52+
The only difference with the :func:`socketio.Server.enter_room` method
53+
is that when the ``namespace`` argument is not given the namespace
54+
associated with the class is used.
55+
"""
56+
return self.server.enter_room(sid, room,
57+
namespace=namespace or self.namespace)
58+
59+
def leave_room(self, sid, room, namespace=None):
60+
"""Leave a room.
61+
62+
The only difference with the :func:`socketio.Server.leave_room` method
63+
is that when the ``namespace`` argument is not given the namespace
64+
associated with the class is used.
65+
"""
66+
return self.server.leave_room(sid, room,
67+
namespace=namespace or self.namespace)
68+
69+
def close_room(self, room, namespace=None):
70+
"""Close a room.
71+
72+
The only difference with the :func:`socketio.Server.close_room` method
73+
is that when the ``namespace`` argument is not given the namespace
74+
associated with the class is used.
75+
"""
76+
return self.server.close_room(room,
77+
namespace=namespace or self.namespace)
78+
79+
def rooms(self, sid, namespace=None):
80+
"""Return the rooms a client is in.
81+
82+
The only difference with the :func:`socketio.Server.rooms` method is
83+
that when the ``namespace`` argument is not given the namespace
84+
associated with the class is used.
85+
"""
86+
return self.server.rooms(sid, namespace=namespace or self.namespace)
87+
88+
def disconnect(self, sid, namespace=None):
89+
"""Disconnect a client.
90+
91+
The only difference with the :func:`socketio.Server.disconnect` method
92+
is that when the ``namespace`` argument is not given the namespace
93+
associated with the class is used.
94+
"""
95+
return self.server.disconnect(sid,
96+
namespace=namespace or self.namespace)

socketio/server.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from . import base_manager
77
from . import packet
8+
from . import namespace
89

910

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

7879
self.environ = {}
7980
self.handlers = {}
81+
self.namespace_handlers = {}
8082

8183
self._binary_packet = []
8284

@@ -150,6 +152,19 @@ def set_handler(handler):
150152
return set_handler
151153
set_handler(handler)
152154

155+
def register_namespace(self, namespace_handler):
156+
"""Register a namespace handler object.
157+
158+
:param namespace_handler: A namespace subclass that handles all the
159+
event traffic for a namespace. This method
160+
accepts the class itself, or an instance.
161+
"""
162+
if not isinstance(namespace_handler, namespace.Namespace):
163+
raise ValueError('Not a namespace instance')
164+
namespace_handler.set_server(self)
165+
self.namespace_handlers[namespace_handler.namespace] = \
166+
namespace_handler
167+
153168
def emit(self, event, data=None, room=None, skip_sid=None, namespace=None,
154169
callback=None):
155170
"""Emit a custom event to one or more connected clients.
@@ -424,9 +439,15 @@ def _handle_ack(self, sid, namespace, id, data):
424439

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

446+
# or else, forward the event to a namepsace handler if one exists
447+
elif namespace in self.namespace_handlers:
448+
return self.namespace_handlers[namespace].trigger_event(
449+
event, *args)
450+
430451
def _handle_eio_connect(self, sid, environ):
431452
"""Handle the Engine.IO connection event."""
432453
self.environ[sid] = environ

tests/test_namespace.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import unittest
2+
import six
3+
if six.PY3:
4+
from unittest import mock
5+
else:
6+
import mock
7+
8+
from socketio import namespace
9+
10+
11+
class TestNamespace(unittest.TestCase):
12+
def test_connect_event(self):
13+
result = {}
14+
15+
class MyNamespace(namespace.Namespace):
16+
def on_connect(self, sid, environ):
17+
result['result'] = (sid, environ)
18+
19+
ns = MyNamespace('/foo')
20+
ns.set_server(mock.MagicMock())
21+
ns.trigger_event('connect', 'sid', {'foo': 'bar'})
22+
self.assertEqual(result['result'], ('sid', {'foo': 'bar'}))
23+
24+
def test_disconnect_event(self):
25+
result = {}
26+
27+
class MyNamespace(namespace.Namespace):
28+
def on_disconnect(self, sid):
29+
result['result'] = sid
30+
31+
ns = MyNamespace('/foo')
32+
ns.set_server(mock.MagicMock())
33+
ns.trigger_event('disconnect', 'sid')
34+
self.assertEqual(result['result'], 'sid')
35+
36+
def test_event(self):
37+
result = {}
38+
39+
class MyNamespace(namespace.Namespace):
40+
def on_custom_message(self, sid, data):
41+
result['result'] = (sid, data)
42+
43+
ns = MyNamespace('/foo')
44+
ns.set_server(mock.MagicMock())
45+
ns.trigger_event('custom_message', 'sid', {'data': 'data'})
46+
self.assertEqual(result['result'], ('sid', {'data': 'data'}))
47+
48+
def test_event_not_found(self):
49+
result = {}
50+
51+
class MyNamespace(namespace.Namespace):
52+
def on_custom_message(self, sid, data):
53+
result['result'] = (sid, data)
54+
55+
ns = MyNamespace('/foo')
56+
ns.set_server(mock.MagicMock())
57+
ns.trigger_event('another_custom_message', 'sid', {'data': 'data'})
58+
self.assertEqual(result, {})
59+
60+
def test_emit(self):
61+
ns = namespace.Namespace('/foo')
62+
ns.set_server(mock.MagicMock())
63+
ns.emit('ev', data='data', room='room', skip_sid='skip',
64+
callback='cb')
65+
ns.server.emit.assert_called_with(
66+
'ev', data='data', room='room', skip_sid='skip', namespace='/foo',
67+
callback='cb')
68+
ns.emit('ev', data='data', room='room', skip_sid='skip',
69+
namespace='/bar', callback='cb')
70+
ns.server.emit.assert_called_with(
71+
'ev', data='data', room='room', skip_sid='skip', namespace='/bar',
72+
callback='cb')
73+
74+
def test_send(self):
75+
ns = namespace.Namespace('/foo')
76+
ns.set_server(mock.MagicMock())
77+
ns.send(data='data', room='room', skip_sid='skip', callback='cb')
78+
ns.server.send.assert_called_with(
79+
'data', room='room', skip_sid='skip', namespace='/foo',
80+
callback='cb')
81+
ns.send(data='data', room='room', skip_sid='skip', namespace='/bar',
82+
callback='cb')
83+
ns.server.send.assert_called_with(
84+
'data', room='room', skip_sid='skip', namespace='/bar',
85+
callback='cb')
86+
87+
def test_enter_room(self):
88+
ns = namespace.Namespace('/foo')
89+
ns.set_server(mock.MagicMock())
90+
ns.enter_room('sid', 'room')
91+
ns.server.enter_room.assert_called_with('sid', 'room',
92+
namespace='/foo')
93+
ns.enter_room('sid', 'room', namespace='/bar')
94+
ns.server.enter_room.assert_called_with('sid', 'room',
95+
namespace='/bar')
96+
97+
def test_leave_room(self):
98+
ns = namespace.Namespace('/foo')
99+
ns.set_server(mock.MagicMock())
100+
ns.leave_room('sid', 'room')
101+
ns.server.leave_room.assert_called_with('sid', 'room',
102+
namespace='/foo')
103+
ns.leave_room('sid', 'room', namespace='/bar')
104+
ns.server.leave_room.assert_called_with('sid', 'room',
105+
namespace='/bar')
106+
107+
def test_close_room(self):
108+
ns = namespace.Namespace('/foo')
109+
ns.set_server(mock.MagicMock())
110+
ns.close_room('room')
111+
ns.server.close_room.assert_called_with('room', namespace='/foo')
112+
ns.close_room('room', namespace='/bar')
113+
ns.server.close_room.assert_called_with('room', namespace='/bar')
114+
115+
def test_rooms(self):
116+
ns = namespace.Namespace('/foo')
117+
ns.set_server(mock.MagicMock())
118+
ns.rooms('sid')
119+
ns.server.rooms.assert_called_with('sid', namespace='/foo')
120+
ns.rooms('sid', namespace='/bar')
121+
ns.server.rooms.assert_called_with('sid', namespace='/bar')
122+
123+
def test_disconnect(self):
124+
ns = namespace.Namespace('/foo')
125+
ns.set_server(mock.MagicMock())
126+
ns.disconnect('sid')
127+
ns.server.disconnect.assert_called_with('sid', namespace='/foo')
128+
ns.disconnect('sid', namespace='/bar')
129+
ns.server.disconnect.assert_called_with('sid', namespace='/bar')

tests/test_server.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from socketio import packet
1212
from socketio import server
13+
from socketio import namespace
1314

1415

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

390+
def test_namespace_handler(self, eio):
391+
result = {}
392+
393+
class MyNamespace(namespace.Namespace):
394+
def on_connect(self, sid, environ):
395+
result['result'] = (sid, environ)
396+
397+
def on_disconnect(self, sid):
398+
result['result'] = ('disconnect', sid)
399+
400+
def on_foo(self, sid, data):
401+
result['result'] = (sid, data)
402+
403+
def on_bar(self, sid):
404+
result['result'] = 'bar'
405+
406+
def on_baz(self, sid, data1, data2):
407+
result['result'] = (data1, data2)
408+
409+
s = server.Server()
410+
s.register_namespace(MyNamespace('/foo'))
411+
s._handle_eio_connect('123', 'environ')
412+
s._handle_eio_message('123', '0/foo')
413+
self.assertEqual(result['result'], ('123', 'environ'))
414+
s._handle_eio_message('123', '2/foo,["foo","a"]')
415+
self.assertEqual(result['result'], ('123', 'a'))
416+
s._handle_eio_message('123', '2/foo,["bar"]')
417+
self.assertEqual(result['result'], 'bar')
418+
s._handle_eio_message('123', '2/foo,["baz","a","b"]')
419+
self.assertEqual(result['result'], ('a', 'b'))
420+
s.disconnect('123', '/foo')
421+
self.assertEqual(result['result'], ('disconnect', '123'))
422+
423+
def test_bad_namespace_handler(self, eio):
424+
class Dummy(object):
425+
pass
426+
427+
s = server.Server()
428+
self.assertRaises(ValueError, s.register_namespace, 123)
429+
self.assertRaises(ValueError, s.register_namespace, Dummy)
430+
self.assertRaises(ValueError, s.register_namespace, Dummy())
431+
389432
def test_logger(self, eio):
390433
s = server.Server(logger=False)
391434
self.assertEqual(s.logger.getEffectiveLevel(), logging.ERROR)

0 commit comments

Comments
 (0)