Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #976: Add support for websocket send_json and receive_json #984

Merged
merged 8 commits into from
Jul 24, 2016
11 changes: 5 additions & 6 deletions aiohttp/web_ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ def send_bytes(self, data):
type(data))
self._writer.send(data, binary=True)

def send_json(self, data, *, dumps=json.dumps):
Copy link
Member

@asvetlov asvetlov Jul 23, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please call .send_str() instead of working on low level.

self.send_str(dumps(data))

@asyncio.coroutine
def wait_closed(self): # pragma: no cover
warnings.warn(
Expand Down Expand Up @@ -288,12 +291,8 @@ def receive_bytes(self):

@asyncio.coroutine
def receive_json(self, *, loads=json.loads):
msg = yield from self.receive()
if msg.tp != MsgType.text:
raise TypeError(
"Received message {}:{!r} is not str".format(msg.tp, msg.data)
)
return msg.json(loads=loads)
data = yield from self.receive_str()
return loads(data)

def write(self, data):
raise RuntimeError("Cannot call .write() for websocket")
Expand Down
26 changes: 26 additions & 0 deletions aiohttp/websocket_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import asyncio

import sys
import json
from enum import IntEnum

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

def send_json(self, data, *, dumps=json.dumps):
self.send_str(dumps(data))

@asyncio.coroutine
def close(self, *, code=1000, message=b''):
if not self._closed:
Expand Down Expand Up @@ -171,6 +175,28 @@ def receive(self):
finally:
self._waiting = False

@asyncio.coroutine
def receive_str(self):
msg = yield from self.receive()
if msg.tp != MsgType.text:
raise TypeError(
"Received message {}:{!r} is not str".format(msg.tp, msg.data))
return msg.data

@asyncio.coroutine
def receive_bytes(self):
msg = yield from self.receive()
if msg.tp != MsgType.binary:
raise TypeError(
"Received message {}:{!r} is not bytes".format(msg.tp,
msg.data))
return msg.data

@asyncio.coroutine
def receive_json(self, *, loads=json.loads):
data = yield from self.receive_str()
return loads(data)

if PY_35:
@asyncio.coroutine
def __aiter__(self):
Expand Down
50 changes: 50 additions & 0 deletions docs/client_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,22 @@ manually.
:raise TypeError: if data is not :class:`bytes`,
:class:`bytearray` or :class:`memoryview`.

.. method:: send_json(data, *, dumps=json.loads)

Send *data* to peer as JSON string.

:param data: data to send.

:param callable dumps: any :term:`callable` that accepts an object and
returns a JSON string
(:func:`json.dumps` by default).

:raise RuntimeError: if connection is not started or closing

:raise ValueError: if data is not serializable object

:raise TypeError: if value returned by :term:`dumps` is not :class:`str`

.. comethod:: close(*, code=1000, message=b'')

A :ref:`coroutine<coroutine>` that initiates closing handshake by sending
Expand Down Expand Up @@ -1284,6 +1300,40 @@ manually.
:return: :class:`~aiohttp.websocket.Message`, `tp` is types of
`~aiohttp.MsgType`

.. coroutinemethod:: receive_str()

A :ref:`coroutine<coroutine>` that calls :meth:`receive` but
also asserts the message type is
:const:`~aiohttp.websocket.MSG_TEXT`.

:return str: peer's message content.

:raise TypeError: if message is :const:`~aiohttp.websocket.MSG_BINARY`.

.. coroutinemethod:: receive_bytes()

A :ref:`coroutine<coroutine>` that calls :meth:`receive` but
also asserts the message type is
:const:`~aiohttp.websocket.MSG_BINARY`.

:return bytes: peer's message content.

:raise TypeError: if message is :const:`~aiohttp.websocket.MSG_TEXT`.

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

A :ref:`coroutine<coroutine>` that calls :meth:`receive_str` and loads
the JSON string to a Python dict.

:param callable loads: any :term:`callable` that accepts
:class:`str` and returns :class:`dict`
with parsed JSON (:func:`json.loads` by
default).

:return dict: loaded JSON content

:raise TypeError: if message is :const:`~aiohttp.websocket.MSG_BINARY`.
:raise ValueError: if message is not valid JSON.

Utilities
---------
Expand Down
21 changes: 18 additions & 3 deletions docs/web_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,22 @@ WebSocketResponse
:raise TypeError: if data is not :class:`bytes`,
:class:`bytearray` or :class:`memoryview`.

.. method:: send_json(data, *, dumps=json.loads)

Send *data* to peer as JSON string.

:param data: data to send.

:param callable dumps: any :term:`callable` that accepts an object and
returns a JSON string
(:func:`json.dumps` by default).

:raise RuntimeError: if connection is not started or closing

:raise ValueError: if data is not serializable object

:raise TypeError: if value returned by :term:`dumps` is not :class:`str`

.. coroutinemethod:: close(*, code=1000, message=b'')

A :ref:`coroutine<coroutine>` that initiates closing
Expand Down Expand Up @@ -883,9 +899,8 @@ WebSocketResponse

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

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

:param callable loads: any :term:`callable` that accepts
:class:`str` and returns :class:`dict`
Expand Down
30 changes: 30 additions & 0 deletions tests/test_web_websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ def test_nonstarted_send_bytes(self):
with self.assertRaises(RuntimeError):
ws.send_bytes(b'bytes')

def test_nonstarted_send_json(self):
ws = WebSocketResponse()
with self.assertRaises(RuntimeError):
ws.send_json({'type': 'json'})

def test_nonstarted_close(self):
ws = WebSocketResponse()
with self.assertRaises(RuntimeError):
Expand All @@ -90,6 +95,16 @@ def go():

self.loop.run_until_complete(go())

def test_nonstarted_receive_json(self):

@asyncio.coroutine
def go():
ws = WebSocketResponse()
with self.assertRaises(RuntimeError):
yield from ws.receive_json()

self.loop.run_until_complete(go())

def test_receive_str_nonstring(self):

@asyncio.coroutine
Expand Down Expand Up @@ -142,6 +157,13 @@ def test_send_bytes_nonbytes(self):
with self.assertRaises(TypeError):
ws.send_bytes('string')

def test_send_json_nonjson(self):
req = self.make_request('GET', '/')
ws = WebSocketResponse()
self.loop.run_until_complete(ws.prepare(req))
with self.assertRaises(TypeError):
ws.send_json(set())

def test_write(self):
ws = WebSocketResponse()
with self.assertRaises(RuntimeError):
Expand Down Expand Up @@ -196,6 +218,14 @@ def test_send_bytes_closed(self):
with self.assertRaises(RuntimeError):
ws.send_bytes(b'bytes')

def test_send_json_closed(self):
req = self.make_request('GET', '/')
ws = WebSocketResponse()
self.loop.run_until_complete(ws.prepare(req))
self.loop.run_until_complete(ws.close())
with self.assertRaises(RuntimeError):
ws.send_json({'type': 'json'})

def test_ping_closed(self):
req = self.make_request('GET', '/')
ws = WebSocketResponse()
Expand Down
36 changes: 29 additions & 7 deletions tests/test_web_websocket_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,12 @@ def test_websocket_json_invalid_message(create_app_and_client):
def handler(request):
ws = web.WebSocketResponse()
yield from ws.prepare(request)
msg = yield from ws.receive()

try:
msg.json()
yield from ws.receive_json()
except ValueError:
ws.send_str("ValueError raised: '%s'" % msg.data)
ws.send_str('ValueError was raised')
else:
raise Exception("No ValueError was raised")
raise Exception('No Exception')
finally:
yield from ws.close()
return ws
Expand All @@ -57,8 +55,32 @@ def handler(request):
payload = 'NOT A VALID JSON STRING'
ws.send_str(payload)

resp = yield from ws.receive()
assert payload in resp.data
data = yield from ws.receive_str()
assert 'ValueError was raised' in data


@pytest.mark.run_loop
def test_websocket_send_json(create_app_and_client):
@asyncio.coroutine
def handler(request):
ws = web.WebSocketResponse()
yield from ws.prepare(request)

data = yield from ws.receive_json()
ws.send_json(data)

yield from ws.close()
return ws

app, client = yield from create_app_and_client()
app.router.add_route('GET', '/', handler)

ws = yield from client.ws_connect('/')
expected_value = 'value'
ws.send_json({'test': expected_value})

data = yield from ws.receive_json()
assert data['test'] == expected_value


@pytest.mark.run_loop
Expand Down
35 changes: 35 additions & 0 deletions tests/test_web_websocket_functional_oldstyle.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,41 @@ def go():

self.loop.run_until_complete(go())

def test_send_recv_json(self):
closed = helpers.create_future(self.loop)

@asyncio.coroutine
def handler(request):
ws = web.WebSocketResponse()
yield from ws.prepare(request)
data = yield from ws.receive_json()
ws.send_json({'response': data['request']})
yield from ws.close()
closed.set_result(1)
return ws

@asyncio.coroutine
def go():
_, _, url = yield from self.create_server('GET', '/', handler)
resp, reader, writer = yield from self.connect_ws(url)
writer.send('{"request": "test"}')
msg = yield from reader.read()
data = msg.json()
self.assertEqual(msg.tp, websocket.MSG_TEXT)
self.assertEqual(data['response'], 'test')

msg = yield from reader.read()
self.assertEqual(msg.tp, websocket.MSG_CLOSE)
self.assertEqual(msg.data, 1000)
self.assertEqual(msg.extra, '')

writer.close()

yield from closed
resp.close()

self.loop.run_until_complete(go())

def test_auto_pong_with_closing_by_peer(self):

closed = helpers.create_future(self.loop)
Expand Down
2 changes: 2 additions & 0 deletions tests/test_websocket_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ def test_send_data_after_close(self, m_req, m_os, WebSocketWriter):
self.assertRaises(RuntimeError, resp.pong)
self.assertRaises(RuntimeError, resp.send_str, 's')
self.assertRaises(RuntimeError, resp.send_bytes, b'b')
self.assertRaises(RuntimeError, resp.send_json, {})

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

self.assertRaises(TypeError, resp.send_str, b's')
self.assertRaises(TypeError, resp.send_bytes, 'b')
self.assertRaises(TypeError, resp.send_json, set())

@mock.patch('aiohttp.client.WebSocketWriter')
@mock.patch('aiohttp.client.os')
Expand Down
32 changes: 28 additions & 4 deletions tests/test_websocket_client_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ def handler(request):
resp = yield from client.ws_connect('/')
resp.send_str('ask')

msg = yield from resp.receive()
assert msg.data == 'ask/answer'
data = yield from resp.receive_str()
assert data == 'ask/answer'
yield from resp.close()


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

resp.send_bytes(b'ask')

msg = yield from resp.receive()
assert msg.data == b'ask/answer'
data = yield from resp.receive_bytes()
assert data == b'ask/answer'

yield from resp.close()


@pytest.mark.run_loop
def test_send_recv_json(create_app_and_client):

@asyncio.coroutine
def handler(request):
ws = web.WebSocketResponse()
yield from ws.prepare(request)

data = yield from ws.receive_json()
ws.send_json({'response': data['request']})
yield from ws.close()
return ws

app, client = yield from create_app_and_client()
app.router.add_route('GET', '/', handler)
resp = yield from client.ws_connect('/')
payload = {'request': 'test'}
resp.send_json(payload)

data = yield from resp.receive_json()
assert data['response'] == payload['request']
yield from resp.close()


Expand Down