Skip to content

Commit

Permalink
Catch-all event handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
miguelgrinberg committed Sep 3, 2021
1 parent dff3d3d commit 28569d4
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 31 deletions.
22 changes: 22 additions & 0 deletions docs/client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,28 @@ or can also be coroutines::
async def message(data):
print('I received a message!')

Catch-All Event 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

Asyncio clients can also use a coroutine::

@sio.on('*')
async def catch_all(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.

Connect, Connect Error and Disconnect Event Handlers
----------------------------------------------------

The ``connect``, ``connect_error`` and ``disconnect`` events are special; they
are invoked automatically when a client connects or disconnects from the
server::
Expand Down
22 changes: 22 additions & 0 deletions docs/server.rst
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,28 @@ 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
------------------------

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

Asyncio servers can also use a coroutine::

@sio.on('*')
async def catch_all(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.

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

The ``connect`` and ``disconnect`` events are special; they are invoked
automatically when a client connects or disconnects from the server::

Expand Down
25 changes: 16 additions & 9 deletions src/socketio/asyncio_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,15 +418,22 @@ 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 and event in self.handlers[namespace]:
if asyncio.iscoroutinefunction(self.handlers[namespace][event]):
try:
ret = await self.handlers[namespace][event](*args)
except asyncio.CancelledError: # pragma: no cover
ret = None
else:
ret = self.handlers[namespace][event](*args)
return ret
if namespace in self.handlers:
handler = None
if event in self.handlers[namespace]:
handler = self.handlers[namespace][event]
elif '*' 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

# or else, forward the event to a namepsace handler if one exists
elif namespace in self.namespace_handlers:
Expand Down
26 changes: 16 additions & 10 deletions src/socketio/asyncio_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,16 +524,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 and event in self.handlers[namespace]:
if asyncio.iscoroutinefunction(self.handlers[namespace][event]) \
is True:
try:
ret = await self.handlers[namespace][event](*args)
except asyncio.CancelledError: # pragma: no cover
ret = None
else:
ret = self.handlers[namespace][event](*args)
return ret
if namespace in self.handlers:
handler = None
if event in self.handlers[namespace]:
handler = self.handlers[namespace][event]
elif '*' 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

# or else, forward the event to a namepsace handler if one exists
elif namespace in self.namespace_handlers:
Expand Down
7 changes: 5 additions & 2 deletions src/socketio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,8 +609,11 @@ 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 and event in self.handlers[namespace]:
return self.handlers[namespace][event](*args)
if namespace in self.handlers:
if event in self.handlers[namespace]:
return self.handlers[namespace][event](*args)
elif '*' in self.handlers[namespace]:
return self.handlers[namespace]['*'](event, *args)

# or else, forward the event to a namespace handler if one exists
elif namespace in self.namespace_handlers:
Expand Down
7 changes: 5 additions & 2 deletions src/socketio/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -732,8 +732,11 @@ 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 and event in self.handlers[namespace]:
return self.handlers[namespace][event](*args)
if namespace in self.handlers:
if event in self.handlers[namespace]:
return self.handlers[namespace][event](*args)
elif '*' in self.handlers[namespace]:
return self.handlers[namespace]['*'](event, *args)

# or else, forward the event to a namespace handler if one exists
elif namespace in self.namespace_handlers:
Expand Down
8 changes: 8 additions & 0 deletions tests/asyncio/test_asyncio_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -833,16 +833,24 @@ def test_handle_error_unknown_namespace(self):
def test_trigger_event(self):
c = asyncio_client.AsyncClient()
handler = mock.MagicMock()
catchall_handler = mock.MagicMock()
c.on('foo', handler)
c.on('*', catchall_handler)
_run(c._trigger_event('foo', '/', 1, '2'))
_run(c._trigger_event('bar', '/', 1, '2', 3))
handler.assert_called_once_with(1, '2')
catchall_handler.assert_called_once_with('bar', 1, '2', 3)

def test_trigger_event_namespace(self):
c = asyncio_client.AsyncClient()
handler = AsyncMock()
catchall_handler = AsyncMock()
c.on('foo', handler, namespace='/bar')
c.on('*', catchall_handler, namespace='/bar')
_run(c._trigger_event('foo', '/bar', 1, '2'))
_run(c._trigger_event('bar', '/bar', 1, '2', 3))
handler.mock.assert_called_once_with(1, '2')
catchall_handler.mock.assert_called_once_with('bar', 1, '2', 3)

def test_trigger_event_class_namespace(self):
c = asyncio_client.AsyncClient()
Expand Down
18 changes: 14 additions & 4 deletions tests/asyncio/test_asyncio_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,18 +618,28 @@ def test_handle_event(self, eio):
s = asyncio_server.AsyncServer(async_handlers=False)
sid = s.manager.connect('123', '/')
handler = AsyncMock()
s.on('my message', handler)
catchall_handler = AsyncMock()
s.on('msg', handler)
s.on('*', catchall_handler)
_run(s._handle_eio_message('123', '2["msg","a","b"]'))
_run(s._handle_eio_message('123', '2["my message","a","b","c"]'))
handler.mock.assert_called_once_with(sid, 'a', 'b', 'c')
handler.mock.assert_called_once_with(sid, 'a', 'b')
catchall_handler.mock.assert_called_once_with(
'my message', sid, 'a', 'b', 'c')

def test_handle_event_with_namespace(self, eio):
eio.return_value.send = AsyncMock()
s = asyncio_server.AsyncServer(async_handlers=False)
sid = s.manager.connect('123', '/foo')
handler = mock.MagicMock()
s.on('my message', handler, namespace='/foo')
catchall_handler = mock.MagicMock()
s.on('msg', handler, namespace='/foo')
s.on('*', catchall_handler, namespace='/foo')
_run(s._handle_eio_message('123', '2/foo,["msg","a","b"]'))
_run(s._handle_eio_message('123', '2/foo,["my message","a","b","c"]'))
handler.assert_called_once_with(sid, 'a', 'b', 'c')
handler.assert_called_once_with(sid, 'a', 'b')
catchall_handler.assert_called_once_with(
'my message', sid, 'a', 'b', 'c')

def test_handle_event_with_disconnected_namespace(self, eio):
eio.return_value.send = AsyncMock()
Expand Down
8 changes: 8 additions & 0 deletions tests/common/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -934,16 +934,24 @@ def test_handle_error_unknown_namespace(self):
def test_trigger_event(self):
c = client.Client()
handler = mock.MagicMock()
catchall_handler = mock.MagicMock()
c.on('foo', handler)
c.on('*', catchall_handler)
c._trigger_event('foo', '/', 1, '2')
c._trigger_event('bar', '/', 1, '2', 3)
handler.assert_called_once_with(1, '2')
catchall_handler.assert_called_once_with('bar', 1, '2', 3)

def test_trigger_event_namespace(self):
c = client.Client()
handler = mock.MagicMock()
catchall_handler = mock.MagicMock()
c.on('foo', handler, namespace='/bar')
c.on('*', catchall_handler, namespace='/bar')
c._trigger_event('foo', '/bar', 1, '2')
c._trigger_event('bar', '/bar', 1, '2', 3)
handler.assert_called_once_with(1, '2')
catchall_handler.assert_called_once_with('bar', 1, '2', 3)

def test_trigger_event_class_namespace(self):
c = client.Client()
Expand Down
18 changes: 14 additions & 4 deletions tests/common/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,17 +546,27 @@ def test_handle_event(self, eio):
s = server.Server(async_handlers=False)
s.manager.connect('123', '/')
handler = mock.MagicMock()
s.on('my message', handler)
catchall_handler = mock.MagicMock()
s.on('msg', handler)
s.on('*', catchall_handler)
s._handle_eio_message('123', '2["msg","a","b"]')
s._handle_eio_message('123', '2["my message","a","b","c"]')
handler.assert_called_once_with('1', 'a', 'b', 'c')
handler.assert_called_once_with('1', 'a', 'b')
catchall_handler.assert_called_once_with(
'my message', '1', 'a', 'b', 'c')

def test_handle_event_with_namespace(self, eio):
s = server.Server(async_handlers=False)
s.manager.connect('123', '/foo')
handler = mock.MagicMock()
s.on('my message', handler, namespace='/foo')
catchall_handler = mock.MagicMock()
s.on('msg', handler, namespace='/foo')
s.on('*', catchall_handler, namespace='/foo')
s._handle_eio_message('123', '2/foo,["msg","a","b"]')
s._handle_eio_message('123', '2/foo,["my message","a","b","c"]')
handler.assert_called_once_with('1', 'a', 'b', 'c')
handler.assert_called_once_with('1', 'a', 'b')
catchall_handler.assert_called_once_with(
'my message', '1', 'a', 'b', 'c')

def test_handle_event_with_disconnected_namespace(self, eio):
s = server.Server(async_handlers=False)
Expand Down

0 comments on commit 28569d4

Please sign in to comment.