diff --git a/docs/client.rst b/docs/client.rst index 3f487ee2..62d42b27 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -40,45 +40,51 @@ appropriate client class:: Defining Event Handlers ----------------------- -To responds to events triggered by the connection or the server, event Handler -functions must be defined using the ``on`` decorator:: +The Socket.IO protocol is event based. When a server wants to communicate with +a client it *emits* an event. Each event has a name, and a list of +arguments. The client registers event handler functions with the +:func:`socketio.Client.event` or :func:`socketio.Client.on` decorators:: - @sio.on('connect') - def on_connect(): - print('I\'m connected!') - - @sio.on('message') - def on_message(data): + @sio.event + def message(data): print('I received a message!') @sio.on('my message') def on_message(data): - print('I received a custom message!') + print('I received a message!') - @sio.on('disconnect') - def on_disconnect(): - print('I\'m disconnected!') +In the first example the event name is obtained from the name of the +handler function. The second example is slightly more verbose, but it +allows the event name to be different than the function name or to include +characters that are illegal in function names, such as spaces. -For the ``asyncio`` server, event handlers can be regular functions as above, +For the ``asyncio`` client, event handlers can be regular functions as above, or can also be coroutines:: - @sio.on('message') - async def on_message(data): + @sio.event + async def message(data): print('I received a message!') -The argument given to the ``on`` decorator is the event name. The predefined -events that are supported are ``connect``, ``message`` and ``disconnect``. The -application can define any other desired event names. +The ``connect`` and ``disconnect`` events are special; they are invoked +automatically when a client connects or disconnects from the server:: + + @sio.event + def connect(): + print("I'm connected!") + + @sio.event + def disconnect(): + print("I'm disconnected!") Note that the ``disconnect`` handler is invoked for application initiated disconnects, server initiated disconnects, or accidental disconnects, for -example due to networking failures. In the case of an accidental disconnection, -the client is going to attempt to reconnect immediately after invoking the -disconnect handler. As soon as the connection is re-established the connect -handler will be invoked once again. +example due to networking failures. In the case of an accidental +disconnection, the client is going to attempt to reconnect immediately after +invoking the disconnect handler. As soon as the connection is re-established +the connect handler will be invoked once again. -The ``data`` argument passed to the ``'message'`` and custom event Handlers -contains application-specific data provided by the server. +If the server includes arguments with an event, those are passed to the +handler function as arguments. Connecting to a Server ---------------------- @@ -109,24 +115,15 @@ Or in the case of ``asyncio``, as a coroutine:: await sio.emit('my message', {'foo': 'bar'}) The single argument provided to the method is the data that is passed on -to the server. The data can be of type ``str``, ``bytes``, ``dict`` or -``list``. The data included inside dictionaries and lists is also -constrained to these types. +to the server. The data can be of type ``str``, ``bytes``, ``dict``, +``list`` or ``tuple``. When sending a ``tuple``, the elements in it need to +be of any of the other four allowed types. The elements of the tuple will be +passed as multiple arguments to the server-side event handler function. The ``emit()`` method can be invoked inside an event handler as a response to a server event, or in any other part of the application, including in background tasks. -For convenience, a ``send()`` method is also provided. This method accepts -a data element as its only argument, and emits the standard ``message`` -event with it:: - - sio.send('some data') - -In the case of ``asyncio``, ``send()`` is a coroutine:: - - await sio.send('some data') - Event Callbacks --------------- @@ -137,8 +134,8 @@ client can provide a list of return values that are to be passed on to the callback function set up by the server. This is achieves simply by returning the desired values from the handler function:: - @sio.on('my event', namespace='/chat') - def my_event_handler(sid, data): + @sio.event + def my_event(sid, data): # handle the message return "OK", 123 @@ -163,11 +160,15 @@ namespace:: sio.connect('http://localhost:5000', namespaces=['/chat']) To define event handlers on a namespace, the ``namespace`` argument must be -added to the ``on`` decorator:: +added to the corresponding decorator:: + + @sio.event(namespace='/chat') + def my_custom_event(sid, data): + pass @sio.on('connect', namespace='/chat') def on_connect(): - print('I\'m connected to the /chat namespace!') + print("I'm connected to the /chat namespace!") Likewise, the client can emit an event to the server on a namespace by providing its in the ``emit()`` call:: diff --git a/docs/intro.rst b/docs/intro.rst index 464cd1d6..eedb112b 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -26,17 +26,17 @@ The example that follows shows a simple Python client: sio = socketio.Client() - @sio.on('connect') - def on_connect(): + @sio.event + def connect(): print('connection established') - @sio.on('my message') - def on_message(data): + @sio.event + def my_message(data): print('message received with ', data) sio.emit('my response', {'response': 'my response'}) - @sio.on('disconnect') - def on_disconnect(): + @sio.event + def disconnect(): print('disconnected from server') sio.connect('http://localhost:5000') @@ -71,15 +71,15 @@ asynchronous server: '/': {'content_type': 'text/html', 'filename': 'index.html'} }) - @sio.on('connect') + @sio.event def connect(sid, environ): print('connect ', sid) - @sio.on('my message') - def message(sid, data): + @sio.event + def my_message(sid, data): print('message ', data) - @sio.on('disconnect') + @sio.event def disconnect(sid): print('disconnect ', sid) @@ -103,16 +103,16 @@ Uvicorn web server: with open('index.html') as f: return web.Response(text=f.read(), content_type='text/html') - @sio.on('connect', namespace='/chat') + @sio.event def connect(sid, environ): print("connect ", sid) - @sio.on('chat message', namespace='/chat') - async def message(sid, data): + @sio.event + async def chat_message(sid, data): print("message ", data) await sio.emit('reply', room=sid) - @sio.on('disconnect', namespace='/chat') + @sio.event def disconnect(sid): print('disconnect ', sid) diff --git a/docs/server.rst b/docs/server.rst index a151325a..4902db03 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -135,30 +135,39 @@ Defining Event Handlers The Socket.IO protocol is event based. When a client wants to communicate with the server it *emits* an event. Each event has a name, and a list of arguments. The server registers event handler functions with the -:func:`socketio.Server.on` decorator:: +:func:`socketio.Server.event` or :func:`socketio.Server.on` decorators:: + + @sio.event + def my_event(sid, data): + pass @sio.on('my custom event') - def my_custom_event(sid, data): + def another_event(sid, data): pass +In the first example the event name is obtained from the name of the handler +function. The second example is slightly more verbose, but it allows the event +name to be different than the function name or to include characters that are +illegal in function names, such as spaces. + For asyncio servers, event handlers can optionally be given as coroutines:: - @sio.on('my custom event') - async def my_custom_event(sid, data): + @sio.event + async def my_event(sid, data): pass 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. -The ``connect`` and ``disconnect`` are special; they are invoked automatically -when a client connects or disconnects from the server:: +The ``connect`` and ``disconnect`` events are special; they are invoked +automatically when a client connects or disconnects from the server:: - @sio.on('connect') + @sio.event def connect(sid, environ): print('connect ', sid) - @sio.on('disconnect') + @sio.event def disconnect(sid): print('disconnect ', sid) @@ -172,9 +181,9 @@ headers. After inspecting the request, the connect event handler can return Sometimes it is useful to pass data back to the client being rejected. In that case instead of returning ``False`` :class:`socketio.exceptions.ConnectionRefusedError` can be raised, and all of -its argument will be sent to the client with the rejection:: +its arguments will be sent to the client with the rejection message:: - @sio.on('connect') + @sio.event def connect(sid, environ): raise ConnectionRefusedError('authentication failed') @@ -210,8 +219,8 @@ has processed the event. While this is entirely managed by the client, the server can provide a list of values that are to be passed on to the callback function, simply by returning them from the handler function:: - @sio.on('my event', namespace='/chat') - def my_event_handler(sid, data): + @sio.event + def my_event(sid, data): # handle the message return "OK", 123 @@ -240,6 +249,10 @@ that use multiple namespaces specify the correct namespace when setting up their event handlers and rooms, using the optional ``namespace`` argument available in all the methods in the :class:`socketio.Server` class:: + @sio.event(namespace='/chat') + def my_custom_event(sid, data): + pass + @sio.on('my custom event', namespace='/chat') def my_custom_event(sid, data): pass @@ -322,11 +335,11 @@ rooms as needed and can be moved between rooms as often as necessary. :: - @sio.on('chat') + @sio.event def begin_chat(sid): sio.enter_room(sid, 'chat_users') - @sio.on('exit_chat') + @sio.event def exit_chat(sid): sio.leave_room(sid, 'chat_users') @@ -338,8 +351,8 @@ during the broadcast. :: - @sio.on('my message') - def message(sid, data): + @sio.event + def my_message(sid, data): sio.emit('my reply', data, room='chat_users', skip_sid=sid) User Sessions @@ -353,52 +366,52 @@ of the connection, such as usernames or user ids. The ``save_session()`` and ``get_session()`` methods are used to store and retrieve information in the user session:: - @sio.on('connect') - def on_connect(sid, environ): + @sio.event + def connect(sid, environ): username = authenticate_user(environ) sio.save_session(sid, {'username': username}) - @sio.on('message') - def on_message(sid, data): + @sio.event + def message(sid, data): session = sio.get_session(sid) print('message from ', session['username']) For the ``asyncio`` server, these methods are coroutines:: - @sio.on('connect') - async def on_connect(sid, environ): + @sio.event + async def connect(sid, environ): username = authenticate_user(environ) await sio.save_session(sid, {'username': username}) - @sio.on('message') - async def on_message(sid, data): + @sio.event + async def message(sid, data): session = await sio.get_session(sid) print('message from ', session['username']) The session can also be manipulated with the `session()` context manager:: - @sio.on('connect') - def on_connect(sid, environ): + @sio.event + def connect(sid, environ): username = authenticate_user(environ) with sio.session(sid) as session: session['username'] = username - @sio.on('message') - def on_message(sid, data): + @sio.event + def message(sid, data): with sio.session(sid) as session: print('message from ', session['username']) For the ``asyncio`` server, an asynchronous context manager is used:: - @sio.on('connect') - def on_connect(sid, environ): + @sio.event + def connect(sid, environ): username = authenticate_user(environ) async with sio.session(sid) as session: session['username'] = username - @sio.on('message') - def on_message(sid, data): + @sio.event + def message(sid, data): async with sio.session(sid) as session: print('message from ', session['username']) diff --git a/socketio/client.py b/socketio/client.py index cede4ac2..a3766406 100644 --- a/socketio/client.py +++ b/socketio/client.py @@ -151,6 +151,41 @@ def set_handler(handler): return set_handler set_handler(handler) + def event(self, *args, **kwargs): + """Decorator to register an event handler. + + This is a simplified version of the ``on()`` method that takes the + event name from the decorated function. + + Example usage:: + + @sio.event + def my_event(data): + print('Received data: ', data) + + The above example is equivalent to:: + + @sio.on('my_event') + def my_event(data): + print('Received data: ', data) + + A custom namespace can be given as an argument to the decorator:: + + @sio.event(namespace='/test') + def my_event(data): + print('Received data: ', data) + """ + if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): + # the decorator was invoked without arguments + # args[0] is the decorated function + return self.on(args[0].__name__)(args[0]) + else: + # the decorator was invoked with arguments + def set_handler(handler): + return self.on(handler.__name__, *args, **kwargs)(handler) + + return set_handler + def register_namespace(self, namespace_handler): """Register a namespace handler object. diff --git a/socketio/server.py b/socketio/server.py index 89edca50..8bce1d92 100644 --- a/socketio/server.py +++ b/socketio/server.py @@ -186,6 +186,41 @@ def set_handler(handler): return set_handler set_handler(handler) + def event(self, *args, **kwargs): + """Decorator to register an event handler. + + This is a simplified version of the ``on()`` method that takes the + event name from the decorated function. + + Example usage:: + + @sio.event + def my_event(data): + print('Received data: ', data) + + The above example is equivalent to:: + + @sio.on('my_event') + def my_event(data): + print('Received data: ', data) + + A custom namespace can be given as an argument to the decorator:: + + @sio.event(namespace='/test') + def my_event(data): + print('Received data: ', data) + """ + if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): + # the decorator was invoked without arguments + # args[0] is the decorated function + return self.on(args[0].__name__)(args[0]) + else: + # the decorator was invoked with arguments + def set_handler(handler): + return self.on(handler.__name__, *args, **kwargs)(handler) + + return set_handler + def register_namespace(self, namespace_handler): """Register a namespace handler object. diff --git a/tests/common/test_client.py b/tests/common/test_client.py index b84ad7b8..6fef6c31 100644 --- a/tests/common/test_client.py +++ b/tests/common/test_client.py @@ -96,6 +96,30 @@ def bar(): self.assertEqual(c.handlers['/']['disconnect'], bar) self.assertEqual(c.handlers['/foo']['disconnect'], bar) + def test_event(self): + c = client.Client() + + @c.event + def connect(): + pass + + @c.event + def foo(): + pass + + @c.event + def bar(): + pass + + @c.event(namespace='/foo') + def disconnect(): + pass + + self.assertEqual(c.handlers['/']['connect'], connect) + self.assertEqual(c.handlers['/']['foo'], foo) + self.assertEqual(c.handlers['/']['bar'], bar) + self.assertEqual(c.handlers['/foo']['disconnect'], disconnect) + def test_namespace_handler(self): class MyNamespace(namespace.ClientNamespace): pass diff --git a/tests/common/test_server.py b/tests/common/test_server.py index 2b9f9447..c8f99518 100644 --- a/tests/common/test_server.py +++ b/tests/common/test_server.py @@ -48,6 +48,30 @@ def bar(): self.assertEqual(s.handlers['/']['disconnect'], bar) self.assertEqual(s.handlers['/foo']['disconnect'], bar) + def test_event(self, eio): + s = server.Server() + + @s.event + def connect(): + pass + + @s.event + def foo(): + pass + + @s.event() + def bar(): + pass + + @s.event(namespace='/foo') + def disconnect(): + pass + + self.assertEqual(s.handlers['/']['connect'], connect) + self.assertEqual(s.handlers['/']['foo'], foo) + self.assertEqual(s.handlers['/']['bar'], bar) + self.assertEqual(s.handlers['/foo']['disconnect'], disconnect) + def test_emit(self, eio): mgr = mock.MagicMock() s = server.Server(client_manager=mgr)