Skip to content

Commit

Permalink
Support catch-all namespaces (Fixes #1288)
Browse files Browse the repository at this point in the history
  • Loading branch information
mooomooo authored and miguelgrinberg committed Jan 6, 2024
1 parent 0aa8683 commit 8012413
Show file tree
Hide file tree
Showing 12 changed files with 396 additions and 77 deletions.
38 changes: 31 additions & 7 deletions docs/client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -253,25 +253,49 @@ or can also be coroutines::
If the server includes arguments with an event, those are passed to the
handler function as arguments.

Catch-All Event Handlers
~~~~~~~~~~~~~~~~~~~~~~~~
Catch-All Event and Namespace Handlers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

A "catch-all" event handler is invoked for any events that do not have an
event handler. You can define a catch-all handler using ``'*'`` as event name::

@sio.on('*')
def catch_all(event, data):
pass
def any_event(event, sid, data):
pass

Asyncio clients can also use a coroutine::
Asyncio servers can also use a coroutine::

@sio.on('*')
async def catch_all(event, data):
pass
async def any_event(event, sid, data):
pass

A catch-all event handler receives the event name as a first argument. The
remaining arguments are the same as for a regular event handler.

The ``connect`` and ``disconnect`` events have to be defined explicitly and are
not invoked on a catch-all event handler.

Similarily, a "catch-all" namespace handler is invoked for any connected
namespaces that do not have an explicitly defined event handler. As with
catch-all events, ``'*'`` is used in place of a namespace::

@sio.on('my_event', namespace='*')
def my_event_any_namespace(namespace, sid, data):
pass

For these events, the namespace is passed as first argument, followed by the
regular arguments of the event.

Lastly, it is also possible to define a "catch-all" handler for all events on
all namespaces::

@sio.on('*', namespace='*')
def any_event_any_namespace(event, namespace, sid, data):
pass

Event handlers with catch-all events and namespaces receive the event name and
the namespace as first and second arguments.

Connect, Connect Error and Disconnect Event Handlers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
33 changes: 27 additions & 6 deletions docs/server.rst
Original file line number Diff line number Diff line change
Expand Up @@ -178,28 +178,49 @@ The ``sid`` argument is the Socket.IO session id, a unique identifier of each
client connection. All the events sent by a given client will have the same
``sid`` value.

Catch-All Event Handlers
------------------------
Catch-All Event and Namespace Handlers
--------------------------------------

A "catch-all" event handler is invoked for any events that do not have an
event handler. You can define a catch-all handler using ``'*'`` as event name::

@sio.on('*')
def catch_all(event, sid, data):
pass
def any_event(event, sid, data):
pass

Asyncio servers can also use a coroutine::

@sio.on('*')
async def catch_all(event, sid, data):
pass
async def any_event(event, sid, data):
pass

A catch-all event handler receives the event name as a first argument. The
remaining arguments are the same as for a regular event handler.

The ``connect`` and ``disconnect`` events have to be defined explicitly and are
not invoked on a catch-all event handler.

Similarily, a "catch-all" namespace handler is invoked for any connected
namespaces that do not have an explicitly defined event handler. As with
catch-all events, ``'*'`` is used in place of a namespace::

@sio.on('my_event', namespace='*')
def my_event_any_namespace(namespace, sid, data):
pass

For these events, the namespace is passed as first argument, followed by the
regular arguments of the event.

Lastly, it is also possible to define a "catch-all" handler for all events on
all namespaces::

@sio.on('*', namespace='*')
def any_event_any_namespace(event, namespace, sid, data):
pass

Event handlers with catch-all events and namespaces receive the event name and
the namespace as first and second arguments.

Connect and Disconnect Event Handlers
-------------------------------------

Expand Down
33 changes: 13 additions & 20 deletions src/socketio/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,28 +429,21 @@ async def _handle_error(self, namespace, data):
async 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:
handler = None
if event in self.handlers[namespace]:
handler = self.handlers[namespace][event]
elif event not in self.reserved_events and \
'*' in self.handlers[namespace]:
handler = self.handlers[namespace]['*']
args = (event, *args)
if handler:
if asyncio.iscoroutinefunction(handler):
try:
ret = await handler(*args)
except asyncio.CancelledError: # pragma: no cover
ret = None
else:
ret = handler(*args)
return ret
handler, args = self._get_event_handler(event, namespace, args)
if handler:
if asyncio.iscoroutinefunction(handler):
try:
ret = await handler(*args)
except asyncio.CancelledError: # pragma: no cover
ret = None
else:
ret = handler(*args)
return ret

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

async def _handle_reconnect(self):
if self._reconnect_abort is None: # pragma: no cover
Expand Down
38 changes: 15 additions & 23 deletions src/socketio/async_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,30 +617,22 @@ async def _handle_ack(self, eio_sid, namespace, id, data):
async 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:
handler = None
if event in self.handlers[namespace]:
handler = self.handlers[namespace][event]
elif event not in self.reserved_events and \
'*' in self.handlers[namespace]:
handler = self.handlers[namespace]['*']
args = (event, *args)
if handler:
if asyncio.iscoroutinefunction(handler):
try:
ret = await handler(*args)
except asyncio.CancelledError: # pragma: no cover
ret = None
else:
ret = handler(*args)
return ret
handler, args = self._get_event_handler(event, namespace, args)
if handler:
if asyncio.iscoroutinefunction(handler):
try:
ret = await handler(*args)
except asyncio.CancelledError: # pragma: no cover
ret = None
else:
return self.not_handled

# or else, forward the event to a namepsace handler if one exists
elif namespace in self.namespace_handlers: # pragma: no branch
return await self.namespace_handlers[namespace].trigger_event(
event, *args)
ret = handler(*args)
return ret
# or else, forward the event to a namespace handler if one exists
handler, args = self._get_namespace_handler(namespace, args)
if handler:
return await handler.trigger_event(event, *args)
else:
return self.not_handled

async def _handle_eio_connect(self, eio_sid, environ):
"""Handle the Engine.IO connection event."""
Expand Down
40 changes: 40 additions & 0 deletions src/socketio/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,46 @@ def transport(self):
"""
return self.eio.transport()

def _get_event_handler(self, event, namespace, args):
# return the appropriate application event handler
#
# Resolution priority:
# - self.handlers[namespace][event]
# - self.handlers[namespace]["*"]
# - self.handlers["*"][event]
# - self.handlers["*"]["*"]
handler = None
if namespace in self.handlers:
if event in self.handlers[namespace]:
handler = self.handlers[namespace][event]
elif event not in self.reserved_events and \
'*' in self.handlers[namespace]:
handler = self.handlers[namespace]['*']
args = (event, *args)
elif '*' in self.handlers:
if event in self.handlers['*']:
handler = self.handlers['*'][event]
args = (namespace, *args)
elif event not in self.reserved_events and \
'*' in self.handlers['*']:
handler = self.handlers['*']['*']
args = (event, namespace, *args)
return handler, args

def _get_namespace_handler(self, namespace, args):
# Return the appropriate application event handler.
#
# Resolution priority:
# - self.namespace_handlers[namespace]
# - self.namespace_handlers["*"]
handler = None
if namespace in self.namespace_handlers:
handler = self.namespace_handlers[namespace]
elif '*' in self.namespace_handlers:
handler = self.namespace_handlers['*']
args = (namespace, *args)
return handler, args

def _generate_ack_id(self, namespace, callback):
"""Generate a unique identifier for an ACK packet."""
namespace = namespace or '/'
Expand Down
42 changes: 42 additions & 0 deletions src/socketio/base_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,48 @@ def get_environ(self, sid, namespace=None):
eio_sid = self.manager.eio_sid_from_sid(sid, namespace or '/')
return self.environ.get(eio_sid)

def _get_event_handler(self, event, namespace, args):
# Return the appropriate application event handler
#
# Resolution priority:
# - self.handlers[namespace][event]
# - self.handlers[namespace]["*"]
# - self.handlers["*"][event]
# - self.handlers["*"]["*"]
handler = None
print(event, namespace)
print(namespace in self.handlers)
if namespace in self.handlers:
if event in self.handlers[namespace]:
handler = self.handlers[namespace][event]
elif event not in self.reserved_events and \
'*' in self.handlers[namespace]:
handler = self.handlers[namespace]['*']
args = (event, *args)
elif '*' in self.handlers:
if event in self.handlers['*']:
handler = self.handlers['*'][event]
args = (namespace, *args)
elif event not in self.reserved_events and \
'*' in self.handlers['*']:
handler = self.handlers['*']['*']
args = (event, namespace, *args)
return handler, args

def _get_namespace_handler(self, namespace, args):
# Return the appropriate application event handler.
#
# Resolution priority:
# - self.namespace_handlers[namespace]
# - self.namespace_handlers["*"]
handler = None
if namespace in self.namespace_handlers:
handler = self.namespace_handlers[namespace]
elif '*' in self.namespace_handlers:
handler = self.namespace_handlers['*']
args = (namespace, *args)
return handler, args

def _handle_eio_connect(self): # pragma: no cover
raise NotImplementedError()

Expand Down
15 changes: 6 additions & 9 deletions src/socketio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,17 +404,14 @@ def _handle_error(self, namespace, 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:
if event in self.handlers[namespace]:
return self.handlers[namespace][event](*args)
elif event not in self.reserved_events and \
'*' in self.handlers[namespace]:
return self.handlers[namespace]['*'](event, *args)
handler, args = self._get_event_handler(event, namespace, args)
if handler:
return handler(*args)

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

def _handle_reconnect(self):
if self._reconnect_abort is None: # pragma: no cover
Expand Down
20 changes: 8 additions & 12 deletions src/socketio/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,19 +604,15 @@ def _handle_ack(self, eio_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:
if event in self.handlers[namespace]:
return self.handlers[namespace][event](*args)
elif event not in self.reserved_events and \
'*' in self.handlers[namespace]:
return self.handlers[namespace]['*'](event, *args)
else:
return self.not_handled

handler, args = self._get_event_handler(event, namespace, args)
if handler:
return handler(*args)
# or else, forward the event to a namespace handler if one exists
elif namespace in self.namespace_handlers: # pragma: no branch
return self.namespace_handlers[namespace].trigger_event(
event, *args)
handler, args = self._get_namespace_handler(namespace, args)
if handler:
return handler.trigger_event(event, *args)
else:
return self.not_handled

def _handle_eio_connect(self, eio_sid, environ):
"""Handle the Engine.IO connection event."""
Expand Down
32 changes: 32 additions & 0 deletions tests/async/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,38 @@ def on_foo(self, a, b):
_run(c._trigger_event('foo', '/', 1, '2'))
assert result == [1, '2']

def test_trigger_event_with_catchall_class_namespace(self):
result = {}

class MyNamespace(async_namespace.AsyncClientNamespace):
def on_connect(self, ns):
result['result'] = (ns,)

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

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

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

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

c = async_client.AsyncClient()
c.register_namespace(MyNamespace('*'))
_run(c._trigger_event('connect', '/foo'))
assert result['result'] == ('/foo',)
_run(c._trigger_event('foo', '/foo', 'a'))
assert result['result'] == ('/foo', 'a')
_run(c._trigger_event('bar', '/foo'))
assert result['result'] == 'bar/foo'
_run(c._trigger_event('baz', '/foo', 'a', 'b'))
assert result['result'] == ('/foo', 'a', 'b')
_run(c._trigger_event('disconnect', '/foo'))
assert result['result'] == ('disconnect', '/foo')

def test_trigger_event_unknown_namespace(self):
c = async_client.AsyncClient()
result = []
Expand Down
Loading

0 comments on commit 8012413

Please sign in to comment.