diff --git a/docs/index.rst b/docs/index.rst index f3ca6343..c82249b7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,25 +22,32 @@ You can install this package in the usual way using ``pip``:: Requirements ------------ -Since version 1.0, this extension is compatible with both Python 2.7 and -Python 3.3+. The asynchronous services that this package relies on can be -selected among three choices: +Flask-SocketIO is compatible with both Python 2.7 and Python 3.3+. The +asynchronous services that this package relies on can be selected among three +choices: - `eventlet `_ is the best performant option, with support for long-polling and WebSocket transports. -- `gevent `_ is the framework used in previous - releases of this extension. The long-polling transport is fully supported. - To add support for WebSocket, the `gevent-websocket `_ - package must be installed as well. The use of gevent and gevent-websocket - is also a performant option, but slightly lower than eventlet. +- `gevent `_ is supported in a number of different + configurations. The long-polling transport is fully supported with the + gevent package, but unlike eventlet, gevent does not have native WebSocket + support. To add support for WebSocket there are currently two options. The + `gevent-websocket `_ + package adds WebSocket support to gevent, but unfortunately this package is + current only available for Python 2. The other alternative is to use the + `uWSGI `_ web server, which + comes with WebSocket functionality. The use of gevent is also a performant + option, but slightly lower than eventlet. - The Flask development server based on Werkzeug can be used as well, with the caveat that it lacks the performance of the other two options, so it should only be used to simplify the development workflow. This option only supports the long-polling transport. The extension automatically detects which asynchronous framework to use based -on what is installed. Preference is given to eventlet, followed by gevent. If -neither one is installed, then the Flask development server is used. +on what is installed. Preference is given to eventlet, followed by gevent. +For WebSocket support in gevent, uWSGI is preferred, followed by +gevent-websocket. If neither eventlet nor gevent are installed, then the Flask +development server is used. If using multiple processes, a message queue service is used by the processes to coordinate operations such as broadcasting. The supported queues are @@ -54,74 +61,6 @@ written in Swift, Java and C++. Unofficial clients may also work, as long as they implement the `Socket.IO protocol `_. -Differences With Flask-SocketIO Versions 0.x -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Older versions of Flask-SocketIO had a completely different set of -requirements. Those old versions had a dependency on -`gevent-socketio `_ and -`gevent-websocket `_, which -are not required in release 1.0. - -In spite of the change in dependencies, there aren't many significant -changes introduced in version 1.0. Below is a detailed list of -the actual differences: - -- Release 1.0 drops support for Python 2.6, and adds support for Python 3.3, - Python 3.4, and pypy. -- Releases 0.x required an old version of the Socket.IO Javascript client. - Starting with release 1.0, the current releases of Socket.IO and Engine.IO - are supported. Releases of the Socket.IO client prior to 1.0 are no - supported. The Swift and C++ official Socket.IO clients are now supported - as well. -- The 0.x releases depended on gevent, gevent-socketio and gevent-websocket. - In release 1.0 gevent-socketio is not used anymore, and gevent is one of - three options for backend web server, with eventlet and any regular - multi-threaded WSGI server, including Flask's development web server. -- The Socket.IO server options have changed in release 1.0. They can be - provided in the SocketIO constructor, or in the ``run()`` call. The options - provided in these two are merged before they are used. -- The 0.x releases exposed the gevent-socketio connection as - ``request.namespace``. In release 1.0 this is not available anymore. The - request object defines ``request.namespace`` as the name of the namespace - being handled, and adds ``request.sid``, defined as the unique session ID - for the client connection, and ``request.event``, which contains the event - name and arguments. -- To get the list of rooms a client was in the 0.x release required the - application to use a private structure of gevent-socketio, with the - expression ``request.namespace.rooms``. This is not available in release - 1.0, which includes a proper ``rooms()`` function. -- The recommended "trick" to send a message to an individual client was to - put each client in a separate room, then address messages to the desired - room. This was formalized in release 1.0, where clients are assigned a room - automatically when they connect. -- The ``'connect'`` event for the global namespace did not fire on releases - prior to 1.0. This has been fixed and now this event fires as expected. -- Support for client-side callbacks was introduced in release 1.0. - -Upgrading to Flask-SocketIO 1.x and 2.x from older releases -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -On the client side, you need to upgrade your Socket.IO Javascript client from -the 0.9.x releases to the 1.3.x or newer releases. - -On the server side, there are a few points to consider: - -- If you wish to continue using gevent, then uninstall gevent-socketio from - your virtual environment, as this package is not used anymore and may - collide with its replacement, python-socketio. -- If you want to have slightly better performance and stability, then it is - recommended that you switch to eventlet. To do this, uninstall gevent, - gevent-socketio and gevent-websocket, and install eventlet. -- If your application uses monkey patching and you switched to eventlet, call - `eventlet.monkey_patch()` instead of gevent's `monkey.patch_all()`. Also, - any calls to gevent must be replaced with equivalent calls to eventlet. -- Any uses of `request.namespace` must be replaced with direct calls into the - Flask-SocketIO functions. For example, `request.namespace.rooms` must be - replaced with the `rooms()` function. -- Any uses of internal gevent-socketio objects must be removed, as this - package is not a dependency anymore. - Initialization -------------- @@ -395,6 +334,44 @@ connection. This is so that the client can be authenticated at this point. Note that connection and disconnection events are sent individually on each namespace used. +Class-Based Namespaces +---------------------- + +As an alternative to the decorator-based event handlers described above, the +event handlers that belong to a namespace can be created as methods of a +class. The :class:`flask_socketio.Namespace` is provided as a base class to +create class-based namespaces:: + + from flask_socketio import Namespace, emit + + class MyCustomNamespace(Namespace): + def on_connect(): + pass + + def on_disconnect(): + pass + + def on_my_event(data): + emit('my_response', data) + + socketio.on_namespace(MyCustomNamespace('/test')) + +When class-based namespaces are used, any events received by the server are +dispatched to a method named as the event name with the ``on_`` prefix. For +example, event ``my_event`` will be handled by a method named ``on_my_event``. +If an event is received for which there is no corresponding method defined in +the namespace class, then the event is ignored. All event names used in +class-based namespaces must used characters that are legal in method names. + +As a convenience to methods defined in a class-based namespace, the namespace +instance includes versions of several of the methods in the +:class:`flask_socketio.SocketIO` class that default to the proper namespace +when the ``namespace`` argument is not given. + +If an event has a handler in a class-based namespace, and also a +decorator-based function handler, only the decorated function handler is +invoked. + Error Handling -------------- @@ -552,6 +529,9 @@ or gevent are installed. If neither of these are installed, then the application runs on Flask's development web server, which is not appropriate for production use. +Unfortunately this option is not available when using gevent with uWSGI. See +the uWSGI section below for information on this option. + Gunicorn Web Server ~~~~~~~~~~~~~~~~~~~ @@ -587,18 +567,17 @@ all the examples above include the ``-w 1`` option. uWSGI Web Server ~~~~~~~~~~~~~~~~ -At this time, uWSGI is not a good choice of web server for a SocketIO -application due to the following limitations: +When using the uWSGI server in combination with gevent, the Socket.IO server +can take advantage of uWSGI’s native WebSocket support. -- The ``'eventlet'`` async mode cannot be used, as uWSGI currently does not - support web servers based on eventlet. -- The ``'gevent'`` async mode is supported, but uWSGI is currently - incompatible with the gevent-websocket package, so only the long-polling - transport can be used. -- The native WebSocket support available from uWSGI is not based on eventlet - or gevent, so it cannot be used at this time. If possible, a WebSocket - transport based on the uWSGI WebSocket implementation will be made available - in a future release. +A complete explanation of the configuration and usage of the uWSGI server is +beyond the scope of this documentation. The uWSGI server is a fairly complex +package that provides a large and comprehensive set of options. It must be +compiled with WebSocket and SSL support for the WebSocket transport to be +available. As way of an introduction, the following command starts a uWSGI +server for the example application app.py on port 5000:: + + $ uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file app.py --callable app Using nginx as a WebSocket Reverse Proxy ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -723,6 +702,73 @@ your external process does use a coroutine framework for whatever reason, then monkey patching is likely required, so that the message queue accesses coroutine friendly functions and classes. +Upgrading to Flask-SocketIO 1.x and 2.x from the 0.x releases +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Older versions of Flask-SocketIO had a completely different set of +requirements. Those old versions had a dependency on +`gevent-socketio `_ and +`gevent-websocket `_, which +are not required in release 1.0. + +In spite of the change in dependencies, there aren't many significant +changes introduced in version 1.0. Below is a detailed list of +the actual differences: + +- Release 1.0 drops support for Python 2.6, and adds support for Python 3.3, + Python 3.4, and pypy. +- Releases 0.x required an old version of the Socket.IO Javascript client. + Starting with release 1.0, the current releases of Socket.IO and Engine.IO + are supported. Releases of the Socket.IO client prior to 1.0 are no + supported. The Swift and C++ official Socket.IO clients are now supported + as well. +- The 0.x releases depended on gevent, gevent-socketio and gevent-websocket. + In release 1.0 gevent-socketio is not used anymore, and gevent is one of + three options for backend web server, with eventlet and any regular + multi-threaded WSGI server, including Flask's development web server. +- The Socket.IO server options have changed in release 1.0. They can be + provided in the SocketIO constructor, or in the ``run()`` call. The options + provided in these two are merged before they are used. +- The 0.x releases exposed the gevent-socketio connection as + ``request.namespace``. In release 1.0 this is not available anymore. The + request object defines ``request.namespace`` as the name of the namespace + being handled, and adds ``request.sid``, defined as the unique session ID + for the client connection, and ``request.event``, which contains the event + name and arguments. +- To get the list of rooms a client was in the 0.x release required the + application to use a private structure of gevent-socketio, with the + expression ``request.namespace.rooms``. This is not available in release + 1.0, which includes a proper ``rooms()`` function. +- The recommended "trick" to send a message to an individual client was to + put each client in a separate room, then address messages to the desired + room. This was formalized in release 1.0, where clients are assigned a room + automatically when they connect. +- The ``'connect'`` event for the global namespace did not fire on releases + prior to 1.0. This has been fixed and now this event fires as expected. +- Support for client-side callbacks was introduced in release 1.0. + +To upgrade to the newer Flask-SocketIO releases, you need to upgrade your +Socket.IO client to a client that is compatible with the Socket.IO 1.0 +protocol. For the JavaScript client, the 1.3.x and 1.4.x releases have been +extensively tested and found compatible. + +On the server side, there are a few points to consider: + +- If you wish to continue using gevent, then uninstall gevent-socketio from + your virtual environment, as this package is not used anymore and may + collide with its replacement, python-socketio. +- If you want to have slightly better performance and stability, then it is + recommended that you switch to eventlet. To do this, uninstall gevent, + gevent-socketio and gevent-websocket, and install eventlet. +- If your application uses monkey patching and you switched to eventlet, call + `eventlet.monkey_patch()` instead of gevent's `monkey.patch_all()`. Also, + any calls to gevent must be replaced with equivalent calls to eventlet. +- Any uses of `request.namespace` must be replaced with direct calls into the + Flask-SocketIO functions. For example, `request.namespace.rooms` must be + replaced with the `rooms()` function. +- Any uses of internal gevent-socketio objects must be removed, as this + package is not a dependency anymore. + API Reference ------------- @@ -736,5 +782,7 @@ API Reference .. autofunction:: close_room .. autofunction:: rooms .. autofunction:: disconnect +.. autoclass:: Namespace + :members: .. autoclass:: SocketIOTestClient :members: diff --git a/example/app.py b/example/app.py index 7c1f24b9..08428167 100755 --- a/example/app.py +++ b/example/app.py @@ -20,7 +20,7 @@ def background_thread(): while True: socketio.sleep(10) count += 1 - socketio.emit('my response', + socketio.emit('my_response', {'data': 'Server generated event', 'count': count}, namespace='/test') @@ -30,17 +30,17 @@ def index(): return render_template('index.html', async_mode=socketio.async_mode) -@socketio.on('my event', namespace='/test') +@socketio.on('my_event', namespace='/test') def test_message(message): session['receive_count'] = session.get('receive_count', 0) + 1 - emit('my response', + emit('my_response', {'data': message['data'], 'count': session['receive_count']}) -@socketio.on('my broadcast event', namespace='/test') +@socketio.on('my_broadcast_event', namespace='/test') def test_broadcast_message(message): session['receive_count'] = session.get('receive_count', 0) + 1 - emit('my response', + emit('my_response', {'data': message['data'], 'count': session['receive_count']}, broadcast=True) @@ -49,7 +49,7 @@ def test_broadcast_message(message): def join(message): join_room(message['room']) session['receive_count'] = session.get('receive_count', 0) + 1 - emit('my response', + emit('my_response', {'data': 'In rooms: ' + ', '.join(rooms()), 'count': session['receive_count']}) @@ -58,39 +58,39 @@ def join(message): def leave(message): leave_room(message['room']) session['receive_count'] = session.get('receive_count', 0) + 1 - emit('my response', + emit('my_response', {'data': 'In rooms: ' + ', '.join(rooms()), 'count': session['receive_count']}) -@socketio.on('close room', namespace='/test') +@socketio.on('close_room', namespace='/test') def close(message): session['receive_count'] = session.get('receive_count', 0) + 1 - emit('my response', {'data': 'Room ' + message['room'] + ' is closing.', + emit('my_response', {'data': 'Room ' + message['room'] + ' is closing.', 'count': session['receive_count']}, room=message['room']) close_room(message['room']) -@socketio.on('my room event', namespace='/test') +@socketio.on('my_room_event', namespace='/test') def send_room_message(message): session['receive_count'] = session.get('receive_count', 0) + 1 - emit('my response', + emit('my_response', {'data': message['data'], 'count': session['receive_count']}, room=message['room']) -@socketio.on('disconnect request', namespace='/test') +@socketio.on('disconnect_request', namespace='/test') def disconnect_request(): session['receive_count'] = session.get('receive_count', 0) + 1 - emit('my response', + emit('my_response', {'data': 'Disconnected!', 'count': session['receive_count']}) disconnect() -@socketio.on('my ping', namespace='/test') +@socketio.on('my_ping', namespace='/test') def ping_pong(): - emit('my pong') + emit('my_pong') @socketio.on('connect', namespace='/test') @@ -98,7 +98,7 @@ def test_connect(): global thread if thread is None: thread = socketio.start_background_task(target=background_thread) - emit('my response', {'data': 'Connected', 'count': 0}) + emit('my_response', {'data': 'Connected', 'count': 0}) @socketio.on('disconnect', namespace='/test') diff --git a/example/app_namespace.py b/example/app_namespace.py new file mode 100755 index 00000000..41e11f4b --- /dev/null +++ b/example/app_namespace.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +from flask import Flask, render_template, session, request +from flask_socketio import SocketIO, Namespace, emit, join_room, leave_room, \ + close_room, rooms, disconnect + +# Set this variable to "threading", "eventlet" or "gevent" to test the +# different async modes, or leave it set to None for the application to choose +# the best option based on installed packages. +async_mode = None + +app = Flask(__name__) +app.config['SECRET_KEY'] = 'secret!' +socketio = SocketIO(app, async_mode=async_mode) +thread = None + + +def background_thread(): + """Example of how to send server generated events to clients.""" + count = 0 + while True: + socketio.sleep(10) + count += 1 + socketio.emit('my_response', + {'data': 'Server generated event', 'count': count}, + namespace='/test') + + +@app.route('/') +def index(): + return render_template('index.html', async_mode=socketio.async_mode) + + +class MyNamespace(Namespace): + def on_my_event(self, message): + session['receive_count'] = session.get('receive_count', 0) + 1 + emit('my_response', + {'data': message['data'], 'count': session['receive_count']}) + + def on_my_broadcast_event(self, message): + session['receive_count'] = session.get('receive_count', 0) + 1 + emit('my_response', + {'data': message['data'], 'count': session['receive_count']}, + broadcast=True) + + def on_join(self, message): + join_room(message['room']) + session['receive_count'] = session.get('receive_count', 0) + 1 + emit('my_response', + {'data': 'In rooms: ' + ', '.join(rooms()), + 'count': session['receive_count']}) + + def on_leave(self, message): + leave_room(message['room']) + session['receive_count'] = session.get('receive_count', 0) + 1 + emit('my_response', + {'data': 'In rooms: ' + ', '.join(rooms()), + 'count': session['receive_count']}) + + def on_close_room(self, message): + session['receive_count'] = session.get('receive_count', 0) + 1 + emit('my_response', {'data': 'Room ' + message['room'] + ' is closing.', + 'count': session['receive_count']}, + room=message['room']) + close_room(message['room']) + + def on_my_room_event(self, message): + session['receive_count'] = session.get('receive_count', 0) + 1 + emit('my_response', + {'data': message['data'], 'count': session['receive_count']}, + room=message['room']) + + def on_disconnect_request(self): + session['receive_count'] = session.get('receive_count', 0) + 1 + emit('my_response', + {'data': 'Disconnected!', 'count': session['receive_count']}) + disconnect() + + def on_my_ping(self): + emit('my_pong') + + def on_connect(self): + global thread + if thread is None: + thread = socketio.start_background_task(target=background_thread) + emit('my_response', {'data': 'Connected', 'count': 0}) + + def on_disconnect(self): + print('Client disconnected', request.sid) + + +socketio.on_namespace(MyNamespace('/test')) + + +if __name__ == '__main__': + socketio.run(app, debug=True) diff --git a/example/templates/index.html b/example/templates/index.html index 577041eb..7f6a37e8 100644 --- a/example/templates/index.html +++ b/example/templates/index.html @@ -22,14 +22,14 @@ // The callback function is invoked when a connection with the // server is established. socket.on('connect', function() { - socket.emit('my event', {data: 'I\'m connected!'}); + socket.emit('my_event', {data: 'I\'m connected!'}); }); // Event handler for server sent data. // The callback function is invoked whenever the server emits data // to the client. The data is then displayed in the "Received" // section of the page. - socket.on('my response', function(msg) { + socket.on('my_response', function(msg) { $('#log').append('
' + $('
').text('Received #' + msg.count + ': ' + msg.data).html()); }); @@ -40,13 +40,13 @@ var start_time; window.setInterval(function() { start_time = (new Date).getTime(); - socket.emit('my ping'); + socket.emit('my_ping'); }, 1000); // Handler for the "pong" message. When the pong is received, the // time from the ping is stored, and the average of the last 30 // samples is average and displayed. - socket.on('my pong', function() { + socket.on('my_pong', function() { var latency = (new Date).getTime() - start_time; ping_pong_times.push(latency); ping_pong_times = ping_pong_times.slice(-30); // keep last 30 samples @@ -60,11 +60,11 @@ // These accept data from the user and send it to the server in a // variety of ways $('form#emit').submit(function(event) { - socket.emit('my event', {data: $('#emit_data').val()}); + socket.emit('my_event', {data: $('#emit_data').val()}); return false; }); $('form#broadcast').submit(function(event) { - socket.emit('my broadcast event', {data: $('#broadcast_data').val()}); + socket.emit('my_broadcast_event', {data: $('#broadcast_data').val()}); return false; }); $('form#join').submit(function(event) { @@ -76,15 +76,15 @@ return false; }); $('form#send_room').submit(function(event) { - socket.emit('my room event', {room: $('#room_name').val(), data: $('#room_data').val()}); + socket.emit('my_room_event', {room: $('#room_name').val(), data: $('#room_data').val()}); return false; }); $('form#close').submit(function(event) { - socket.emit('close room', {room: $('#close_room').val()}); + socket.emit('close_room', {room: $('#close_room').val()}); return false; }); $('form#disconnect').submit(function(event) { - socket.emit('disconnect request'); + socket.emit('disconnect_request'); return false; }); }); diff --git a/flask_socketio/__init__.py b/flask_socketio/__init__.py index 0e4b9b2e..28cfc947 100644 --- a/flask_socketio/__init__.py +++ b/flask_socketio/__init__.py @@ -19,6 +19,7 @@ from werkzeug.debug import DebuggedApplication from werkzeug.serving import run_with_reloader +from .namespace import Namespace from .test_client import SocketIOTestClient @@ -84,11 +85,15 @@ class SocketIO(object): The Engine.IO server configuration supports the following settings: - :param async_mode: The library used for asynchronous operations. Valid - options are "threading", "eventlet" and "gevent". If - this argument is not given, "eventlet" is tried first, - then "gevent", and finally "threading". The websocket - transport is not supported in "threading" mode. + :param async_mode: The asynchronous model to use. See the Deployment + section in the documentation for a description of the + available options. Valid async modes are + ``threading``, ``eventlet``, ``gevent`` and + ``gevent_uwsgi``. If this argument is not given, + ``eventlet`` is tried first, then ``gevent_uwsgi``, + then ``gevent``, and finally ``threading``. The + first async mode that has all its dependencies installed + is then one that is chosen. :param ping_timeout: The time in seconds that the client waits for the server to respond before disconnecting. :param ping_interval: The interval in seconds at which the client pings @@ -117,6 +122,7 @@ def __init__(self, app=None, **kwargs): self.server_options = None self.wsgi_server = None self.handlers = [] + self.namespace_handlers = [] self.exception_handlers = {} self.default_exception_handler = None if app is not None or len(kwargs) > 0: @@ -168,6 +174,9 @@ def loads(*args, **kwargs): self.async_mode = self.server.async_mode for handler in self.handlers: self.server.on(handler[0], handler[1], namespace=handler[2]) + for namespace_handler in self.namespace_handlers: + self.server.register_namespace(namespace_handler) + if app is not None: # here we attach the SocketIO middlware to the SocketIO object so it # can be referenced later if debug middleware needs to be inserted @@ -198,36 +207,9 @@ def handle_my_custom_event(json): def decorator(handler): def _handler(sid, *args): - if sid not in self.server.environ: - # we don't have record of this client, ignore this event - return '', 400 - app = self.server.environ[sid]['flask.app'] - with app.request_context(self.server.environ[sid]): - if 'saved_session' in self.server.environ[sid]: - self._copy_session( - self.server.environ[sid]['saved_session'], - flask.session) - flask.request.sid = sid - flask.request.namespace = namespace - flask.request.event = {'message': message, 'args': args} - try: - if message == 'connect': - ret = handler() - else: - ret = handler(*args) - except: - err_handler = self.exception_handlers.get( - namespace, self.default_exception_handler) - if err_handler is None: - raise - type, value, traceback = sys.exc_info() - return err_handler(value) - if flask.session.modified and sid in self.server.environ: - self.server.environ[sid]['saved_session'] = {} - self._copy_session( - flask.session, - self.server.environ[sid]['saved_session']) - return ret + return self._handle_event(handler, message, namespace, sid, + *args) + if self.server: self.server.on(message, _handler, namespace=namespace) else: @@ -300,6 +282,15 @@ def on_foo_event(json): """ self.on(message, namespace=namespace)(handler) + def on_namespace(self, namespace_handler): + if not isinstance(namespace_handler, Namespace): + raise ValueError('Not a namespace instance.') + namespace_handler._set_socketio(self) + if self.server: + self.server.register_namespace(namespace_handler) + else: + self.namespace_handlers.append(namespace_handler) + def emit(self, event, *args, **kwargs): """Emit a server generated SocketIO event. @@ -561,6 +552,38 @@ def test_client(self, app, namespace=None): """Return a simple SocketIO client that can be used for unit tests.""" return SocketIOTestClient(app, self, namespace) + def _handle_event(self, handler, message, namespace, sid, *args): + if sid not in self.server.environ: + # we don't have record of this client, ignore this event + return '', 400 + app = self.server.environ[sid]['flask.app'] + with app.request_context(self.server.environ[sid]): + if 'saved_session' in self.server.environ[sid]: + self._copy_session( + self.server.environ[sid]['saved_session'], + flask.session) + flask.request.sid = sid + flask.request.namespace = namespace + flask.request.event = {'message': message, 'args': args} + try: + if message == 'connect': + ret = handler() + else: + ret = handler(*args) + except: + err_handler = self.exception_handlers.get( + namespace, self.default_exception_handler) + if err_handler is None: + raise + type, value, traceback = sys.exc_info() + return err_handler(value) + if flask.session.modified and sid in self.server.environ: + self.server.environ[sid]['saved_session'] = {} + self._copy_session( + flask.session, + self.server.environ[sid]['saved_session']) + return ret + def _copy_session(self, src, dest): for k in src: dest[k] = src[k] diff --git a/flask_socketio/namespace.py b/flask_socketio/namespace.py new file mode 100644 index 00000000..914ff381 --- /dev/null +++ b/flask_socketio/namespace.py @@ -0,0 +1,47 @@ +from socketio import Namespace as _Namespace + + +class Namespace(_Namespace): + def __init__(self, namespace=None): + super(Namespace, self).__init__(namespace) + self.socketio = None + + def _set_socketio(self, socketio): + self.socketio = socketio + + def trigger_event(self, event, *args): + """Dispatch an event to the proper handler method. + + In the most common usage, this method is not overloaded by subclasses, + as it performs the routing of events to methods. However, this + method can be overriden if special dispatching rules are needed, or if + having a single method that catches all events is desired. + """ + handler_name = 'on_' + event + if not hasattr(self, handler_name): + # there is no handler for this event, so we ignore it + return + handler = getattr(self, handler_name) + return self.socketio._handle_event(handler, event, self.namespace, + *args) + + def emit(self, event, data=None, room=None, include_self=True, + namespace=None, callback=None): + """Emit a custom event to one or more connected clients.""" + return self.socketio.emit(event, data, room=room, + include_self=include_self, + namespace=namespace or self.namespace, + callback=callback) + + def send(self, data, room=None, include_self=True, namespace=None, + callback=None): + """Send a message to one or more connected clients.""" + return self.socketio.send(data, room=room, include_self=include_self, + namespace=namespace or self.namespace, + callback=callback) + + def close_room(self, room, namespace=None): + """Close a room.""" + return self.socketio.close_room(room=room, + namespace=namespace or self.namespace) + diff --git a/setup.py b/setup.py index b9136097..2e554586 100755 --- a/setup.py +++ b/setup.py @@ -27,8 +27,8 @@ }, install_requires=[ 'Flask>=0.9', - 'python-socketio>=1.4', - 'python-engineio>=0.9.2' + 'python-socketio>=1.5.0', + 'python-engineio>=1.0.0' ], tests_require=[ 'coverage' diff --git a/test_socketio.py b/test_socketio.py index ce05818d..eea7d15b 100755 --- a/test_socketio.py +++ b/test_socketio.py @@ -1,11 +1,12 @@ import unittest import coverage -cov = coverage.coverage() +cov = coverage.coverage(branch=True) cov.start() from flask import Flask, session, request -from flask_socketio import SocketIO, send, emit, join_room, leave_room +from flask_socketio import SocketIO, send, emit, join_room, leave_room, \ + Namespace app = Flask(__name__) app.config['SECRET_KEY'] = 'secret' @@ -183,6 +184,40 @@ def raise_error_default(data): raise AssertionError() +class MyNamespace(Namespace): + def on_connect(self): + send('connected-ns') + + def on_disconnect(self): + global disconnected + disconnected = '/ns' + + def on_message(self, message): + send(message) + if message == 'test session': + session['a'] = 'b' + if message not in "test noackargs": + return message + + def on_json(self, data): + send(data, json=True, broadcast=True) + if not data.get('noackargs'): + return data + + def on_my_custom_event(self, data): + emit('my custom response', data) + if not data.get('noackargs'): + return data + + def on_other_custom_event(self, data): + global request_event_data + request_event_data = request.event + emit('my custom response', data) + + +socketio.on_namespace(MyNamespace('/ns')) + + class TestSocketIO(unittest.TestCase): @classmethod def setUpClass(cls): @@ -191,7 +226,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): cov.stop() - cov.report(include='flask_socketio/__init__.py') + cov.report(include='flask_socketio/*', show_missing=True) def setUp(self): pass @@ -463,6 +498,55 @@ def test_on_event(self): self.assertEqual(received[0]['name'], 'my custom namespace response') self.assertEqual(received[0]['args'][0]['a'], 'b') + def test_connect_class_based(self): + client = socketio.test_client(app, namespace='/ns') + received = client.get_received('/ns') + self.assertEqual(len(received), 1) + self.assertEqual(received[0]['args'], 'connected-ns') + client.disconnect('/ns') + + def test_disconnect_class_based(self): + global disconnected + disconnected = None + client = socketio.test_client(app, namespace='/ns') + client.disconnect('/ns') + self.assertEqual(disconnected, '/ns') + + def test_send_class_based(self): + client = socketio.test_client(app, namespace='/ns') + client.get_received('/ns') + client.send('echo this message back', namespace='/ns') + received = client.get_received('/ns') + self.assertTrue(len(received) == 1) + self.assertTrue(received[0]['args'] == 'echo this message back') + + def test_send_json_class_based(self): + client = socketio.test_client(app, namespace='/ns') + client.get_received('/ns') + client.send({'a': 'b'}, json=True, namespace='/ns') + received = client.get_received('/ns') + self.assertEqual(len(received), 1) + self.assertEqual(received[0]['args']['a'], 'b') + + def test_emit_class_based(self): + client = socketio.test_client(app, namespace='/ns') + client.get_received('/ns') + client.emit('my_custom_event', {'a': 'b'}, namespace='/ns') + received = client.get_received('/ns') + self.assertEqual(len(received), 1) + self.assertEqual(len(received[0]['args']), 1) + self.assertEqual(received[0]['name'], 'my custom response') + self.assertEqual(received[0]['args'][0]['a'], 'b') + + def test_request_event_data_class_based(self): + client = socketio.test_client(app, namespace='/ns') + client.get_received('/ns') + global request_event_data + request_event_data = None + client.emit('other_custom_event', 'foo', namespace='/ns') + expected_data = {'message': 'other_custom_event', 'args': ('foo',)} + self.assertEqual(request_event_data, expected_data) + if __name__ == '__main__': unittest.main()