Skip to content

Commit 76f02ca

Browse files
committed
Make serve an async context manager.
Fix python-websockets#86.
1 parent 13f2e40 commit 76f02ca

8 files changed

Lines changed: 102 additions & 20 deletions

File tree

docs/changelog.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ Changelog
66

77
*In development*
88

9+
* :func:`~websockets.server.serve` can be used as an asynchronous context
10+
manager on Python ≥ 3.5.
11+
912
* Added support rejecting incoming connections by customizing
1013
:meth:`~websockets.server.WebSocketServerProtocol.get_response_status()`.
1114

docs/deployment.rst

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,12 @@ Graceful shutdown
9191
-----------------
9292

9393
You may want to close connections gracefully when shutting down the server,
94-
perhaps after executing some cleanup logic.
94+
perhaps after executing some cleanup logic. There are two ways to achieve this
95+
with the object returned by :func:`~websockets.server.serve`:
9596

96-
The proper way to do this is to call the ``close()`` method of the object
97-
returned by :func:`~websockets.server.serve`, then wait for ``wait_closed()``
98-
to complete.
97+
- using it as a asynchronous context manager, or
98+
- calling its ``close()`` method, then waiting for its ``wait_closed()``
99+
method to complete.
99100

100101
Tasks that handle connections will be cancelled, in the sense that
101102
:meth:`~websockets.protocol.WebSocketCommonProtocol.recv` raises
@@ -107,6 +108,11 @@ Here's a full example (Unix-only):
107108

108109
.. literalinclude:: ../example/shutdown.py
109110

111+
``async``, ``await``, and asynchronous context managers aren't available in
112+
Python < 3.5. Here's the equivalent for older Python versions:
113+
114+
.. literalinclude:: ../example/oldshutdown.py
115+
110116
It's more difficult to achieve the same effect on Windows. Some third-party
111117
projects try to help with this problem.
112118

example/oldshutdown.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env python
2+
3+
import asyncio
4+
import signal
5+
import websockets
6+
7+
async def echo(websocket, path):
8+
while True:
9+
try:
10+
msg = await websocket.recv()
11+
except websockets.ConnectionClosed:
12+
pass
13+
else:
14+
await websocket.send(msg)
15+
16+
loop = asyncio.get_event_loop()
17+
18+
# Create the server.
19+
start_server = websockets.serve(echo, 'localhost', 8765)
20+
server = loop.run_until_complete(start_server)
21+
22+
# Run the server until SIGTERM.
23+
stop = asyncio.Future()
24+
loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)
25+
loop.run_until_complete(stop)
26+
27+
# Shut down the server.
28+
server.close()
29+
loop.run_until_complete(server.wait_closed())

example/shutdown.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,15 @@ async def echo(websocket, path):
1313
else:
1414
await websocket.send(msg)
1515

16-
loop = asyncio.get_event_loop()
16+
async def echo_server(stop):
17+
async with websockets.serve(echo, 'localhost', 8765):
18+
await stop
1719

18-
# Create the server.
19-
start_server = websockets.serve(echo, 'localhost', 8765)
20-
server = loop.run_until_complete(start_server)
20+
loop = asyncio.get_event_loop()
2121

22-
# Run the server until SIGTERM.
23-
stop_server = asyncio.Future()
24-
loop.add_signal_handler(signal.SIGTERM, stop_server.set_result, None)
25-
loop.run_until_complete(stop_server)
22+
# The stop condition is set when receiving SIGTERM.
23+
stop = asyncio.Future()
24+
loop.add_signal_handler(signal.SIGTERM, stop.set_result, None)
2625

27-
# Shut down the server.
28-
server.close()
29-
loop.run_until_complete(server.wait_closed())
26+
# Run the server until the stop condition is met.
27+
loop.run_until_complete(echo_server(stop))

websockets/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def connect(uri, *,
147147
send and receive messages.
148148
149149
:func:`connect` is a wrapper around the event loop's
150-
:meth:`~asyncio.BaseEventLoop.create_connection` method. Extra keyword
150+
:meth:`~asyncio.BaseEventLoop.create_connection` method. Unknown keyword
151151
arguments are passed to :meth:`~asyncio.BaseEventLoop.create_connection`.
152152
153153
For example, you can set the ``ssl`` keyword argument to a

websockets/py35/client_server.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,27 @@ def setUp(self):
1717
def tearDown(self):
1818
self.loop.close()
1919

20-
def test_basic(self):
20+
def test_client(self):
2121
server = serve(handler, 'localhost', 8642)
2222
self.server = self.loop.run_until_complete(server)
2323

24-
async def basic():
24+
async def run_client():
2525
async with connect('ws://localhost:8642/') as client:
2626
await client.send("Hello!")
2727
reply = await client.recv()
2828
self.assertEqual(reply, "Hello!")
2929

30-
self.loop.run_until_complete(basic())
30+
self.loop.run_until_complete(run_client())
3131

3232
self.server.close()
3333
self.loop.run_until_complete(self.server.wait_closed())
34+
35+
def test_server(self):
36+
async def run_server():
37+
async with serve(handler, 'localhost', 8642):
38+
client = await connect('ws://localhost:8642/')
39+
await client.send("Hello!")
40+
reply = await client.recv()
41+
self.assertEqual(reply, "Hello!")
42+
43+
self.loop.run_until_complete(run_server())

websockets/py35/server.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
class Serve:
2+
"""
3+
This class wraps :func:`~websockets.server.serve` on Python ≥ 3.5.
4+
5+
This allows using it as an asynchronous context manager.
6+
7+
"""
8+
def __init__(self, *args, **kwargs):
9+
self.server = self.__class__.__wrapped__(*args, **kwargs)
10+
11+
async def __aenter__(self):
12+
self.server = await self
13+
return self.server
14+
15+
async def __aexit__(self, exc_type, exc_value, traceback):
16+
self.server.close()
17+
await self.server.wait_closed()
18+
19+
def __await__(self):
20+
return (yield from self.server)
21+
22+
__iter__ = __await__

websockets/server.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ def serve(ws_handler, host=None, port=None, *,
407407
408408
:func:`serve` is a wrapper around the event loop's
409409
:meth:`~asyncio.BaseEventLoop.create_server` method. ``host``, ``port`` as
410-
well as extra keyword arguments are passed to
410+
well as unknown keyword arguments are passed to
411411
:meth:`~asyncio.BaseEventLoop.create_server`.
412412
413413
For example, you can set the ``ssl`` keyword argument to a
@@ -441,6 +441,9 @@ def serve(ws_handler, host=None, port=None, *,
441441
logger.setLevel(logging.ERROR)
442442
logger.addHandler(logging.StreamHandler())
443443
444+
On Python 3.5, :func:`serve` can be used as a asynchronous context
445+
manager. In that case, the server is shut down when exiting the context.
446+
444447
"""
445448
if loop is None:
446449
loop = asyncio.get_event_loop()
@@ -462,3 +465,14 @@ def serve(ws_handler, host=None, port=None, *,
462465
ws_server.wrap(server)
463466

464467
return ws_server
468+
469+
470+
try:
471+
from .py35.server import Serve
472+
except (SyntaxError, ImportError): # pragma: no cover
473+
pass
474+
else:
475+
Serve.__wrapped__ = serve
476+
# Copy over docstring to support building documentation on Python 3.5.
477+
Serve.__doc__ = serve.__doc__
478+
serve = Serve

0 commit comments

Comments
 (0)