From ecae8b0288d74ffe16466a47c7f4f1dddf36d0b4 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 7 Jan 2013 16:46:12 -0500 Subject: [PATCH 01/19] Updated TODO --- README.rst | 17 +++++++++++------ TODO.rst | 7 +++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index bba2b1d..5ed4179 100644 --- a/README.rst +++ b/README.rst @@ -38,7 +38,7 @@ Emit. :: socketIO = SocketIO('localhost', 8000) socketIO.emit('aaa', {'bbb': 'ccc'}) - socketIO.wait(seconds=1) + socketIO.wait(seconds=1) # Exit after one second Emit with callback. :: @@ -49,7 +49,7 @@ Emit with callback. :: socketIO = SocketIO('localhost', 8000) socketIO.emit('aaa', {'bbb': 'ccc'}, on_response) - socketIO.wait(forCallbacks=True) + socketIO.wait(forCallbacks=True) # Exit after callbacks run Define events. :: @@ -60,7 +60,7 @@ Define events. :: socketIO = SocketIO('localhost', 8000) socketIO.on('ddd', on_ddd) - socketIO.wait() + socketIO.wait() # Loop until CTRL-C Define events in a namespace. :: @@ -72,7 +72,7 @@ Define events in a namespace. :: self.socketIO.emit('eee', {'fff': 'ggg'}) socketIO = SocketIO('localhost', 8000, Namespace) - socketIO.wait() + socketIO.wait() # Loop until CTRL-C Define standard events. :: @@ -93,7 +93,7 @@ Define standard events. :: print '[Message] %s: %s' % (id, message) socketIO = SocketIO('localhost', 8000, Namespace) - socketIO.wait() + socketIO.wait() # Loop until CTRL-C Define different behavior for different channels on a single socket. :: @@ -117,8 +117,13 @@ Define different behavior for different channels on a single socket. :: mainSocket = SocketIO('localhost', 8000, MainNamespace) chatSocket = mainSocket.connect('/chat', ChatNamespace) newsSocket = mainSocket.connect('/news', NewsNamespace) - mainSocket.wait() + mainSocket.wait() # Loop until CTRL-C +Open secure websockets (HTTPS / WSS) behind a proxy. :: + + SocketIO('localhost', 8000, + secure=True, + proxies={'http': 'http://proxy.example.com:8080'}) License ------- diff --git a/TODO.rst b/TODO.rst index e69de29..e3e5408 100644 --- a/TODO.rst +++ b/TODO.rst @@ -0,0 +1,7 @@ +Let user define a proxy #5 +Let user emit without arguments #5 +Review forks + Integrate Zac's fork #6 + Integrate Sajal's fork #7 + Integrate Francis's fork #10 + Investigate #8 From 6f12132e88c60a81fdfa53c26c6e7e5e088e8cbe Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 7 Jan 2013 17:14:15 -0500 Subject: [PATCH 02/19] Updated setup.py --- TODO.rst | 10 +++++----- setup.py | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/TODO.rst b/TODO.rst index e3e5408..bf51bc3 100644 --- a/TODO.rst +++ b/TODO.rst @@ -1,7 +1,7 @@ Let user define a proxy #5 Let user emit without arguments #5 -Review forks - Integrate Zac's fork #6 - Integrate Sajal's fork #7 - Integrate Francis's fork #10 - Investigate #8 +Integrate Zac's fork #6 +Integrate Sajal's fork #7 +Integrate Francis's fork #10 +Investigate issue #8 +Integrate Paul's fork diff --git a/setup.py b/setup.py index 2651b7a..0ca72de 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ CHANGES = open(os.path.join(here, 'CHANGES.rst')).read() setup( name='socketIO-client', - version='0.3', + version='0.4', description='A socket.io client library', long_description=README + '\n\n' + CHANGES, license='MIT', @@ -24,6 +24,7 @@ setup( url='https://github.com/invisibleroads/socketIO-client', install_requires=[ 'anyjson', + 'gevent-socketio', 'websocket-client', ], packages=find_packages(), From 0a5b069cddfbfcaf37970f3991b226a6653483fb Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sat, 9 Feb 2013 19:12:21 -0800 Subject: [PATCH 03/19] Added test to check that child threads die when parent dies --- CHANGES.rst | 3 + README.rst | 13 +- TODO.rst | 7 +- serve_tests.py | 17 ++- setup.py | 1 - socketIO_client/__init__.py | 263 ++++++++++++++++++++---------------- socketIO_client/tests.py | 26 +++- 7 files changed, 193 insertions(+), 137 deletions(-) mode change 100755 => 100644 serve_tests.py mode change 100755 => 100644 setup.py diff --git a/CHANGES.rst b/CHANGES.rst index 8afe01b..1e5d0f7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,6 @@ +0.4 +--- + 0.3 --- - Added support for secure connections diff --git a/README.rst b/README.rst index 5ed4179..504e0d5 100644 --- a/README.rst +++ b/README.rst @@ -35,10 +35,9 @@ Activate isolated environment. :: Emit. :: from socketIO_client import SocketIO - - socketIO = SocketIO('localhost', 8000) - socketIO.emit('aaa', {'bbb': 'ccc'}) - socketIO.wait(seconds=1) # Exit after one second + with SocketIO('localhost', 8000) as socketIO: + socketIO.emit('aaa') + socketIO.wait(1) # Wait a second Emit with callback. :: @@ -47,9 +46,9 @@ Emit with callback. :: def on_response(*args): print args - socketIO = SocketIO('localhost', 8000) - socketIO.emit('aaa', {'bbb': 'ccc'}, on_response) - socketIO.wait(forCallbacks=True) # Exit after callbacks run + with SocketIO('localhost', 8000) as socketIO: + socketIO.emit('aaa', {'bbb': 'ccc'}, on_response) + socketIO.wait(seconds=1, forCallbacks=True) # Wait for callback Define events. :: diff --git a/TODO.rst b/TODO.rst index bf51bc3..e362fcd 100644 --- a/TODO.rst +++ b/TODO.rst @@ -1,5 +1,8 @@ -Let user define a proxy #5 -Let user emit without arguments #5 ++ Fix unittests ++ Fix exceptions when websocket server disappears + +Fix thread exceptions + Integrate Zac's fork #6 Integrate Sajal's fork #7 Integrate Francis's fork #10 diff --git a/serve_tests.py b/serve_tests.py old mode 100755 new mode 100644 index 47c946a..faea52e --- a/serve_tests.py +++ b/serve_tests.py @@ -1,7 +1,14 @@ 'Launch this server in another terminal window before running tests' -from socketio import socketio_manage -from socketio.namespace import BaseNamespace -from socketio.server import SocketIOServer +import sys +try: + from socketio import socketio_manage + from socketio.namespace import BaseNamespace + from socketio.server import SocketIOServer +except ImportError: + from setuptools.command import easy_install + easy_install.main(['-U', 'gevent-socketio']) + print('\nPlease run the script again to launch the test server.') + sys.exit(1) class Namespace(BaseNamespace): @@ -25,5 +32,7 @@ class Application(object): if __name__ == '__main__': - socketIOServer = SocketIOServer(('0.0.0.0', 8000), Application()) + port = 8000 + print 'Starting server at port %s' % port + socketIOServer = SocketIOServer(('0.0.0.0', port), Application()) socketIOServer.serve_forever() diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 0ca72de..0f79923 --- a/setup.py +++ b/setup.py @@ -24,7 +24,6 @@ setup( url='https://github.com/invisibleroads/socketIO-client', install_requires=[ 'anyjson', - 'gevent-socketio', 'websocket-client', ], packages=find_packages(), diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index e32996d..493748a 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,11 +1,16 @@ -import websocket +import sys +import traceback + +import socket from anyjson import dumps, loads +from functools import partial from threading import Thread, Event from time import sleep from urllib import urlopen +from websocket import WebSocketConnectionClosedException, create_connection -__version__ = '0.3' +__version__ = '0.4' PROTOCOL = 1 # SocketIO protocol version @@ -16,7 +21,7 @@ class BaseNamespace(object): # pragma: no cover def __init__(self, socketIO): self.socketIO = socketIO - def on_connect(self, socketIO): + def on_connect(self): pass def on_disconnect(self): @@ -46,54 +51,69 @@ class BaseNamespace(object): # pragma: no cover class SocketIO(object): - messageID = 0 + _messageID = 0 - def __init__(self, host, port, Namespace=BaseNamespace, secure=False): - self.host = host - self.port = int(port) - self.namespace = Namespace(self) - self.secure = secure - self.__connect() + def __init__(self, host, port, Namespace=BaseNamespace, secure=False, proxies=None): + self._host = host + self._port = int(port) + self._namespace = Namespace(self) + self._secure = secure + self._proxies = proxies + self._connect() - heartbeatInterval = self.heartbeatTimeout - 2 - self.heartbeatThread = RhythmicThread(heartbeatInterval, - self._send_heartbeat) - self.heartbeatThread.start() + heartbeatInterval = self._heartbeatTimeout - 2 + self._heartbeatThread = RhythmicThread(heartbeatInterval, self._send_heartbeat) + self._heartbeatThread.start() - self.channelByName = {} - self.callbackByEvent = {} - self.namespaceThread = ListenerThread(self) - self.namespaceThread.start() + self._channelByName = {} + self._callbackByEvent = {} + self._namespaceThread = ListenerThread(self._recv_packet, self._get_callback) + self._namespaceThread.start() - def __del__(self): # pragma: no cover - self.heartbeatThread.cancel() - self.namespaceThread.cancel() - self.connection.close() + def __enter__(self): + return self - def __connect(self): - baseURL = '%s:%d/socket.io/%s' % (self.host, self.port, PROTOCOL) + def __exit__(self, exc_type, exc_value, traceback): + self.__del__() + + def __del__(self): + self._heartbeatThread.cancel() + self._namespaceThread.cancel() + self._connection.close() + + def _connect(self): + baseURL = '%s:%d/socket.io/%s' % (self._host, self._port, PROTOCOL) try: response = urlopen('%s://%s/' % ( - 'https' if self.secure else 'http', baseURL)) + 'https' if self._secure else 'http', baseURL), + proxies=self._proxies) except IOError: # pragma: no cover raise SocketIOError('Could not start connection') if 200 != response.getcode(): # pragma: no cover raise SocketIOError('Could not establish connection') responseParts = response.readline().split(':') - self.sessionID = responseParts[0] - self.heartbeatTimeout = int(responseParts[1]) - self.connectionTimeout = int(responseParts[2]) - self.supportedTransports = responseParts[3].split(',') - if 'websocket' not in self.supportedTransports: + self._sessionID = responseParts[0] + self._heartbeatTimeout = int(responseParts[1]) + self._connectionTimeout = int(responseParts[2]) + self._supportedTransports = responseParts[3].split(',') + if 'websocket' not in self._supportedTransports: raise SocketIOError('Could not parse handshake') # pragma: no cover socketURL = '%s://%s/websocket/%s' % ( - 'wss' if self.secure else 'ws', baseURL, self.sessionID) - self.connection = websocket.create_connection(socketURL) + 'wss' if self._secure else 'ws', baseURL, self._sessionID) + self._connection = create_connection(socketURL) def _recv_packet(self): code, packetID, channelName, data = -1, None, None, None - packet = self.connection.recv() - packetParts = packet.split(':', 3) + try: + packet = self._connection.recv() + except WebSocketConnectionClosedException: + raise SocketIOConnectionError('Lost connection (Connection closed)') + except socket.timeout: + raise SocketIOConnectionError('Lost connection (Connection timed out)') + try: + packetParts = packet.split(':', 3) + except AttributeError: + raise SocketIOPacketError('Received invalid packet (%s)' % packet) packetCount = len(packetParts) if 4 == packetCount: code, packetID, channelName, data = packetParts @@ -104,34 +124,36 @@ class SocketIO(object): return int(code), packetID, channelName, data def _send_packet(self, code, channelName='', data='', callback=None): - self.connection.send(':'.join([ - str(code), - self.set_callback(callback) if callback else '', - channelName, - data])) + callbackNumber = self._set_callback(callback) if callback else '' + packetParts = [str(code), callbackNumber, channelName, data] + try: + self._connection.send(':'.join(packetParts)) + except socket.error: + raise SocketIOPacketError('Could not send packet') def disconnect(self, channelName=''): self._send_packet(0, channelName) if channelName: - del self.channelByName[channelName] + del self._channelByName[channelName] else: self.__del__() @property def connected(self): - return self.connection.connected + return self._connection.connected def connect(self, channelName, Namespace=BaseNamespace): channel = Channel(self, channelName, Namespace) - self.channelByName[channelName] = channel + self._channelByName[channelName] = channel self._send_packet(1, channelName) return channel def _send_heartbeat(self): try: self._send_packet(2) - except: - self.__del__() + except SocketIOPacketError: + print 'Could not send heartbeat' + pass def message(self, messageData, callback=None, channelName=''): if isinstance(messageData, basestring): @@ -144,40 +166,39 @@ class SocketIO(object): def emit(self, eventName, *eventArguments, **eventKeywords): code = 5 - if callable(eventArguments[-1]): + callback = None + if eventArguments and callable(eventArguments[-1]): callback = eventArguments[-1] eventArguments = eventArguments[:-1] - else: - callback = None channelName = eventKeywords.get('channelName', '') data = dumps(dict(name=eventName, args=eventArguments)) self._send_packet(code, channelName, data, callback) - def get_callback(self, channelName, eventName): + def _get_callback(self, channelName, eventName): 'Get callback associated with channelName and eventName' - socketIO = self.channelByName[channelName] if channelName else self + socketIO = self._channelByName[channelName] if channelName else self try: - return socketIO.callbackByEvent[eventName] + return socketIO._callbackByEvent[eventName] except KeyError: pass - namespace = socketIO.namespace def callback_(*eventArguments): - return namespace.on_(eventName, *eventArguments) - return getattr(namespace, name_callback(eventName), callback_) + return socketIO._namespace.on_(eventName, *eventArguments) + callbackName = 'on_' + eventName.replace(' ', '_') + return getattr(socketIO._namespace, callbackName, callback_) - def set_callback(self, callback): + def _set_callback(self, callback): 'Set callback that will be called after receiving an acknowledgment' - self.messageID += 1 - self.namespaceThread.set_callback(self.messageID, callback) - return '%s+' % self.messageID + self._messageID += 1 + self._namespaceThread.set_callback(self._messageID, callback) + return '%s+' % self._messageID def on(self, eventName, callback): - self.callbackByEvent[eventName] = callback + self._callbackByEvent[eventName] = callback def wait(self, seconds=None, forCallbacks=False): if forCallbacks: - self.namespaceThread.wait_for_callbacks(seconds) + self._namespaceThread.wait_for_callbacks(seconds) elif seconds: sleep(seconds) else: @@ -191,24 +212,22 @@ class SocketIO(object): class Channel(object): def __init__(self, socketIO, channelName, Namespace): - self.socketIO = socketIO - self.channelName = channelName - self.namespace = Namespace(self) - self.callbackByEvent = {} + self._socketIO = socketIO + self._channelName = channelName + self._namespace = Namespace(self) + self._callbackByEvent = {} def disconnect(self): - self.socketIO.disconnect(self.channelName) + self._socketIO.disconnect(self._channelName) def emit(self, eventName, *eventArguments): - self.socketIO.emit(eventName, *eventArguments, - channelName=self.channelName) + self._socketIO.emit(eventName, *eventArguments, channelName=self._channelName) def message(self, messageData, callback=None): - self.socketIO.message(messageData, callback, - channelName=self.channelName) + self._socketIO.message(messageData, callback, channelName=self._channelName) def on(self, eventName, eventCallback): - self.callbackByEvent[eventName] = eventCallback + self._callbackByEvent[eventName] = eventCallback class ListenerThread(Thread): @@ -216,34 +235,43 @@ class ListenerThread(Thread): daemon = True - def __init__(self, socketIO): + def __init__(self, recv_packet, get_callback): super(ListenerThread, self).__init__() - self.socketIO = socketIO self.done = Event() self.waitingForCallbacks = Event() self.callbackByMessageID = {} - self.get_callback = self.socketIO.get_callback + self.recv_packet = recv_packet + self.get_callback = get_callback def run(self): - while not self.done.is_set(): - try: - code, packetID, channelName, data = self.socketIO._recv_packet() - except: - continue - try: - delegate = { - 0: self.on_disconnect, - 1: self.on_connect, - 2: self.on_heartbeat, - 3: self.on_message, - 4: self.on_json, - 5: self.on_event, - 6: self.on_acknowledgment, - 7: self.on_error, - }[code] - except KeyError: - continue - delegate(packetID, channelName, data) + try: + while not self.done.is_set(): + try: + code, packetID, channelName, data = self.recv_packet() + except SocketIOConnectionError, error: + print error + return + except SocketIOPacketError, error: + print error + continue + get_channel_callback = partial(self.get_callback, channelName) + try: + delegate = { + 0: self.on_disconnect, + 1: self.on_connect, + 2: self.on_heartbeat, + 3: self.on_message, + 4: self.on_json, + 5: self.on_event, + 6: self.on_acknowledgment, + 7: self.on_error, + }[code] + except KeyError: + continue + delegate(packetID, get_channel_callback, data) + except: + exc_type, exc_value, exc_traceback = sys.exc_info() + open('tracebacks.log', 'a+t').write('\n'.join(traceback.format_tb(exc_traceback))) def cancel(self): self.done.set() @@ -255,33 +283,28 @@ class ListenerThread(Thread): def set_callback(self, messageID, callback): self.callbackByMessageID[messageID] = callback - def on_disconnect(self, packetID, channelName, data): - callback = self.get_callback(channelName, 'disconnect') - callback() + def on_disconnect(self, packetID, get_channel_callback, data): + get_channel_callback('disconnect')() - def on_connect(self, packetID, channelName, data): - callback = self.get_callback(channelName, 'connect') - callback(self.socketIO) + def on_connect(self, packetID, get_channel_callback, data): + get_channel_callback('connect')() - def on_heartbeat(self, packetID, channelName, data): + def on_heartbeat(self, packetID, get_channel_callback, data): pass - def on_message(self, packetID, channelName, data): - callback = self.get_callback(channelName, 'message') - callback(data) + def on_message(self, packetID, get_channel_callback, data): + get_channel_callback('message')(data) - def on_json(self, packetID, channelName, data): - callback = self.get_callback(channelName, 'message') - callback(loads(data)) + def on_json(self, packetID, get_channel_callback, data): + get_channel_callback('message')(loads(data)) - def on_event(self, packetID, channelName, data): + def on_event(self, packetID, get_channel_callback, data): valueByName = loads(data) eventName = valueByName['name'] eventArguments = valueByName['args'] - callback = self.get_callback(channelName, eventName) - callback(*eventArguments) + get_channel_callback(eventName)(*eventArguments) - def on_acknowledgment(self, packetID, channelName, data): + def on_acknowledgment(self, packetID, get_channel_callback, data): dataParts = data.split('+', 1) messageID = int(dataParts[0]) arguments = loads(dataParts[1]) or [] @@ -296,21 +319,20 @@ class ListenerThread(Thread): if self.waitingForCallbacks.is_set() and not callbackCount: self.cancel() - def on_error(self, packetID, channelName, data): + def on_error(self, packetID, get_channel_callback, data): reason, advice = data.split('+', 1) - callback = self.get_callback(channelName, 'error') - callback(reason, advice) + get_channel_callback('error')(reason, advice) class RhythmicThread(Thread): - 'Execute rhythmicFunction every few seconds' + 'Execute call every few seconds' daemon = True - def __init__(self, intervalInSeconds, rhythmicFunction, *args, **kw): + def __init__(self, intervalInSeconds, call, *args, **kw): super(RhythmicThread, self).__init__() self.intervalInSeconds = intervalInSeconds - self.rhythmicFunction = rhythmicFunction + self.call = call self.args = args self.kw = kw self.done = Event() @@ -318,10 +340,11 @@ class RhythmicThread(Thread): def run(self): try: while not self.done.is_set(): - self.rhythmicFunction(*self.args, **self.kw) + self.call(*self.args, **self.kw) self.done.wait(self.intervalInSeconds) except: - pass + exc_type, exc_value, exc_traceback = sys.exc_info() + open('tracebacks.log', 'a+t').write('\n'.join(traceback.format_tb(exc_traceback))) def cancel(self): self.done.set() @@ -331,5 +354,9 @@ class SocketIOError(Exception): pass -def name_callback(eventName): - return 'on_' + eventName.replace(' ', '_') +class SocketIOConnectionError(SocketIOError): + pass + + +class SocketIOPacketError(SocketIOError): + pass diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index a85c48e..53a6cf6 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -15,10 +15,16 @@ class TestSocketIO(TestCase): self.assertEqual(socketIO.connected, False) def test_emit(self): + socketIO = SocketIO('localhost', 8000, Namespace) + socketIO.emit('aaa') + sleep(0.5) + self.assertEqual(socketIO._namespace.payload, '') + + def test_emit_with_payload(self): socketIO = SocketIO('localhost', 8000, Namespace) socketIO.emit('aaa', PAYLOAD) sleep(0.5) - self.assertEqual(socketIO.namespace.payload, PAYLOAD) + self.assertEqual(socketIO._namespace.payload, PAYLOAD) def test_emit_with_callback(self): global ON_RESPONSE_CALLED @@ -43,16 +49,26 @@ class TestSocketIO(TestCase): newsSocket = mainSocket.connect('/news', Namespace) newsSocket.emit('aaa', PAYLOAD) sleep(0.5) - self.assertNotEqual(mainSocket.namespace.payload, PAYLOAD) - self.assertNotEqual(chatSocket.namespace.payload, PAYLOAD) - self.assertEqual(newsSocket.namespace.payload, PAYLOAD) + self.assertNotEqual(mainSocket._namespace.payload, PAYLOAD) + self.assertNotEqual(chatSocket._namespace.payload, PAYLOAD) + self.assertEqual(newsSocket._namespace.payload, PAYLOAD) + + def test_delete(self): + socketIO = SocketIO('localhost', 8000) + childThreads = [ + socketIO._heartbeatThread, + socketIO._namespaceThread, + ] + del socketIO + for childThread in childThreads: + self.assertEqual(True, childThread.done.is_set()) class Namespace(BaseNamespace): payload = None - def on_ddd(self, data): + def on_ddd(self, data=''): self.payload = data From 4f2187c536610ef275bc1b554a773b8b72dede20 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 11 Feb 2013 08:32:22 -0800 Subject: [PATCH 04/19] Added experiments --- TODO.rst | 3 +- experiments/callableweakref-tests.py | 170 ++++++++++++ experiments/callableweakref.py | 52 ++++ experiments/t0.py | 6 + experiments/t1.py | 7 + experiments/t10.py | 16 ++ experiments/t11.py | 21 ++ experiments/t12.py | 32 +++ experiments/t2.py | 19 ++ experiments/t3.py | 19 ++ experiments/t4.py | 26 ++ experiments/t5.py | 30 ++ socketIO_client/__init__.py | 391 +++++++++++++-------------- 13 files changed, 595 insertions(+), 197 deletions(-) create mode 100644 experiments/callableweakref-tests.py create mode 100644 experiments/callableweakref.py create mode 100644 experiments/t0.py create mode 100644 experiments/t1.py create mode 100644 experiments/t10.py create mode 100644 experiments/t11.py create mode 100644 experiments/t12.py create mode 100644 experiments/t2.py create mode 100644 experiments/t3.py create mode 100644 experiments/t4.py create mode 100644 experiments/t5.py diff --git a/TODO.rst b/TODO.rst index e362fcd..b15199b 100644 --- a/TODO.rst +++ b/TODO.rst @@ -1,7 +1,8 @@ + Fix unittests + Fix exceptions when websocket server disappears - Fix thread exceptions + Finish creating low-level _SocketIO to eliminate cyclic references + Move namespace and callback handling to _ListenerThread Integrate Zac's fork #6 Integrate Sajal's fork #7 diff --git a/experiments/callableweakref-tests.py b/experiments/callableweakref-tests.py new file mode 100644 index 0000000..c7a95ec --- /dev/null +++ b/experiments/callableweakref-tests.py @@ -0,0 +1,170 @@ +# Note 1: The expected behavior of weakref to an instance is that it should +# return None if no other strong references to the instance exist, +# signalling that the instance can be safely garbage collected. +# +# Note 2: The expected behavior of weakref to a method is that it should +# return None if no other strong references to the parent instance exist, +# signalling that the parent instance can be safely garbage collected. +# +# Note 3: The IPython interpreter stores its own references and will +# produce results different from those of the default Python interpreter. +import callableweakref +import weakref +import unittest + + +class TestCallableWeakRef(unittest.TestCase): + + def test_instanceDirectWeakref_dies_on_arrival(self): + 'Assert that weakref works as expected' + instanceDirectWeakref = weakref.ref(Instance()) + assert instanceDirectWeakref() is None + + def test_instanceIndirectWeakref_dies_when_instance_dies(self): + 'Assert that weakref works as expected' + instance = Instance() + instanceIndirectWeakref = weakref.ref(instance) + assert instanceIndirectWeakref() is instance + del instance + assert instanceIndirectWeakref() is None + + def test_boundMethodDirectWeakref_dies_on_arrival(self): + 'Assert that weakref does not work as expected' + instance = Instance() + boundMethodDirectWeakref = weakref.ref(instance.call) + assert boundMethodDirectWeakref() is None # Should be instance + + def test_boundMethodIndirectWeakref_lives_when_instance_dies(self): + 'Assert that weakref works as expected' + instance = Instance() + boundMethod = instance.call + boundMethodIndirectWeakref = weakref.ref(boundMethod) + assert boundMethodIndirectWeakref() is boundMethod + del instance + assert boundMethodIndirectWeakref() is boundMethod + del boundMethod + assert boundMethodIndirectWeakref() is None + + def test_unboundMethodDirectWeakref_dies_on_arrival(self): + 'Assert that weakref does not work as expected' + unboundMethodDirectWeakref = weakref.ref(Instance.call) + assert unboundMethodDirectWeakref() is None # Should be Instance.call + + def test_unboundMethodIndirectWeakref_dies_when_unboundMethod_dies(self): + 'Assert that weakref works as expected' + unboundMethod = Instance.call + unboundMethodIndirectWeakref = weakref.ref(unboundMethod) + assert unboundMethodIndirectWeakref() is unboundMethod + del unboundMethod + assert unboundMethodIndirectWeakref() is None + + def test_classFunctionDirectWeakref_dies_on_arrival(self): + 'Assert that weakref works as expected' + classFunctionDirectWeakref = weakref.ref(Instance()) + assert classFunctionDirectWeakref() is None + + def test_classFunctionIndirectWeakref_dies_when_classFunction_dies(self): + 'Assert that weakref works as expected' + classFunction = Instance() + classFunctionIndirectWeakref = weakref.ref(classFunction) + assert classFunctionIndirectWeakref() is classFunction + del classFunction + assert classFunctionIndirectWeakref() is None + + def test_normalFunctionDirectWeakref_dies_when_normal_function_dies(self): + 'Assert that weakref works as expected' + call = lambda: None + normalFunctionDirectWeakref = weakref.ref(call) + assert normalFunctionDirectWeakref() is call + del call + assert normalFunctionDirectWeakref() is None + + def test_normalFunctionIndirectWeakref_dies_when_normal_function_dies(self): + 'Assert that weakref works as expected' + call = lambda: None + normalFunction = call + normalFunctionIndirectWeakref = weakref.ref(normalFunction) + assert normalFunctionIndirectWeakref() is normalFunction + del normalFunction + assert normalFunctionIndirectWeakref() is call + del call + assert normalFunctionIndirectWeakref() is None + + # Assert that CallableWeakref works as expected for callables + + def test_boundMethodDirectCallableWeakref_dies_when_instance_dies(self): + instance = Instance() + boundMethodDirectCallableWeakref = callableweakref.ref(instance.call) + self.assertEqual(boundMethodDirectCallableWeakref(), instance.call) + del instance + assert boundMethodDirectCallableWeakref() is None + + def test_boundMethodIndirectCallableWeakref_dies_when_instance_dies(self): + instance = Instance() + boundMethod = instance.call + boundMethodIndirectCallableWeakref = callableweakref.ref(boundMethod) + self.assertEqual(boundMethodIndirectCallableWeakref(), boundMethod) + del instance + self.assertEqual(boundMethodIndirectCallableWeakref(), boundMethod) + del boundMethod + assert boundMethodIndirectCallableWeakref() is None + + def test_unboundMethodDirectCallableWeakref_lives_on_arrival(self): + unboundMethodDirectCallableWeakref = callableweakref.ref(Instance.call) + self.assertEqual(unboundMethodDirectCallableWeakref(), Instance.call) + + def test_unboundMethodIndirectCallableWeakref_lives_on_arrival(self): + unboundMethod = Instance.call + unboundMethodIndirectCallableWeakref = callableweakref.ref(unboundMethod) + self.assertEqual(unboundMethodIndirectCallableWeakref(), unboundMethod) + + def test_classMethodIndirectCallableWeakref_lives_on_arrival(self): + classMethod = Instance.call_classmethod + classMethodIndirectCallableWeakref = callableweakref.ref(classMethod) + self.assertEqual(classMethodIndirectCallableWeakref(), classMethod) + + def test_staticMethodIndirectCallableWeakref_lives_on_arrival(self): + staticMethod = Instance.call_staticmethod + staticMethodIndirectCallableWeakref = callableweakref.ref(staticMethod) + self.assertEqual(staticMethodIndirectCallableWeakref(), staticMethod) + + def test_classFunctionDirectCallableWeakref_dies_on_arrival(self): + classFunctionDirectCallableWeakref = callableweakref.ref(Instance()) + assert classFunctionDirectCallableWeakref() is None + + def test_classFunctionIndirectCallableWeakref_dies_when_classFunction_dies(self): + classFunction = Instance() + classFunctionIndirectCallableWeakref = callableweakref.ref(classFunction) + self.assertEqual(classFunctionIndirectCallableWeakref(), classFunction.__call__) + del classFunction + assert classFunctionIndirectCallableWeakref() is None + + def test_normalFunctionDirectCallableWeakref_lives_on_arrival(self): + call = lambda: None + normalFunctionDirectCallableWeakref = callableweakref.ref(call) + self.assertEqual(normalFunctionDirectCallableWeakref(), call) + + def test_normalFunctionIndirectCallableWeakref_lives_on_arrival(self): + call = lambda: None + normalFunction = call + normalFunctionIndirectWeakref = callableweakref.ref(normalFunction) + self.assertEqual(normalFunctionIndirectWeakref(), normalFunction) + del normalFunction + self.assertEqual(normalFunctionIndirectWeakref(), call) + + +class Instance(object): + + def __call__(self): + pass + + def call(self): + pass + + @classmethod + def call_classmethod(Class): + pass + + @staticmethod + def call_staticmethod(): + pass diff --git a/experiments/callableweakref.py b/experiments/callableweakref.py new file mode 100644 index 0000000..788592c --- /dev/null +++ b/experiments/callableweakref.py @@ -0,0 +1,52 @@ +import types +import weakref + + +def ref(function): + return CallableWeakReference(function) + + +class CallableWeakReference(object): + + def __init__(self, function): + 'Create a weak reference to a callable' + try: + if function.im_self: + # We have a bound method or class method + self._reference = weakref.ref(function.im_self) + else: + # We have an unbound method + self._reference = None + self._function = function.im_func + self._class = function.im_class + except AttributeError: + try: + function.func_code + # We have a normal function or static method + self._reference = None + self._function = function + self._class = None + except AttributeError: + function = function.__call__ + # We have a class masquerading as a function + self._reference = weakref.ref(function.im_self) + self._function = function.im_func + self._class = function.im_class + + def __call__(self): + if self.dead: + return + if self._reference: + # We have a bound method + return types.MethodType(self._function, self._reference(), self._class) + elif self._class: + return types.MethodType(self._function, None, self._class) + else: + return self._function + + @property + def dead(self): + if self._reference and not self._reference(): + # We have a bound method whose parent reference has died + return True + return False diff --git a/experiments/t0.py b/experiments/t0.py new file mode 100644 index 0000000..3980052 --- /dev/null +++ b/experiments/t0.py @@ -0,0 +1,6 @@ +from socketIO_client import SocketIO + +s = SocketIO('localhost', 8000) +del s +from time import sleep +sleep(3) diff --git a/experiments/t1.py b/experiments/t1.py new file mode 100644 index 0000000..0955ed6 --- /dev/null +++ b/experiments/t1.py @@ -0,0 +1,7 @@ +class O(object): + + def __del__(self): + print '__del__()' + + +o = O() diff --git a/experiments/t10.py b/experiments/t10.py new file mode 100644 index 0000000..24733d9 --- /dev/null +++ b/experiments/t10.py @@ -0,0 +1,16 @@ +class A(object): + + def __init__(self): + self.b = B(self) + + def __del__(self): + print '__del__()' + + +class B(object): + + def __init__(self, a): + self.a = a + + +a = A() diff --git a/experiments/t11.py b/experiments/t11.py new file mode 100644 index 0000000..11121de --- /dev/null +++ b/experiments/t11.py @@ -0,0 +1,21 @@ +class A(object): + + def __init__(self): + self.c = Common() + self.b = B(self.c) + + def __del__(self): + print '__del__()' + + +class B(object): + + def __init__(self, c): + self.c = c + + +class Common(object): + pass + + +a = A() diff --git a/experiments/t12.py b/experiments/t12.py new file mode 100644 index 0000000..b4e9bb3 --- /dev/null +++ b/experiments/t12.py @@ -0,0 +1,32 @@ +# Will the destructor of a class be called if its two children have cyclic +# references to each other? Yes. + + +class Parent(object): + + def __init__(self): + self.child = Child() + + def __del__(self): + print 'Parent.__del__()' + + +class Child(object): + + def __init__(self): + self.grandChild = GrandChild(self) + + def __del__(self): + print 'Child.__del__()' + + +class GrandChild(object): + + def __init__(self, parent): + self.parent = parent + + def __del__(self): + print 'GrandChild.__del__()' + + +parent = Parent() diff --git a/experiments/t2.py b/experiments/t2.py new file mode 100644 index 0000000..3ad124c --- /dev/null +++ b/experiments/t2.py @@ -0,0 +1,19 @@ +import weakref + + +class O(object): + + def __init__(self): + self.p = P(self) + + def __del__(self): + print '__del__()' + + +class P(object): + + def __init__(self, parent): + self.parent = weakref.ref(parent) + + +o = O() diff --git a/experiments/t3.py b/experiments/t3.py new file mode 100644 index 0000000..2de3123 --- /dev/null +++ b/experiments/t3.py @@ -0,0 +1,19 @@ +class O(object): + + def __init__(self): + self.p = P(self.f) + + def __del__(self): + print '__del__()' + + def f(self): + pass + + +class P(object): + + def __init__(self, parentMethod): + self.parentMethod = parentMethod + + +o = O() diff --git a/experiments/t4.py b/experiments/t4.py new file mode 100644 index 0000000..da46dfe --- /dev/null +++ b/experiments/t4.py @@ -0,0 +1,26 @@ +import weakref + + +class O(object): + + def __init__(self): + self.p = P(self.f) + + def __del__(self): + print '__del__()' + + def f(self): + pass + + +class P(object): + + def __init__(self, parentMethod): + self.parentMethod = weakref.ref(parentMethod) + + def show(self): + print self.parentMethod + + +o = O() +o.p.show() # Dead on arrival diff --git a/experiments/t5.py b/experiments/t5.py new file mode 100644 index 0000000..a2ff710 --- /dev/null +++ b/experiments/t5.py @@ -0,0 +1,30 @@ +import callableweakref + + +class O(object): + + def __init__(self): + self.p = P(self.f) + + def __del__(self): + print '__del__()' + + def f(self): + print 'f()' + + +class P(object): + + def __init__(self, parentMethod): + self.parentMethod = callableweakref.ref(parentMethod) + + def show(self): + print self.parentMethod + + def run(self): + self.parentMethod()() + + +o = O() +o.p.show() +o.p.run() diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 493748a..9ae06a2 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,18 +1,13 @@ -import sys -import traceback - import socket +import weakref from anyjson import dumps, loads from functools import partial -from threading import Thread, Event +from threading import Event, Thread from time import sleep from urllib import urlopen from websocket import WebSocketConnectionClosedException, create_connection -__version__ = '0.4' - - PROTOCOL = 1 # SocketIO protocol version @@ -49,112 +44,64 @@ class BaseNamespace(object): # pragma: no cover print '[Reconnect]', args +class Channel(object): + + def __init__(self, socketIO, channelName, Namespace): + self._socketIO = weakref.proxy(socketIO) + self._channelName = channelName + self._namespace = Namespace(self) + self._callbackByEvent = {} + + def disconnect(self): + self._socketIO.disconnect(self._channelName) + + def emit(self, eventName, *eventArguments): + self._socketIO.emit(eventName, *eventArguments, channelName=self._channelName) + + def message(self, messageData, callback=None): + self._socketIO.message(messageData, callback, channelName=self._channelName) + + def on(self, eventName, eventCallback): + self._callbackByEvent[eventName] = eventCallback + + class SocketIO(object): - _messageID = 0 - def __init__(self, host, port, Namespace=BaseNamespace, secure=False, proxies=None): - self._host = host - self._port = int(port) - self._namespace = Namespace(self) - self._secure = secure - self._proxies = proxies - self._connect() + self._socketIO = _SocketIO(host, port, secure, proxies) - heartbeatInterval = self._heartbeatTimeout - 2 - self._heartbeatThread = RhythmicThread(heartbeatInterval, self._send_heartbeat) + self._heartbeatThread = _RhythmicThread( + self._socketIO.heartbeatTimeout, + self._socketIO.send_heartbeat) self._heartbeatThread.start() - self._channelByName = {} - self._callbackByEvent = {} - self._namespaceThread = ListenerThread(self._recv_packet, self._get_callback) + self._namespace = Namespace(self._socketIO) + self._namespaceThread = _ListenerThread(self._socketIO) self._namespaceThread.start() def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): - self.__del__() + self.disconnect() def __del__(self): - self._heartbeatThread.cancel() - self._namespaceThread.cancel() - self._connection.close() - - def _connect(self): - baseURL = '%s:%d/socket.io/%s' % (self._host, self._port, PROTOCOL) - try: - response = urlopen('%s://%s/' % ( - 'https' if self._secure else 'http', baseURL), - proxies=self._proxies) - except IOError: # pragma: no cover - raise SocketIOError('Could not start connection') - if 200 != response.getcode(): # pragma: no cover - raise SocketIOError('Could not establish connection') - responseParts = response.readline().split(':') - self._sessionID = responseParts[0] - self._heartbeatTimeout = int(responseParts[1]) - self._connectionTimeout = int(responseParts[2]) - self._supportedTransports = responseParts[3].split(',') - if 'websocket' not in self._supportedTransports: - raise SocketIOError('Could not parse handshake') # pragma: no cover - socketURL = '%s://%s/websocket/%s' % ( - 'wss' if self._secure else 'ws', baseURL, self._sessionID) - self._connection = create_connection(socketURL) - - def _recv_packet(self): - code, packetID, channelName, data = -1, None, None, None - try: - packet = self._connection.recv() - except WebSocketConnectionClosedException: - raise SocketIOConnectionError('Lost connection (Connection closed)') - except socket.timeout: - raise SocketIOConnectionError('Lost connection (Connection timed out)') - try: - packetParts = packet.split(':', 3) - except AttributeError: - raise SocketIOPacketError('Received invalid packet (%s)' % packet) - packetCount = len(packetParts) - if 4 == packetCount: - code, packetID, channelName, data = packetParts - elif 3 == packetCount: - code, packetID, channelName = packetParts - elif 1 == packetCount: # pragma: no cover - code = packetParts[0] - return int(code), packetID, channelName, data - - def _send_packet(self, code, channelName='', data='', callback=None): - callbackNumber = self._set_callback(callback) if callback else '' - packetParts = [str(code), callbackNumber, channelName, data] - try: - self._connection.send(':'.join(packetParts)) - except socket.error: - raise SocketIOPacketError('Could not send packet') + self.disconnect() def disconnect(self, channelName=''): self._send_packet(0, channelName) if channelName: - del self._channelByName[channelName] + del self.channelByName[channelName] else: - self.__del__() - - @property - def connected(self): - return self._connection.connected + self._heartbeatThread.cancel() + self._namespaceThread.cancel() def connect(self, channelName, Namespace=BaseNamespace): channel = Channel(self, channelName, Namespace) - self._channelByName[channelName] = channel - self._send_packet(1, channelName) + self.channelByName[channelName] = channel + self.send_packet(1, channelName) return channel - def _send_heartbeat(self): - try: - self._send_packet(2) - except SocketIOPacketError: - print 'Could not send heartbeat' - pass - def message(self, messageData, callback=None, channelName=''): if isinstance(messageData, basestring): code = 3 @@ -174,25 +121,6 @@ class SocketIO(object): data = dumps(dict(name=eventName, args=eventArguments)) self._send_packet(code, channelName, data, callback) - def _get_callback(self, channelName, eventName): - 'Get callback associated with channelName and eventName' - socketIO = self._channelByName[channelName] if channelName else self - try: - return socketIO._callbackByEvent[eventName] - except KeyError: - pass - - def callback_(*eventArguments): - return socketIO._namespace.on_(eventName, *eventArguments) - callbackName = 'on_' + eventName.replace(' ', '_') - return getattr(socketIO._namespace, callbackName, callback_) - - def _set_callback(self, callback): - 'Set callback that will be called after receiving an acknowledgment' - self._messageID += 1 - self._namespaceThread.set_callback(self._messageID, callback) - return '%s+' % self._messageID - def on(self, eventName, callback): self._callbackByEvent[eventName] = callback @@ -203,75 +131,179 @@ class SocketIO(object): sleep(seconds) else: try: - while self.connected: + while self._socketIO.connected: sleep(1) except KeyboardInterrupt: pass -class Channel(object): +class _SocketIO(object): + 'Low-level interface to remove cyclic references in child threads' - def __init__(self, socketIO, channelName, Namespace): - self._socketIO = socketIO - self._channelName = channelName - self._namespace = Namespace(self) - self._callbackByEvent = {} + messageID = 0 - def disconnect(self): - self._socketIO.disconnect(self._channelName) + def __init__(self, host, port, secure=False, proxies=None): + self.connect(host, port, secure, proxies) + self.callbackByMessageID = {} + self.callbackByEvent = {} + self.channelByName = {} - def emit(self, eventName, *eventArguments): - self._socketIO.emit(eventName, *eventArguments, channelName=self._channelName) + def __del__(self): + self.connection.close() - def message(self, messageData, callback=None): - self._socketIO.message(messageData, callback, channelName=self._channelName) + def connect(self, host, port, secure, proxies): + baseURL = '%s:%d/socket.io/%s' % (host, port, PROTOCOL) + targetScheme = 'https' if secure else 'http' + targetURL = '%s://%s/' % (targetScheme, baseURL) + try: + response = urlopen(targetURL, proxies=proxies) + except IOError: # pragma: no cover + raise SocketIOError('Could not start connection') + if 200 != response.getcode(): # pragma: no cover + raise SocketIOError('Could not establish connection') + responseParts = response.readline().split(':') + sessionID = responseParts[0] + heartbeatTimeout = int(responseParts[1]) + # connectionTimeout = int(responseParts[2]) + supportedTransports = responseParts[3].split(',') + if 'websocket' not in supportedTransports: + raise SocketIOError('Could not parse handshake') # pragma: no cover + socketScheme = 'wss' if secure else 'ws' + socketURL = '%s://%s/websocket/%s' % (socketScheme, baseURL, sessionID) + self.connection = create_connection(socketURL) + self.heartbeatInterval = heartbeatTimeout - 2 - def on(self, eventName, eventCallback): - self._callbackByEvent[eventName] = eventCallback + def recv_packet(self): + code, packetID, channelName, data = -1, None, None, None + try: + packet = self.connection.recv() + except WebSocketConnectionClosedException: + raise SocketIOConnectionError('Lost connection (Connection closed)') + except socket.timeout: + raise SocketIOConnectionError('Lost connection (Connection timed out)') + try: + packetParts = packet.split(':', 3) + except AttributeError: + raise SocketIOPacketError('Received invalid packet (%s)' % packet) + packetCount = len(packetParts) + if 4 == packetCount: + code, packetID, channelName, data = packetParts + elif 3 == packetCount: + code, packetID, channelName = packetParts + elif 1 == packetCount: # pragma: no cover + code = packetParts[0] + return int(code), packetID, channelName, data + + def send_packet(self, code, channelName='', data='', callback=None): + callbackNumber = self.set_messageID_callback(callback) if callback else '' + packetParts = [str(code), callbackNumber, channelName, data] + try: + self.connection.send(':'.join(packetParts)) + except socket.error: + raise SocketIOPacketError('Could not send packet') + + def set_messageID_callback(self, callback): + 'Set callback that will be called after receiving an acknowledgment' + self.messageID += 1 + self.callbackByMessageID[self.messageID] = callback + return '%s+' % self.messageID + + def get_messageID_callback(self, messageID): + 'Get callback associated with messageID' + try: + callback = self.callbackByMessageID[messageID] + del self.callbackByMessageID[messageID] + return callback + except KeyError: + return + + @property + def has_messageID_callback(self): + return True if self.callbackByMessageID else False + + def get_event_callback(self, channelName, eventName): + 'Get callback associated with channelName and eventName' + _socketIO = self.channelByName[channelName] if channelName else self + try: + return _socketIO.callbackByEvent[eventName] + except KeyError: + pass + + def callback_(*eventArguments): + return _socketIO.namespace.on_(eventName, *eventArguments) + callbackName = 'on_' + eventName.replace(' ', '_') + return getattr(_socketIO.namespace, callbackName, callback_) + + @property + def connected(self): + return self.connection.connected + + def send_heartbeat(self): + try: + self.send_packet(2) + except SocketIOPacketError: + print 'Could not send heartbeat' + pass -class ListenerThread(Thread): +class _RhythmicThread(Thread): + 'Execute call every few seconds' + + daemon = True + + def __init__(self, intervalInSeconds, call, *args, **kw): + super(_RhythmicThread, self).__init__() + self.intervalInSeconds = intervalInSeconds + self.call + self.args = args + self.kw = kw + self.done = Event() + + def run(self): + while not self.done.is_set(): + self.call(*self.args, **self.kw) + self.done.wait(self.intervalInSeconds) + + def cancel(self): + self.done.set() + + +class _ListenerThread(Thread): 'Process messages from SocketIO server' daemon = True - def __init__(self, recv_packet, get_callback): - super(ListenerThread, self).__init__() + def __init__(self, _socketIO, get_event_callback, ): + super(_ListenerThread, self).__init__() + self._socketIO = _socketIO self.done = Event() self.waitingForCallbacks = Event() - self.callbackByMessageID = {} - self.recv_packet = recv_packet - self.get_callback = get_callback def run(self): - try: - while not self.done.is_set(): - try: - code, packetID, channelName, data = self.recv_packet() - except SocketIOConnectionError, error: - print error - return - except SocketIOPacketError, error: - print error - continue - get_channel_callback = partial(self.get_callback, channelName) - try: - delegate = { - 0: self.on_disconnect, - 1: self.on_connect, - 2: self.on_heartbeat, - 3: self.on_message, - 4: self.on_json, - 5: self.on_event, - 6: self.on_acknowledgment, - 7: self.on_error, - }[code] - except KeyError: - continue - delegate(packetID, get_channel_callback, data) - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - open('tracebacks.log', 'a+t').write('\n'.join(traceback.format_tb(exc_traceback))) + while not self.done.is_set(): + try: + code, packetID, channelName, data = self._socketIO.recv_packet() + except SocketIOConnectionError, error: + print error + return + except SocketIOPacketError, error: + print error + continue + get_channel_callback = partial(self._socketIO.get_event_callback, channelName) + try: + delegate = { + 0: self.on_disconnect, + 1: self.on_connect, + 2: self.on_heartbeat, + 3: self.on_message, + 4: self.on_json, + 5: self.on_event, + 6: self.on_acknowledgment, + 7: self.on_error, + }[code] + except KeyError: + continue + delegate(packetID, get_channel_callback, data) def cancel(self): self.done.set() @@ -280,9 +312,6 @@ class ListenerThread(Thread): self.waitingForCallbacks.set() self.join(seconds) - def set_callback(self, messageID, callback): - self.callbackByMessageID[messageID] = callback - def on_disconnect(self, packetID, get_channel_callback, data): get_channel_callback('disconnect')() @@ -308,48 +337,18 @@ class ListenerThread(Thread): dataParts = data.split('+', 1) messageID = int(dataParts[0]) arguments = loads(dataParts[1]) or [] - try: - callback = self.callbackByMessageID[messageID] - except KeyError: - pass - else: - del self.callbackByMessageID[messageID] - callback(*arguments) - callbackCount = len(self.callbackByMessageID) - if self.waitingForCallbacks.is_set() and not callbackCount: - self.cancel() + callback = self._socketIO.get_messageID_callback(messageID) + if not callback: + return + callback(*arguments) + if self.waitingForCallbacks.is_set() and not self._socketIO.has_messageID_callback: + self.cancel() def on_error(self, packetID, get_channel_callback, data): reason, advice = data.split('+', 1) get_channel_callback('error')(reason, advice) -class RhythmicThread(Thread): - 'Execute call every few seconds' - - daemon = True - - def __init__(self, intervalInSeconds, call, *args, **kw): - super(RhythmicThread, self).__init__() - self.intervalInSeconds = intervalInSeconds - self.call = call - self.args = args - self.kw = kw - self.done = Event() - - def run(self): - try: - while not self.done.is_set(): - self.call(*self.args, **self.kw) - self.done.wait(self.intervalInSeconds) - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - open('tracebacks.log', 'a+t').write('\n'.join(traceback.format_tb(exc_traceback))) - - def cancel(self): - self.done.set() - - class SocketIOError(Exception): pass From 01bfefdd8ba4bc597b16d6cbf84ea365d2081125 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Wed, 13 Feb 2013 08:27:58 -0800 Subject: [PATCH 05/19] Trying to refactor the damn code to remove cyclic references --- README.rst | 27 +-- socketIO_client/__init__.py | 381 +++++++++++++++++++----------------- socketIO_client/tests.py | 33 ++-- 3 files changed, 233 insertions(+), 208 deletions(-) diff --git a/README.rst b/README.rst index 504e0d5..a1984c2 100644 --- a/README.rst +++ b/README.rst @@ -70,7 +70,8 @@ Define events in a namespace. :: def on_ddd(self, *args): self.socketIO.emit('eee', {'fff': 'ggg'}) - socketIO = SocketIO('localhost', 8000, Namespace) + socketIO = SocketIO('localhost', 8000) + socketIO.define(Namespace) socketIO.wait() # Loop until CTRL-C Define standard events. :: @@ -79,7 +80,7 @@ Define standard events. :: class Namespace(BaseNamespace): - def on_connect(self, socketIO): + def on_connect(self): print '[Connected]' def on_disconnect(self): @@ -91,32 +92,36 @@ Define standard events. :: def on_message(self, id, message): print '[Message] %s: %s' % (id, message) - socketIO = SocketIO('localhost', 8000, Namespace) + socketIO = SocketIO('localhost', 8000) + socketIO.define(Namespace) socketIO.wait() # Loop until CTRL-C -Define different behavior for different channels on a single socket. :: +Define different namespaces on a single socket. :: from socketIO_client import SocketIO, BaseNamespace - class MainNamespace(BaseNamespace): + class MainNamespace(Channel): def on_aaa(self, *args): print 'aaa', args - class ChatNamespace(BaseNamespace): + class ChatNamespace(Channel): def on_bbb(self, *args): print 'bbb', args - class NewsNamespace(BaseNamespace): + class NewsNamespace(Channel): def on_ccc(self, *args): print 'ccc', args - mainSocket = SocketIO('localhost', 8000, MainNamespace) - chatSocket = mainSocket.connect('/chat', ChatNamespace) - newsSocket = mainSocket.connect('/news', NewsNamespace) - mainSocket.wait() # Loop until CTRL-C + socketIO = SocketIO('localhost', 8000) + socketIO.define(MainNamespace) + chatSocket = socketIO.define(ChatNamespace, '/chat') + chatSocket.emit('bbb') + newsSocket = socketIO.define(NewsNamespace, '/news') + newsSocket.emit('ccc') + socketIO.wait() # Loop until CTRL-C Open secure websockets (HTTPS / WSS) behind a proxy. :: diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 9ae06a2..b247fb4 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,7 +1,5 @@ import socket -import weakref from anyjson import dumps, loads -from functools import partial from threading import Event, Thread from time import sleep from urllib import urlopen @@ -13,8 +11,8 @@ PROTOCOL = 1 # SocketIO protocol version class BaseNamespace(object): # pragma: no cover - def __init__(self, socketIO): - self.socketIO = socketIO + def __init__(self, _socketIO): + self._socketIO = _socketIO def on_connect(self): pass @@ -44,40 +42,21 @@ class BaseNamespace(object): # pragma: no cover print '[Reconnect]', args -class Channel(object): - - def __init__(self, socketIO, channelName, Namespace): - self._socketIO = weakref.proxy(socketIO) - self._channelName = channelName - self._namespace = Namespace(self) - self._callbackByEvent = {} - - def disconnect(self): - self._socketIO.disconnect(self._channelName) - - def emit(self, eventName, *eventArguments): - self._socketIO.emit(eventName, *eventArguments, channelName=self._channelName) - - def message(self, messageData, callback=None): - self._socketIO.message(messageData, callback, channelName=self._channelName) - - def on(self, eventName, eventCallback): - self._callbackByEvent[eventName] = eventCallback - - class SocketIO(object): - def __init__(self, host, port, Namespace=BaseNamespace, secure=False, proxies=None): + def __init__(self, host, port, secure=False, proxies=None): self._socketIO = _SocketIO(host, port, secure, proxies) + self._channelByPath = {} - self._heartbeatThread = _RhythmicThread( + self._rhythmicThread = _RhythmicThread( self._socketIO.heartbeatTimeout, self._socketIO.send_heartbeat) - self._heartbeatThread.start() + self._rhythmicThread.start() - self._namespace = Namespace(self._socketIO) - self._namespaceThread = _ListenerThread(self._socketIO) - self._namespaceThread.start() + self._listenerThread = _ListenerThread( + self._socketIO, + self._channelByPath) + self._listenerThread.start() def __enter__(self): return self @@ -88,20 +67,30 @@ class SocketIO(object): def __del__(self): self.disconnect() - def disconnect(self, channelName=''): - self._send_packet(0, channelName) - if channelName: - del self.channelByName[channelName] - else: - self._heartbeatThread.cancel() - self._namespaceThread.cancel() + @property + def connected(self): + return self._socketIO.connection.connected - def connect(self, channelName, Namespace=BaseNamespace): - channel = Channel(self, channelName, Namespace) - self.channelByName[channelName] = channel - self.send_packet(1, channelName) + def disconnect(self, channelPath=''): + self._socketIO.disconnect(channelPath) + if channelPath: + del self._channelByPath[channelPath] + else: + self._rhythmicThread.cancel() + self._listenerThread.cancel() + + def define(self, Namespace, channelPath=''): + self._socketIO.connect(channelPath) + channel = Channel(self._socketIO, Namespace, channelPath) + self._channelByPath[channelPath] = channel return channel + def get_namespace(self, channelPath=''): + return self._channelByPath[channelPath].get_namespace() + + def on(self, eventName, eventCallback): + self._callbackByEvent[eventName] = callback + def message(self, messageData, callback=None, channelName=''): if isinstance(messageData, basestring): code = 3 @@ -111,7 +100,8 @@ class SocketIO(object): data = dumps(messageData) self._send_packet(code, channelName, data, callback) - def emit(self, eventName, *eventArguments, **eventKeywords): + def emit(self, eventName, *eventArguments): + self._socketIO.emit(eventName, *eventArguments) code = 5 callback = None if eventArguments and callable(eventArguments[-1]): @@ -121,12 +111,9 @@ class SocketIO(object): data = dumps(dict(name=eventName, args=eventArguments)) self._send_packet(code, channelName, data, callback) - def on(self, eventName, callback): - self._callbackByEvent[eventName] = callback - def wait(self, seconds=None, forCallbacks=False): if forCallbacks: - self._namespaceThread.wait_for_callbacks(seconds) + self.__listenerThread.wait_for_callbacks(seconds) elif seconds: sleep(seconds) else: @@ -137,115 +124,6 @@ class SocketIO(object): pass -class _SocketIO(object): - 'Low-level interface to remove cyclic references in child threads' - - messageID = 0 - - def __init__(self, host, port, secure=False, proxies=None): - self.connect(host, port, secure, proxies) - self.callbackByMessageID = {} - self.callbackByEvent = {} - self.channelByName = {} - - def __del__(self): - self.connection.close() - - def connect(self, host, port, secure, proxies): - baseURL = '%s:%d/socket.io/%s' % (host, port, PROTOCOL) - targetScheme = 'https' if secure else 'http' - targetURL = '%s://%s/' % (targetScheme, baseURL) - try: - response = urlopen(targetURL, proxies=proxies) - except IOError: # pragma: no cover - raise SocketIOError('Could not start connection') - if 200 != response.getcode(): # pragma: no cover - raise SocketIOError('Could not establish connection') - responseParts = response.readline().split(':') - sessionID = responseParts[0] - heartbeatTimeout = int(responseParts[1]) - # connectionTimeout = int(responseParts[2]) - supportedTransports = responseParts[3].split(',') - if 'websocket' not in supportedTransports: - raise SocketIOError('Could not parse handshake') # pragma: no cover - socketScheme = 'wss' if secure else 'ws' - socketURL = '%s://%s/websocket/%s' % (socketScheme, baseURL, sessionID) - self.connection = create_connection(socketURL) - self.heartbeatInterval = heartbeatTimeout - 2 - - def recv_packet(self): - code, packetID, channelName, data = -1, None, None, None - try: - packet = self.connection.recv() - except WebSocketConnectionClosedException: - raise SocketIOConnectionError('Lost connection (Connection closed)') - except socket.timeout: - raise SocketIOConnectionError('Lost connection (Connection timed out)') - try: - packetParts = packet.split(':', 3) - except AttributeError: - raise SocketIOPacketError('Received invalid packet (%s)' % packet) - packetCount = len(packetParts) - if 4 == packetCount: - code, packetID, channelName, data = packetParts - elif 3 == packetCount: - code, packetID, channelName = packetParts - elif 1 == packetCount: # pragma: no cover - code = packetParts[0] - return int(code), packetID, channelName, data - - def send_packet(self, code, channelName='', data='', callback=None): - callbackNumber = self.set_messageID_callback(callback) if callback else '' - packetParts = [str(code), callbackNumber, channelName, data] - try: - self.connection.send(':'.join(packetParts)) - except socket.error: - raise SocketIOPacketError('Could not send packet') - - def set_messageID_callback(self, callback): - 'Set callback that will be called after receiving an acknowledgment' - self.messageID += 1 - self.callbackByMessageID[self.messageID] = callback - return '%s+' % self.messageID - - def get_messageID_callback(self, messageID): - 'Get callback associated with messageID' - try: - callback = self.callbackByMessageID[messageID] - del self.callbackByMessageID[messageID] - return callback - except KeyError: - return - - @property - def has_messageID_callback(self): - return True if self.callbackByMessageID else False - - def get_event_callback(self, channelName, eventName): - 'Get callback associated with channelName and eventName' - _socketIO = self.channelByName[channelName] if channelName else self - try: - return _socketIO.callbackByEvent[eventName] - except KeyError: - pass - - def callback_(*eventArguments): - return _socketIO.namespace.on_(eventName, *eventArguments) - callbackName = 'on_' + eventName.replace(' ', '_') - return getattr(_socketIO.namespace, callbackName, callback_) - - @property - def connected(self): - return self.connection.connected - - def send_heartbeat(self): - try: - self.send_packet(2) - except SocketIOPacketError: - print 'Could not send heartbeat' - pass - - class _RhythmicThread(Thread): 'Execute call every few seconds' @@ -273,23 +151,32 @@ class _ListenerThread(Thread): daemon = True - def __init__(self, _socketIO, get_event_callback, ): + def __init__(self, _socketIO, _channelByPath): super(_ListenerThread, self).__init__() self._socketIO = _socketIO + self._channelByPath = _channelByPath self.done = Event() - self.waitingForCallbacks = Event() + self.waiting = Event() + + def cancel(self): + self.done.set() + + def wait_for_callbacks(self, seconds): + self.waiting.set() + # Block callingThread until listenerThread terminates + self.join(seconds) def run(self): while not self.done.is_set(): try: - code, packetID, channelName, data = self._socketIO.recv_packet() + code, packetID, channelPath, data = self._socketIO.recv_packet() except SocketIOConnectionError, error: print error return except SocketIOPacketError, error: print error continue - get_channel_callback = partial(self._socketIO.get_event_callback, channelName) + channel = self._channelByPath[channelPath] try: delegate = { 0: self.on_disconnect, @@ -303,50 +190,174 @@ class _ListenerThread(Thread): }[code] except KeyError: continue - delegate(packetID, get_channel_callback, data) + delegate(packetID, channel._get_eventCallback, data) - def cancel(self): - self.done.set() + def on_disconnect(self, packetID, get_eventCallback, data): + get_eventCallback('disconnect')() - def wait_for_callbacks(self, seconds): - self.waitingForCallbacks.set() - self.join(seconds) + def on_connect(self, packetID, get_eventCallback, data): + get_eventCallback('connect')() - def on_disconnect(self, packetID, get_channel_callback, data): - get_channel_callback('disconnect')() + def on_heartbeat(self, packetID, get_eventCallback, data): + get_eventCallback('heartbeat')() - def on_connect(self, packetID, get_channel_callback, data): - get_channel_callback('connect')() + def on_message(self, packetID, get_eventCallback, data): + get_eventCallback('message')(data) - def on_heartbeat(self, packetID, get_channel_callback, data): - pass + def on_json(self, packetID, get_eventCallback, data): + get_eventCallback('message')(loads(data)) - def on_message(self, packetID, get_channel_callback, data): - get_channel_callback('message')(data) - - def on_json(self, packetID, get_channel_callback, data): - get_channel_callback('message')(loads(data)) - - def on_event(self, packetID, get_channel_callback, data): + def on_event(self, packetID, get_eventCallback, data): valueByName = loads(data) eventName = valueByName['name'] eventArguments = valueByName['args'] - get_channel_callback(eventName)(*eventArguments) + get_eventCallback(eventName)(*eventArguments) - def on_acknowledgment(self, packetID, get_channel_callback, data): + def on_acknowledgment(self, packetID, get_eventCallback, data): dataParts = data.split('+', 1) messageID = int(dataParts[0]) arguments = loads(dataParts[1]) or [] - callback = self._socketIO.get_messageID_callback(messageID) + callback = self._socketIO.get_messageCallback(messageID) if not callback: return callback(*arguments) - if self.waitingForCallbacks.is_set() and not self._socketIO.has_messageID_callback: + if self.waiting.is_set() and not self._socketIO.has_messageCallback: self.cancel() - def on_error(self, packetID, get_channel_callback, data): + def on_error(self, packetID, get_eventCallback, data): reason, advice = data.split('+', 1) - get_channel_callback('error')(reason, advice) + get_eventCallback('error')(reason, advice) + + +class _SocketIO(object): + 'Low-level interface to remove cyclic references in child threads' + + messageID = 0 + self.callbackByMessageID = {} + self.callbackByEvent = {} + + + def __init__(self, host, port, secure, proxies): + baseURL = '%s:%d/socket.io/%s' % (host, port, PROTOCOL) + targetScheme = 'https' if secure else 'http' + targetURL = '%s://%s/' % (targetScheme, baseURL) + try: + response = urlopen(targetURL, proxies=proxies) + except IOError: # pragma: no cover + raise SocketIOError('Could not start connection') + if 200 != response.getcode(): # pragma: no cover + raise SocketIOError('Could not establish connection') + responseParts = response.readline().split(':') + sessionID = responseParts[0] + heartbeatTimeout = int(responseParts[1]) + # connectionTimeout = int(responseParts[2]) + supportedTransports = responseParts[3].split(',') + if 'websocket' not in supportedTransports: + raise SocketIOError('Could not parse handshake') # pragma: no cover + socketScheme = 'wss' if secure else 'ws' + socketURL = '%s://%s/websocket/%s' % (socketScheme, baseURL, sessionID) + self.connection = create_connection(socketURL) + self.heartbeatInterval = heartbeatTimeout - 2 + + def __del__(self): + self.connection.close() + + def get_channel(self, channelPath): + + def get_channel(self, channelName): + return self.channelByName[channelName] + pass + + def recv_packet(self): + code, packetID, channelName, data = -1, None, None, None + try: + packet = self.connection.recv() + except WebSocketConnectionClosedException: + raise SocketIOConnectionError('Lost connection (Connection closed)') + except socket.timeout: + raise SocketIOConnectionError('Lost connection (Connection timed out)') + try: + packetParts = packet.split(':', 3) + except AttributeError: + raise SocketIOPacketError('Received invalid packet (%s)' % packet) + packetCount = len(packetParts) + if 4 == packetCount: + code, packetID, channelName, data = packetParts + elif 3 == packetCount: + code, packetID, channelName = packetParts + elif 1 == packetCount: # pragma: no cover + code = packetParts[0] + return int(code), packetID, channelName, data + + def send_packet(self, code, channelName='', data='', callback=None): + callbackNumber = self.set_messageCallback(callback) if callback else '' + packetParts = [str(code), callbackNumber, channelName, data] + try: + self.connection.send(':'.join(packetParts)) + except socket.error: + raise SocketIOPacketError('Could not send packet') + + def disconnect(self, channelPath): + self.send_packet(0, channelPath) + + def connect(self, channelPath): + self.send_packet(1, channelPath) + + def send_heartbeat(self): + try: + self.send_packet(2) + except SocketIOPacketError: + print 'Could not send heartbeat' + pass + + def set_messageCallback(self, callback): + 'Set callback that will be called after receiving an acknowledgment' + self.messageID += 1 + self.callbackByMessageID[self.messageID] = callback + return '%s+' % self.messageID + + def get_messageCallback(self, messageID): + try: + callback = self.callbackByMessageID[messageID] + del self.callbackByMessageID[messageID] + return callback + except KeyError: + return + + @property + def has_messageCallback(self): + return True if self.callbackByMessageID else False + + +class Channel(object): + + def __init__(self, _socketIO, Namespace, channelPath): + self._socketIO = _socketIO + self._namespace = Namespace(_socketIO) + self._channelPath = channelPath + self._callbackByEvent = {} + + def on(self, eventName, eventCallback): + self._callbackByEvent[eventName] = eventCallback + + def message(self, messageData, messageCallback=None): + self._socketIO.message(messageData, messageCallback, channelPath=self._channelPath) + + def emit(self, eventName, *eventArguments): + self._socketIO.emit(eventName, *eventArguments, channelPath=self._channelPath) + + def get_namespace(self): + return self._namespace + + def _get_eventCallback(self, eventName): + # Check callbacks defined by on() + try: + return self._callbackByEvent[eventName] + except KeyError: + pass + # Check callbacks defined explicitly or use on_() + defaultCallback = lambda *eventArguments: self.get_namespace().on_(eventName, *eventArguments) + return getattr(self, 'on_' + eventName.replace(' ', '_'), defaultCallback) class SocketIOError(Exception): diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index 53a6cf6..1cbf3b6 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -13,18 +13,26 @@ class TestSocketIO(TestCase): socketIO = SocketIO('localhost', 8000) socketIO.disconnect() self.assertEqual(socketIO.connected, False) + childThreads = [ + socketIO._rhythmicThread, + socketIO._listenerThread, + ] + for childThread in childThreads: + self.assertEqual(True, childThread.done.is_set()) def test_emit(self): - socketIO = SocketIO('localhost', 8000, Namespace) + socketIO = SocketIO('localhost', 8000) + socketIO.define(Namespace) socketIO.emit('aaa') sleep(0.5) - self.assertEqual(socketIO._namespace.payload, '') + self.assertEqual(socketIO.get_namespace().payload, '') def test_emit_with_payload(self): - socketIO = SocketIO('localhost', 8000, Namespace) + socketIO = SocketIO('localhost', 8000) + socketIO.define(Namespace) socketIO.emit('aaa', PAYLOAD) sleep(0.5) - self.assertEqual(socketIO._namespace.payload, PAYLOAD) + self.assertEqual(socketIO.get_namespace().payload, PAYLOAD) def test_emit_with_callback(self): global ON_RESPONSE_CALLED @@ -44,20 +52,21 @@ class TestSocketIO(TestCase): self.assertEqual(ON_RESPONSE_CALLED, True) def test_channels(self): - mainSocket = SocketIO('localhost', 8000, Namespace) - chatSocket = mainSocket.connect('/chat', Namespace) - newsSocket = mainSocket.connect('/news', Namespace) + socketIO = SocketIO('localhost', 8000) + mainSocket = socketIO.define(Namespace) + chatSocket = socketIO.define(Namespace, '/chat') + newsSocket = socketIO.define(Namespace, '/news') newsSocket.emit('aaa', PAYLOAD) sleep(0.5) - self.assertNotEqual(mainSocket._namespace.payload, PAYLOAD) - self.assertNotEqual(chatSocket._namespace.payload, PAYLOAD) - self.assertEqual(newsSocket._namespace.payload, PAYLOAD) + self.assertNotEqual(mainSocket.get_namespace().payload, PAYLOAD) + self.assertNotEqual(chatSocket.get_namespace().payload, PAYLOAD) + self.assertEqual(newsSocket.get_namespace().payload, PAYLOAD) def test_delete(self): socketIO = SocketIO('localhost', 8000) childThreads = [ - socketIO._heartbeatThread, - socketIO._namespaceThread, + socketIO._rhythmicThread, + socketIO._listenerThread, ] del socketIO for childThread in childThreads: From 0368202f2bfc961be48fdfe55b4429b2baf06149 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Tue, 19 Feb 2013 08:10:35 -0800 Subject: [PATCH 06/19] Passed tests --- experiments/callableweakref-tests.py | 170 --------------------------- experiments/callableweakref.py | 52 -------- experiments/t0.py | 6 - experiments/t1.py | 7 -- experiments/t10.py | 16 --- experiments/t11.py | 21 ---- experiments/t12.py | 32 ----- experiments/t2.py | 19 --- experiments/t3.py | 19 --- experiments/t4.py | 26 ---- experiments/t5.py | 30 ----- socketIO_client/__init__.py | 166 ++++++++++++++------------ socketIO_client/tests.py | 3 +- 13 files changed, 92 insertions(+), 475 deletions(-) delete mode 100644 experiments/callableweakref-tests.py delete mode 100644 experiments/callableweakref.py delete mode 100644 experiments/t0.py delete mode 100644 experiments/t1.py delete mode 100644 experiments/t10.py delete mode 100644 experiments/t11.py delete mode 100644 experiments/t12.py delete mode 100644 experiments/t2.py delete mode 100644 experiments/t3.py delete mode 100644 experiments/t4.py delete mode 100644 experiments/t5.py diff --git a/experiments/callableweakref-tests.py b/experiments/callableweakref-tests.py deleted file mode 100644 index c7a95ec..0000000 --- a/experiments/callableweakref-tests.py +++ /dev/null @@ -1,170 +0,0 @@ -# Note 1: The expected behavior of weakref to an instance is that it should -# return None if no other strong references to the instance exist, -# signalling that the instance can be safely garbage collected. -# -# Note 2: The expected behavior of weakref to a method is that it should -# return None if no other strong references to the parent instance exist, -# signalling that the parent instance can be safely garbage collected. -# -# Note 3: The IPython interpreter stores its own references and will -# produce results different from those of the default Python interpreter. -import callableweakref -import weakref -import unittest - - -class TestCallableWeakRef(unittest.TestCase): - - def test_instanceDirectWeakref_dies_on_arrival(self): - 'Assert that weakref works as expected' - instanceDirectWeakref = weakref.ref(Instance()) - assert instanceDirectWeakref() is None - - def test_instanceIndirectWeakref_dies_when_instance_dies(self): - 'Assert that weakref works as expected' - instance = Instance() - instanceIndirectWeakref = weakref.ref(instance) - assert instanceIndirectWeakref() is instance - del instance - assert instanceIndirectWeakref() is None - - def test_boundMethodDirectWeakref_dies_on_arrival(self): - 'Assert that weakref does not work as expected' - instance = Instance() - boundMethodDirectWeakref = weakref.ref(instance.call) - assert boundMethodDirectWeakref() is None # Should be instance - - def test_boundMethodIndirectWeakref_lives_when_instance_dies(self): - 'Assert that weakref works as expected' - instance = Instance() - boundMethod = instance.call - boundMethodIndirectWeakref = weakref.ref(boundMethod) - assert boundMethodIndirectWeakref() is boundMethod - del instance - assert boundMethodIndirectWeakref() is boundMethod - del boundMethod - assert boundMethodIndirectWeakref() is None - - def test_unboundMethodDirectWeakref_dies_on_arrival(self): - 'Assert that weakref does not work as expected' - unboundMethodDirectWeakref = weakref.ref(Instance.call) - assert unboundMethodDirectWeakref() is None # Should be Instance.call - - def test_unboundMethodIndirectWeakref_dies_when_unboundMethod_dies(self): - 'Assert that weakref works as expected' - unboundMethod = Instance.call - unboundMethodIndirectWeakref = weakref.ref(unboundMethod) - assert unboundMethodIndirectWeakref() is unboundMethod - del unboundMethod - assert unboundMethodIndirectWeakref() is None - - def test_classFunctionDirectWeakref_dies_on_arrival(self): - 'Assert that weakref works as expected' - classFunctionDirectWeakref = weakref.ref(Instance()) - assert classFunctionDirectWeakref() is None - - def test_classFunctionIndirectWeakref_dies_when_classFunction_dies(self): - 'Assert that weakref works as expected' - classFunction = Instance() - classFunctionIndirectWeakref = weakref.ref(classFunction) - assert classFunctionIndirectWeakref() is classFunction - del classFunction - assert classFunctionIndirectWeakref() is None - - def test_normalFunctionDirectWeakref_dies_when_normal_function_dies(self): - 'Assert that weakref works as expected' - call = lambda: None - normalFunctionDirectWeakref = weakref.ref(call) - assert normalFunctionDirectWeakref() is call - del call - assert normalFunctionDirectWeakref() is None - - def test_normalFunctionIndirectWeakref_dies_when_normal_function_dies(self): - 'Assert that weakref works as expected' - call = lambda: None - normalFunction = call - normalFunctionIndirectWeakref = weakref.ref(normalFunction) - assert normalFunctionIndirectWeakref() is normalFunction - del normalFunction - assert normalFunctionIndirectWeakref() is call - del call - assert normalFunctionIndirectWeakref() is None - - # Assert that CallableWeakref works as expected for callables - - def test_boundMethodDirectCallableWeakref_dies_when_instance_dies(self): - instance = Instance() - boundMethodDirectCallableWeakref = callableweakref.ref(instance.call) - self.assertEqual(boundMethodDirectCallableWeakref(), instance.call) - del instance - assert boundMethodDirectCallableWeakref() is None - - def test_boundMethodIndirectCallableWeakref_dies_when_instance_dies(self): - instance = Instance() - boundMethod = instance.call - boundMethodIndirectCallableWeakref = callableweakref.ref(boundMethod) - self.assertEqual(boundMethodIndirectCallableWeakref(), boundMethod) - del instance - self.assertEqual(boundMethodIndirectCallableWeakref(), boundMethod) - del boundMethod - assert boundMethodIndirectCallableWeakref() is None - - def test_unboundMethodDirectCallableWeakref_lives_on_arrival(self): - unboundMethodDirectCallableWeakref = callableweakref.ref(Instance.call) - self.assertEqual(unboundMethodDirectCallableWeakref(), Instance.call) - - def test_unboundMethodIndirectCallableWeakref_lives_on_arrival(self): - unboundMethod = Instance.call - unboundMethodIndirectCallableWeakref = callableweakref.ref(unboundMethod) - self.assertEqual(unboundMethodIndirectCallableWeakref(), unboundMethod) - - def test_classMethodIndirectCallableWeakref_lives_on_arrival(self): - classMethod = Instance.call_classmethod - classMethodIndirectCallableWeakref = callableweakref.ref(classMethod) - self.assertEqual(classMethodIndirectCallableWeakref(), classMethod) - - def test_staticMethodIndirectCallableWeakref_lives_on_arrival(self): - staticMethod = Instance.call_staticmethod - staticMethodIndirectCallableWeakref = callableweakref.ref(staticMethod) - self.assertEqual(staticMethodIndirectCallableWeakref(), staticMethod) - - def test_classFunctionDirectCallableWeakref_dies_on_arrival(self): - classFunctionDirectCallableWeakref = callableweakref.ref(Instance()) - assert classFunctionDirectCallableWeakref() is None - - def test_classFunctionIndirectCallableWeakref_dies_when_classFunction_dies(self): - classFunction = Instance() - classFunctionIndirectCallableWeakref = callableweakref.ref(classFunction) - self.assertEqual(classFunctionIndirectCallableWeakref(), classFunction.__call__) - del classFunction - assert classFunctionIndirectCallableWeakref() is None - - def test_normalFunctionDirectCallableWeakref_lives_on_arrival(self): - call = lambda: None - normalFunctionDirectCallableWeakref = callableweakref.ref(call) - self.assertEqual(normalFunctionDirectCallableWeakref(), call) - - def test_normalFunctionIndirectCallableWeakref_lives_on_arrival(self): - call = lambda: None - normalFunction = call - normalFunctionIndirectWeakref = callableweakref.ref(normalFunction) - self.assertEqual(normalFunctionIndirectWeakref(), normalFunction) - del normalFunction - self.assertEqual(normalFunctionIndirectWeakref(), call) - - -class Instance(object): - - def __call__(self): - pass - - def call(self): - pass - - @classmethod - def call_classmethod(Class): - pass - - @staticmethod - def call_staticmethod(): - pass diff --git a/experiments/callableweakref.py b/experiments/callableweakref.py deleted file mode 100644 index 788592c..0000000 --- a/experiments/callableweakref.py +++ /dev/null @@ -1,52 +0,0 @@ -import types -import weakref - - -def ref(function): - return CallableWeakReference(function) - - -class CallableWeakReference(object): - - def __init__(self, function): - 'Create a weak reference to a callable' - try: - if function.im_self: - # We have a bound method or class method - self._reference = weakref.ref(function.im_self) - else: - # We have an unbound method - self._reference = None - self._function = function.im_func - self._class = function.im_class - except AttributeError: - try: - function.func_code - # We have a normal function or static method - self._reference = None - self._function = function - self._class = None - except AttributeError: - function = function.__call__ - # We have a class masquerading as a function - self._reference = weakref.ref(function.im_self) - self._function = function.im_func - self._class = function.im_class - - def __call__(self): - if self.dead: - return - if self._reference: - # We have a bound method - return types.MethodType(self._function, self._reference(), self._class) - elif self._class: - return types.MethodType(self._function, None, self._class) - else: - return self._function - - @property - def dead(self): - if self._reference and not self._reference(): - # We have a bound method whose parent reference has died - return True - return False diff --git a/experiments/t0.py b/experiments/t0.py deleted file mode 100644 index 3980052..0000000 --- a/experiments/t0.py +++ /dev/null @@ -1,6 +0,0 @@ -from socketIO_client import SocketIO - -s = SocketIO('localhost', 8000) -del s -from time import sleep -sleep(3) diff --git a/experiments/t1.py b/experiments/t1.py deleted file mode 100644 index 0955ed6..0000000 --- a/experiments/t1.py +++ /dev/null @@ -1,7 +0,0 @@ -class O(object): - - def __del__(self): - print '__del__()' - - -o = O() diff --git a/experiments/t10.py b/experiments/t10.py deleted file mode 100644 index 24733d9..0000000 --- a/experiments/t10.py +++ /dev/null @@ -1,16 +0,0 @@ -class A(object): - - def __init__(self): - self.b = B(self) - - def __del__(self): - print '__del__()' - - -class B(object): - - def __init__(self, a): - self.a = a - - -a = A() diff --git a/experiments/t11.py b/experiments/t11.py deleted file mode 100644 index 11121de..0000000 --- a/experiments/t11.py +++ /dev/null @@ -1,21 +0,0 @@ -class A(object): - - def __init__(self): - self.c = Common() - self.b = B(self.c) - - def __del__(self): - print '__del__()' - - -class B(object): - - def __init__(self, c): - self.c = c - - -class Common(object): - pass - - -a = A() diff --git a/experiments/t12.py b/experiments/t12.py deleted file mode 100644 index b4e9bb3..0000000 --- a/experiments/t12.py +++ /dev/null @@ -1,32 +0,0 @@ -# Will the destructor of a class be called if its two children have cyclic -# references to each other? Yes. - - -class Parent(object): - - def __init__(self): - self.child = Child() - - def __del__(self): - print 'Parent.__del__()' - - -class Child(object): - - def __init__(self): - self.grandChild = GrandChild(self) - - def __del__(self): - print 'Child.__del__()' - - -class GrandChild(object): - - def __init__(self, parent): - self.parent = parent - - def __del__(self): - print 'GrandChild.__del__()' - - -parent = Parent() diff --git a/experiments/t2.py b/experiments/t2.py deleted file mode 100644 index 3ad124c..0000000 --- a/experiments/t2.py +++ /dev/null @@ -1,19 +0,0 @@ -import weakref - - -class O(object): - - def __init__(self): - self.p = P(self) - - def __del__(self): - print '__del__()' - - -class P(object): - - def __init__(self, parent): - self.parent = weakref.ref(parent) - - -o = O() diff --git a/experiments/t3.py b/experiments/t3.py deleted file mode 100644 index 2de3123..0000000 --- a/experiments/t3.py +++ /dev/null @@ -1,19 +0,0 @@ -class O(object): - - def __init__(self): - self.p = P(self.f) - - def __del__(self): - print '__del__()' - - def f(self): - pass - - -class P(object): - - def __init__(self, parentMethod): - self.parentMethod = parentMethod - - -o = O() diff --git a/experiments/t4.py b/experiments/t4.py deleted file mode 100644 index da46dfe..0000000 --- a/experiments/t4.py +++ /dev/null @@ -1,26 +0,0 @@ -import weakref - - -class O(object): - - def __init__(self): - self.p = P(self.f) - - def __del__(self): - print '__del__()' - - def f(self): - pass - - -class P(object): - - def __init__(self, parentMethod): - self.parentMethod = weakref.ref(parentMethod) - - def show(self): - print self.parentMethod - - -o = O() -o.p.show() # Dead on arrival diff --git a/experiments/t5.py b/experiments/t5.py deleted file mode 100644 index a2ff710..0000000 --- a/experiments/t5.py +++ /dev/null @@ -1,30 +0,0 @@ -import callableweakref - - -class O(object): - - def __init__(self): - self.p = P(self.f) - - def __del__(self): - print '__del__()' - - def f(self): - print 'f()' - - -class P(object): - - def __init__(self, parentMethod): - self.parentMethod = callableweakref.ref(parentMethod) - - def show(self): - print self.parentMethod - - def run(self): - self.parentMethod()() - - -o = O() -o.p.show() -o.p.run() diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index b247fb4..b850b1c 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -47,9 +47,10 @@ class SocketIO(object): def __init__(self, host, port, secure=False, proxies=None): self._socketIO = _SocketIO(host, port, secure, proxies) self._channelByPath = {} + self.define(BaseNamespace) # Define default namespace self._rhythmicThread = _RhythmicThread( - self._socketIO.heartbeatTimeout, + self._socketIO.heartbeatInterval, self._socketIO.send_heartbeat) self._rhythmicThread.start() @@ -65,14 +66,15 @@ class SocketIO(object): self.disconnect() def __del__(self): - self.disconnect() + self.disconnect(force=True) @property def connected(self): - return self._socketIO.connection.connected + return self._socketIO.connected - def disconnect(self, channelPath=''): - self._socketIO.disconnect(channelPath) + def disconnect(self, channelPath='', force=False): + if self.connected: + self._socketIO.disconnect(channelPath, force) if channelPath: del self._channelByPath[channelPath] else: @@ -88,37 +90,23 @@ class SocketIO(object): def get_namespace(self, channelPath=''): return self._channelByPath[channelPath].get_namespace() - def on(self, eventName, eventCallback): - self._callbackByEvent[eventName] = callback + def on(self, eventName, eventCallback, channelPath=''): + return self._channelByPath[channelPath].on(eventName, eventCallback) - def message(self, messageData, callback=None, channelName=''): - if isinstance(messageData, basestring): - code = 3 - data = messageData - else: - code = 4 - data = dumps(messageData) - self._send_packet(code, channelName, data, callback) + def message(self, messageData, messageCallback=None, channelPath=''): + self._socketIO.message(messageData, messageCallback, channelPath) - def emit(self, eventName, *eventArguments): - self._socketIO.emit(eventName, *eventArguments) - code = 5 - callback = None - if eventArguments and callable(eventArguments[-1]): - callback = eventArguments[-1] - eventArguments = eventArguments[:-1] - channelName = eventKeywords.get('channelName', '') - data = dumps(dict(name=eventName, args=eventArguments)) - self._send_packet(code, channelName, data, callback) + def emit(self, eventName, *eventArguments, **eventKeywords): + self._socketIO.emit(eventName, *eventArguments, **eventKeywords) def wait(self, seconds=None, forCallbacks=False): if forCallbacks: - self.__listenerThread.wait_for_callbacks(seconds) + self._listenerThread.wait_for_callbacks(seconds) elif seconds: sleep(seconds) else: try: - while self._socketIO.connected: + while self.connected: sleep(1) except KeyboardInterrupt: pass @@ -132,7 +120,7 @@ class _RhythmicThread(Thread): def __init__(self, intervalInSeconds, call, *args, **kw): super(_RhythmicThread, self).__init__() self.intervalInSeconds = intervalInSeconds - self.call + self.call = call self.args = args self.kw = kw self.done = Event() @@ -176,8 +164,8 @@ class _ListenerThread(Thread): except SocketIOPacketError, error: print error continue - channel = self._channelByPath[channelPath] try: + channel = self._channelByPath[channelPath] delegate = { 0: self.on_disconnect, 1: self.on_connect, @@ -199,7 +187,8 @@ class _ListenerThread(Thread): get_eventCallback('connect')() def on_heartbeat(self, packetID, get_eventCallback, data): - get_eventCallback('heartbeat')() + # get_eventCallback('heartbeat')() + pass def on_message(self, packetID, get_eventCallback, data): get_eventCallback('message')(data) @@ -217,10 +206,10 @@ class _ListenerThread(Thread): dataParts = data.split('+', 1) messageID = int(dataParts[0]) arguments = loads(dataParts[1]) or [] - callback = self._socketIO.get_messageCallback(messageID) - if not callback: + messageCallback = self._socketIO.get_messageCallback(messageID) + if not messageCallback: return - callback(*arguments) + messageCallback(*arguments) if self.waiting.is_set() and not self._socketIO.has_messageCallback: self.cancel() @@ -233,9 +222,6 @@ class _SocketIO(object): 'Low-level interface to remove cyclic references in child threads' messageID = 0 - self.callbackByMessageID = {} - self.callbackByEvent = {} - def __init__(self, host, port, secure, proxies): baseURL = '%s:%d/socket.io/%s' % (host, port, PROTOCOL) @@ -258,47 +244,18 @@ class _SocketIO(object): socketURL = '%s://%s/websocket/%s' % (socketScheme, baseURL, sessionID) self.connection = create_connection(socketURL) self.heartbeatInterval = heartbeatTimeout - 2 + self.callbackByMessageID = {} def __del__(self): - self.connection.close() + self.disconnect(force=True) - def get_channel(self, channelPath): - - def get_channel(self, channelName): - return self.channelByName[channelName] - pass - - def recv_packet(self): - code, packetID, channelName, data = -1, None, None, None - try: - packet = self.connection.recv() - except WebSocketConnectionClosedException: - raise SocketIOConnectionError('Lost connection (Connection closed)') - except socket.timeout: - raise SocketIOConnectionError('Lost connection (Connection timed out)') - try: - packetParts = packet.split(':', 3) - except AttributeError: - raise SocketIOPacketError('Received invalid packet (%s)' % packet) - packetCount = len(packetParts) - if 4 == packetCount: - code, packetID, channelName, data = packetParts - elif 3 == packetCount: - code, packetID, channelName = packetParts - elif 1 == packetCount: # pragma: no cover - code = packetParts[0] - return int(code), packetID, channelName, data - - def send_packet(self, code, channelName='', data='', callback=None): - callbackNumber = self.set_messageCallback(callback) if callback else '' - packetParts = [str(code), callbackNumber, channelName, data] - try: - self.connection.send(':'.join(packetParts)) - except socket.error: - raise SocketIOPacketError('Could not send packet') - - def disconnect(self, channelPath): - self.send_packet(0, channelPath) + def disconnect(self, channelPath='', force=False): + if not self.connected: + return + if channelPath: + self.send_packet(0, channelPath) + elif not force: + self.connection.close() def connect(self, channelPath): self.send_packet(1, channelPath) @@ -310,6 +267,26 @@ class _SocketIO(object): print 'Could not send heartbeat' pass + def message(self, messageData, messageCallback, channelPath): + if isinstance(messageData, basestring): + code = 3 + data = messageData + else: + code = 4 + data = dumps(messageData) + self.send_packet(code, channelPath, data, messageCallback) + + def emit(self, eventName, *eventArguments, **eventKeywords): + code = 5 + if eventArguments and callable(eventArguments[-1]): + messageCallback = eventArguments[-1] + eventArguments = eventArguments[:-1] + else: + messageCallback = None + channelPath = eventKeywords.get('channelPath', '') + data = dumps(dict(name=eventName, args=eventArguments)) + self.send_packet(code, channelPath, data, messageCallback) + def set_messageCallback(self, callback): 'Set callback that will be called after receiving an acknowledgment' self.messageID += 1 @@ -328,6 +305,41 @@ class _SocketIO(object): def has_messageCallback(self): return True if self.callbackByMessageID else False + def recv_packet(self): + code, packetID, channelPath, data = -1, None, None, None + try: + packet = self.connection.recv() + except WebSocketConnectionClosedException: + raise SocketIOConnectionError('Lost connection (Connection closed)') + except socket.timeout: + raise SocketIOConnectionError('Lost connection (Connection timed out)') + except socket.error: + raise SocketIOConnectionError('Lost connection') + try: + packetParts = packet.split(':', 3) + except AttributeError: + raise SocketIOPacketError('Received invalid packet (%s)' % packet) + packetCount = len(packetParts) + if 4 == packetCount: + code, packetID, channelPath, data = packetParts + elif 3 == packetCount: + code, packetID, channelPath = packetParts + elif 1 == packetCount: # pragma: no cover + code = packetParts[0] + return int(code), packetID, channelPath, data + + def send_packet(self, code, channelPath='', data='', messageCallback=None): + callbackNumber = self.set_messageCallback(messageCallback) if messageCallback else '' + packetParts = [str(code), callbackNumber, channelPath, data] + try: + self.connection.send(':'.join(packetParts)) + except socket.error: + raise SocketIOPacketError('Could not send packet') + + @property + def connected(self): + return self.connection.connected + class Channel(object): @@ -356,8 +368,10 @@ class Channel(object): except KeyError: pass # Check callbacks defined explicitly or use on_() - defaultCallback = lambda *eventArguments: self.get_namespace().on_(eventName, *eventArguments) - return getattr(self, 'on_' + eventName.replace(' ', '_'), defaultCallback) + eventNamespace = self.get_namespace() + callbackName = 'on_' + eventName.replace(' ', '_') + defaultCallback = lambda *eventArguments: eventNamespace.on_(eventName, *eventArguments) + return getattr(eventNamespace, callbackName, defaultCallback) class SocketIOError(Exception): diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index 1cbf3b6..6442e1e 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -12,7 +12,7 @@ class TestSocketIO(TestCase): def test_disconnect(self): socketIO = SocketIO('localhost', 8000) socketIO.disconnect() - self.assertEqual(socketIO.connected, False) + self.assertEqual(False, socketIO.connected) childThreads = [ socketIO._rhythmicThread, socketIO._listenerThread, @@ -78,6 +78,7 @@ class Namespace(BaseNamespace): payload = None def on_ddd(self, data=''): + print '[Event] ddd(%s)' % data self.payload = data From 4f39d1f92282f84e43df8fc026a804a6bcac13fb Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 21 Feb 2013 09:08:58 -0800 Subject: [PATCH 07/19] Added tests for Channel.message() --- socketIO_client/__init__.py | 35 ++++++++++++++++++++++------------- socketIO_client/tests.py | 28 +++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index b850b1c..920fa48 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -87,11 +87,14 @@ class SocketIO(object): self._channelByPath[channelPath] = channel return channel + def get_channel(self, channelPath=''): + return self._channelByPath[channelPath] + def get_namespace(self, channelPath=''): - return self._channelByPath[channelPath].get_namespace() + return self.get_channel(channelPath).get_namespace() def on(self, eventName, eventCallback, channelPath=''): - return self._channelByPath[channelPath].on(eventName, eventCallback) + return self.get_channel(channelPath).on(eventName, eventCallback) def message(self, messageData, messageCallback=None, channelPath=''): self._socketIO.message(messageData, messageCallback, channelPath) @@ -166,17 +169,22 @@ class _ListenerThread(Thread): continue try: channel = self._channelByPath[channelPath] + except KeyError: + print 'Received unexpected channelPath (%s)' % channelPath + continue + try: delegate = { - 0: self.on_disconnect, - 1: self.on_connect, - 2: self.on_heartbeat, - 3: self.on_message, - 4: self.on_json, - 5: self.on_event, - 6: self.on_acknowledgment, - 7: self.on_error, + '0': self.on_disconnect, + '1': self.on_connect, + '2': self.on_heartbeat, + '3': self.on_message, + '4': self.on_json, + '5': self.on_event, + '6': self.on_acknowledgment, + '7': self.on_error, }[code] except KeyError: + print 'Received unexpected code (%s)' % code continue delegate(packetID, channel._get_eventCallback, data) @@ -250,6 +258,7 @@ class _SocketIO(object): self.disconnect(force=True) def disconnect(self, channelPath='', force=False): + 'Set force=True to skip closing the websocket' if not self.connected: return if channelPath: @@ -306,7 +315,6 @@ class _SocketIO(object): return True if self.callbackByMessageID else False def recv_packet(self): - code, packetID, channelPath, data = -1, None, None, None try: packet = self.connection.recv() except WebSocketConnectionClosedException: @@ -320,13 +328,14 @@ class _SocketIO(object): except AttributeError: raise SocketIOPacketError('Received invalid packet (%s)' % packet) packetCount = len(packetParts) + code, packetID, channelPath, data = None, None, None, None if 4 == packetCount: code, packetID, channelPath, data = packetParts elif 3 == packetCount: code, packetID, channelPath = packetParts - elif 1 == packetCount: # pragma: no cover + elif 1 == packetCount: code = packetParts[0] - return int(code), packetID, channelPath, data + return code, packetID, channelPath, data def send_packet(self, code, channelPath='', data='', messageCallback=None): callbackNumber = self.set_messageCallback(messageCallback) if messageCallback else '' diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index 6442e1e..a662c45 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -24,14 +24,14 @@ class TestSocketIO(TestCase): socketIO = SocketIO('localhost', 8000) socketIO.define(Namespace) socketIO.emit('aaa') - sleep(0.5) + sleep(0.1) self.assertEqual(socketIO.get_namespace().payload, '') def test_emit_with_payload(self): socketIO = SocketIO('localhost', 8000) socketIO.define(Namespace) socketIO.emit('aaa', PAYLOAD) - sleep(0.5) + sleep(0.1) self.assertEqual(socketIO.get_namespace().payload, PAYLOAD) def test_emit_with_callback(self): @@ -42,13 +42,21 @@ class TestSocketIO(TestCase): socketIO.wait(forCallbacks=True) self.assertEqual(ON_RESPONSE_CALLED, True) + def test_message(self): + global ON_RESPONSE_CALLED + ON_RESPONSE_CALLED = False + socketIO = SocketIO('localhost', 8000) + socketIO.message(PAYLOAD, on_response) + socketIO.wait(forCallbacks=True) + self.assertEqual(ON_RESPONSE_CALLED, True) + def test_events(self): global ON_RESPONSE_CALLED ON_RESPONSE_CALLED = False socketIO = SocketIO('localhost', 8000) socketIO.on('ddd', on_response) socketIO.emit('aaa', PAYLOAD) - sleep(0.5) + sleep(0.1) self.assertEqual(ON_RESPONSE_CALLED, True) def test_channels(self): @@ -56,12 +64,22 @@ class TestSocketIO(TestCase): mainSocket = socketIO.define(Namespace) chatSocket = socketIO.define(Namespace, '/chat') newsSocket = socketIO.define(Namespace, '/news') - newsSocket.emit('aaa', PAYLOAD) - sleep(0.5) self.assertNotEqual(mainSocket.get_namespace().payload, PAYLOAD) self.assertNotEqual(chatSocket.get_namespace().payload, PAYLOAD) + self.assertNotEqual(newsSocket.get_namespace().payload, PAYLOAD) + newsSocket.emit('aaa', PAYLOAD) + sleep(0.1) self.assertEqual(newsSocket.get_namespace().payload, PAYLOAD) + def test_channels_with_callback(self): + global ON_RESPONSE_CALLED + ON_RESPONSE_CALLED = False + socketIO = SocketIO('localhost', 8000) + mainSocket = socketIO.get_channel() + mainSocket.message(PAYLOAD, on_response) + sleep(0.1) + self.assertEqual(ON_RESPONSE_CALLED, True) + def test_delete(self): socketIO = SocketIO('localhost', 8000) childThreads = [ From 187c6fbca10762074d2feabe7cfcd64fa9dbbf7d Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Sun, 14 Apr 2013 23:23:30 -0700 Subject: [PATCH 08/19] Reviewed code --- .gitignore | 9 +++++---- LICENSE | 2 +- TODO.rst | 13 +++++++++---- socketIO_client/__init__.py | 10 +++++----- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 15e22ab..6cf8c00 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ *~ +*.sw[op] +*.py[cod] +*.egg *.egg-info -*.pyc -*.swo -*.swp -.coverage build dist +sdist +.coverage diff --git a/LICENSE b/LICENSE index 2e7adea..4e68cea 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012 Roy Hyunjin Han and contributors +Copyright (c) 2013 Roy Hyunjin Han and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/TODO.rst b/TODO.rst index b15199b..4da9fa4 100644 --- a/TODO.rst +++ b/TODO.rst @@ -1,9 +1,14 @@ ++ Identify what needs to be done + + check __init__.py + + check tests.py + + check serve_tests.py + Fix unittests + Fix exceptions when websocket server disappears -Fix thread exceptions - Finish creating low-level _SocketIO to eliminate cyclic references - Move namespace and callback handling to _ListenerThread - ++ Fix thread exceptions + + Finish creating low-level _SocketIO to eliminate cyclic references + + Move namespace and callback handling to _ListenerThread += Resolve pull requests +Resolve issues Integrate Zac's fork #6 Integrate Sajal's fork #7 Integrate Francis's fork #10 diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 920fa48..e5b50be 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,15 +1,16 @@ import socket from anyjson import dumps, loads -from threading import Event, Thread +from threading import Thread, Event from time import sleep from urllib import urlopen from websocket import WebSocketConnectionClosedException, create_connection -PROTOCOL = 1 # SocketIO protocol version +PROTOCOL = 1 # socket.io protocol version class BaseNamespace(object): # pragma: no cover + 'Define socket.io behavior' def __init__(self, _socketIO): self._socketIO = _socketIO @@ -138,7 +139,7 @@ class _RhythmicThread(Thread): class _ListenerThread(Thread): - 'Process messages from SocketIO server' + 'Process messages from socket.io server' daemon = True @@ -195,7 +196,6 @@ class _ListenerThread(Thread): get_eventCallback('connect')() def on_heartbeat(self, packetID, get_eventCallback, data): - # get_eventCallback('heartbeat')() pass def on_message(self, packetID, get_eventCallback, data): @@ -207,7 +207,7 @@ class _ListenerThread(Thread): def on_event(self, packetID, get_eventCallback, data): valueByName = loads(data) eventName = valueByName['name'] - eventArguments = valueByName['args'] + eventArguments = valueByName.get('args', []) get_eventCallback(eventName)(*eventArguments) def on_acknowledgment(self, packetID, get_eventCallback, data): From ee91f821719a313f071ca51d10f6b8f116ba40ec Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 15 Apr 2013 01:15:21 -0700 Subject: [PATCH 09/19] Preparing to merge changes from zratic --- README.rst | 15 +++--- serve_tests.py | 15 ++---- socketIO_client/__init__.py | 3 +- socketIO_client/tests.py | 93 +++++++++++++++---------------------- 4 files changed, 52 insertions(+), 74 deletions(-) diff --git a/README.rst b/README.rst index a1984c2..93571c7 100644 --- a/README.rst +++ b/README.rst @@ -86,11 +86,11 @@ Define standard events. :: def on_disconnect(self): print '[Disconnected]' - def on_error(self, name, message): - print '[Error] %s: %s' % (name, message) + def on_error(self, reason, advice): + print '[Error] %s' % advice - def on_message(self, id, message): - print '[Message] %s: %s' % (id, message) + def on_message(self, messageData): + print '[Message] %s' % messageData socketIO = SocketIO('localhost', 8000) socketIO.define(Namespace) @@ -100,17 +100,17 @@ Define different namespaces on a single socket. :: from socketIO_client import SocketIO, BaseNamespace - class MainNamespace(Channel): + class MainNamespace(BaseNamespace): def on_aaa(self, *args): print 'aaa', args - class ChatNamespace(Channel): + class ChatNamespace(BaseNamespace): def on_bbb(self, *args): print 'bbb', args - class NewsNamespace(Channel): + class NewsNamespace(BaseNamespace): def on_ccc(self, *args): print 'ccc', args @@ -129,6 +129,7 @@ Open secure websockets (HTTPS / WSS) behind a proxy. :: secure=True, proxies={'http': 'http://proxy.example.com:8080'}) + License ------- This software is available under the MIT License. diff --git a/serve_tests.py b/serve_tests.py index faea52e..24df950 100644 --- a/serve_tests.py +++ b/serve_tests.py @@ -1,5 +1,4 @@ 'Launch this server in another terminal window before running tests' -import sys try: from socketio import socketio_manage from socketio.namespace import BaseNamespace @@ -8,17 +7,13 @@ except ImportError: from setuptools.command import easy_install easy_install.main(['-U', 'gevent-socketio']) print('\nPlease run the script again to launch the test server.') - sys.exit(1) + import sys; sys.exit(1) class Namespace(BaseNamespace): def on_aaa(self, *args): - self.socket.send_packet(dict( - type='event', - name='ddd', - args=args, - endpoint=self.ns_name)) + self.emit('aaa_response', *args) class Application(object): @@ -32,7 +27,7 @@ class Application(object): if __name__ == '__main__': - port = 8000 - print 'Starting server at port %s' % port - socketIOServer = SocketIOServer(('0.0.0.0', port), Application()) + from socketIO_client.tests import PORT + print 'Starting server at port %s' % PORT + socketIOServer = SocketIOServer(('0.0.0.0', PORT), Application()) socketIOServer.serve_forever() diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index e5b50be..b3268ca 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -286,7 +286,6 @@ class _SocketIO(object): self.send_packet(code, channelPath, data, messageCallback) def emit(self, eventName, *eventArguments, **eventKeywords): - code = 5 if eventArguments and callable(eventArguments[-1]): messageCallback = eventArguments[-1] eventArguments = eventArguments[:-1] @@ -294,7 +293,7 @@ class _SocketIO(object): messageCallback = None channelPath = eventKeywords.get('channelPath', '') data = dumps(dict(name=eventName, args=eventArguments)) - self.send_packet(code, channelPath, data, messageCallback) + self.send_packet(5, channelPath, data, messageCallback) def set_messageCallback(self, callback): 'Set callback that will be called after receiving an acknowledgment' diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index a662c45..cbeaca9 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -3,67 +3,53 @@ from time import sleep from unittest import TestCase -PAYLOAD = {'bbb': 'ccc'} ON_RESPONSE_CALLED = False +PORT = 8000 +PAYLOAD = {'xxx': 'yyy'} class TestSocketIO(TestCase): - def test_disconnect(self): - socketIO = SocketIO('localhost', 8000) - socketIO.disconnect() - self.assertEqual(False, socketIO.connected) - childThreads = [ - socketIO._rhythmicThread, - socketIO._listenerThread, - ] - for childThread in childThreads: - self.assertEqual(True, childThread.done.is_set()) - - def test_emit(self): - socketIO = SocketIO('localhost', 8000) - socketIO.define(Namespace) - socketIO.emit('aaa') - sleep(0.1) - self.assertEqual(socketIO.get_namespace().payload, '') - - def test_emit_with_payload(self): - socketIO = SocketIO('localhost', 8000) - socketIO.define(Namespace) - socketIO.emit('aaa', PAYLOAD) - sleep(0.1) - self.assertEqual(socketIO.get_namespace().payload, PAYLOAD) - - def test_emit_with_callback(self): + def setUp(self): global ON_RESPONSE_CALLED ON_RESPONSE_CALLED = False - socketIO = SocketIO('localhost', 8000) - socketIO.emit('aaa', PAYLOAD, on_response) - socketIO.wait(forCallbacks=True) + self.socketIO = SocketIO('localhost', PORT) + + def tearDown(self): + del self.socketIO + + def test_emit(self): + self.socketIO.define(Namespace) + self.socketIO.emit('aaa') + sleep(0.1) + self.assertEqual(self.socketIO.get_namespace().payload, '') + + def test_emit_with_payload(self): + self.socketIO.define(Namespace) + self.socketIO.emit('aaa', PAYLOAD) + sleep(0.1) + self.assertEqual(self.socketIO.get_namespace().payload, PAYLOAD) + + def test_emit_with_callback(self): + self.socketIO.emit('aaa', PAYLOAD, on_response) + self.socketIO.wait(forCallbacks=True) self.assertEqual(ON_RESPONSE_CALLED, True) def test_message(self): - global ON_RESPONSE_CALLED - ON_RESPONSE_CALLED = False - socketIO = SocketIO('localhost', 8000) - socketIO.message(PAYLOAD, on_response) - socketIO.wait(forCallbacks=True) + self.socketIO.message(PAYLOAD, on_response) + self.socketIO.wait(forCallbacks=True) self.assertEqual(ON_RESPONSE_CALLED, True) def test_events(self): - global ON_RESPONSE_CALLED - ON_RESPONSE_CALLED = False - socketIO = SocketIO('localhost', 8000) - socketIO.on('ddd', on_response) - socketIO.emit('aaa', PAYLOAD) + self.socketIO.on('aaa_response', on_response) + self.socketIO.emit('aaa', PAYLOAD) sleep(0.1) self.assertEqual(ON_RESPONSE_CALLED, True) def test_channels(self): - socketIO = SocketIO('localhost', 8000) - mainSocket = socketIO.define(Namespace) - chatSocket = socketIO.define(Namespace, '/chat') - newsSocket = socketIO.define(Namespace, '/news') + mainSocket = self.socketIO.define(Namespace) + chatSocket = self.socketIO.define(Namespace, '/chat') + newsSocket = self.socketIO.define(Namespace, '/news') self.assertNotEqual(mainSocket.get_namespace().payload, PAYLOAD) self.assertNotEqual(chatSocket.get_namespace().payload, PAYLOAD) self.assertNotEqual(newsSocket.get_namespace().payload, PAYLOAD) @@ -72,31 +58,28 @@ class TestSocketIO(TestCase): self.assertEqual(newsSocket.get_namespace().payload, PAYLOAD) def test_channels_with_callback(self): - global ON_RESPONSE_CALLED - ON_RESPONSE_CALLED = False - socketIO = SocketIO('localhost', 8000) - mainSocket = socketIO.get_channel() + mainSocket = self.socketIO.get_channel() mainSocket.message(PAYLOAD, on_response) sleep(0.1) self.assertEqual(ON_RESPONSE_CALLED, True) - def test_delete(self): - socketIO = SocketIO('localhost', 8000) + def test_disconnect(self): childThreads = [ - socketIO._rhythmicThread, - socketIO._listenerThread, + self.socketIO._rhythmicThread, + self.socketIO._listenerThread, ] - del socketIO + self.socketIO.disconnect() for childThread in childThreads: self.assertEqual(True, childThread.done.is_set()) + self.assertEqual(False, self.socketIO.connected) class Namespace(BaseNamespace): payload = None - def on_ddd(self, data=''): - print '[Event] ddd(%s)' % data + def on_aaa_response(self, data=''): + print '[Event] aaa_response(%s)' % data self.payload = data From 1669e90177de9bba4084bd650b1d5caf756bf2c5 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Mon, 15 Apr 2013 09:01:33 -0700 Subject: [PATCH 10/19] Updated TODO --- TODO.goals | 9 +++++++++ TODO.rst | 16 ---------------- 2 files changed, 9 insertions(+), 16 deletions(-) create mode 100644 TODO.goals delete mode 100644 TODO.rst diff --git a/TODO.goals b/TODO.goals new file mode 100644 index 0000000..7ae3dbd --- /dev/null +++ b/TODO.goals @@ -0,0 +1,9 @@ += Resolve pull requests + = Resolve pull request #6 +Resolve issues + Investigate issue #8 +Examine forks + Integrate Zac's fork #6 + Integrate Sajal's fork #7 + Integrate Francis's fork #10 + Integrate Paul's fork diff --git a/TODO.rst b/TODO.rst deleted file mode 100644 index 4da9fa4..0000000 --- a/TODO.rst +++ /dev/null @@ -1,16 +0,0 @@ -+ Identify what needs to be done - + check __init__.py - + check tests.py - + check serve_tests.py -+ Fix unittests -+ Fix exceptions when websocket server disappears -+ Fix thread exceptions - + Finish creating low-level _SocketIO to eliminate cyclic references - + Move namespace and callback handling to _ListenerThread -= Resolve pull requests -Resolve issues -Integrate Zac's fork #6 -Integrate Sajal's fork #7 -Integrate Francis's fork #10 -Investigate issue #8 -Integrate Paul's fork From 133bd6f3098e1c9067eb124fbde04b184187ad08 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 18 Apr 2013 07:32:46 -0700 Subject: [PATCH 11/19] Added emit with callback in serve_tests --- TODO.goals | 3 +++ serve_tests.py | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/TODO.goals b/TODO.goals index 7ae3dbd..502f59f 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,5 +1,8 @@ = Resolve pull requests = Resolve pull request #6 + Add unit test for handling server callback + + Add emit with callback in serve_tests + Use json with ensure_ascii=False Resolve issues Investigate issue #8 Examine forks diff --git a/serve_tests.py b/serve_tests.py index 24df950..35bf41d 100644 --- a/serve_tests.py +++ b/serve_tests.py @@ -4,10 +4,11 @@ try: from socketio.namespace import BaseNamespace from socketio.server import SocketIOServer except ImportError: + import sys from setuptools.command import easy_install easy_install.main(['-U', 'gevent-socketio']) print('\nPlease run the script again to launch the test server.') - import sys; sys.exit(1) + sys.exit(1) class Namespace(BaseNamespace): @@ -15,8 +16,13 @@ class Namespace(BaseNamespace): def on_aaa(self, *args): self.emit('aaa_response', *args) + def on_bbb(self, *args): + def callback(*args): + self.emit('callback_response', *args) + self.emit('bbb_response', *args, callback=callback) -class Application(object): + +class App(object): def __call__(self, environ, start_response): socketio_manage(environ, { @@ -29,5 +35,8 @@ class Application(object): if __name__ == '__main__': from socketIO_client.tests import PORT print 'Starting server at port %s' % PORT - socketIOServer = SocketIOServer(('0.0.0.0', PORT), Application()) - socketIOServer.serve_forever() + try: + server = SocketIOServer(('0.0.0.0', PORT), App()) + server.serve_forever() + except KeyboardInterrupt: + pass From f725889de70358c226d579dd671d253466b373a0 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 18 Apr 2013 08:15:47 -0700 Subject: [PATCH 12/19] Assumed that websockets use unicode and not ascii --- TODO.goals | 2 +- socketIO_client/__init__.py | 26 ++++++++++++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/TODO.goals b/TODO.goals index 502f59f..9730d82 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,8 +1,8 @@ = Resolve pull requests = Resolve pull request #6 Add unit test for handling server callback + + Use json with ensure_ascii=False + Add emit with callback in serve_tests - Use json with ensure_ascii=False Resolve issues Investigate issue #8 Examine forks diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index b3268ca..36124fe 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -1,5 +1,5 @@ import socket -from anyjson import dumps, loads +from json import dumps, loads from threading import Thread, Event from time import sleep from urllib import urlopen @@ -46,6 +46,13 @@ class BaseNamespace(object): # pragma: no cover class SocketIO(object): def __init__(self, host, port, secure=False, proxies=None): + """ + Create a socket.io client that connects to a socket.io server + at the specified host and port. Set secure=True to use HTTPS/WSS. + + SocketIO('localhost', 8000, secure=True, + proxies={'https': 'https://proxy.example.com:8080'}) + """ self._socketIO = _SocketIO(host, port, secure, proxies) self._channelByPath = {} self.define(BaseNamespace) # Define default namespace @@ -67,15 +74,15 @@ class SocketIO(object): self.disconnect() def __del__(self): - self.disconnect(force=True) + self.disconnect(closeSocket=False) @property def connected(self): return self._socketIO.connected - def disconnect(self, channelPath='', force=False): + def disconnect(self, channelPath='', closeSocket=True): if self.connected: - self._socketIO.disconnect(channelPath, force) + self._socketIO.disconnect(channelPath, closeSocket) if channelPath: del self._channelByPath[channelPath] else: @@ -255,15 +262,14 @@ class _SocketIO(object): self.callbackByMessageID = {} def __del__(self): - self.disconnect(force=True) + self.disconnect(closeSocket=False) - def disconnect(self, channelPath='', force=False): - 'Set force=True to skip closing the websocket' + def disconnect(self, channelPath='', closeSocket=True): if not self.connected: return if channelPath: self.send_packet(0, channelPath) - elif not force: + elif closeSocket: self.connection.close() def connect(self, channelPath): @@ -282,7 +288,7 @@ class _SocketIO(object): data = messageData else: code = 4 - data = dumps(messageData) + data = dumps(messageData, ensure_ascii=False) self.send_packet(code, channelPath, data, messageCallback) def emit(self, eventName, *eventArguments, **eventKeywords): @@ -292,7 +298,7 @@ class _SocketIO(object): else: messageCallback = None channelPath = eventKeywords.get('channelPath', '') - data = dumps(dict(name=eventName, args=eventArguments)) + data = dumps(dict(name=eventName, args=eventArguments), ensure_ascii=False) self.send_packet(5, channelPath, data, messageCallback) def set_messageCallback(self, callback): From 75d1fa2adc3950c149896e7fbca71e44407efcc9 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 18 Apr 2013 08:58:52 -0700 Subject: [PATCH 13/19] Merged Channel functionality into BaseNamespace --- CHANGES.rst | 2 + socketIO_client/__init__.py | 141 +++++++++++++++++------------------- socketIO_client/tests.py | 24 +++--- 3 files changed, 79 insertions(+), 88 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1e5d0f7..6ff0115 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,7 @@ 0.4 --- +- Merged Channel functionality into BaseNamespace thanks to Alexandre Bourget +- Added support for unicode thanks to Zac Lee 0.3 --- diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 36124fe..6102be9 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -12,8 +12,10 @@ PROTOCOL = 1 # socket.io protocol version class BaseNamespace(object): # pragma: no cover 'Define socket.io behavior' - def __init__(self, _socketIO): + def __init__(self, _socketIO, namespacePath): self._socketIO = _socketIO + self._namespacePath = namespacePath + self._callbackByEvent = {} def on_connect(self): pass @@ -27,7 +29,7 @@ class BaseNamespace(object): # pragma: no cover def on_message(self, messageData): print '[Message] %s' % messageData - def on_(self, eventName, *eventArguments): + def on_default(self, eventName, *eventArguments): print '[Event] %s%s' % (eventName, eventArguments) def on_open(self, *args): @@ -42,6 +44,29 @@ class BaseNamespace(object): # pragma: no cover def on_reconnect(self, *args): print '[Reconnect]', args + def message(self, messageData, messageCallback=None): + self._socketIO.message( + messageData, messageCallback, namespacePath=self._namespacePath) + + def emit(self, eventName, *eventArguments): + self._socketIO.emit( + eventName, *eventArguments, namespacePath=self._namespacePath) + + def on(self, eventName, eventCallback): + self._callbackByEvent[eventName] = eventCallback + + def _get_eventCallback(self, eventName): + # Check callbacks defined by on() + try: + return self._callbackByEvent[eventName] + except KeyError: + pass + + # Check callbacks defined explicitly or use on_default() + def callback(*eventArguments): + return self.on_default(eventName, *eventArguments) + return getattr(self, 'on_' + eventName.replace(' ', '_'), callback) + class SocketIO(object): @@ -54,7 +79,7 @@ class SocketIO(object): proxies={'https': 'https://proxy.example.com:8080'}) """ self._socketIO = _SocketIO(host, port, secure, proxies) - self._channelByPath = {} + self._namespaceByPath = {} self.define(BaseNamespace) # Define default namespace self._rhythmicThread = _RhythmicThread( @@ -64,7 +89,7 @@ class SocketIO(object): self._listenerThread = _ListenerThread( self._socketIO, - self._channelByPath) + self._namespaceByPath) self._listenerThread.start() def __enter__(self): @@ -80,32 +105,29 @@ class SocketIO(object): def connected(self): return self._socketIO.connected - def disconnect(self, channelPath='', closeSocket=True): + def disconnect(self, namespacePath='', closeSocket=True): if self.connected: - self._socketIO.disconnect(channelPath, closeSocket) - if channelPath: - del self._channelByPath[channelPath] + self._socketIO.disconnect(namespacePath, closeSocket) + if namespacePath: + del self._namespaceByPath[namespacePath] else: self._rhythmicThread.cancel() self._listenerThread.cancel() - def define(self, Namespace, channelPath=''): - self._socketIO.connect(channelPath) - channel = Channel(self._socketIO, Namespace, channelPath) - self._channelByPath[channelPath] = channel - return channel + def define(self, Namespace, namespacePath=''): + self._socketIO.connect(namespacePath) + namespace = Namespace(self._socketIO, namespacePath) + self._namespaceByPath[namespacePath] = namespace + return namespace - def get_channel(self, channelPath=''): - return self._channelByPath[channelPath] + def get_namespace(self, namespacePath=''): + return self._namespaceByPath[namespacePath] - def get_namespace(self, channelPath=''): - return self.get_channel(channelPath).get_namespace() + def on(self, eventName, eventCallback, namespacePath=''): + return self.get_namespace(namespacePath).on(eventName, eventCallback) - def on(self, eventName, eventCallback, channelPath=''): - return self.get_channel(channelPath).on(eventName, eventCallback) - - def message(self, messageData, messageCallback=None, channelPath=''): - self._socketIO.message(messageData, messageCallback, channelPath) + def message(self, messageData, messageCallback=None, namespacePath=''): + self._socketIO.message(messageData, messageCallback, namespacePath) def emit(self, eventName, *eventArguments, **eventKeywords): self._socketIO.emit(eventName, *eventArguments, **eventKeywords) @@ -150,10 +172,10 @@ class _ListenerThread(Thread): daemon = True - def __init__(self, _socketIO, _channelByPath): + def __init__(self, _socketIO, _namespaceByPath): super(_ListenerThread, self).__init__() self._socketIO = _socketIO - self._channelByPath = _channelByPath + self._namespaceByPath = _namespaceByPath self.done = Event() self.waiting = Event() @@ -168,7 +190,7 @@ class _ListenerThread(Thread): def run(self): while not self.done.is_set(): try: - code, packetID, channelPath, data = self._socketIO.recv_packet() + code, packetID, namespacePath, data = self._socketIO.recv_packet() except SocketIOConnectionError, error: print error return @@ -176,9 +198,9 @@ class _ListenerThread(Thread): print error continue try: - channel = self._channelByPath[channelPath] + namespace = self._namespaceByPath[namespacePath] except KeyError: - print 'Received unexpected channelPath (%s)' % channelPath + print 'Received unexpected namespacePath (%s)' % namespacePath continue try: delegate = { @@ -194,7 +216,7 @@ class _ListenerThread(Thread): except KeyError: print 'Received unexpected code (%s)' % code continue - delegate(packetID, channel._get_eventCallback, data) + delegate(packetID, namespace._get_eventCallback, data) def on_disconnect(self, packetID, get_eventCallback, data): get_eventCallback('disconnect')() @@ -264,16 +286,16 @@ class _SocketIO(object): def __del__(self): self.disconnect(closeSocket=False) - def disconnect(self, channelPath='', closeSocket=True): + def disconnect(self, namespacePath='', closeSocket=True): if not self.connected: return - if channelPath: - self.send_packet(0, channelPath) + if namespacePath: + self.send_packet(0, namespacePath) elif closeSocket: self.connection.close() - def connect(self, channelPath): - self.send_packet(1, channelPath) + def connect(self, namespacePath): + self.send_packet(1, namespacePath) def send_heartbeat(self): try: @@ -282,14 +304,14 @@ class _SocketIO(object): print 'Could not send heartbeat' pass - def message(self, messageData, messageCallback, channelPath): + def message(self, messageData, messageCallback, namespacePath): if isinstance(messageData, basestring): code = 3 data = messageData else: code = 4 data = dumps(messageData, ensure_ascii=False) - self.send_packet(code, channelPath, data, messageCallback) + self.send_packet(code, namespacePath, data, messageCallback) def emit(self, eventName, *eventArguments, **eventKeywords): if eventArguments and callable(eventArguments[-1]): @@ -297,9 +319,9 @@ class _SocketIO(object): eventArguments = eventArguments[:-1] else: messageCallback = None - channelPath = eventKeywords.get('channelPath', '') + namespacePath = eventKeywords.get('namespacePath', '') data = dumps(dict(name=eventName, args=eventArguments), ensure_ascii=False) - self.send_packet(5, channelPath, data, messageCallback) + self.send_packet(5, namespacePath, data, messageCallback) def set_messageCallback(self, callback): 'Set callback that will be called after receiving an acknowledgment' @@ -333,18 +355,18 @@ class _SocketIO(object): except AttributeError: raise SocketIOPacketError('Received invalid packet (%s)' % packet) packetCount = len(packetParts) - code, packetID, channelPath, data = None, None, None, None + code, packetID, namespacePath, data = None, None, None, None if 4 == packetCount: - code, packetID, channelPath, data = packetParts + code, packetID, namespacePath, data = packetParts elif 3 == packetCount: - code, packetID, channelPath = packetParts + code, packetID, namespacePath = packetParts elif 1 == packetCount: code = packetParts[0] - return code, packetID, channelPath, data + return code, packetID, namespacePath, data - def send_packet(self, code, channelPath='', data='', messageCallback=None): + def send_packet(self, code, namespacePath='', data='', messageCallback=None): callbackNumber = self.set_messageCallback(messageCallback) if messageCallback else '' - packetParts = [str(code), callbackNumber, channelPath, data] + packetParts = [str(code), callbackNumber, namespacePath, data] try: self.connection.send(':'.join(packetParts)) except socket.error: @@ -355,39 +377,6 @@ class _SocketIO(object): return self.connection.connected -class Channel(object): - - def __init__(self, _socketIO, Namespace, channelPath): - self._socketIO = _socketIO - self._namespace = Namespace(_socketIO) - self._channelPath = channelPath - self._callbackByEvent = {} - - def on(self, eventName, eventCallback): - self._callbackByEvent[eventName] = eventCallback - - def message(self, messageData, messageCallback=None): - self._socketIO.message(messageData, messageCallback, channelPath=self._channelPath) - - def emit(self, eventName, *eventArguments): - self._socketIO.emit(eventName, *eventArguments, channelPath=self._channelPath) - - def get_namespace(self): - return self._namespace - - def _get_eventCallback(self, eventName): - # Check callbacks defined by on() - try: - return self._callbackByEvent[eventName] - except KeyError: - pass - # Check callbacks defined explicitly or use on_() - eventNamespace = self.get_namespace() - callbackName = 'on_' + eventName.replace(' ', '_') - defaultCallback = lambda *eventArguments: eventNamespace.on_(eventName, *eventArguments) - return getattr(eventNamespace, callbackName, defaultCallback) - - class SocketIOError(Exception): pass diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index cbeaca9..cc9d3e7 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -46,20 +46,20 @@ class TestSocketIO(TestCase): sleep(0.1) self.assertEqual(ON_RESPONSE_CALLED, True) - def test_channels(self): - mainSocket = self.socketIO.define(Namespace) - chatSocket = self.socketIO.define(Namespace, '/chat') - newsSocket = self.socketIO.define(Namespace, '/news') - self.assertNotEqual(mainSocket.get_namespace().payload, PAYLOAD) - self.assertNotEqual(chatSocket.get_namespace().payload, PAYLOAD) - self.assertNotEqual(newsSocket.get_namespace().payload, PAYLOAD) - newsSocket.emit('aaa', PAYLOAD) + def test_namespaces(self): + mainNamespace = self.socketIO.define(Namespace) + chatNamespace = self.socketIO.define(Namespace, '/chat') + newsNamespace = self.socketIO.define(Namespace, '/news') + self.assertNotEqual(mainNamespace.payload, PAYLOAD) + self.assertNotEqual(chatNamespace.payload, PAYLOAD) + self.assertNotEqual(newsNamespace.payload, PAYLOAD) + newsNamespace.emit('aaa', PAYLOAD) sleep(0.1) - self.assertEqual(newsSocket.get_namespace().payload, PAYLOAD) + self.assertEqual(newsNamespace.payload, PAYLOAD) - def test_channels_with_callback(self): - mainSocket = self.socketIO.get_channel() - mainSocket.message(PAYLOAD, on_response) + def test_namespaces_with_callback(self): + mainNamespace = self.socketIO.get_namespace() + mainNamespace.message(PAYLOAD, on_response) sleep(0.1) self.assertEqual(ON_RESPONSE_CALLED, True) From 25922c5b03e8483c954b928e925edb2759fbd152 Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 18 Apr 2013 09:12:20 -0700 Subject: [PATCH 14/19] Updated README --- README.rst | 22 +++++++++++++--------- socketIO_client/__init__.py | 2 +- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 93571c7..02080ca 100644 --- a/README.rst +++ b/README.rst @@ -4,9 +4,9 @@ Here is a socket.io_ client library for Python. You can use it to write test co Thanks to rod_ for the `StackOverflow question and answer`__ on which this code is based. -Thanks to liris_ for websocket-client_ and to guille_ for the `socket.io specification`_. +Thanks to `Hiroki Ohtani`_ for websocket-client_, `Guillermo Rauch`_ for the `socket.io specification`_ and `Alexandre Bourget`_ for gevent-socketio_. -Thanks to `Paul Kienzle`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_ for submitting code to expand support of the socket.io protocol. +Thanks to `Paul Kienzle`_, `Zac Lee`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_, `Lucas Klein`_ for submitting code to expand support of the socket.io protocol. Installation @@ -68,7 +68,7 @@ Define events in a namespace. :: class Namespace(BaseNamespace): def on_ddd(self, *args): - self.socketIO.emit('eee', {'fff': 'ggg'}) + self.emit('eee', {'fff': 'ggg'}) socketIO = SocketIO('localhost', 8000) socketIO.define(Namespace) @@ -117,10 +117,10 @@ Define different namespaces on a single socket. :: socketIO = SocketIO('localhost', 8000) socketIO.define(MainNamespace) - chatSocket = socketIO.define(ChatNamespace, '/chat') - chatSocket.emit('bbb') - newsSocket = socketIO.define(NewsNamespace, '/news') - newsSocket.emit('ccc') + chatNamespace = socketIO.define(ChatNamespace, '/chat') + chatNamespace.emit('bbb') + newsNamespace = socketIO.define(NewsNamespace, '/news') + newsNamespace.emit('ccc') socketIO.wait() # Loop until CTRL-C Open secure websockets (HTTPS / WSS) behind a proxy. :: @@ -139,10 +139,14 @@ This software is available under the MIT License. .. _rod: http://stackoverflow.com/users/370115/rod .. _StackOverflowQA: http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client __ StackOverflowQA_ -.. _liris: https://github.com/liris .. _websocket-client: https://github.com/liris/websocket-client -.. _guille: https://github.com/guille .. _socket.io specification: https://github.com/LearnBoost/socket.io-spec +.. _gevent-socketio: https://github.com/abourget/gevent-socketio +.. _Hiroki Ohtani: https://github.com/liris +.. _Guillermo Rauch: https://github.com/guille +.. _Alexandre Bourget: https://github.com/abourget .. _Paul Kienzle: https://github.com/pkienzle .. _Josh VanderLinden: https://github.com/codekoala .. _Ian Fitzpatrick: https://github.com/GraphEffect +.. _Zac Lee: https://github.com/zratic +.. _Lucas Klein: https://github.com/lukashed diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 6102be9..faa0a1f 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -73,7 +73,7 @@ class SocketIO(object): def __init__(self, host, port, secure=False, proxies=None): """ Create a socket.io client that connects to a socket.io server - at the specified host and port. Set secure=True to use HTTPS/WSS. + at the specified host and port. Set secure=True to use HTTPS / WSS. SocketIO('localhost', 8000, secure=True, proxies={'https': 'https://proxy.example.com:8080'}) From fc2ddfe46b286cb93aaf1cfccd1d4bf507b75ccf Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 18 Apr 2013 09:25:37 -0700 Subject: [PATCH 15/19] Updated README --- README.rst | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index 02080ca..8f7bc34 100644 --- a/README.rst +++ b/README.rst @@ -2,12 +2,6 @@ socketIO-client =============== Here is a socket.io_ client library for Python. You can use it to write test code for your socket.io_ server. -Thanks to rod_ for the `StackOverflow question and answer`__ on which this code is based. - -Thanks to `Hiroki Ohtani`_ for websocket-client_, `Guillermo Rauch`_ for the `socket.io specification`_ and `Alexandre Bourget`_ for gevent-socketio_. - -Thanks to `Paul Kienzle`_, `Zac Lee`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_, `Lucas Klein`_ for submitting code to expand support of the socket.io protocol. - Installation ------------ @@ -135,18 +129,31 @@ License This software is available under the MIT License. +Credits +------- +- `Guillermo Rauch`_ wrote the `socket.io specification`_. +- `Hiroki Ohtani`_ wrote websocket-client_. +- rod_ wrote a `prototype for a python client to a socket.io server`_ on StackOverflow. +- `Alexandre Bourget`_ wrote gevent-socketio_, which is a socket.io server written in python. +- `Paul Kienzle`_, `Zac Lee`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_, `Lucas Klein`_ submitted code to expand support of the socket.io protocol. + + .. _socket.io: http://socket.io -.. _rod: http://stackoverflow.com/users/370115/rod -.. _StackOverflowQA: http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client -__ StackOverflowQA_ -.. _websocket-client: https://github.com/liris/websocket-client -.. _socket.io specification: https://github.com/LearnBoost/socket.io-spec -.. _gevent-socketio: https://github.com/abourget/gevent-socketio -.. _Hiroki Ohtani: https://github.com/liris + .. _Guillermo Rauch: https://github.com/guille +.. _socket.io specification: https://github.com/LearnBoost/socket.io-spec + +.. _Hiroki Ohtani: https://github.com/liris +.. _websocket-client: https://github.com/liris/websocket-client + +.. _rod: http://stackoverflow.com/users/370115/rod +.. _prototype for a python client to a socket.io server: http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client + .. _Alexandre Bourget: https://github.com/abourget +.. _gevent-socketio: https://github.com/abourget/gevent-socketio + .. _Paul Kienzle: https://github.com/pkienzle +.. _Zac Lee: https://github.com/zratic .. _Josh VanderLinden: https://github.com/codekoala .. _Ian Fitzpatrick: https://github.com/GraphEffect -.. _Zac Lee: https://github.com/zratic .. _Lucas Klein: https://github.com/lukashed From 61918597b70edc0c7925d55dc66f6bdab4e9768a Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Thu, 18 Apr 2013 09:26:23 -0700 Subject: [PATCH 16/19] Updated README --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 8f7bc34..4f3e123 100644 --- a/README.rst +++ b/README.rst @@ -133,8 +133,8 @@ Credits ------- - `Guillermo Rauch`_ wrote the `socket.io specification`_. - `Hiroki Ohtani`_ wrote websocket-client_. -- rod_ wrote a `prototype for a python client to a socket.io server`_ on StackOverflow. -- `Alexandre Bourget`_ wrote gevent-socketio_, which is a socket.io server written in python. +- rod_ wrote a `prototype for a Python client to a socket.io server`_ on StackOverflow. +- `Alexandre Bourget`_ wrote gevent-socketio_, which is a socket.io server written in Python. - `Paul Kienzle`_, `Zac Lee`_, `Josh VanderLinden`_, `Ian Fitzpatrick`_, `Lucas Klein`_ submitted code to expand support of the socket.io protocol. @@ -147,7 +147,7 @@ Credits .. _websocket-client: https://github.com/liris/websocket-client .. _rod: http://stackoverflow.com/users/370115/rod -.. _prototype for a python client to a socket.io server: http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client +.. _prototype for a Python client to a socket.io server: http://stackoverflow.com/questions/6692908/formatting-messages-to-send-to-socket-io-node-js-server-from-python-client .. _Alexandre Bourget: https://github.com/abourget .. _gevent-socketio: https://github.com/abourget/gevent-socketio From 18d2a1ea8c8940910b859de867112b387a49e03d Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Wed, 24 Apr 2013 09:58:24 -0700 Subject: [PATCH 17/19] Added support for acks thanks to zratic --- socketIO_client/__init__.py | 178 ++++++++++++++++++++---------------- socketIO_client/tests.py | 69 +++++++------- 2 files changed, 138 insertions(+), 109 deletions(-) diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index faa0a1f..18cb1fe 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -12,9 +12,9 @@ PROTOCOL = 1 # socket.io protocol version class BaseNamespace(object): # pragma: no cover 'Define socket.io behavior' - def __init__(self, _socketIO, namespacePath): + def __init__(self, _socketIO, path): self._socketIO = _socketIO - self._namespacePath = namespacePath + self._path = path self._callbackByEvent = {} def on_connect(self): @@ -26,11 +26,16 @@ class BaseNamespace(object): # pragma: no cover def on_error(self, reason, advice): print '[Error] %s' % advice - def on_message(self, messageData): - print '[Message] %s' % messageData + def on_message(self, data): + print '[Message] %s' % data - def on_default(self, eventName, *eventArguments): - print '[Event] %s%s' % (eventName, eventArguments) + def on_default(self, event, *args): + callback, args = find_callback(args) + arguments = [str(_) for _ in args] + if callback: + arguments.append('callback(*args)') + callback() + print '[Event] %s(%s)' % (event, ', '.join(arguments)) def on_open(self, *args): print '[Open]', args @@ -44,28 +49,25 @@ class BaseNamespace(object): # pragma: no cover def on_reconnect(self, *args): print '[Reconnect]', args - def message(self, messageData, messageCallback=None): - self._socketIO.message( - messageData, messageCallback, namespacePath=self._namespacePath) + def message(self, data, callback=None): + self._socketIO.message(data, callback, path=self._path) - def emit(self, eventName, *eventArguments): - self._socketIO.emit( - eventName, *eventArguments, namespacePath=self._namespacePath) + def emit(self, event, *args, **kw): + kw['path'] = self._path + self._socketIO.emit(event, *args, **kw) - def on(self, eventName, eventCallback): - self._callbackByEvent[eventName] = eventCallback + def on(self, event, callback): + self._callbackByEvent[event] = callback - def _get_eventCallback(self, eventName): + def _get_eventCallback(self, event): # Check callbacks defined by on() try: - return self._callbackByEvent[eventName] + return self._callbackByEvent[event] except KeyError: pass - # Check callbacks defined explicitly or use on_default() - def callback(*eventArguments): - return self.on_default(eventName, *eventArguments) - return getattr(self, 'on_' + eventName.replace(' ', '_'), callback) + callback = lambda *args: self.on_default(event, *args) + return getattr(self, 'on_' + event.replace(' ', '_'), callback) class SocketIO(object): @@ -99,38 +101,38 @@ class SocketIO(object): self.disconnect() def __del__(self): - self.disconnect(closeSocket=False) + self.disconnect(close=False) @property def connected(self): return self._socketIO.connected - def disconnect(self, namespacePath='', closeSocket=True): + def disconnect(self, path='', close=True): if self.connected: - self._socketIO.disconnect(namespacePath, closeSocket) - if namespacePath: - del self._namespaceByPath[namespacePath] + self._socketIO.disconnect(path, close) + if path: + del self._namespaceByPath[path] else: self._rhythmicThread.cancel() self._listenerThread.cancel() - def define(self, Namespace, namespacePath=''): - self._socketIO.connect(namespacePath) - namespace = Namespace(self._socketIO, namespacePath) - self._namespaceByPath[namespacePath] = namespace + def define(self, Namespace, path=''): + self._socketIO.connect(path) + namespace = Namespace(self._socketIO, path) + self._namespaceByPath[path] = namespace return namespace - def get_namespace(self, namespacePath=''): - return self._namespaceByPath[namespacePath] + def get_namespace(self, path=''): + return self._namespaceByPath[path] - def on(self, eventName, eventCallback, namespacePath=''): - return self.get_namespace(namespacePath).on(eventName, eventCallback) + def on(self, event, callback, path=''): + return self.get_namespace(path).on(event, callback) - def message(self, messageData, messageCallback=None, namespacePath=''): - self._socketIO.message(messageData, messageCallback, namespacePath) + def message(self, data, callback=None, path=''): + self._socketIO.message(data, callback, path) - def emit(self, eventName, *eventArguments, **eventKeywords): - self._socketIO.emit(eventName, *eventArguments, **eventKeywords) + def emit(self, event, *args, **kw): + self._socketIO.emit(event, *args, **kw) def wait(self, seconds=None, forCallbacks=False): if forCallbacks: @@ -187,10 +189,13 @@ class _ListenerThread(Thread): # Block callingThread until listenerThread terminates self.join(seconds) + def get_ackCallback(self, packetID): + return lambda *args: self._socketIO.ack(packetID, *args) + def run(self): while not self.done.is_set(): try: - code, packetID, namespacePath, data = self._socketIO.recv_packet() + code, packetID, path, data = self._socketIO.recv_packet() except SocketIOConnectionError, error: print error return @@ -198,9 +203,9 @@ class _ListenerThread(Thread): print error continue try: - namespace = self._namespaceByPath[namespacePath] + namespace = self._namespaceByPath[path] except KeyError: - print 'Received unexpected namespacePath (%s)' % namespacePath + print 'Received unexpected path (%s)' % path continue try: delegate = { @@ -228,25 +233,33 @@ class _ListenerThread(Thread): pass def on_message(self, packetID, get_eventCallback, data): - get_eventCallback('message')(data) + args = [data] + if packetID: + args.append(self.get_ackCallback(packetID)) + get_eventCallback('message')(args) def on_json(self, packetID, get_eventCallback, data): - get_eventCallback('message')(loads(data)) + args = [loads(data)] + if packetID: + args.append(self.get_ackCallback(packetID)) + get_eventCallback('message')(args) def on_event(self, packetID, get_eventCallback, data): valueByName = loads(data) - eventName = valueByName['name'] - eventArguments = valueByName.get('args', []) - get_eventCallback(eventName)(*eventArguments) + event = valueByName['name'] + args = valueByName.get('args', []) + if packetID: + args.append(self.get_ackCallback(packetID)) + get_eventCallback(event)(*args) def on_acknowledgment(self, packetID, get_eventCallback, data): dataParts = data.split('+', 1) messageID = int(dataParts[0]) - arguments = loads(dataParts[1]) or [] - messageCallback = self._socketIO.get_messageCallback(messageID) - if not messageCallback: + args = loads(dataParts[1]) or [] + callback = self._socketIO.get_messageCallback(messageID) + if not callback: return - messageCallback(*arguments) + callback(*args) if self.waiting.is_set() and not self._socketIO.has_messageCallback: self.cancel() @@ -284,18 +297,18 @@ class _SocketIO(object): self.callbackByMessageID = {} def __del__(self): - self.disconnect(closeSocket=False) + self.disconnect(close=False) - def disconnect(self, namespacePath='', closeSocket=True): + def disconnect(self, path='', close=True): if not self.connected: return - if namespacePath: - self.send_packet(0, namespacePath) - elif closeSocket: + if path: + self.send_packet(0, path) + elif close: self.connection.close() - def connect(self, namespacePath): - self.send_packet(1, namespacePath) + def connect(self, path): + self.send_packet(1, path) def send_heartbeat(self): try: @@ -304,24 +317,25 @@ class _SocketIO(object): print 'Could not send heartbeat' pass - def message(self, messageData, messageCallback, namespacePath): - if isinstance(messageData, basestring): + def message(self, data, callback, path): + if isinstance(data, basestring): code = 3 - data = messageData + packetData = data else: code = 4 - data = dumps(messageData, ensure_ascii=False) - self.send_packet(code, namespacePath, data, messageCallback) + packetData = dumps(data, ensure_ascii=False) + self.send_packet(code, path, packetData, callback) - def emit(self, eventName, *eventArguments, **eventKeywords): - if eventArguments and callable(eventArguments[-1]): - messageCallback = eventArguments[-1] - eventArguments = eventArguments[:-1] - else: - messageCallback = None - namespacePath = eventKeywords.get('namespacePath', '') - data = dumps(dict(name=eventName, args=eventArguments), ensure_ascii=False) - self.send_packet(5, namespacePath, data, messageCallback) + def emit(self, event, *args, **kw): + callback, args = find_callback(args, kw) + packetData = dumps(dict(name=event, args=args), ensure_ascii=False) + path = kw.get('path', '') + self.send_packet(5, path, packetData, callback) + + def ack(self, packetID, *args): + packetID = packetID.rstrip('+') + packetData = '%s+%s' % (packetID, dumps(args, ensure_ascii=False)) if args else packetID + self.send_packet(6, data=packetData) def set_messageCallback(self, callback): 'Set callback that will be called after receiving an acknowledgment' @@ -355,18 +369,18 @@ class _SocketIO(object): except AttributeError: raise SocketIOPacketError('Received invalid packet (%s)' % packet) packetCount = len(packetParts) - code, packetID, namespacePath, data = None, None, None, None + code, packetID, path, data = None, None, None, None if 4 == packetCount: - code, packetID, namespacePath, data = packetParts + code, packetID, path, data = packetParts elif 3 == packetCount: - code, packetID, namespacePath = packetParts + code, packetID, path = packetParts elif 1 == packetCount: code = packetParts[0] - return code, packetID, namespacePath, data + return code, packetID, path, data - def send_packet(self, code, namespacePath='', data='', messageCallback=None): - callbackNumber = self.set_messageCallback(messageCallback) if messageCallback else '' - packetParts = [str(code), callbackNumber, namespacePath, data] + def send_packet(self, code, path='', data='', callback=None): + packetID = self.set_messageCallback(callback) if callback else '' + packetParts = [str(code), packetID, path, data] try: self.connection.send(':'.join(packetParts)) except socket.error: @@ -387,3 +401,13 @@ class SocketIOConnectionError(SocketIOError): class SocketIOPacketError(SocketIOError): pass + + +def find_callback(args, kw=None): + 'Return callback whether passed as a last argument or as a keyword' + if args and callable(args[-1]): + return args[-1], args[:-1] + try: + return kw['callback'], args + except (KeyError, TypeError): + return None, args diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index cc9d3e7..92ffe5f 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -1,9 +1,8 @@ -from socketIO_client import SocketIO, BaseNamespace +from socketIO_client import SocketIO, BaseNamespace, find_callback from time import sleep from unittest import TestCase -ON_RESPONSE_CALLED = False PORT = 8000 PAYLOAD = {'xxx': 'yyy'} @@ -11,13 +10,28 @@ PAYLOAD = {'xxx': 'yyy'} class TestSocketIO(TestCase): def setUp(self): - global ON_RESPONSE_CALLED - ON_RESPONSE_CALLED = False self.socketIO = SocketIO('localhost', PORT) + self.called_on_response = False def tearDown(self): del self.socketIO + def on_response(self, *args): + self.called_on_response = True + callback, args = find_callback(args) + if callback: + callback(*args) + + def test_disconnect(self): + childThreads = [ + self.socketIO._rhythmicThread, + self.socketIO._listenerThread, + ] + self.socketIO.disconnect() + for childThread in childThreads: + self.assertEqual(True, childThread.done.is_set()) + self.assertEqual(False, self.socketIO.connected) + def test_emit(self): self.socketIO.define(Namespace) self.socketIO.emit('aaa') @@ -31,20 +45,26 @@ class TestSocketIO(TestCase): self.assertEqual(self.socketIO.get_namespace().payload, PAYLOAD) def test_emit_with_callback(self): - self.socketIO.emit('aaa', PAYLOAD, on_response) - self.socketIO.wait(forCallbacks=True) - self.assertEqual(ON_RESPONSE_CALLED, True) + self.socketIO.emit('aaa', PAYLOAD, self.on_response) + self.socketIO.wait(seconds=0.1, forCallbacks=True) + self.assertEqual(self.called_on_response, True) - def test_message(self): - self.socketIO.message(PAYLOAD, on_response) - self.socketIO.wait(forCallbacks=True) - self.assertEqual(ON_RESPONSE_CALLED, True) - - def test_events(self): - self.socketIO.on('aaa_response', on_response) + def test_emit_with_event(self): + self.socketIO.on('aaa_response', self.on_response) self.socketIO.emit('aaa', PAYLOAD) sleep(0.1) - self.assertEqual(ON_RESPONSE_CALLED, True) + self.assertEqual(self.called_on_response, True) + + def test_message(self): + self.socketIO.message(PAYLOAD, self.on_response) + self.socketIO.wait(seconds=0.1, forCallbacks=True) + self.assertEqual(self.called_on_response, True) + + def test_ack(self): + self.socketIO.on('bbb_response', self.on_response) + self.socketIO.emit('bbb', PAYLOAD) + sleep(0.1) + self.assertEqual(self.called_on_response, True) def test_namespaces(self): mainNamespace = self.socketIO.define(Namespace) @@ -59,19 +79,9 @@ class TestSocketIO(TestCase): def test_namespaces_with_callback(self): mainNamespace = self.socketIO.get_namespace() - mainNamespace.message(PAYLOAD, on_response) + mainNamespace.message(PAYLOAD, self.on_response) sleep(0.1) - self.assertEqual(ON_RESPONSE_CALLED, True) - - def test_disconnect(self): - childThreads = [ - self.socketIO._rhythmicThread, - self.socketIO._listenerThread, - ] - self.socketIO.disconnect() - for childThread in childThreads: - self.assertEqual(True, childThread.done.is_set()) - self.assertEqual(False, self.socketIO.connected) + self.assertEqual(self.called_on_response, True) class Namespace(BaseNamespace): @@ -81,8 +91,3 @@ class Namespace(BaseNamespace): def on_aaa_response(self, data=''): print '[Event] aaa_response(%s)' % data self.payload = data - - -def on_response(*args): - global ON_RESPONSE_CALLED - ON_RESPONSE_CALLED = True From 77a8e72c1f9a4fd7ea92d8b77f7af3a5b120fcef Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Fri, 26 Apr 2013 09:34:47 -0700 Subject: [PATCH 18/19] Fixes #6 --- README.rst | 64 ++++++------- TODO.goals | 4 +- serve_tests.js | 71 +++++++++++++++ serve_tests.py | 42 --------- socketIO_client/__init__.py | 87 ++++++++++++------ socketIO_client/tests.py | 177 ++++++++++++++++++++++++++---------- 6 files changed, 286 insertions(+), 159 deletions(-) create mode 100644 serve_tests.js delete mode 100644 serve_tests.py diff --git a/README.rst b/README.rst index 4f3e123..3c348b8 100644 --- a/README.rst +++ b/README.rst @@ -16,7 +16,7 @@ Installation source $VIRTUAL_ENV/bin/activate # Install package - easy_install -U socketIO-client + pip install -U socketIO-client Usage @@ -29,31 +29,33 @@ Activate isolated environment. :: Emit. :: from socketIO_client import SocketIO + with SocketIO('localhost', 8000) as socketIO: socketIO.emit('aaa') - socketIO.wait(1) # Wait a second + socketIO.wait(seconds=1) Emit with callback. :: from socketIO_client import SocketIO - def on_response(*args): - print args + def on_bbb_response(*args): + print 'on_bbb_response', args with SocketIO('localhost', 8000) as socketIO: - socketIO.emit('aaa', {'bbb': 'ccc'}, on_response) - socketIO.wait(seconds=1, forCallbacks=True) # Wait for callback + socketIO.emit('bbb', {'xxx': 'yyy'}, on_bbb_response) + socketIO.wait_for_callbacks(seconds=1) Define events. :: from socketIO_client import SocketIO - def on_ddd(*args): - print args + def on_aaa_response(*args): + print 'on_aaa_response', args socketIO = SocketIO('localhost', 8000) - socketIO.on('ddd', on_ddd) - socketIO.wait() # Loop until CTRL-C + socketIO.on('aaa_response', on_aaa_response) + socketIO.emit('aaa') + socketIO.wait(seconds=1) Define events in a namespace. :: @@ -61,12 +63,14 @@ Define events in a namespace. :: class Namespace(BaseNamespace): - def on_ddd(self, *args): - self.emit('eee', {'fff': 'ggg'}) + def on_aaa_response(self, *args): + print 'on_aaa_response', args + self.emit('bbb') socketIO = SocketIO('localhost', 8000) socketIO.define(Namespace) - socketIO.wait() # Loop until CTRL-C + socketIO.emit('aaa') + socketIO.wait(seconds=1) Define standard events. :: @@ -77,51 +81,37 @@ Define standard events. :: def on_connect(self): print '[Connected]' - def on_disconnect(self): - print '[Disconnected]' - - def on_error(self, reason, advice): - print '[Error] %s' % advice - - def on_message(self, messageData): - print '[Message] %s' % messageData - socketIO = SocketIO('localhost', 8000) socketIO.define(Namespace) - socketIO.wait() # Loop until CTRL-C + socketIO.wait(seconds=1) Define different namespaces on a single socket. :: from socketIO_client import SocketIO, BaseNamespace - class MainNamespace(BaseNamespace): - - def on_aaa(self, *args): - print 'aaa', args - class ChatNamespace(BaseNamespace): - def on_bbb(self, *args): - print 'bbb', args + def on_aaa_response(self, *args): + print 'on_aaa_response', args class NewsNamespace(BaseNamespace): - def on_ccc(self, *args): - print 'ccc', args + def on_aaa_response(self, *args): + print 'on_aaa_response', args socketIO = SocketIO('localhost', 8000) - socketIO.define(MainNamespace) chatNamespace = socketIO.define(ChatNamespace, '/chat') - chatNamespace.emit('bbb') newsNamespace = socketIO.define(NewsNamespace, '/news') - newsNamespace.emit('ccc') - socketIO.wait() # Loop until CTRL-C + + chatNamespace.emit('aaa') + newsNamespace.emit('aaa') + socketIO.wait(seconds=1) Open secure websockets (HTTPS / WSS) behind a proxy. :: SocketIO('localhost', 8000, secure=True, - proxies={'http': 'http://proxy.example.com:8080'}) + proxies={'https': 'https://proxy.example.com:8080'}) License diff --git a/TODO.goals b/TODO.goals index 9730d82..d76ca64 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,6 +1,6 @@ = Resolve pull requests - = Resolve pull request #6 - Add unit test for handling server callback + + Resolve pull request #6 + + Add unit test for handling server callback + Use json with ensure_ascii=False + Add emit with callback in serve_tests Resolve issues diff --git a/serve_tests.js b/serve_tests.js new file mode 100644 index 0000000..82c8024 --- /dev/null +++ b/serve_tests.js @@ -0,0 +1,71 @@ +var io = require('socket.io').listen(8000); + +var main = io.of('').on('connection', function(socket) { + socket.on('message', function(data, fn) { + if (fn) { // Client expects a callback + if (data) { + fn(data); + } else { + fn(); + } + } else if (typeof data === 'object') { + socket.json.send(data ? data : 'message_response'); // object or null + } else { + socket.send(data ? data : 'message_response'); // string or '' + } + }); + socket.on('emit', function() { + socket.emit('emit_response'); + }); + socket.on('emit_with_payload', function(payload) { + socket.emit('emit_with_payload_response', payload); + }); + socket.on('emit_with_multiple_payloads', function(payload, payload) { + socket.emit('emit_with_multiple_payloads_response', payload, payload); + }); + socket.on('emit_with_callback', function(fn) { + fn(); + }); + socket.on('emit_with_callback_with_payload', function(fn) { + fn(PAYLOAD); + }); + socket.on('emit_with_callback_with_multiple_payloads', function(fn) { + fn(PAYLOAD, PAYLOAD); + }); + socket.on('emit_with_event', function(payload) { + socket.emit('emit_with_event_response', payload); + }); + socket.on('ack', function(payload) { + socket.emit('ack_response', payload, function(payload) { + socket.emit('ack_callback_response', payload); + }); + }); + socket.on('aaa', function() { + socket.emit('aaa_response', PAYLOAD); + }); + socket.on('bbb', function(payload, fn) { + if (fn) { + fn(payload); + } + }); +}); + +var chat = io.of('/chat').on('connection', function (socket) { + socket.on('emit_with_payload', function(payload) { + socket.emit('emit_with_payload_response', payload); + }); + socket.on('aaa', function() { + socket.emit('aaa_response', 'in chat'); + }); +}); + +var news = io.of('/news').on('connection', function (socket) { + socket.on('emit_with_payload', function(payload) { + socket.emit('emit_with_payload_response', payload); + }); + socket.on('aaa', function() { + socket.emit('aaa_response', 'in news'); + }); +}); + +var PAYLOAD = {'xxx': 'yyy'}; diff --git a/serve_tests.py b/serve_tests.py deleted file mode 100644 index 35bf41d..0000000 --- a/serve_tests.py +++ /dev/null @@ -1,42 +0,0 @@ -'Launch this server in another terminal window before running tests' -try: - from socketio import socketio_manage - from socketio.namespace import BaseNamespace - from socketio.server import SocketIOServer -except ImportError: - import sys - from setuptools.command import easy_install - easy_install.main(['-U', 'gevent-socketio']) - print('\nPlease run the script again to launch the test server.') - sys.exit(1) - - -class Namespace(BaseNamespace): - - def on_aaa(self, *args): - self.emit('aaa_response', *args) - - def on_bbb(self, *args): - def callback(*args): - self.emit('callback_response', *args) - self.emit('bbb_response', *args, callback=callback) - - -class App(object): - - def __call__(self, environ, start_response): - socketio_manage(environ, { - '': Namespace, - '/chat': Namespace, - '/news': Namespace, - }) - - -if __name__ == '__main__': - from socketIO_client.tests import PORT - print 'Starting server at port %s' % PORT - try: - server = SocketIOServer(('0.0.0.0', PORT), App()) - server.serve_forever() - except KeyboardInterrupt: - pass diff --git a/socketIO_client/__init__.py b/socketIO_client/__init__.py index 18cb1fe..5d3499d 100644 --- a/socketIO_client/__init__.py +++ b/socketIO_client/__init__.py @@ -16,25 +16,39 @@ class BaseNamespace(object): # pragma: no cover self._socketIO = _socketIO self._path = path self._callbackByEvent = {} + self.initialize() + + def initialize(self): + 'Initialize custom variables here; you can override this method' + pass def on_connect(self): + 'Called when socket is connecting; you can override this method' pass def on_disconnect(self): + 'Called when socket is disconnecting; you can override this method' pass def on_error(self, reason, advice): + 'Called when server sends an error; you can override this method' print '[Error] %s' % advice def on_message(self, data): + 'Called when server sends a message; you can override this method' print '[Message] %s' % data - def on_default(self, event, *args): + def on_event(self, event, *args): + """ + Called when server emits an event; you can override this method. + Called only if the program cannot find a more specific event handler, + such as one defined by namespace.on('my_event', my_function). + """ callback, args = find_callback(args) - arguments = [str(_) for _ in args] + arguments = [repr(_) for _ in args] if callback: arguments.append('callback(*args)') - callback() + callback(*args) print '[Event] %s(%s)' % (event, ', '.join(arguments)) def on_open(self, *args): @@ -49,7 +63,7 @@ class BaseNamespace(object): # pragma: no cover def on_reconnect(self, *args): print '[Reconnect]', args - def message(self, data, callback=None): + def message(self, data='', callback=None): self._socketIO.message(data, callback, path=self._path) def emit(self, event, *args, **kw): @@ -57,6 +71,7 @@ class BaseNamespace(object): # pragma: no cover self._socketIO.emit(event, *args, **kw) def on(self, event, callback): + 'Define a callback to handle a custom event emitted by the server' self._callbackByEvent[event] = callback def _get_eventCallback(self, event): @@ -65,8 +80,8 @@ class BaseNamespace(object): # pragma: no cover return self._callbackByEvent[event] except KeyError: pass - # Check callbacks defined explicitly or use on_default() - callback = lambda *args: self.on_default(event, *args) + # Check callbacks defined explicitly or use on_event() + callback = lambda *args: self.on_event(event, *args) return getattr(self, 'on_' + event.replace(' ', '_'), callback) @@ -117,7 +132,8 @@ class SocketIO(object): self._listenerThread.cancel() def define(self, Namespace, path=''): - self._socketIO.connect(path) + if path: + self._socketIO.connect(path) namespace = Namespace(self._socketIO, path) self._namespaceByPath[path] = namespace return namespace @@ -128,17 +144,15 @@ class SocketIO(object): def on(self, event, callback, path=''): return self.get_namespace(path).on(event, callback) - def message(self, data, callback=None, path=''): + def message(self, data='', callback=None, path=''): self._socketIO.message(data, callback, path) def emit(self, event, *args, **kw): self._socketIO.emit(event, *args, **kw) - def wait(self, seconds=None, forCallbacks=False): - if forCallbacks: - self._listenerThread.wait_for_callbacks(seconds) - elif seconds: - sleep(seconds) + def wait(self, seconds=None): + if seconds: + self._listenerThread.wait(seconds) else: try: while self.connected: @@ -146,6 +160,9 @@ class SocketIO(object): except KeyboardInterrupt: pass + def wait_for_callbacks(self, seconds=None): + self._listenerThread.wait_for_callbacks(seconds) + class _RhythmicThread(Thread): 'Execute call every few seconds' @@ -179,15 +196,18 @@ class _ListenerThread(Thread): self._socketIO = _socketIO self._namespaceByPath = _namespaceByPath self.done = Event() - self.waiting = Event() + self.ready = Event() + self.ready.set() def cancel(self): self.done.set() + def wait(self, seconds): + self.done.wait(seconds) + def wait_for_callbacks(self, seconds): - self.waiting.set() - # Block callingThread until listenerThread terminates - self.join(seconds) + self.ready.clear() + self.ready.wait(seconds) def get_ackCallback(self, packetID): return lambda *args: self._socketIO.ack(packetID, *args) @@ -215,7 +235,7 @@ class _ListenerThread(Thread): '3': self.on_message, '4': self.on_json, '5': self.on_event, - '6': self.on_acknowledgment, + '6': self.on_ack, '7': self.on_error, }[code] except KeyError: @@ -236,13 +256,13 @@ class _ListenerThread(Thread): args = [data] if packetID: args.append(self.get_ackCallback(packetID)) - get_eventCallback('message')(args) + get_eventCallback('message')(*args) def on_json(self, packetID, get_eventCallback, data): args = [loads(data)] if packetID: args.append(self.get_ackCallback(packetID)) - get_eventCallback('message')(args) + get_eventCallback('message')(*args) def on_event(self, packetID, get_eventCallback, data): valueByName = loads(data) @@ -252,16 +272,16 @@ class _ListenerThread(Thread): args.append(self.get_ackCallback(packetID)) get_eventCallback(event)(*args) - def on_acknowledgment(self, packetID, get_eventCallback, data): + def on_ack(self, packetID, get_eventCallback, data): dataParts = data.split('+', 1) messageID = int(dataParts[0]) - args = loads(dataParts[1]) or [] + args = loads(dataParts[1]) if len(dataParts) > 1 else [] callback = self._socketIO.get_messageCallback(messageID) if not callback: return callback(*args) - if self.waiting.is_set() and not self._socketIO.has_messageCallback: - self.cancel() + if not self._socketIO.has_messageCallback: + self.ready.set() def on_error(self, packetID, get_eventCallback, data): reason, advice = data.split('+', 1) @@ -289,7 +309,7 @@ class _SocketIO(object): # connectionTimeout = int(responseParts[2]) supportedTransports = responseParts[3].split(',') if 'websocket' not in supportedTransports: - raise SocketIOError('Could not parse handshake') # pragma: no cover + raise SocketIOError('Could not parse handshake') socketScheme = 'wss' if secure else 'ws' socketURL = '%s://%s/websocket/%s' % (socketScheme, baseURL, sessionID) self.connection = create_connection(socketURL) @@ -334,7 +354,10 @@ class _SocketIO(object): def ack(self, packetID, *args): packetID = packetID.rstrip('+') - packetData = '%s+%s' % (packetID, dumps(args, ensure_ascii=False)) if args else packetID + packetData = '%s+%s' % ( + packetID, + dumps(args, ensure_ascii=False), + ) if args else packetID self.send_packet(6, data=packetData) def set_messageCallback(self, callback): @@ -359,11 +382,14 @@ class _SocketIO(object): try: packet = self.connection.recv() except WebSocketConnectionClosedException: - raise SocketIOConnectionError('Lost connection (Connection closed)') + text = 'Lost connection (Connection closed)' + raise SocketIOConnectionError(text) except socket.timeout: - raise SocketIOConnectionError('Lost connection (Connection timed out)') + text = 'Lost connection (Connection timed out)' + raise SocketIOConnectionError(text) except socket.error: - raise SocketIOConnectionError('Lost connection') + text = 'Lost connection' + raise SocketIOConnectionError(text) try: packetParts = packet.split(':', 3) except AttributeError: @@ -382,7 +408,8 @@ class _SocketIO(object): packetID = self.set_messageCallback(callback) if callback else '' packetParts = [str(code), packetID, path, data] try: - self.connection.send(':'.join(packetParts)) + packet = ':'.join(packetParts) + self.connection.send(packet) except socket.error: raise SocketIOPacketError('Could not send packet') diff --git a/socketIO_client/tests.py b/socketIO_client/tests.py index 92ffe5f..bdbfbd5 100644 --- a/socketIO_client/tests.py +++ b/socketIO_client/tests.py @@ -1,16 +1,17 @@ from socketIO_client import SocketIO, BaseNamespace, find_callback -from time import sleep from unittest import TestCase +HOST = 'localhost' PORT = 8000 +DATA = 'xxx' PAYLOAD = {'xxx': 'yyy'} class TestSocketIO(TestCase): def setUp(self): - self.socketIO = SocketIO('localhost', PORT) + self.socketIO = SocketIO(HOST, PORT) self.called_on_response = False def tearDown(self): @@ -18,76 +19,156 @@ class TestSocketIO(TestCase): def on_response(self, *args): self.called_on_response = True - callback, args = find_callback(args) - if callback: - callback(*args) + for arg in args: + if isinstance(arg, dict): + self.assertEqual(arg, PAYLOAD) + else: + self.assertEqual(arg, DATA) + + def is_connected(self, socketIO, connected): + childThreads = [ + socketIO._rhythmicThread, + socketIO._listenerThread, + ] + for childThread in childThreads: + self.assertEqual(not connected, childThread.done.is_set()) + self.assertEqual(connected, socketIO.connected) def test_disconnect(self): - childThreads = [ - self.socketIO._rhythmicThread, - self.socketIO._listenerThread, - ] + 'Terminate child threads after disconnect' + self.is_connected(self.socketIO, True) self.socketIO.disconnect() - for childThread in childThreads: - self.assertEqual(True, childThread.done.is_set()) - self.assertEqual(False, self.socketIO.connected) + self.is_connected(self.socketIO, False) + # Use context manager + with SocketIO(HOST, PORT) as self.socketIO: + self.is_connected(self.socketIO, True) + self.is_connected(self.socketIO, False) + + def test_message(self): + 'Message' + self.socketIO.define(Namespace) + self.socketIO.message() + self.socketIO.wait(0.1) + namespace = self.socketIO.get_namespace() + self.assertEqual(namespace.response, 'message_response') + + def test_message_with_data(self): + 'Message with data' + self.socketIO.define(Namespace) + self.socketIO.message(DATA) + self.socketIO.wait(0.1) + namespace = self.socketIO.get_namespace() + self.assertEqual(namespace.response, DATA) + + def test_message_with_payload(self): + 'Message with payload' + self.socketIO.define(Namespace) + self.socketIO.message(PAYLOAD) + self.socketIO.wait(0.1) + namespace = self.socketIO.get_namespace() + self.assertEqual(namespace.response, PAYLOAD) + + def test_message_with_callback(self): + 'Message with callback' + self.socketIO.message(callback=self.on_response) + self.socketIO.wait_for_callbacks(seconds=0.1) + self.assertEqual(self.called_on_response, True) + + def test_message_with_callback_with_data(self): + 'Message with callback with data' + self.socketIO.message(DATA, self.on_response) + self.socketIO.wait_for_callbacks(seconds=0.1) + self.assertEqual(self.called_on_response, True) def test_emit(self): + 'Emit' self.socketIO.define(Namespace) - self.socketIO.emit('aaa') - sleep(0.1) - self.assertEqual(self.socketIO.get_namespace().payload, '') + self.socketIO.emit('emit') + self.socketIO.wait(0.1) + self.assertEqual(self.socketIO.get_namespace().argsByEvent, { + 'emit_response': (), + }) def test_emit_with_payload(self): + 'Emit with payload' self.socketIO.define(Namespace) - self.socketIO.emit('aaa', PAYLOAD) - sleep(0.1) - self.assertEqual(self.socketIO.get_namespace().payload, PAYLOAD) + self.socketIO.emit('emit_with_payload', PAYLOAD) + self.socketIO.wait(0.1) + self.assertEqual(self.socketIO.get_namespace().argsByEvent, { + 'emit_with_payload_response': (PAYLOAD,), + }) + + def test_emit_with_multiple_payloads(self): + 'Emit with multiple payloads' + self.socketIO.define(Namespace) + self.socketIO.emit('emit_with_multiple_payloads', PAYLOAD, PAYLOAD) + self.socketIO.wait(0.1) + self.assertEqual(self.socketIO.get_namespace().argsByEvent, { + 'emit_with_multiple_payloads_response': (PAYLOAD, PAYLOAD), + }) def test_emit_with_callback(self): - self.socketIO.emit('aaa', PAYLOAD, self.on_response) - self.socketIO.wait(seconds=0.1, forCallbacks=True) + 'Emit with callback' + self.socketIO.emit('emit_with_callback', self.on_response) + self.socketIO.wait_for_callbacks(seconds=0.1) + self.assertEqual(self.called_on_response, True) + + def test_emit_with_callback_with_payload(self): + 'Emit with callback with payload' + self.socketIO.emit('emit_with_callback_with_payload', + self.on_response) + self.socketIO.wait_for_callbacks(seconds=0.1) + self.assertEqual(self.called_on_response, True) + + def test_emit_with_callback_with_multiple_payloads(self): + 'Emit with callback with multiple payloads' + self.socketIO.emit('emit_with_callback_with_multiple_payloads', + self.on_response) + self.socketIO.wait_for_callbacks(seconds=0.1) self.assertEqual(self.called_on_response, True) def test_emit_with_event(self): - self.socketIO.on('aaa_response', self.on_response) - self.socketIO.emit('aaa', PAYLOAD) - sleep(0.1) - self.assertEqual(self.called_on_response, True) - - def test_message(self): - self.socketIO.message(PAYLOAD, self.on_response) - self.socketIO.wait(seconds=0.1, forCallbacks=True) + 'Emit to trigger an event' + self.socketIO.on('emit_with_event_response', self.on_response) + self.socketIO.emit('emit_with_event', PAYLOAD) + self.socketIO.wait_for_callbacks(0.1) self.assertEqual(self.called_on_response, True) def test_ack(self): - self.socketIO.on('bbb_response', self.on_response) - self.socketIO.emit('bbb', PAYLOAD) - sleep(0.1) - self.assertEqual(self.called_on_response, True) + 'Trigger server callback' + self.socketIO.define(Namespace) + self.socketIO.emit('ack', PAYLOAD) + self.socketIO.wait(0.1) + self.assertEqual(self.socketIO.get_namespace().argsByEvent, { + 'ack_response': (PAYLOAD,), + 'ack_callback_response': (PAYLOAD,), + }) def test_namespaces(self): + 'Behave differently in different namespaces' mainNamespace = self.socketIO.define(Namespace) chatNamespace = self.socketIO.define(Namespace, '/chat') newsNamespace = self.socketIO.define(Namespace, '/news') - self.assertNotEqual(mainNamespace.payload, PAYLOAD) - self.assertNotEqual(chatNamespace.payload, PAYLOAD) - self.assertNotEqual(newsNamespace.payload, PAYLOAD) - newsNamespace.emit('aaa', PAYLOAD) - sleep(0.1) - self.assertEqual(newsNamespace.payload, PAYLOAD) - - def test_namespaces_with_callback(self): - mainNamespace = self.socketIO.get_namespace() - mainNamespace.message(PAYLOAD, self.on_response) - sleep(0.1) - self.assertEqual(self.called_on_response, True) + newsNamespace.emit('emit_with_payload', PAYLOAD) + self.socketIO.wait(0.1) + self.assertEqual(mainNamespace.argsByEvent, {}) + self.assertEqual(chatNamespace.argsByEvent, {}) + self.assertEqual(newsNamespace.argsByEvent, { + 'emit_with_payload_response': (PAYLOAD,), + }) class Namespace(BaseNamespace): - payload = None + def initialize(self): + self.response = None + self.argsByEvent = {} - def on_aaa_response(self, data=''): - print '[Event] aaa_response(%s)' % data - self.payload = data + def on_message(self, data): + self.response = data + + def on_event(self, event, *args): + callback, args = find_callback(args) + if callback: + callback(*args) + self.argsByEvent[event] = args From a84f2f40bb1cf722f780685ffc9f8028f0dbc08b Mon Sep 17 00:00:00 2001 From: Roy Hyunjin Han Date: Fri, 26 Apr 2013 09:39:42 -0700 Subject: [PATCH 19/19] Updated CHANGES --- CHANGES.rst | 5 +++-- TODO.goals | 5 ----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6ff0115..eeae8ed 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,13 +1,14 @@ 0.4 --- +- Added support for server-side callbacks thanks to Zac Lee +- Added low-level _SocketIO to remove cyclic references - Merged Channel functionality into BaseNamespace thanks to Alexandre Bourget -- Added support for unicode thanks to Zac Lee 0.3 --- - Added support for secure connections - Added socketIO.wait() -- Improved exception handling in heartbeatThread and namespaceThread +- Improved exception handling in _RhythmicThread and _ListenerThread 0.2 --- diff --git a/TODO.goals b/TODO.goals index d76ca64..84b159c 100644 --- a/TODO.goals +++ b/TODO.goals @@ -1,12 +1,7 @@ = Resolve pull requests - + Resolve pull request #6 - + Add unit test for handling server callback - + Use json with ensure_ascii=False - + Add emit with callback in serve_tests Resolve issues Investigate issue #8 Examine forks - Integrate Zac's fork #6 Integrate Sajal's fork #7 Integrate Francis's fork #10 Integrate Paul's fork