Browse Source

Added class-based namespaces.

pull/43/head
Robert Schindler 9 years ago
parent
commit
f69a1c4cce
  1. 40
      docs/index.rst
  2. 5
      socketio/__init__.py
  3. 91
      socketio/namespace.py
  4. 41
      socketio/server.py
  5. 17
      tests/test_server.py

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

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

91
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

41
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."""

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

Loading…
Cancel
Save