Skip to content

Commit 2488b85

Browse files
vitaliemaldurasvetlov
authored andcommitted
Fix #976: Add support for websocket send_json and receive_json (#984)
* Fix #976: Add support for websocket send_json and receive_json * Fix for (#976): use send_str instead of low level send method * Fix for (#976): add test for websocket send_json and receive_json * Fix for (#976): refactor websocket send_json and receive_json * Fix for (#976): refactor tests for websocket send_json and receive_json * Fix for (#976): add documentation for websocket send_json and receive_json * Fix for (#976): serialize receive_str response in receive_json * Fix for (#976): refactor docs for receive_json method
1 parent cf6cb31 commit 2488b85

9 files changed

+223
-20
lines changed

aiohttp/web_ws.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ def send_bytes(self, data):
154154
type(data))
155155
self._writer.send(data, binary=True)
156156

157+
def send_json(self, data, *, dumps=json.dumps):
158+
self.send_str(dumps(data))
159+
157160
@asyncio.coroutine
158161
def write_eof(self):
159162
if self._eof_sent:
@@ -280,12 +283,8 @@ def receive_bytes(self):
280283

281284
@asyncio.coroutine
282285
def receive_json(self, *, loads=json.loads):
283-
msg = yield from self.receive()
284-
if msg.tp != MsgType.text:
285-
raise TypeError(
286-
"Received message {}:{!r} is not str".format(msg.tp, msg.data)
287-
)
288-
return msg.json(loads=loads)
286+
data = yield from self.receive_str()
287+
return loads(data)
289288

290289
def write(self, data):
291290
raise RuntimeError("Cannot call .write() for websocket")

aiohttp/websocket_client.py

+26
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import asyncio
44

55
import sys
6+
import json
67
from enum import IntEnum
78

89
from .websocket import Message
@@ -88,6 +89,9 @@ def send_bytes(self, data):
8889
type(data))
8990
self._writer.send(data, binary=True)
9091

92+
def send_json(self, data, *, dumps=json.dumps):
93+
self.send_str(dumps(data))
94+
9195
@asyncio.coroutine
9296
def close(self, *, code=1000, message=b''):
9397
if not self._closed:
@@ -171,6 +175,28 @@ def receive(self):
171175
finally:
172176
self._waiting = False
173177

178+
@asyncio.coroutine
179+
def receive_str(self):
180+
msg = yield from self.receive()
181+
if msg.tp != MsgType.text:
182+
raise TypeError(
183+
"Received message {}:{!r} is not str".format(msg.tp, msg.data))
184+
return msg.data
185+
186+
@asyncio.coroutine
187+
def receive_bytes(self):
188+
msg = yield from self.receive()
189+
if msg.tp != MsgType.binary:
190+
raise TypeError(
191+
"Received message {}:{!r} is not bytes".format(msg.tp,
192+
msg.data))
193+
return msg.data
194+
195+
@asyncio.coroutine
196+
def receive_json(self, *, loads=json.loads):
197+
data = yield from self.receive_str()
198+
return loads(data)
199+
174200
if PY_35:
175201
@asyncio.coroutine
176202
def __aiter__(self):

docs/client_reference.rst

+50
Original file line numberDiff line numberDiff line change
@@ -1277,6 +1277,22 @@ manually.
12771277
:raise TypeError: if data is not :class:`bytes`,
12781278
:class:`bytearray` or :class:`memoryview`.
12791279

1280+
.. method:: send_json(data, *, dumps=json.loads)
1281+
1282+
Send *data* to peer as JSON string.
1283+
1284+
:param data: data to send.
1285+
1286+
:param callable dumps: any :term:`callable` that accepts an object and
1287+
returns a JSON string
1288+
(:func:`json.dumps` by default).
1289+
1290+
:raise RuntimeError: if connection is not started or closing
1291+
1292+
:raise ValueError: if data is not serializable object
1293+
1294+
:raise TypeError: if value returned by :term:`dumps` is not :class:`str`
1295+
12801296
.. comethod:: close(*, code=1000, message=b'')
12811297

12821298
A :ref:`coroutine<coroutine>` that initiates closing handshake by sending
@@ -1306,6 +1322,40 @@ manually.
13061322
:return: :class:`~aiohttp.websocket.Message`, `tp` is types of
13071323
`~aiohttp.MsgType`
13081324

1325+
.. coroutinemethod:: receive_str()
1326+
1327+
A :ref:`coroutine<coroutine>` that calls :meth:`receive` but
1328+
also asserts the message type is
1329+
:const:`~aiohttp.websocket.MSG_TEXT`.
1330+
1331+
:return str: peer's message content.
1332+
1333+
:raise TypeError: if message is :const:`~aiohttp.websocket.MSG_BINARY`.
1334+
1335+
.. coroutinemethod:: receive_bytes()
1336+
1337+
A :ref:`coroutine<coroutine>` that calls :meth:`receive` but
1338+
also asserts the message type is
1339+
:const:`~aiohttp.websocket.MSG_BINARY`.
1340+
1341+
:return bytes: peer's message content.
1342+
1343+
:raise TypeError: if message is :const:`~aiohttp.websocket.MSG_TEXT`.
1344+
1345+
.. coroutinemethod:: receive_json(*, loads=json.loads)
1346+
1347+
A :ref:`coroutine<coroutine>` that calls :meth:`receive_str` and loads
1348+
the JSON string to a Python dict.
1349+
1350+
:param callable loads: any :term:`callable` that accepts
1351+
:class:`str` and returns :class:`dict`
1352+
with parsed JSON (:func:`json.loads` by
1353+
default).
1354+
1355+
:return dict: loaded JSON content
1356+
1357+
:raise TypeError: if message is :const:`~aiohttp.websocket.MSG_BINARY`.
1358+
:raise ValueError: if message is not valid JSON.
13091359

13101360
Utilities
13111361
---------

docs/web_reference.rst

+18-3
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,22 @@ WebSocketResponse
830830
:raise TypeError: if data is not :class:`bytes`,
831831
:class:`bytearray` or :class:`memoryview`.
832832

833+
.. method:: send_json(data, *, dumps=json.loads)
834+
835+
Send *data* to peer as JSON string.
836+
837+
:param data: data to send.
838+
839+
:param callable dumps: any :term:`callable` that accepts an object and
840+
returns a JSON string
841+
(:func:`json.dumps` by default).
842+
843+
:raise RuntimeError: if connection is not started or closing
844+
845+
:raise ValueError: if data is not serializable object
846+
847+
:raise TypeError: if value returned by :term:`dumps` is not :class:`str`
848+
833849
.. coroutinemethod:: close(*, code=1000, message=b'')
834850

835851
A :ref:`coroutine<coroutine>` that initiates closing
@@ -888,9 +904,8 @@ WebSocketResponse
888904

889905
.. coroutinemethod:: receive_json(*, loads=json.loads)
890906

891-
A :ref:`coroutine<coroutine>` that calls :meth:`receive`, asserts the
892-
message type is :const:`~aiohttp.websocket.MSG_TEXT`, and loads the JSON
893-
string to a Python dict.
907+
A :ref:`coroutine<coroutine>` that calls :meth:`receive_str` and loads the
908+
JSON string to a Python dict.
894909

895910
:param callable loads: any :term:`callable` that accepts
896911
:class:`str` and returns :class:`dict`

tests/test_web_websocket.py

+30
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ def test_nonstarted_send_bytes(self):
6565
with self.assertRaises(RuntimeError):
6666
ws.send_bytes(b'bytes')
6767

68+
def test_nonstarted_send_json(self):
69+
ws = WebSocketResponse()
70+
with self.assertRaises(RuntimeError):
71+
ws.send_json({'type': 'json'})
72+
6873
def test_nonstarted_close(self):
6974
ws = WebSocketResponse()
7075
with self.assertRaises(RuntimeError):
@@ -90,6 +95,16 @@ def go():
9095

9196
self.loop.run_until_complete(go())
9297

98+
def test_nonstarted_receive_json(self):
99+
100+
@asyncio.coroutine
101+
def go():
102+
ws = WebSocketResponse()
103+
with self.assertRaises(RuntimeError):
104+
yield from ws.receive_json()
105+
106+
self.loop.run_until_complete(go())
107+
93108
def test_receive_str_nonstring(self):
94109

95110
@asyncio.coroutine
@@ -142,6 +157,13 @@ def test_send_bytes_nonbytes(self):
142157
with self.assertRaises(TypeError):
143158
ws.send_bytes('string')
144159

160+
def test_send_json_nonjson(self):
161+
req = self.make_request('GET', '/')
162+
ws = WebSocketResponse()
163+
self.loop.run_until_complete(ws.prepare(req))
164+
with self.assertRaises(TypeError):
165+
ws.send_json(set())
166+
145167
def test_write(self):
146168
ws = WebSocketResponse()
147169
with self.assertRaises(RuntimeError):
@@ -196,6 +218,14 @@ def test_send_bytes_closed(self):
196218
with self.assertRaises(RuntimeError):
197219
ws.send_bytes(b'bytes')
198220

221+
def test_send_json_closed(self):
222+
req = self.make_request('GET', '/')
223+
ws = WebSocketResponse()
224+
self.loop.run_until_complete(ws.prepare(req))
225+
self.loop.run_until_complete(ws.close())
226+
with self.assertRaises(RuntimeError):
227+
ws.send_json({'type': 'json'})
228+
199229
def test_ping_closed(self):
200230
req = self.make_request('GET', '/')
201231
ws = WebSocketResponse()

tests/test_web_websocket_functional.py

+29-7
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,12 @@ def test_websocket_json_invalid_message(create_app_and_client):
4040
def handler(request):
4141
ws = web.WebSocketResponse()
4242
yield from ws.prepare(request)
43-
msg = yield from ws.receive()
44-
4543
try:
46-
msg.json()
44+
yield from ws.receive_json()
4745
except ValueError:
48-
ws.send_str("ValueError raised: '%s'" % msg.data)
46+
ws.send_str('ValueError was raised')
4947
else:
50-
raise Exception("No ValueError was raised")
48+
raise Exception('No Exception')
5149
finally:
5250
yield from ws.close()
5351
return ws
@@ -59,8 +57,32 @@ def handler(request):
5957
payload = 'NOT A VALID JSON STRING'
6058
ws.send_str(payload)
6159

62-
resp = yield from ws.receive()
63-
assert payload in resp.data
60+
data = yield from ws.receive_str()
61+
assert 'ValueError was raised' in data
62+
63+
64+
@pytest.mark.run_loop
65+
def test_websocket_send_json(create_app_and_client):
66+
@asyncio.coroutine
67+
def handler(request):
68+
ws = web.WebSocketResponse()
69+
yield from ws.prepare(request)
70+
71+
data = yield from ws.receive_json()
72+
ws.send_json(data)
73+
74+
yield from ws.close()
75+
return ws
76+
77+
app, client = yield from create_app_and_client()
78+
app.router.add_route('GET', '/', handler)
79+
80+
ws = yield from client.ws_connect('/')
81+
expected_value = 'value'
82+
ws.send_json({'test': expected_value})
83+
84+
data = yield from ws.receive_json()
85+
assert data['test'] == expected_value
6486

6587

6688
@pytest.mark.run_loop

tests/test_web_websocket_functional_oldstyle.py

+35
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,41 @@ def go():
117117

118118
self.loop.run_until_complete(go())
119119

120+
def test_send_recv_json(self):
121+
closed = helpers.create_future(self.loop)
122+
123+
@asyncio.coroutine
124+
def handler(request):
125+
ws = web.WebSocketResponse()
126+
yield from ws.prepare(request)
127+
data = yield from ws.receive_json()
128+
ws.send_json({'response': data['request']})
129+
yield from ws.close()
130+
closed.set_result(1)
131+
return ws
132+
133+
@asyncio.coroutine
134+
def go():
135+
_, _, url = yield from self.create_server('GET', '/', handler)
136+
resp, reader, writer = yield from self.connect_ws(url)
137+
writer.send('{"request": "test"}')
138+
msg = yield from reader.read()
139+
data = msg.json()
140+
self.assertEqual(msg.tp, websocket.MSG_TEXT)
141+
self.assertEqual(data['response'], 'test')
142+
143+
msg = yield from reader.read()
144+
self.assertEqual(msg.tp, websocket.MSG_CLOSE)
145+
self.assertEqual(msg.data, 1000)
146+
self.assertEqual(msg.extra, '')
147+
148+
writer.close()
149+
150+
yield from closed
151+
resp.close()
152+
153+
self.loop.run_until_complete(go())
154+
120155
def test_auto_pong_with_closing_by_peer(self):
121156

122157
closed = helpers.create_future(self.loop)

tests/test_websocket_client.py

+2
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ def test_send_data_after_close(self, m_req, m_os, WebSocketWriter):
334334
self.assertRaises(RuntimeError, resp.pong)
335335
self.assertRaises(RuntimeError, resp.send_str, 's')
336336
self.assertRaises(RuntimeError, resp.send_bytes, b'b')
337+
self.assertRaises(RuntimeError, resp.send_json, {})
337338

338339
@mock.patch('aiohttp.client.WebSocketWriter')
339340
@mock.patch('aiohttp.client.os')
@@ -357,6 +358,7 @@ def test_send_data_type_errors(self, m_req, m_os, WebSocketWriter):
357358

358359
self.assertRaises(TypeError, resp.send_str, b's')
359360
self.assertRaises(TypeError, resp.send_bytes, 'b')
361+
self.assertRaises(TypeError, resp.send_json, set())
360362

361363
@mock.patch('aiohttp.client.WebSocketWriter')
362364
@mock.patch('aiohttp.client.os')

tests/test_websocket_client_functional.py

+28-4
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ def handler(request):
2222
resp = yield from client.ws_connect('/')
2323
resp.send_str('ask')
2424

25-
msg = yield from resp.receive()
26-
assert msg.data == 'ask/answer'
25+
data = yield from resp.receive_str()
26+
assert data == 'ask/answer'
2727
yield from resp.close()
2828

2929

@@ -46,9 +46,33 @@ def handler(request):
4646

4747
resp.send_bytes(b'ask')
4848

49-
msg = yield from resp.receive()
50-
assert msg.data == b'ask/answer'
49+
data = yield from resp.receive_bytes()
50+
assert data == b'ask/answer'
51+
52+
yield from resp.close()
53+
54+
55+
@pytest.mark.run_loop
56+
def test_send_recv_json(create_app_and_client):
57+
58+
@asyncio.coroutine
59+
def handler(request):
60+
ws = web.WebSocketResponse()
61+
yield from ws.prepare(request)
62+
63+
data = yield from ws.receive_json()
64+
ws.send_json({'response': data['request']})
65+
yield from ws.close()
66+
return ws
67+
68+
app, client = yield from create_app_and_client()
69+
app.router.add_route('GET', '/', handler)
70+
resp = yield from client.ws_connect('/')
71+
payload = {'request': 'test'}
72+
resp.send_json(payload)
5173

74+
data = yield from resp.receive_json()
75+
assert data['response'] == payload['request']
5276
yield from resp.close()
5377

5478

0 commit comments

Comments
 (0)