From 9f2915887684292b315ff83c512e6c1e1c85e47e Mon Sep 17 00:00:00 2001 From: Nycholas Oliveira Date: Fri, 21 Jun 2024 15:09:20 -0300 Subject: [PATCH] Fix func annotations by signarure (#509) --- examples/wba/minimal.py | 15 +++++++ pyproject.toml | 2 +- .../browse/static/js/apps/browse/services.js | 7 ++- src/flask_jsonrpc/wrappers.py | 17 +++++++- tests/integration/test_app.py | 43 ++++++++++++++++++- tests/test_apps/app/__init__.py | 5 +++ tests/test_apps/async_app/__init__.py | 6 +++ tests/unit/contrib/test_browse.py | 43 ++++++++++++++++++- tests/unit/test_async_client.py | 18 +++++++- tests/unit/test_client.py | 18 +++++++- 10 files changed, 163 insertions(+), 11 deletions(-) diff --git a/examples/wba/minimal.py b/examples/wba/minimal.py index 54a675c9..4e2961f1 100755 --- a/examples/wba/minimal.py +++ b/examples/wba/minimal.py @@ -81,5 +81,20 @@ def fails(_string: Optional[str]) -> NoReturn: raise ValueError('example of fail') +@jsonrpc.method('App.notValidate', validate=False) +def not_validate(s='Oops!'): # noqa: ANN001,ANN202,ANN201 + return f'Not validate: {s}' + + +@jsonrpc.method('App.mixinNotValidate', validate=False) +def mixin_not_validate(s, t: int, u, v: str, x, z): # noqa: ANN001,ANN202,ANN201 + return f'Not validate: {s} {t} {u} {v} {x} {z}' + + +@jsonrpc.method('App.mixinNotValidateReturn', validate=False) +def mixin_not_validate_with_no_return(_s, _t: int, _u, _v: str, _x, _z): # noqa: ANN001,ANN202,ANN201 + pass + + if __name__ == '__main__': app.run(host='0.0.0.0', debug=True) diff --git a/pyproject.toml b/pyproject.toml index 03e7f0e7..9dfda40c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ backend-path = ["src/buildtools"] [project] name = "Flask-JSONRPC" -version = "3.0.1" +version = "4.0.0a0" description = "Adds JSONRPC support to Flask." readme = {file = "README.md", content-type = "text/markdown"} license = {file = "LICENSE"} diff --git a/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/services.js b/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/services.js index f35c5338..21d2415c 100644 --- a/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/services.js +++ b/src/flask_jsonrpc/contrib/browse/static/js/apps/browse/services.js @@ -81,7 +81,12 @@ } if (param.type === 'Object') { - return eval('('+ param.value + ')'); + try { + return eval('('+ param.value + ')'); + } catch (e) { + console.error('Failed to evaluate the object:', param.value); + return param.value; + } } else if (param.type === 'Number') { if (param.value.indexOf('.') !== -1) { return parseFloat(param.value); diff --git a/src/flask_jsonrpc/wrappers.py b/src/flask_jsonrpc/wrappers.py index fb0db752..552d3d44 100644 --- a/src/flask_jsonrpc/wrappers.py +++ b/src/flask_jsonrpc/wrappers.py @@ -25,7 +25,8 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. import typing as t -from inspect import ismethod, signature, isfunction +from inspect import _empty, ismethod, signature, isfunction +from collections import OrderedDict from typeguard import typechecked @@ -76,6 +77,18 @@ def _get_function(self: Self, fn: t.Callable[..., t.Any]) -> t.Callable[..., t.A return fn.__func__ # pytype: disable=attribute-error,bad-return-type raise ValueError('the view function must be either a function or a method') + def _get_type_hints_by_signature( + self: Self, fn: t.Callable[..., t.Any], fn_annotations: t.Dict[str, t.Any] + ) -> t.Dict[str, t.Any]: + sig = signature(fn) + parameters = OrderedDict() + for name in sig.parameters: + parameters[name] = fn_annotations.get(name, t.Any) + parameters['return'] = fn_annotations.get( + 'return', t.Any if sig.return_annotation is _empty else sig.return_annotation + ) + return parameters + def get_jsonrpc_site(self: Self) -> 'JSONRPCSite': raise NotImplementedError('.get_jsonrpc_site must be overridden') @@ -88,6 +101,8 @@ def register_view_function( fn = self._get_function(view_func) fn_options = self._method_options(options) fn_annotations = t.get_type_hints(fn) + if not fn_options['validate']: + fn_annotations = self._get_type_hints_by_signature(fn, fn_annotations) method_name = name if name else getattr(fn, '__name__', '') view_func_wrapped = typechecked(view_func) if fn_options['validate'] else view_func setattr(view_func_wrapped, 'jsonrpc_method_name', method_name) # noqa: B010 diff --git a/tests/integration/test_app.py b/tests/integration/test_app.py index 813ba029..583c544b 100644 --- a/tests/integration/test_app.py +++ b/tests/integration/test_app.py @@ -455,6 +455,31 @@ def test_not_validate_method(self: Self) -> None: self.assertEqual({'id': 1, 'jsonrpc': '2.0', 'result': 'Not validate: OK'}, rv.json()) self.assertEqual(200, rv.status_code) + def test_mixin_not_validate_method(self: Self) -> None: + rv = self.requests.post( + API_URL, + json={ + 'id': 1, + 'jsonrpc': '2.0', + 'method': 'jsonrpc.mixin_not_validate', + 'params': [':)', 1, 3.2, ':D', [1, 2, 3], {1: 1}], + }, + ) + self.assertEqual({'id': 1, 'jsonrpc': '2.0', 'result': 'Not validate: :) 1 3.2 :D [1, 2, 3] {1: 1}'}, rv.json()) + self.assertEqual(200, rv.status_code) + + rv = self.requests.post( + API_URL, + json={ + 'id': 1, + 'jsonrpc': '2.0', + 'method': 'jsonrpc.mixin_not_validate', + 'params': {'s': ':)', 't': 1, 'u': 3.2, 'v': ':D', 'x': [1, 2, 3], 'z': {1: 1}}, + }, + ) + self.assertEqual({'id': 1, 'jsonrpc': '2.0', 'result': 'Not validate: :) 1 3.2 :D [1, 2, 3] {1: 1}'}, rv.json()) + self.assertEqual(200, rv.status_code) + def test_no_return_method(self: Self) -> None: rv = self.requests.post(API_URL, json={'id': 1, 'jsonrpc': '2.0', 'method': 'jsonrpc.noReturn', 'params': []}) self.assertEqual( @@ -668,8 +693,22 @@ def test_system_describe(self: Self) -> None: 'jsonrpc.not_validate': { 'type': 'method', 'options': {'notification': True, 'validate': False}, - 'params': [], - 'returns': {'type': 'Null'}, + 'params': [{'name': 's', 'nullable': False, 'required': False, 'type': 'Object'}], + 'returns': {'type': 'Object'}, + 'description': None, + }, + 'jsonrpc.mixin_not_validate': { + 'type': 'method', + 'options': {'notification': True, 'validate': False}, + 'params': [ + {'name': 's', 'type': 'Object', 'required': False, 'nullable': False}, + {'name': 't', 'type': 'Number', 'required': False, 'nullable': False}, + {'name': 'u', 'type': 'Object', 'required': False, 'nullable': False}, + {'name': 'v', 'type': 'String', 'required': False, 'nullable': False}, + {'name': 'x', 'type': 'Object', 'required': False, 'nullable': False}, + {'name': 'z', 'type': 'Object', 'required': False, 'nullable': False}, + ], + 'returns': {'type': 'Object'}, 'description': None, }, 'jsonrpc.noReturn': { diff --git a/tests/test_apps/app/__init__.py b/tests/test_apps/app/__init__.py index 615d6bc2..1e65dcea 100644 --- a/tests/test_apps/app/__init__.py +++ b/tests/test_apps/app/__init__.py @@ -158,6 +158,11 @@ def return_status_code_and_headers(s: str) -> t.Tuple[str, int, t.Dict[str, t.An def not_validate(s='Oops!'): # noqa: ANN001,ANN202 return f'Not validate: {s}' + # pylint: disable=W0612 + @jsonrpc.method('jsonrpc.mixin_not_validate', validate=False) + def mixin_not_validate(s, t: int, u, v: str, x, z): # noqa: ANN001,ANN202 + return f'Not validate: {s} {t} {u} {v} {x} {z}' + @jsonrpc.method('jsonrpc.noReturn') def no_return(_string: t.Optional[str] = None) -> t.NoReturn: raise ValueError('no return') diff --git a/tests/test_apps/async_app/__init__.py b/tests/test_apps/async_app/__init__.py index 5c04702b..0195235c 100644 --- a/tests/test_apps/async_app/__init__.py +++ b/tests/test_apps/async_app/__init__.py @@ -176,6 +176,12 @@ async def not_validate(s='Oops!'): # noqa: ANN001,ANN202 await asyncio.sleep(0) return f'Not validate: {s}' + # pylint: disable=W0612 + @jsonrpc.method('jsonrpc.mixin_not_validate', validate=False) + async def mixin_not_validate(s, t: int, u, v: str, x, z): # noqa: ANN001,ANN202 + await asyncio.sleep(0) + return f'Not validate: {s} {t} {u} {v} {x} {z}' + @jsonrpc.method('jsonrpc.noReturn') async def no_return(_string: t.Optional[str] = None) -> t.NoReturn: await asyncio.sleep(0) diff --git a/tests/unit/contrib/test_browse.py b/tests/unit/contrib/test_browse.py index 8eeb9b43..3f626bed 100644 --- a/tests/unit/contrib/test_browse.py +++ b/tests/unit/contrib/test_browse.py @@ -51,6 +51,11 @@ def fn2(s: str) -> str: def fn3(s: str) -> str: return f'Foo {s}' + # pylint: disable=W0612 + @jsonrpc.method('app.fn4', validate=False) + def fn4(s, t: int, u, v: str, x, z): # noqa: ANN001,ANN202 + return f'Not validate: {s} {t} {u} {v} {x} {z}' + with app.test_client() as client: rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'app.fn1', 'params': [1]}) assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Foo 1'} @@ -79,6 +84,25 @@ def fn3(s: str) -> str: } assert rv.status_code == 400 + rv = client.post( + '/api', + json={'id': 1, 'jsonrpc': '2.0', 'method': 'app.fn4', 'params': [':)', 1, 3.2, ':D', [1, 2, 3], {1: 1}]}, + ) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': "Not validate: :) 1 3.2 :D [1, 2, 3] {'1': 1}"} + assert rv.status_code == 200 + + rv = client.post( + '/api', + json={ + 'id': 1, + 'jsonrpc': '2.0', + 'method': 'app.fn4', + 'params': {'s': ':)', 't': 1, 'u': 3.2, 'v': ':D', 'x': [1, 2, 3], 'z': {1: 1}}, + }, + ) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': "Not validate: :) 1 3.2 :D [1, 2, 3] {'1': 1}"} + assert rv.status_code == 200 + rv = client.get('/api/browse') assert rv.status_code == 308 @@ -94,8 +118,8 @@ def fn3(s: str) -> str: 'name': 'app.fn1', 'type': 'method', 'options': {'notification': True, 'validate': False}, - 'params': [], - 'returns': {'type': 'Null'}, + 'params': [{'name': 's', 'type': 'Object', 'required': False, 'nullable': False}], + 'returns': {'type': 'Object'}, 'description': 'Function app.fn1', }, { @@ -114,6 +138,21 @@ def fn3(s: str) -> str: 'returns': {'type': 'String'}, 'description': None, }, + { + 'name': 'app.fn4', + 'type': 'method', + 'options': {'notification': True, 'validate': False}, + 'params': [ + {'name': 's', 'type': 'Object', 'required': False, 'nullable': False}, + {'name': 't', 'type': 'Number', 'required': False, 'nullable': False}, + {'name': 'u', 'type': 'Object', 'required': False, 'nullable': False}, + {'name': 'v', 'type': 'String', 'required': False, 'nullable': False}, + {'name': 'x', 'type': 'Object', 'required': False, 'nullable': False}, + {'name': 'z', 'type': 'Object', 'required': False, 'nullable': False}, + ], + 'returns': {'type': 'Object'}, + 'description': None, + }, ] } assert rv.status_code == 200 diff --git a/tests/unit/test_async_client.py b/tests/unit/test_async_client.py index 8109f7d9..2a25c3fd 100644 --- a/tests/unit/test_async_client.py +++ b/tests/unit/test_async_client.py @@ -593,8 +593,22 @@ def test_app_system_describe(async_client: 'FlaskClient') -> None: 'jsonrpc.not_validate': { 'type': 'method', 'options': {'notification': True, 'validate': False}, - 'params': [], - 'returns': {'type': 'Null'}, + 'params': [{'name': 's', 'nullable': False, 'required': False, 'type': 'Object'}], + 'returns': {'type': 'Object'}, + 'description': None, + }, + 'jsonrpc.mixin_not_validate': { + 'type': 'method', + 'options': {'notification': True, 'validate': False}, + 'params': [ + {'name': 's', 'type': 'Object', 'required': False, 'nullable': False}, + {'name': 't', 'type': 'Number', 'required': False, 'nullable': False}, + {'name': 'u', 'type': 'Object', 'required': False, 'nullable': False}, + {'name': 'v', 'type': 'String', 'required': False, 'nullable': False}, + {'name': 'x', 'type': 'Object', 'required': False, 'nullable': False}, + {'name': 'z', 'type': 'Object', 'required': False, 'nullable': False}, + ], + 'returns': {'type': 'Object'}, 'description': None, }, 'jsonrpc.noReturn': { diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 0bb3171d..d92c1e30 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -581,8 +581,22 @@ def test_app_system_describe(client: 'FlaskClient') -> None: 'jsonrpc.not_validate': { 'type': 'method', 'options': {'notification': True, 'validate': False}, - 'params': [], - 'returns': {'type': 'Null'}, + 'params': [{'name': 's', 'nullable': False, 'required': False, 'type': 'Object'}], + 'returns': {'type': 'Object'}, + 'description': None, + }, + 'jsonrpc.mixin_not_validate': { + 'type': 'method', + 'options': {'notification': True, 'validate': False}, + 'params': [ + {'name': 's', 'type': 'Object', 'required': False, 'nullable': False}, + {'name': 't', 'type': 'Number', 'required': False, 'nullable': False}, + {'name': 'u', 'type': 'Object', 'required': False, 'nullable': False}, + {'name': 'v', 'type': 'String', 'required': False, 'nullable': False}, + {'name': 'x', 'type': 'Object', 'required': False, 'nullable': False}, + {'name': 'z', 'type': 'Object', 'required': False, 'nullable': False}, + ], + 'returns': {'type': 'Object'}, 'description': None, }, 'jsonrpc.noReturn': {