Skip to content

Commit 8012413

Browse files
mooomooomiguelgrinberg
authored andcommitted
Support catch-all namespaces (Fixes #1288)
1 parent 0aa8683 commit 8012413

12 files changed

Lines changed: 396 additions & 77 deletions

File tree

docs/client.rst

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -253,25 +253,49 @@ or can also be coroutines::
253253
If the server includes arguments with an event, those are passed to the
254254
handler function as arguments.
255255

256-
Catch-All Event Handlers
257-
~~~~~~~~~~~~~~~~~~~~~~~~
256+
Catch-All Event and Namespace Handlers
257+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
258258

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

262262
@sio.on('*')
263-
def catch_all(event, data):
264-
pass
263+
def any_event(event, sid, data):
264+
pass
265265

266-
Asyncio clients can also use a coroutine::
266+
Asyncio servers can also use a coroutine::
267267

268268
@sio.on('*')
269-
async def catch_all(event, data):
270-
pass
269+
async def any_event(event, sid, data):
270+
pass
271271

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

275+
The ``connect`` and ``disconnect`` events have to be defined explicitly and are
276+
not invoked on a catch-all event handler.
277+
278+
Similarily, a "catch-all" namespace handler is invoked for any connected
279+
namespaces that do not have an explicitly defined event handler. As with
280+
catch-all events, ``'*'`` is used in place of a namespace::
281+
282+
@sio.on('my_event', namespace='*')
283+
def my_event_any_namespace(namespace, sid, data):
284+
pass
285+
286+
For these events, the namespace is passed as first argument, followed by the
287+
regular arguments of the event.
288+
289+
Lastly, it is also possible to define a "catch-all" handler for all events on
290+
all namespaces::
291+
292+
@sio.on('*', namespace='*')
293+
def any_event_any_namespace(event, namespace, sid, data):
294+
pass
295+
296+
Event handlers with catch-all events and namespaces receive the event name and
297+
the namespace as first and second arguments.
298+
275299
Connect, Connect Error and Disconnect Event Handlers
276300
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
277301

docs/server.rst

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,28 +178,49 @@ The ``sid`` argument is the Socket.IO session id, a unique identifier of each
178178
client connection. All the events sent by a given client will have the same
179179
``sid`` value.
180180

181-
Catch-All Event Handlers
182-
------------------------
181+
Catch-All Event and Namespace Handlers
182+
--------------------------------------
183183

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

187187
@sio.on('*')
188-
def catch_all(event, sid, data):
189-
pass
188+
def any_event(event, sid, data):
189+
pass
190190

191191
Asyncio servers can also use a coroutine::
192192

193193
@sio.on('*')
194-
async def catch_all(event, sid, data):
195-
pass
194+
async def any_event(event, sid, data):
195+
pass
196196

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

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

203+
Similarily, a "catch-all" namespace handler is invoked for any connected
204+
namespaces that do not have an explicitly defined event handler. As with
205+
catch-all events, ``'*'`` is used in place of a namespace::
206+
207+
@sio.on('my_event', namespace='*')
208+
def my_event_any_namespace(namespace, sid, data):
209+
pass
210+
211+
For these events, the namespace is passed as first argument, followed by the
212+
regular arguments of the event.
213+
214+
Lastly, it is also possible to define a "catch-all" handler for all events on
215+
all namespaces::
216+
217+
@sio.on('*', namespace='*')
218+
def any_event_any_namespace(event, namespace, sid, data):
219+
pass
220+
221+
Event handlers with catch-all events and namespaces receive the event name and
222+
the namespace as first and second arguments.
223+
203224
Connect and Disconnect Event Handlers
204225
-------------------------------------
205226

src/socketio/async_client.py

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -429,28 +429,21 @@ async def _handle_error(self, namespace, data):
429429
async def _trigger_event(self, event, namespace, *args):
430430
"""Invoke an application event handler."""
431431
# first see if we have an explicit handler for the event
432-
if namespace in self.handlers:
433-
handler = None
434-
if event in self.handlers[namespace]:
435-
handler = self.handlers[namespace][event]
436-
elif event not in self.reserved_events and \
437-
'*' in self.handlers[namespace]:
438-
handler = self.handlers[namespace]['*']
439-
args = (event, *args)
440-
if handler:
441-
if asyncio.iscoroutinefunction(handler):
442-
try:
443-
ret = await handler(*args)
444-
except asyncio.CancelledError: # pragma: no cover
445-
ret = None
446-
else:
447-
ret = handler(*args)
448-
return ret
432+
handler, args = self._get_event_handler(event, namespace, args)
433+
if handler:
434+
if asyncio.iscoroutinefunction(handler):
435+
try:
436+
ret = await handler(*args)
437+
except asyncio.CancelledError: # pragma: no cover
438+
ret = None
439+
else:
440+
ret = handler(*args)
441+
return ret
449442

450443
# or else, forward the event to a namepsace handler if one exists
451-
elif namespace in self.namespace_handlers:
452-
return await self.namespace_handlers[namespace].trigger_event(
453-
event, *args)
444+
handler, args = self._get_namespace_handler(namespace, args)
445+
if handler:
446+
return await handler.trigger_event(event, *args)
454447

455448
async def _handle_reconnect(self):
456449
if self._reconnect_abort is None: # pragma: no cover

src/socketio/async_server.py

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -617,30 +617,22 @@ async def _handle_ack(self, eio_sid, namespace, id, data):
617617
async def _trigger_event(self, event, namespace, *args):
618618
"""Invoke an application event handler."""
619619
# first see if we have an explicit handler for the event
620-
if namespace in self.handlers:
621-
handler = None
622-
if event in self.handlers[namespace]:
623-
handler = self.handlers[namespace][event]
624-
elif event not in self.reserved_events and \
625-
'*' in self.handlers[namespace]:
626-
handler = self.handlers[namespace]['*']
627-
args = (event, *args)
628-
if handler:
629-
if asyncio.iscoroutinefunction(handler):
630-
try:
631-
ret = await handler(*args)
632-
except asyncio.CancelledError: # pragma: no cover
633-
ret = None
634-
else:
635-
ret = handler(*args)
636-
return ret
620+
handler, args = self._get_event_handler(event, namespace, args)
621+
if handler:
622+
if asyncio.iscoroutinefunction(handler):
623+
try:
624+
ret = await handler(*args)
625+
except asyncio.CancelledError: # pragma: no cover
626+
ret = None
637627
else:
638-
return self.not_handled
639-
640-
# or else, forward the event to a namepsace handler if one exists
641-
elif namespace in self.namespace_handlers: # pragma: no branch
642-
return await self.namespace_handlers[namespace].trigger_event(
643-
event, *args)
628+
ret = handler(*args)
629+
return ret
630+
# or else, forward the event to a namespace handler if one exists
631+
handler, args = self._get_namespace_handler(namespace, args)
632+
if handler:
633+
return await handler.trigger_event(event, *args)
634+
else:
635+
return self.not_handled
644636

645637
async def _handle_eio_connect(self, eio_sid, environ):
646638
"""Handle the Engine.IO connection event."""

src/socketio/base_client.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,46 @@ def transport(self):
219219
"""
220220
return self.eio.transport()
221221

222+
def _get_event_handler(self, event, namespace, args):
223+
# return the appropriate application event handler
224+
#
225+
# Resolution priority:
226+
# - self.handlers[namespace][event]
227+
# - self.handlers[namespace]["*"]
228+
# - self.handlers["*"][event]
229+
# - self.handlers["*"]["*"]
230+
handler = None
231+
if namespace in self.handlers:
232+
if event in self.handlers[namespace]:
233+
handler = self.handlers[namespace][event]
234+
elif event not in self.reserved_events and \
235+
'*' in self.handlers[namespace]:
236+
handler = self.handlers[namespace]['*']
237+
args = (event, *args)
238+
elif '*' in self.handlers:
239+
if event in self.handlers['*']:
240+
handler = self.handlers['*'][event]
241+
args = (namespace, *args)
242+
elif event not in self.reserved_events and \
243+
'*' in self.handlers['*']:
244+
handler = self.handlers['*']['*']
245+
args = (event, namespace, *args)
246+
return handler, args
247+
248+
def _get_namespace_handler(self, namespace, args):
249+
# Return the appropriate application event handler.
250+
#
251+
# Resolution priority:
252+
# - self.namespace_handlers[namespace]
253+
# - self.namespace_handlers["*"]
254+
handler = None
255+
if namespace in self.namespace_handlers:
256+
handler = self.namespace_handlers[namespace]
257+
elif '*' in self.namespace_handlers:
258+
handler = self.namespace_handlers['*']
259+
args = (namespace, *args)
260+
return handler, args
261+
222262
def _generate_ack_id(self, namespace, callback):
223263
"""Generate a unique identifier for an ACK packet."""
224264
namespace = namespace or '/'

src/socketio/base_server.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,48 @@ def get_environ(self, sid, namespace=None):
196196
eio_sid = self.manager.eio_sid_from_sid(sid, namespace or '/')
197197
return self.environ.get(eio_sid)
198198

199+
def _get_event_handler(self, event, namespace, args):
200+
# Return the appropriate application event handler
201+
#
202+
# Resolution priority:
203+
# - self.handlers[namespace][event]
204+
# - self.handlers[namespace]["*"]
205+
# - self.handlers["*"][event]
206+
# - self.handlers["*"]["*"]
207+
handler = None
208+
print(event, namespace)
209+
print(namespace in self.handlers)
210+
if namespace in self.handlers:
211+
if event in self.handlers[namespace]:
212+
handler = self.handlers[namespace][event]
213+
elif event not in self.reserved_events and \
214+
'*' in self.handlers[namespace]:
215+
handler = self.handlers[namespace]['*']
216+
args = (event, *args)
217+
elif '*' in self.handlers:
218+
if event in self.handlers['*']:
219+
handler = self.handlers['*'][event]
220+
args = (namespace, *args)
221+
elif event not in self.reserved_events and \
222+
'*' in self.handlers['*']:
223+
handler = self.handlers['*']['*']
224+
args = (event, namespace, *args)
225+
return handler, args
226+
227+
def _get_namespace_handler(self, namespace, args):
228+
# Return the appropriate application event handler.
229+
#
230+
# Resolution priority:
231+
# - self.namespace_handlers[namespace]
232+
# - self.namespace_handlers["*"]
233+
handler = None
234+
if namespace in self.namespace_handlers:
235+
handler = self.namespace_handlers[namespace]
236+
elif '*' in self.namespace_handlers:
237+
handler = self.namespace_handlers['*']
238+
args = (namespace, *args)
239+
return handler, args
240+
199241
def _handle_eio_connect(self): # pragma: no cover
200242
raise NotImplementedError()
201243

src/socketio/client.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -404,17 +404,14 @@ def _handle_error(self, namespace, data):
404404
def _trigger_event(self, event, namespace, *args):
405405
"""Invoke an application event handler."""
406406
# first see if we have an explicit handler for the event
407-
if namespace in self.handlers:
408-
if event in self.handlers[namespace]:
409-
return self.handlers[namespace][event](*args)
410-
elif event not in self.reserved_events and \
411-
'*' in self.handlers[namespace]:
412-
return self.handlers[namespace]['*'](event, *args)
407+
handler, args = self._get_event_handler(event, namespace, args)
408+
if handler:
409+
return handler(*args)
413410

414411
# or else, forward the event to a namespace handler if one exists
415-
elif namespace in self.namespace_handlers:
416-
return self.namespace_handlers[namespace].trigger_event(
417-
event, *args)
412+
handler, args = self._get_namespace_handler(namespace, args)
413+
if handler:
414+
return handler.trigger_event(event, *args)
418415

419416
def _handle_reconnect(self):
420417
if self._reconnect_abort is None: # pragma: no cover

src/socketio/server.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -604,19 +604,15 @@ def _handle_ack(self, eio_sid, namespace, id, data):
604604
def _trigger_event(self, event, namespace, *args):
605605
"""Invoke an application event handler."""
606606
# first see if we have an explicit handler for the event
607-
if namespace in self.handlers:
608-
if event in self.handlers[namespace]:
609-
return self.handlers[namespace][event](*args)
610-
elif event not in self.reserved_events and \
611-
'*' in self.handlers[namespace]:
612-
return self.handlers[namespace]['*'](event, *args)
613-
else:
614-
return self.not_handled
615-
607+
handler, args = self._get_event_handler(event, namespace, args)
608+
if handler:
609+
return handler(*args)
616610
# or else, forward the event to a namespace handler if one exists
617-
elif namespace in self.namespace_handlers: # pragma: no branch
618-
return self.namespace_handlers[namespace].trigger_event(
619-
event, *args)
611+
handler, args = self._get_namespace_handler(namespace, args)
612+
if handler:
613+
return handler.trigger_event(event, *args)
614+
else:
615+
return self.not_handled
620616

621617
def _handle_eio_connect(self, eio_sid, environ):
622618
"""Handle the Engine.IO connection event."""

tests/async/test_client.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,38 @@ def on_foo(self, a, b):
857857
_run(c._trigger_event('foo', '/', 1, '2'))
858858
assert result == [1, '2']
859859

860+
def test_trigger_event_with_catchall_class_namespace(self):
861+
result = {}
862+
863+
class MyNamespace(async_namespace.AsyncClientNamespace):
864+
def on_connect(self, ns):
865+
result['result'] = (ns,)
866+
867+
def on_disconnect(self, ns):
868+
result['result'] = ('disconnect', ns)
869+
870+
def on_foo(self, ns, data):
871+
result['result'] = (ns, data)
872+
873+
def on_bar(self, ns):
874+
result['result'] = 'bar' + ns
875+
876+
def on_baz(self, ns, data1, data2):
877+
result['result'] = (ns, data1, data2)
878+
879+
c = async_client.AsyncClient()
880+
c.register_namespace(MyNamespace('*'))
881+
_run(c._trigger_event('connect', '/foo'))
882+
assert result['result'] == ('/foo',)
883+
_run(c._trigger_event('foo', '/foo', 'a'))
884+
assert result['result'] == ('/foo', 'a')
885+
_run(c._trigger_event('bar', '/foo'))
886+
assert result['result'] == 'bar/foo'
887+
_run(c._trigger_event('baz', '/foo', 'a', 'b'))
888+
assert result['result'] == ('/foo', 'a', 'b')
889+
_run(c._trigger_event('disconnect', '/foo'))
890+
assert result['result'] == ('disconnect', '/foo')
891+
860892
def test_trigger_event_unknown_namespace(self):
861893
c = async_client.AsyncClient()
862894
result = []

0 commit comments

Comments
 (0)