From 66f80dfa24ab992bc8080bff670360042f47c36f Mon Sep 17 00:00:00 2001 From: Tomas Divis Date: Wed, 22 Aug 2018 14:34:29 +0200 Subject: [PATCH 1/5] Parsing json response funcion is now customizable (still defaults to `json.loads`). This allows to implement JSON-RPC class hinting or any other conversions. --- jsonrpc_async/jsonrpc.py | 6 ++++-- tests.py | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/jsonrpc_async/jsonrpc.py b/jsonrpc_async/jsonrpc.py index a9cfccf..91d227f 100644 --- a/jsonrpc_async/jsonrpc.py +++ b/jsonrpc_async/jsonrpc.py @@ -1,5 +1,6 @@ import asyncio import functools +import json import aiohttp import jsonrpc_base @@ -9,7 +10,7 @@ class Server(jsonrpc_base.Server): """A connection to a HTTP JSON-RPC server, backed by aiohttp""" - def __init__(self, url, session=None, **post_kwargs): + def __init__(self, url, session=None, loads=json.loads, **post_kwargs): super().__init__() object.__setattr__(self, 'session', session or aiohttp.ClientSession()) post_kwargs['headers'] = post_kwargs.get('headers', {}) @@ -18,6 +19,7 @@ def __init__(self, url, session=None, **post_kwargs): post_kwargs['headers']['Accept'] = post_kwargs['headers'].get( 'Accept', 'application/json-rpc') self._request = functools.partial(self.session.post, url, **post_kwargs) + self._loads = loads @asyncio.coroutine def send_message(self, message): @@ -38,7 +40,7 @@ def send_message(self, message): return None try: - response_data = yield from response.json() + response_data = yield from response.json(loads=self._loads) except ValueError as value_error: raise TransportError('Cannot deserialize response body', message, value_error) diff --git a/tests.py b/tests.py index ec395a0..53eb986 100644 --- a/tests.py +++ b/tests.py @@ -1,8 +1,8 @@ import asyncio +import mock import unittest import random import json -import inspect import os import aiohttp @@ -21,6 +21,7 @@ except ImportError: from mock import Mock + class JsonTestClient(aiohttp.test_utils.TestClient): def __init__(self, app, **kwargs): super().__init__(TestServer(app), **kwargs) @@ -31,6 +32,7 @@ def request(self, method, path, *args, **kwargs): self.request_callback(method, path, *args, **kwargs) return super().request(method, path, *args, **kwargs) + class TestCase(unittest.TestCase): def assertSameJSON(self, json1, json2): """Tells whether two json strings, once decoded, are the same dictionary""" @@ -40,8 +42,7 @@ def assertRaisesRegex(self, *args, **kwargs): return super(TestCase, self).assertRaisesRegex(*args, **kwargs) -class TestJSONRPCClient(TestCase): - +class TestJSONRPCClientBase(TestCase): def setUp(self): self.loop = setup_test_loop() self.app = self.get_app() @@ -54,7 +55,10 @@ def create_client(app, loop): create_client(self.app, self.loop)) self.loop.run_until_complete(self.client.start_server()) random.randint = Mock(return_value=1) - self.server = Server('/xmlrpc', session=self.client, timeout=0.2) + self.server = self.get_server() + + def get_server(self): + return Server('/xmlrpc', session=self.client, timeout=0.2) def tearDown(self): self.loop.run_until_complete(self.client.close()) @@ -68,6 +72,8 @@ def response_func(request): app.router.add_post('/xmlrpc', response_func) return app + +class TestJSONRPCClient(TestJSONRPCClientBase): def test_pep8_conformance(self): """Test that we conform to PEP8.""" @@ -249,5 +255,25 @@ def handler(request): self.assertIsNone((yield from self.server.subtract(42, 23, _notification=True))) +class TestJSONRPCClientCustomLoads(TestJSONRPCClientBase): + def get_server(self): + self.loads_mock = mock.Mock(wraps=json.loads) + return Server('/xmlrpc', session=self.client, loads=self.loads_mock, timeout=0.2) + + @unittest_run_loop + @asyncio.coroutine + def test_custom_loads(self): + # rpc call with positional parameters: + @asyncio.coroutine + def handler1(request): + request_message = yield from request.json() + self.assertEqual(request_message["params"], [42, 23]) + return aiohttp.web.Response(text='{"jsonrpc": "2.0", "result": 19, "id": 1}', content_type='application/json') + + self.handler = handler1 + self.assertEqual((yield from self.server.subtract(42, 23)), 19) + self.loads_mock.assert_called_once() + + if __name__ == '__main__': unittest.main() From c38acd0e412134b2ae33446e7d3aa29d60807912 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Mon, 3 Sep 2018 18:01:08 -0400 Subject: [PATCH 2/5] Tests import cleanup --- tests.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests.py b/tests.py index 53eb986..de54f90 100644 --- a/tests.py +++ b/tests.py @@ -1,5 +1,5 @@ import asyncio -import mock +from unittest import mock import unittest import random import json @@ -15,12 +15,6 @@ import jsonrpc_base from jsonrpc_async import Server, ProtocolError, TransportError -try: - # python 3.3 - from unittest.mock import Mock -except ImportError: - from mock import Mock - class JsonTestClient(aiohttp.test_utils.TestClient): def __init__(self, app, **kwargs): @@ -54,7 +48,7 @@ def create_client(app, loop): self.client = self.loop.run_until_complete( create_client(self.app, self.loop)) self.loop.run_until_complete(self.client.start_server()) - random.randint = Mock(return_value=1) + random.randint = mock.Mock(return_value=1) self.server = self.get_server() def get_server(self): From 976b4244dd979088bcd142defb1c0def6047b091 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Mon, 3 Sep 2018 18:09:27 -0400 Subject: [PATCH 3/5] Don't override aiohttp's default json loads In the case where our users aren't specifying a manual json.loads method, we'd rather omit that argument, so that aiohttp can use its logic to determine the default json.loads method. --- jsonrpc_async/jsonrpc.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/jsonrpc_async/jsonrpc.py b/jsonrpc_async/jsonrpc.py index 91d227f..1b24a08 100644 --- a/jsonrpc_async/jsonrpc.py +++ b/jsonrpc_async/jsonrpc.py @@ -1,6 +1,5 @@ import asyncio import functools -import json import aiohttp import jsonrpc_base @@ -10,7 +9,7 @@ class Server(jsonrpc_base.Server): """A connection to a HTTP JSON-RPC server, backed by aiohttp""" - def __init__(self, url, session=None, loads=json.loads, **post_kwargs): + def __init__(self, url, session=None, loads=None, **post_kwargs): super().__init__() object.__setattr__(self, 'session', session or aiohttp.ClientSession()) post_kwargs['headers'] = post_kwargs.get('headers', {}) @@ -19,7 +18,10 @@ def __init__(self, url, session=None, loads=json.loads, **post_kwargs): post_kwargs['headers']['Accept'] = post_kwargs['headers'].get( 'Accept', 'application/json-rpc') self._request = functools.partial(self.session.post, url, **post_kwargs) - self._loads = loads + + self._json_args = {} + if loads is not None: + self._json_args['loads'] = loads @asyncio.coroutine def send_message(self, message): @@ -40,7 +42,7 @@ def send_message(self, message): return None try: - response_data = yield from response.json(loads=self._loads) + response_data = yield from response.json(**self._json_args) except ValueError as value_error: raise TransportError('Cannot deserialize response body', message, value_error) From 93776ea14677a7196aa9bfe5cca475b83328ce41 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Mon, 3 Sep 2018 18:12:24 -0400 Subject: [PATCH 4/5] Only allow loads as a keyword argument We're not going to commit to loads being in a particular position in the argument order on our API. --- jsonrpc_async/jsonrpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonrpc_async/jsonrpc.py b/jsonrpc_async/jsonrpc.py index 1b24a08..c8ea942 100644 --- a/jsonrpc_async/jsonrpc.py +++ b/jsonrpc_async/jsonrpc.py @@ -9,7 +9,7 @@ class Server(jsonrpc_base.Server): """A connection to a HTTP JSON-RPC server, backed by aiohttp""" - def __init__(self, url, session=None, loads=None, **post_kwargs): + def __init__(self, url, session=None, *, loads=None, **post_kwargs): super().__init__() object.__setattr__(self, 'session', session or aiohttp.ClientSession()) post_kwargs['headers'] = post_kwargs.get('headers', {}) From 0a61fdc277e800ea0998c9ffab92f696fe87cd87 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Mon, 3 Sep 2018 18:23:00 -0400 Subject: [PATCH 5/5] Python 3.5 compatible assertion --- tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests.py b/tests.py index de54f90..93e01bd 100644 --- a/tests.py +++ b/tests.py @@ -266,7 +266,7 @@ def handler1(request): self.handler = handler1 self.assertEqual((yield from self.server.subtract(42, 23)), 19) - self.loads_mock.assert_called_once() + self.assertEqual(self.loads_mock.call_count, 1) if __name__ == '__main__':