Skip to content

Commit 699ee9c

Browse files
Context manager interface for the simple clients
1 parent 55d6310 commit 699ee9c

File tree

9 files changed

+109
-38
lines changed

9 files changed

+109
-38
lines changed

docs/client.rst

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,26 @@ the application.
3535
Creating a Client Instance
3636
~~~~~~~~~~~~~~~~~~~~~~~~~~
3737

38-
To instantiate a Socket.IO client, create an instance of the appropriate client
39-
class::
38+
The easiest way to create a Socket.IO client is to use the context manager
39+
interface::
40+
41+
import socketio
42+
43+
# standard Python
44+
with socketio.SimpleClient() as sio:
45+
# ... connect to a server and use the client
46+
# ... no need to manually disconnect!
47+
48+
# asyncio
49+
async with socketio.AsyncSimpleClient() as sio:
50+
# ... connect to a server and use the client
51+
# ... no need to manually disconnect!
52+
53+
54+
With this usage the context manager will ensure that the client is properly
55+
disconnected before exiting the ``with`` or ``async with`` block.
56+
57+
If preferred, a client can be manually instantiated::
4058

4159
import socketio
4260

examples/simple-client/async/fiddle_client.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33

44

55
async def main():
6-
sio = socketio.AsyncSimpleClient()
7-
await sio.connect('http://localhost:5000', auth={'token': 'my-token'})
8-
print(await sio.receive())
9-
await sio.disconnect()
6+
async with socketio.AsyncSimpleClient() as sio:
7+
await sio.connect('http://localhost:5000', auth={'token': 'my-token'})
8+
print(await sio.receive())
109

1110

1211
if __name__ == '__main__':

examples/simple-client/async/latency_client.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44

55

66
async def main():
7-
sio = socketio.AsyncSimpleClient()
8-
await sio.connect('http://localhost:5000')
9-
10-
try:
7+
async with socketio.AsyncSimpleClient() as sio:
8+
await sio.connect('http://localhost:5000')
119
while True:
1210
start_timer = time.time()
1311
await sio.emit('ping_from_client')
@@ -17,8 +15,6 @@ async def main():
1715
print('latency is {0:.2f} ms'.format(latency * 1000))
1816

1917
await asyncio.sleep(1)
20-
except (KeyboardInterrupt, asyncio.CancelledError):
21-
await sio.disconnect()
2218

2319

2420
if __name__ == '__main__':

examples/simple-client/sync/fiddle_client.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22

33

44
def main():
5-
sio = socketio.SimpleClient()
6-
sio.connect('http://localhost:5000', auth={'token': 'my-token'})
7-
print(sio.receive())
8-
sio.disconnect()
5+
with socketio.SimpleClient() as sio:
6+
sio.connect('http://localhost:5000', auth={'token': 'my-token'})
7+
print(sio.receive())
98

109

1110
if __name__ == '__main__':

examples/simple-client/sync/latency_client.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33

44

55
def main():
6-
sio = socketio.SimpleClient()
7-
sio.connect('http://localhost:5000')
8-
9-
try:
6+
with socketio.SimpleClient() as sio:
7+
sio.connect('http://localhost:5000')
108
while True:
119
start_timer = time.time()
1210
sio.emit('ping_from_client')
@@ -16,8 +14,6 @@ def main():
1614
print('latency is {0:.2f} ms'.format(latency * 1000))
1715

1816
time.sleep(1)
19-
except KeyboardInterrupt:
20-
sio.disconnect()
2117

2218

2319
if __name__ == '__main__':

src/socketio/asyncio_simple_client.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,21 @@ async def connect(self, url, headers={}, auth=None, transports=None,
5959
self.input_event.clear()
6060
self.client = AsyncClient(*self.client_args, **self.client_kwargs)
6161

62-
@self.client.event
62+
@self.client.event(namespace=self.namespace)
6363
def connect(): # pragma: no cover
6464
self.connected = True
6565
self.connected_event.set()
6666

67-
@self.client.event
67+
@self.client.event(namespace=self.namespace)
6868
def disconnect(): # pragma: no cover
6969
self.connected_event.clear()
7070

71-
@self.client.event
71+
@self.client.event(namespace=self.namespace)
7272
def __disconnect_final(): # pragma: no cover
7373
self.connected = False
7474
self.connected_event.set()
7575

76-
@self.client.on('*')
76+
@self.client.on('*', namespace=self.namespace)
7777
def on_event(event, *args): # pragma: no cover
7878
self.input_buffer.append([event, *args])
7979
self.input_event.set()
@@ -172,8 +172,12 @@ async def receive(self, timeout=None):
172172
the server included arguments with the event, they are returned as
173173
additional list elements.
174174
"""
175-
if not self.input_buffer:
176-
await self.connected_event.wait()
175+
while not self.input_buffer:
176+
try:
177+
await asyncio.wait_for(self.connected_event.wait(),
178+
timeout=timeout)
179+
except asyncio.TimeoutError: # pragma: no cover
180+
raise TimeoutError()
177181
if not self.connected:
178182
raise DisconnectedError()
179183
try:
@@ -189,5 +193,13 @@ async def disconnect(self):
189193
190194
Note: this method is a coroutine.
191195
i """
192-
await self.client.disconnect()
193-
self.client = None
196+
if self.connected:
197+
await self.client.disconnect()
198+
self.client = None
199+
self.connected = False
200+
201+
async def __aenter__(self):
202+
return self
203+
204+
async def __aexit__(self, exc_type, exc_val, exc_tb):
205+
await self.disconnect()

src/socketio/simple_client.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,21 @@ def connect(self, url, headers={}, auth=None, transports=None,
5757
self.input_event.clear()
5858
self.client = Client(*self.client_args, **self.client_kwargs)
5959

60-
@self.client.event
60+
@self.client.event(namespace=self.namespace)
6161
def connect(): # pragma: no cover
6262
self.connected = True
6363
self.connected_event.set()
6464

65-
@self.client.event
65+
@self.client.event(namespace=self.namespace)
6666
def disconnect(): # pragma: no cover
6767
self.connected_event.clear()
6868

69-
@self.client.event
69+
@self.client.event(namespace=self.namespace)
7070
def __disconnect_final(): # pragma: no cover
7171
self.connected = False
7272
self.connected_event.set()
7373

74-
@self.client.on('*')
74+
@self.client.on('*', namespace=self.namespace)
7575
def on_event(event, *args): # pragma: no cover
7676
self.input_buffer.append([event, *args])
7777
self.input_event.set()
@@ -162,8 +162,10 @@ def receive(self, timeout=None):
162162
the server included arguments with the event, they are returned as
163163
additional list elements.
164164
"""
165-
if not self.input_buffer:
166-
self.connected_event.wait()
165+
while not self.input_buffer:
166+
if not self.connected_event.wait(
167+
timeout=timeout): # pragma: no cover
168+
raise TimeoutError()
167169
if not self.connected:
168170
raise DisconnectedError()
169171
if not self.input_event.wait(timeout=timeout):
@@ -173,5 +175,13 @@ def receive(self, timeout=None):
173175

174176
def disconnect(self):
175177
"""Disconnect from the server."""
176-
self.client.disconnect()
177-
self.client = None
178+
if self.connected:
179+
self.client.disconnect()
180+
self.client = None
181+
self.connected = False
182+
183+
def __enter__(self):
184+
return self
185+
186+
def __exit__(self, exc_type, exc_val, exc_tb):
187+
self.disconnect()

tests/asyncio/test_asyncio_simple_client.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,28 @@ def test_connect(self):
3434
assert client.namespace == 'n'
3535
assert not client.input_event.is_set()
3636

37+
def test_connect_context_manager(self):
38+
async def _t():
39+
async with AsyncSimpleClient(123, a='b') as client:
40+
with mock.patch('socketio.asyncio_simple_client.AsyncClient') \
41+
as mock_client:
42+
mock_client.return_value.connect = AsyncMock()
43+
44+
await client.connect('url', headers='h', auth='a',
45+
transports='t', namespace='n',
46+
socketio_path='s')
47+
mock_client.assert_called_once_with(123, a='b')
48+
assert client.client == mock_client()
49+
mock_client().connect.mock.assert_called_once_with(
50+
'url', headers='h', auth='a', transports='t',
51+
namespaces=['n'], socketio_path='s')
52+
mock_client().event.call_count == 3
53+
mock_client().on.called_once_with('*')
54+
assert client.namespace == 'n'
55+
assert not client.input_event.is_set()
56+
57+
_run(_t())
58+
3759
def test_connect_twice(self):
3860
client = AsyncSimpleClient(123, a='b')
3961
client.client = mock.MagicMock()
@@ -158,6 +180,8 @@ def test_disconnect(self):
158180
mc = mock.MagicMock()
159181
mc.disconnect = AsyncMock()
160182
client.client = mc
183+
client.connected = True
184+
_run(client.disconnect())
161185
_run(client.disconnect())
162186
mc.disconnect.mock.assert_called_once_with()
163187
assert client.client is None

tests/common/test_simple_client.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,21 @@ def test_connect(self):
2929
assert client.namespace == 'n'
3030
assert not client.input_event.is_set()
3131

32+
def test_connect_context_manager(self):
33+
with SimpleClient(123, a='b') as client:
34+
with mock.patch('socketio.simple_client.Client') as mock_client:
35+
client.connect('url', headers='h', auth='a', transports='t',
36+
namespace='n', socketio_path='s')
37+
mock_client.assert_called_once_with(123, a='b')
38+
assert client.client == mock_client()
39+
mock_client().connect.assert_called_once_with(
40+
'url', headers='h', auth='a', transports='t',
41+
namespaces=['n'], socketio_path='s')
42+
mock_client().event.call_count == 3
43+
mock_client().on.called_once_with('*')
44+
assert client.namespace == 'n'
45+
assert not client.input_event.is_set()
46+
3247
def test_connect_twice(self):
3348
client = SimpleClient(123, a='b')
3449
client.client = mock.MagicMock()
@@ -141,6 +156,8 @@ def test_disconnect(self):
141156
client = SimpleClient()
142157
mc = mock.MagicMock()
143158
client.client = mc
159+
client.connected = True
160+
client.disconnect()
144161
client.disconnect()
145162
mc.disconnect.assert_called_once_with()
146163
assert client.client is None

0 commit comments

Comments
 (0)