From 0f1fd290c7f8d6427e419511e1cfb54a89e99b05 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Sat, 14 May 2022 18:01:53 +1000 Subject: [PATCH 01/33] Replace Oslash with Returns Fixes #225 --- .github/workflows/code-quality.yml | 2 +- .pre-commit-config.yaml | 6 +- README.md | 6 +- docs/async.md | 6 +- docs/dispatch.md | 10 +- docs/installation.md | 4 +- docs/methods.md | 17 +- examples/aiohttp_server.py | 6 +- examples/aiozmq_server.py | 6 +- examples/asyncio_server.py | 6 +- examples/django_server.py | 4 +- examples/fastapi_server.py | 4 +- examples/flask_server.py | 4 +- examples/http_server.py | 4 +- examples/jsonrpcserver_server.py | 4 +- examples/sanic_server.py | 4 +- examples/socketio_server.py | 4 +- examples/tornado_server.py | 6 +- examples/websockets_server.py | 6 +- examples/werkzeug_server.py | 8 +- examples/zeromq_server.py | 4 +- jsonrpcserver/__init__.py | 10 +- jsonrpcserver/async_dispatcher.py | 64 +++++-- jsonrpcserver/async_main.py | 2 +- jsonrpcserver/async_methods.py | 31 ++++ jsonrpcserver/dispatcher.py | 73 ++++---- jsonrpcserver/methods.py | 9 +- jsonrpcserver/response.py | 17 +- jsonrpcserver/result.py | 18 +- mypy.ini | 3 + setup.py | 2 +- tests/test_async_dispatcher.py | 71 ++++---- tests/test_async_main.py | 8 +- tests/test_dispatcher.py | 259 +++++++++++++---------------- tests/test_main.py | 8 +- tests/test_response.py | 14 +- tests/test_result.py | 10 +- 37 files changed, 385 insertions(+), 335 deletions(-) create mode 100644 jsonrpcserver/async_methods.py create mode 100644 mypy.ini diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 31b5c23..3ce4de1 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -10,7 +10,7 @@ jobs: with: python-version: 3.x - run: pip install --upgrade pip - - run: pip install black==21.6b0 pylint==v3.0.0a3 mypy==v0.902 types-setuptools + - run: pip install black==22.3.0 pylint==v3.0.0a3 mypy==v0.950 types-setuptools - run: black --diff --check $(git ls-files -- '*.py' ':!:tests/*' ':!:docs/*' ':!:examples/*') - run: pylint --disable=all --enable=unused-import $(git ls-files -- '*.py' ':!:tests/*' ':!:docs/*' ':!:examples/*') - run: mypy --strict $(git ls-files -- '*.py' ':!:tests/*' ':!:docs/*' ':!:examples/*') diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 00cdea2..7e7c869 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ fail_fast: true repos: - repo: https://github.com/ambv/black - rev: 21.7b0 + rev: 22.3.0 hooks: - id: black args: [--diff, --check] @@ -13,9 +13,9 @@ repos: args: [--disable=all, --enable=unused-import] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.910 + rev: v0.950 hooks: - id: mypy exclude: (^tests|^examples|^docs) args: [--strict] - additional_dependencies: ['types-setuptools'] + additional_dependencies: ['types-setuptools', 'returns'] diff --git a/README.md b/README.md index a0a47be..db5a4ee 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,11 @@ Process incoming JSON-RPC requests in Python. ```python -from jsonrpcserver import dispatch, method, Success +from jsonrpcserver import dispatch, method, Ok, Result @method -def ping(): - return Success("pong") +def ping() -> Result: + return Ok("pong") response = dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}') # '{"jsonrpc": "2.0", "result": "pong", "id": 1}' diff --git a/docs/async.md b/docs/async.md index 296a74f..e52fde8 100644 --- a/docs/async.md +++ b/docs/async.md @@ -3,11 +3,11 @@ Async dispatch is supported. ```python -from jsonrpcserver import method, Success, async_dispatch +from jsonrpcserver import async_dispatch, async_method, Ok, Result -@method +@async_method async def ping() -> Result: - return Success("pong") + return Ok("pong") await async_dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}') ``` diff --git a/docs/dispatch.md b/docs/dispatch.md index c53e342..1e743ec 100644 --- a/docs/dispatch.md +++ b/docs/dispatch.md @@ -14,13 +14,13 @@ and gives a JSON-RPC response. ### methods -This lets you specify a group of methods to dispatch to. It's an alternative to -using the `@method` decorator. The value should be a dict mapping function -names to functions. +This lets you specify the methods to dispatch to. It's an alternative to using +the `@method` decorator. The value should be a dict mapping function names to +functions. ```python def ping(): - return Success("pong") + return Ok("pong") dispatch(request, methods={"ping": ping}) ``` @@ -35,7 +35,7 @@ If specified, this will be the first argument to all methods. ```python @method def greet(context, name): - return Success(context + " " + name) + return Ok(context + " " + name) >>> dispatch('{"jsonrpc": "2.0", "method": "greet", "params": ["Beau"], "id": 1}', context="Hello") '{"jsonrpc": "2.0", "result": "Hello Beau", "id": 1}' diff --git a/docs/installation.md b/docs/installation.md index 6c40ec8..caf8468 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -3,11 +3,11 @@ Create a `server.py`: ```python -from jsonrpcserver import Success, method, serve +from jsonrpcserver import method, serve, Ok @method def ping(): - return Success("pong") + return Ok("pong") if __name__ == "__main__": serve() diff --git a/docs/methods.md b/docs/methods.md index dd5be3b..cba2406 100644 --- a/docs/methods.md +++ b/docs/methods.md @@ -4,20 +4,21 @@ Methods are functions that can be called by a JSON-RPC request. To write one, decorate a function with `@method`: ```python -from jsonrpcserver import method, Result, Success, Error +from jsonrpcserver import method, Error, Ok, Result @method def ping() -> Result: - return Success("pong") + return Ok("pong") ``` -If you don't need to respond with any value simply `return Success()`. +If you don't need to respond with any value simply `return Ok()`. ## Responses -Methods return either `Success` or `Error`. These are the [JSON-RPC response +Methods return either `Ok` or `Error`. These are the [JSON-RPC response objects](https://www.jsonrpc.org/specification#response_object) (excluding the -`jsonrpc` and `id` parts). `Error` takes a code, message, and optionally 'data'. +`jsonrpc` and `id` parts). `Error` takes a code, message, and optionally +'data'. ```python @method @@ -36,7 +37,7 @@ Methods can accept arguments. ```python @method def hello(name: str) -> Result: - return Success("Hello " + name) + return Ok("Hello " + name) ``` Testing it: @@ -53,13 +54,13 @@ The JSON-RPC error code for this is **-32602**. A shortcut, *InvalidParams*, is included so you don't need to remember that. ```python -from jsonrpcserver import method, Result, InvalidParams, Success, dispatch +from jsonrpcserver import dispatch, method, InvalidParams, Ok, Result @method def within_range(num: int) -> Result: if num not in range(1, 5): return InvalidParams("Value must be 1-5") - return Success() + return Ok() ``` This is the same as saying diff --git a/examples/aiohttp_server.py b/examples/aiohttp_server.py index 2f65545..ab05312 100644 --- a/examples/aiohttp_server.py +++ b/examples/aiohttp_server.py @@ -1,10 +1,10 @@ from aiohttp import web -from jsonrpcserver import method, Result, Success, async_dispatch +from jsonrpcserver import async_dispatch, async_method, Ok, Result -@method +@async_method async def ping() -> Result: - return Success("pong") + return Ok("pong") async def handle(request): diff --git a/examples/aiozmq_server.py b/examples/aiozmq_server.py index 69c4282..8d3e723 100644 --- a/examples/aiozmq_server.py +++ b/examples/aiozmq_server.py @@ -1,12 +1,12 @@ -from jsonrpcserver import method, Result, Success, async_dispatch +from jsonrpcserver import async_dispatch, async_method, Ok, Result import aiozmq import asyncio import zmq -@method +@async_method async def ping() -> Result: - return Success("pong") + return Ok("pong") async def main(): diff --git a/examples/asyncio_server.py b/examples/asyncio_server.py index b580061..950cf6e 100644 --- a/examples/asyncio_server.py +++ b/examples/asyncio_server.py @@ -2,13 +2,13 @@ import asyncio import json -from jsonrpcserver import method, Result, Success, async_dispatch +from jsonrpcserver import async_dispatch, async_method, Ok, Result -@method +@async_method async def sleep_() -> Result: await asyncio.sleep(1) - return Success() + return Ok() async def handle(request: str) -> None: diff --git a/examples/django_server.py b/examples/django_server.py index 085767a..2ff2654 100644 --- a/examples/django_server.py +++ b/examples/django_server.py @@ -1,11 +1,11 @@ from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt -from jsonrpcserver import method, Result, Success, dispatch +from jsonrpcserver import dispatch, method, Ok, Result @method def ping() -> Result: - return Success("pong") + return Ok("pong") @csrf_exempt diff --git a/examples/fastapi_server.py b/examples/fastapi_server.py index 086b441..81954a4 100644 --- a/examples/fastapi_server.py +++ b/examples/fastapi_server.py @@ -1,5 +1,5 @@ from fastapi import FastAPI, Request, Response -from jsonrpcserver import Result, Success, dispatch, method +from jsonrpcserver import dispatch, method, Ok, Result import uvicorn app = FastAPI() @@ -7,7 +7,7 @@ @method def ping() -> Result: - return Success("pong") + return Ok("pong") @app.post("/") diff --git a/examples/flask_server.py b/examples/flask_server.py index 491773a..6afdf15 100644 --- a/examples/flask_server.py +++ b/examples/flask_server.py @@ -1,12 +1,12 @@ from flask import Flask, Response, request -from jsonrpcserver import method, Result, Success, dispatch +from jsonrpcserver import dispatch, method, Ok, Result app = Flask(__name__) @method def ping() -> Result: - return Success("pong") + return Ok("pong") @app.route("/", methods=["POST"]) diff --git a/examples/http_server.py b/examples/http_server.py index 4757de1..ca1b00a 100644 --- a/examples/http_server.py +++ b/examples/http_server.py @@ -1,11 +1,11 @@ from http.server import BaseHTTPRequestHandler, HTTPServer -from jsonrpcserver import method, Result, Success, dispatch +from jsonrpcserver import dispatch, method, Ok, Result @method def ping() -> Result: - return Success("pong") + return Ok("pong") class TestHttpServer(BaseHTTPRequestHandler): diff --git a/examples/jsonrpcserver_server.py b/examples/jsonrpcserver_server.py index 36dbb90..60b18f2 100644 --- a/examples/jsonrpcserver_server.py +++ b/examples/jsonrpcserver_server.py @@ -1,9 +1,9 @@ -from jsonrpcserver import method, Result, Success, serve +from jsonrpcserver import method, serve, Ok, Result @method def ping() -> Result: - return Success("pong") + return Ok("pong") if __name__ == "__main__": diff --git a/examples/sanic_server.py b/examples/sanic_server.py index cd5fe7e..666917d 100644 --- a/examples/sanic_server.py +++ b/examples/sanic_server.py @@ -1,14 +1,14 @@ from sanic import Sanic from sanic.request import Request from sanic.response import json -from jsonrpcserver import Result, Success, dispatch_to_serializable, method +from jsonrpcserver import dispatch_to_serializable, method, Ok, Result app = Sanic("JSON-RPC app") @method def ping() -> Result: - return Success("pong") + return Ok("pong") @app.route("/", methods=["POST"]) diff --git a/examples/socketio_server.py b/examples/socketio_server.py index 6689172..72b699b 100644 --- a/examples/socketio_server.py +++ b/examples/socketio_server.py @@ -1,6 +1,6 @@ from flask import Flask from flask_socketio import SocketIO, send -from jsonrpcserver import method, Result, Success, dispatch +from jsonrpcserver import dispatch, method, Ok, Result app = Flask(__name__) socketio = SocketIO(app) @@ -8,7 +8,7 @@ @method def ping() -> Result: - return Success("pong") + return Ok("pong") @socketio.on("message") diff --git a/examples/tornado_server.py b/examples/tornado_server.py index ebd60fa..02c565c 100644 --- a/examples/tornado_server.py +++ b/examples/tornado_server.py @@ -1,10 +1,10 @@ -from jsonrpcserver import method, Result, Success, async_dispatch +from jsonrpcserver import async_dispatch, async_method, Ok, Result from tornado import ioloop, web -@method +@async_method async def ping() -> Result: - return Success("pong") + return Ok("pong") class MainHandler(web.RequestHandler): diff --git a/examples/websockets_server.py b/examples/websockets_server.py index abafd66..3b0680c 100644 --- a/examples/websockets_server.py +++ b/examples/websockets_server.py @@ -1,12 +1,12 @@ import asyncio -from jsonrpcserver import method, Success, Result, async_dispatch +from jsonrpcserver import async_dispatch, async_method, Ok, Result import websockets -@method +@async_method async def ping() -> Result: - return Success("pong") + return Ok("pong") async def main(websocket, path): diff --git a/examples/werkzeug_server.py b/examples/werkzeug_server.py index 7fd6404..79819e6 100644 --- a/examples/werkzeug_server.py +++ b/examples/werkzeug_server.py @@ -1,16 +1,18 @@ -from jsonrpcserver import method, Result, Success, dispatch +from jsonrpcserver import method, Result, Ok, dispatch from werkzeug.serving import run_simple from werkzeug.wrappers import Request, Response @method def ping() -> Result: - return Success("pong") + return Ok("pong") @Request.application def application(request): - return Response(dispatch(request.data.decode()), 200, mimetype="application/json") + return Response( + dispatch(request.get_data().decode()), 200, mimetype="application/json" + ) if __name__ == "__main__": diff --git a/examples/zeromq_server.py b/examples/zeromq_server.py index 773acca..7473342 100644 --- a/examples/zeromq_server.py +++ b/examples/zeromq_server.py @@ -1,4 +1,4 @@ -from jsonrpcserver import method, Result, Success, dispatch +from jsonrpcserver import dispatch, method, Ok, Result import zmq socket = zmq.Context().socket(zmq.REP) @@ -6,7 +6,7 @@ @method def ping() -> Result: - return Success("pong") + return Ok("pong") if __name__ == "__main__": diff --git a/jsonrpcserver/__init__.py b/jsonrpcserver/__init__.py index f28d26c..0c2589c 100644 --- a/jsonrpcserver/__init__.py +++ b/jsonrpcserver/__init__.py @@ -3,11 +3,13 @@ "Error", "InvalidParams", "JsonRpcError", + "Ok", "Result", "Success", "async_dispatch", "async_dispatch_to_response", "async_dispatch_to_serializable", + "async_method", "dispatch", "dispatch_to_response", "dispatch_to_serializable", @@ -15,14 +17,20 @@ "serve", ] +from returns.result import Result as R from .async_main import ( dispatch as async_dispatch, dispatch_to_response as async_dispatch_to_response, dispatch_to_serializable as async_dispatch_to_serializable, ) +from .async_methods import method as async_method from .exceptions import JsonRpcError from .main import dispatch, dispatch_to_response, dispatch_to_serializable from .methods import method -from .result import Error, InvalidParams, Result, Success +from .result import Error, InvalidParams, Ok, SuccessResult, ErrorResult from .server import serve as serve + +# For backward compatibility +Result = R[SuccessResult, ErrorResult] +Success = Ok diff --git a/jsonrpcserver/async_dispatcher.py b/jsonrpcserver/async_dispatcher.py index 2ed3e8a..f1758ac 100644 --- a/jsonrpcserver/async_dispatcher.py +++ b/jsonrpcserver/async_dispatcher.py @@ -1,12 +1,13 @@ """Async version of dispatcher.py""" from functools import partial +from inspect import signature from itertools import starmap from typing import Any, Callable, Iterable, Tuple, Union import asyncio import logging -from oslash.either import Left # type: ignore +from returns.result import Failure, Result, Success from .dispatcher import ( Deserialized, @@ -15,53 +16,84 @@ extract_args, extract_kwargs, extract_list, - get_method, not_notification, to_response, - validate_args, validate_request, validate_result, ) from .exceptions import JsonRpcError -from .methods import Method, Methods +from .async_methods import Method, Methods from .request import Request -from .result import Result, InternalErrorResult, ErrorResult +from .result import ( + ErrorResult, + InternalErrorResult, + InvalidParamsResult, + MethodNotFoundResult, + SuccessResult, +) from .response import Response, ServerErrorResponse from .utils import make_list -async def call(request: Request, context: Any, method: Method) -> Result: +async def call( + request: Request, context: Any, method: Method +) -> Result[SuccessResult, ErrorResult]: try: result = await method( *extract_args(request, context), **extract_kwargs(request) ) validate_result(result) except JsonRpcError as exc: - return Left(ErrorResult(code=exc.code, message=exc.message, data=exc.data)) + return Failure(ErrorResult(code=exc.code, message=exc.message, data=exc.data)) except Exception as exc: # Other error inside method - Internal error logging.exception(exc) - return Left(InternalErrorResult(str(exc))) + return Failure(InternalErrorResult(str(exc))) return result +def validate_args( + request: Request, context: Any, func: Method +) -> Result[Method, ErrorResult]: + """Ensure the method can be called with the arguments given. + + Returns: Either the function to be called, or an Invalid Params error result. + """ + try: + signature(func).bind(*extract_args(request, context), **extract_kwargs(request)) + except TypeError as exc: + return Failure(InvalidParamsResult(str(exc))) + return Success(func) + + +def get_method(methods: Methods, method_name: str) -> Result[Method, ErrorResult]: + """Get the requested method from the methods dict. + + Returns: Either the function to be called, or a Method Not Found result. + """ + try: + return Success(methods[method_name]) + except KeyError: + return Failure(MethodNotFoundResult(method_name)) + + async def dispatch_request( methods: Methods, context: Any, request: Request -) -> Tuple[Request, Result]: +) -> Tuple[Request, Result[SuccessResult, ErrorResult]]: method = get_method(methods, request.method).bind( partial(validate_args, request, context) ) return ( request, method - if isinstance(method, Left) - else await call(request, context, method._value), + if isinstance(method, Failure) + else await call(request, context, method.unwrap()), ) async def dispatch_deserialized( methods: Methods, context: Any, - post_process: Callable[[Response], Iterable[Any]], + post_process: Callable[[Response], Response], deserialized: Deserialized, ) -> Union[Response, Iterable[Response], None]: results = await asyncio.gather( @@ -85,7 +117,7 @@ async def dispatch_to_response_pure( validator: Callable[[Deserialized], Deserialized], methods: Methods, context: Any, - post_process: Callable[[Response], Iterable[Any]], + post_process: Callable[[Response], Response], request: str, ) -> Union[Response, Iterable[Response], None]: try: @@ -94,11 +126,11 @@ async def dispatch_to_response_pure( ) return ( post_process(result) - if isinstance(result, Left) + if isinstance(result, Failure) else await dispatch_deserialized( - methods, context, post_process, result._value + methods, context, post_process, result.unwrap() ) ) except Exception as exc: logging.exception(exc) - return post_process(Left(ServerErrorResponse(str(exc), None))) + return post_process(Failure(ServerErrorResponse(str(exc), None))) diff --git a/jsonrpcserver/async_main.py b/jsonrpcserver/async_main.py index a5db4fb..061e110 100644 --- a/jsonrpcserver/async_main.py +++ b/jsonrpcserver/async_main.py @@ -3,9 +3,9 @@ from typing import Any, Callable, Dict, Iterable, List, Optional, Union, cast from .async_dispatcher import dispatch_to_response_pure +from .async_methods import Methods, global_methods from .dispatcher import Deserialized from .main import default_validator, default_deserializer -from .methods import Methods, global_methods from .response import Response, to_serializable from .sentinels import NOCONTEXT from .utils import identity diff --git a/jsonrpcserver/async_methods.py b/jsonrpcserver/async_methods.py new file mode 100644 index 0000000..3364bd2 --- /dev/null +++ b/jsonrpcserver/async_methods.py @@ -0,0 +1,31 @@ +from typing import Any, Awaitable, Callable, Dict, Optional, cast + +from returns.result import Result + +from .result import ErrorResult, SuccessResult + +Method = Callable[..., Awaitable[Result[SuccessResult, ErrorResult]]] +Methods = Dict[str, Method] +global_methods: Methods = dict() + + +def method( + f: Optional[Method] = None, name: Optional[str] = None +) -> Callable[..., Awaitable[Any]]: + """A decorator to add a function into jsonrpcserver's internal global_methods dict. + The global_methods dict will be used by default unless a methods argument is passed + to `dispatch`. + + Functions can be renamed by passing a name argument: + + @method(name=bar) + def foo(): + ... + """ + + def decorator(func: Method) -> Method: + nonlocal name + global_methods[name or func.__name__] = func + return func + + return decorator(f) if callable(f) else cast(Method, decorator) diff --git a/jsonrpcserver/dispatcher.py b/jsonrpcserver/dispatcher.py index 74e2644..f4ab864 100644 --- a/jsonrpcserver/dispatcher.py +++ b/jsonrpcserver/dispatcher.py @@ -7,16 +7,16 @@ from typing import Any, Callable, Dict, Iterable, List, Tuple, Union import logging -from oslash.either import Either, Left, Right # type: ignore +from returns.result import Result, Failure, Success from .exceptions import JsonRpcError from .methods import Method, Methods from .request import Request from .response import ( + Response, ErrorResponse, InvalidRequestResponse, ParseErrorResponse, - Response, ServerErrorResponse, SuccessResponse, ) @@ -25,7 +25,6 @@ InternalErrorResult, InvalidParamsResult, MethodNotFoundResult, - Result, SuccessResult, ) from .sentinels import NOCONTEXT, NOID @@ -36,7 +35,7 @@ def extract_list( is_batch: bool, responses: Iterable[Response] -) -> Union[Response, List[Response], None]: +) -> Union[Response, List[Response], None,]: """This is the inverse of make_list. Here we extract a response back out of the list if it wasn't a batch request originally. Also applies a JSON-RPC rule: we do not respond to batches of notifications. @@ -64,7 +63,9 @@ def extract_list( return response_list[0] -def to_response(request: Request, result: Result) -> Response: +def to_response( + request: Request, result: Result[SuccessResult, ErrorResult] +) -> Response: """Maps a Request plus a Result to a Response. A Response is just a Result plus the id from the original Request. @@ -77,9 +78,9 @@ def to_response(request: Request, result: Result) -> Response: """ assert request.id is not NOID return ( - Left(ErrorResponse(**result._error._asdict(), id=request.id)) - if isinstance(result, Left) - else Right(SuccessResponse(**result._value._asdict(), id=request.id)) + Failure(ErrorResponse(**result.failure()._asdict(), id=request.id)) + if isinstance(result, Failure) + else Success(SuccessResponse(**result.unwrap()._asdict(), id=request.id)) ) @@ -102,19 +103,23 @@ def extract_kwargs(request: Request) -> Dict[str, Any]: return request.params if isinstance(request.params, dict) else {} -def validate_result(result: Result) -> None: +def validate_result(result: Result[SuccessResult, ErrorResult]) -> None: """Validate the return value from a method. Raises an AssertionError if the result returned from a method is invalid. Returns: None """ - assert (isinstance(result, Left) and isinstance(result._error, ErrorResult)) or ( - isinstance(result, Right) and isinstance(result._value, SuccessResult) + assert ( + isinstance(result, Failure) and isinstance(result.failure(), ErrorResult) + ) or ( + isinstance(result, Success) and isinstance(result.unwrap(), SuccessResult) ), f"The method did not return a valid Result (returned {result!r})" -def call(request: Request, context: Any, method: Method) -> Result: +def call( + request: Request, context: Any, method: Method +) -> Result[SuccessResult, ErrorResult]: """Call the method. Handles any exceptions raised in the method, being sure to return an Error response. @@ -130,17 +135,17 @@ def call(request: Request, context: Any, method: Method) -> Result: # Raising JsonRpcError inside the method is an alternative way of returning an error # response. except JsonRpcError as exc: - return Left(ErrorResult(code=exc.code, message=exc.message, data=exc.data)) + return Failure(ErrorResult(code=exc.code, message=exc.message, data=exc.data)) # Any other uncaught exception inside method - internal error. except Exception as exc: logging.exception(exc) - return Left(InternalErrorResult(str(exc))) + return Failure(InternalErrorResult(str(exc))) return result def validate_args( request: Request, context: Any, func: Method -) -> Either[ErrorResult, Method]: +) -> Result[Method, ErrorResult]: """Ensure the method can be called with the arguments given. Returns: Either the function to be called, or an Invalid Params error result. @@ -148,24 +153,24 @@ def validate_args( try: signature(func).bind(*extract_args(request, context), **extract_kwargs(request)) except TypeError as exc: - return Left(InvalidParamsResult(str(exc))) - return Right(func) + return Failure(InvalidParamsResult(str(exc))) + return Success(func) -def get_method(methods: Methods, method_name: str) -> Either[ErrorResult, Method]: +def get_method(methods: Methods, method_name: str) -> Result[Method, ErrorResult]: """Get the requested method from the methods dict. Returns: Either the function to be called, or a Method Not Found result. """ try: - return Right(methods[method_name]) + return Success(methods[method_name]) except KeyError: - return Left(MethodNotFoundResult(method_name)) + return Failure(MethodNotFoundResult(method_name)) def dispatch_request( methods: Methods, context: Any, request: Request -) -> Tuple[Request, Result]: +) -> Tuple[Request, Result[SuccessResult, ErrorResult]]: """Get the method, validates the arguments and calls the method. Returns: A tuple containing the Result of the method, along with the original @@ -198,14 +203,14 @@ def not_notification(request_result: Any) -> bool: def dispatch_deserialized( methods: Methods, context: Any, - post_process: Callable[[Response], Iterable[Any]], + post_process: Callable[[Response], Response], deserialized: Deserialized, -) -> Union[Response, List[Response], None]: +) -> Union[Response, List[Response], None,]: """This is simply continuing the pipeline from dispatch_to_response_pure. It exists only to be an abstraction, otherwise that function is doing too much. It continues on from the request string having been parsed and validated. - Returns: A Response, a list of Responses, or None. If post_process is passed, it's + Returns: A Result, a list of Results, or None. If post_process is passed, it's applied to the Response(s). """ results = map( @@ -218,7 +223,7 @@ def dispatch_deserialized( def validate_request( validator: Callable[[Deserialized], Deserialized], request: Deserialized -) -> Either[ErrorResponse, Deserialized]: +) -> Result[Deserialized, ErrorResponse]: """Validate the request against a JSON-RPC schema. Ensures the parsed request is valid JSON-RPC. @@ -231,24 +236,24 @@ def validate_request( # unknown. Any exception raised we assume the request is invalid and return an # "invalid request" response. except Exception as exc: - return Left(InvalidRequestResponse("The request failed schema validation")) - return Right(request) + return Failure(InvalidRequestResponse("The request failed schema validation")) + return Success(request) def deserialize_request( deserializer: Callable[[str], Deserialized], request: str -) -> Either[ErrorResponse, Deserialized]: +) -> Result[Deserialized, ErrorResponse]: """Parse the JSON request string. Returns: Either the deserialized request or a "Parse Error" response. """ try: - return Right(deserializer(request)) + return Success(deserializer(request)) # Since the deserializer is unknown, the specific exception that will be raised is # also unknown. Any exception raised we assume the request is invalid, return a # parse error response. except Exception as exc: - return Left(ParseErrorResponse(str(exc))) + return Failure(ParseErrorResponse(str(exc))) def dispatch_to_response_pure( @@ -257,7 +262,7 @@ def dispatch_to_response_pure( validator: Callable[[Deserialized], Deserialized], methods: Methods, context: Any, - post_process: Callable[[Response], Iterable[Any]], + post_process: Callable[[Response], Response], request: str, ) -> Union[Response, List[Response], None]: """A function from JSON-RPC request string to Response namedtuple(s), (yet to be @@ -273,10 +278,10 @@ def dispatch_to_response_pure( ) return ( post_process(result) - if isinstance(result, Left) - else dispatch_deserialized(methods, context, post_process, result._value) + if isinstance(result, Failure) + else dispatch_deserialized(methods, context, post_process, result.unwrap()) ) except Exception as exc: # There was an error with the jsonrpcserver library. logging.exception(exc) - return post_process(Left(ServerErrorResponse(str(exc), None))) + return post_process(Failure(ServerErrorResponse(str(exc), None))) diff --git a/jsonrpcserver/methods.py b/jsonrpcserver/methods.py index f0d8fbe..9ba30d8 100644 --- a/jsonrpcserver/methods.py +++ b/jsonrpcserver/methods.py @@ -13,12 +13,13 @@ """ from typing import Any, Callable, Dict, Optional, cast -from .result import Result +from returns.result import Result -Method = Callable[..., Result] -Methods = Dict[str, Method] +from .result import ErrorResult, SuccessResult -global_methods = dict() +Method = Callable[..., Result[SuccessResult, ErrorResult]] +Methods = Dict[str, Method] +global_methods: Methods = dict() def method( diff --git a/jsonrpcserver/response.py b/jsonrpcserver/response.py index cc5d010..e802bad 100644 --- a/jsonrpcserver/response.py +++ b/jsonrpcserver/response.py @@ -2,9 +2,9 @@ https://www.jsonrpc.org/specification#response_object """ -from typing import Any, Dict, List, Type, NamedTuple, Union +from typing import Any, Dict, List, NamedTuple, Union -from oslash.either import Either, Left # type: ignore +from returns.result import Result, Failure from .codes import ( ERROR_INVALID_REQUEST, @@ -39,8 +39,7 @@ class ErrorResponse(NamedTuple): id: Any -Response = Either[ErrorResponse, SuccessResponse] -ResponseType = Type[Either[ErrorResponse, SuccessResponse]] +Response = Result[SuccessResponse, ErrorResponse] def ParseErrorResponse(data: Any) -> ErrorResponse: @@ -86,15 +85,15 @@ def serialize_success(response: SuccessResponse) -> Dict[str, Any]: return {"jsonrpc": "2.0", "result": response.result, "id": response.id} -def to_serializable_one(response: ResponseType) -> Union[Deserialized, None]: +def to_serializable_one(response: Response) -> Union[Deserialized, None]: return ( - serialize_error(response._error) - if isinstance(response, Left) - else serialize_success(response._value) + serialize_error(response.failure()) + if isinstance(response, Failure) + else serialize_success(response.unwrap()) ) -def to_serializable(response: ResponseType) -> Union[Deserialized, None]: +def to_serializable(response: Response) -> Union[Deserialized, None]: if response is None: return None elif isinstance(response, List): diff --git a/jsonrpcserver/result.py b/jsonrpcserver/result.py index 889b24e..2943fd9 100644 --- a/jsonrpcserver/result.py +++ b/jsonrpcserver/result.py @@ -8,7 +8,7 @@ """ from typing import Any, NamedTuple -from oslash.either import Either, Left, Right # type: ignore +from returns.result import Failure, Result, Success from .codes import ERROR_INVALID_PARAMS, ERROR_METHOD_NOT_FOUND, ERROR_INTERNAL_ERROR from .sentinels import NODATA @@ -30,10 +30,6 @@ def __repr__(self) -> str: return f"ErrorResult(code={self.code!r}, message={self.message!r}, data={self.data!r})" -# Union of the two valid result types -Result = Either[ErrorResult, SuccessResult] - - # Helpers @@ -52,16 +48,16 @@ def InvalidParamsResult(data: Any = NODATA) -> ErrorResult: # Helpers (the public functions) -def Success(*args: Any, **kwargs: Any) -> Either[ErrorResult, SuccessResult]: - return Right(SuccessResult(*args, **kwargs)) +def Ok(*args: Any, **kwargs: Any) -> Result[SuccessResult, ErrorResult]: + return Success(SuccessResult(*args, **kwargs)) -def Error(*args: Any, **kwargs: Any) -> Either[ErrorResult, SuccessResult]: - return Left(ErrorResult(*args, **kwargs)) +def Error(*args: Any, **kwargs: Any) -> Result[SuccessResult, ErrorResult]: + return Failure(ErrorResult(*args, **kwargs)) -def InvalidParams(*args: Any, **kwargs: Any) -> Either[ErrorResult, SuccessResult]: +def InvalidParams(*args: Any, **kwargs: Any) -> Result[SuccessResult, ErrorResult]: """InvalidParams is a shortcut to save you from having to pass the Invalid Params JSON-RPC code to Error. """ - return Left(InvalidParamsResult(*args, **kwargs)) + return Failure(InvalidParamsResult(*args, **kwargs)) diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..9bb71fa --- /dev/null +++ b/mypy.ini @@ -0,0 +1,3 @@ +[mypy] +plugins = + returns.contrib.mypy.returns_plugin diff --git a/setup.py b/setup.py index 9a1ef13..cec2012 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ ], }, include_package_data=True, - install_requires=["jsonschema<5", "oslash<1"], + install_requires=["jsonschema<5", "returns<1"], license="MIT", long_description=README, long_description_content_type="text/markdown", diff --git a/tests/test_async_dispatcher.py b/tests/test_async_dispatcher.py index 0d89fd2..8c898ba 100644 --- a/tests/test_async_dispatcher.py +++ b/tests/test_async_dispatcher.py @@ -1,7 +1,7 @@ from unittest.mock import patch import pytest -from oslash.either import Left, Right +from returns.result import Failure, Success from jsonrpcserver.async_dispatcher import ( call, @@ -14,18 +14,18 @@ from jsonrpcserver.exceptions import JsonRpcError from jsonrpcserver.request import Request from jsonrpcserver.response import ErrorResponse, SuccessResponse -from jsonrpcserver.result import ErrorResult, Result, Success, SuccessResult +from jsonrpcserver.result import ErrorResult, Result, Ok, SuccessResult from jsonrpcserver.sentinels import NOCONTEXT, NODATA from jsonrpcserver.utils import identity async def ping() -> Result: - return Success("pong") + return Ok("pong") @pytest.mark.asyncio async def test_call(): - assert await call(Request("ping", [], 1), NOCONTEXT, ping) == Right( + assert await call(Request("ping", [], 1), NOCONTEXT, ping) == Success( SuccessResult("pong") ) @@ -35,7 +35,7 @@ async def test_call_raising_jsonrpcerror(): def method(): raise JsonRpcError(code=1, message="foo", data=NODATA) - assert await call(Request("ping", [], 1), NOCONTEXT, method) == Left( + assert await call(Request("ping", [], 1), NOCONTEXT, method) == Failure( ErrorResult(1, "foo") ) @@ -45,7 +45,7 @@ async def test_call_raising_exception(): def method(): raise ValueError("foo") - assert await call(Request("ping", [], 1), NOCONTEXT, method) == Left( + assert await call(Request("ping", [], 1), NOCONTEXT, method) == Failure( ErrorResult(ERROR_INTERNAL_ERROR, "Internal error", "foo") ) @@ -55,52 +55,43 @@ async def test_dispatch_request(): request = Request("ping", [], 1) assert await dispatch_request({"ping": ping}, NOCONTEXT, request) == ( request, - Right(SuccessResult("pong")), + Success(SuccessResult("pong")), ) @pytest.mark.asyncio async def test_dispatch_deserialized(): - assert ( - await dispatch_deserialized( - {"ping": ping}, - NOCONTEXT, - identity, - {"jsonrpc": "2.0", "method": "ping", "id": 1}, - ) - == Right(SuccessResponse("pong", 1)) - ) + assert await dispatch_deserialized( + {"ping": ping}, + NOCONTEXT, + identity, + {"jsonrpc": "2.0", "method": "ping", "id": 1}, + ) == Success(SuccessResponse("pong", 1)) @pytest.mark.asyncio async def test_dispatch_to_response_pure_success(): - assert ( - await dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"ping": ping}, - request='{"jsonrpc": "2.0", "method": "ping", "id": 1}', - ) - == Right(SuccessResponse("pong", 1)) - ) + assert await dispatch_to_response_pure( + deserializer=default_deserializer, + validator=default_validator, + post_process=identity, + context=NOCONTEXT, + methods={"ping": ping}, + request='{"jsonrpc": "2.0", "method": "ping", "id": 1}', + ) == Success(SuccessResponse("pong", 1)) @patch("jsonrpcserver.async_dispatcher.dispatch_request", side_effect=ValueError("foo")) @pytest.mark.asyncio async def test_dispatch_to_response_pure_server_error(*_): async def foo(): - return Success() - - assert ( - await dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"foo": foo}, - request='{"jsonrpc": "2.0", "method": "foo", "id": 1}', - ) - == Left(ErrorResponse(ERROR_SERVER_ERROR, "Server error", "foo", None)) - ) + return Ok() + + assert await dispatch_to_response_pure( + deserializer=default_deserializer, + validator=default_validator, + post_process=identity, + context=NOCONTEXT, + methods={"foo": foo}, + request='{"jsonrpc": "2.0", "method": "foo", "id": 1}', + ) == Failure(ErrorResponse(ERROR_SERVER_ERROR, "Server error", "foo", None)) diff --git a/tests/test_async_main.py b/tests/test_async_main.py index be605d1..13645ae 100644 --- a/tests/test_async_main.py +++ b/tests/test_async_main.py @@ -1,6 +1,6 @@ import pytest -from oslash.either import Right +from returns.result import Success from jsonrpcserver.async_main import ( dispatch_to_response, @@ -8,18 +8,18 @@ dispatch_to_json, ) from jsonrpcserver.response import SuccessResponse -from jsonrpcserver.result import Result, Success +from jsonrpcserver.result import Result, Ok async def ping() -> Result: - return Success("pong") + return Ok("pong") @pytest.mark.asyncio async def test_dispatch_to_response(): assert await dispatch_to_response( '{"jsonrpc": "2.0", "method": "ping", "id": 1}', {"ping": ping} - ) == Right(SuccessResponse("pong", 1)) + ) == Success(SuccessResponse("pong", 1)) @pytest.mark.asyncio diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 145c4c5..b094bdf 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -4,8 +4,9 @@ import json import pytest -from oslash.either import Left, Right +from returns.result import Failure, Success +from jsonrpcserver import Result from jsonrpcserver.codes import ( ERROR_INTERNAL_ERROR, ERROR_INVALID_PARAMS, @@ -38,20 +39,13 @@ from jsonrpcserver.methods import method from jsonrpcserver.request import Request from jsonrpcserver.response import ErrorResponse, SuccessResponse -from jsonrpcserver.result import ( - ErrorResult, - InvalidParams, - Result, - Success, - SuccessResult, - NODATA, -) +from jsonrpcserver.result import ErrorResult, InvalidParams, Ok, SuccessResult, NODATA from jsonrpcserver.sentinels import NOCONTEXT, NOID from jsonrpcserver.utils import identity def ping() -> Result: - return Success("pong") + return Ok("pong") # extract_list @@ -80,21 +74,21 @@ def test_extract_list_batch_all_notifications(): def test_to_response_SuccessResult(): assert to_response( - Request("ping", [], sentinel.id), Right(SuccessResult(sentinel.result)) - ) == Right(SuccessResponse(sentinel.result, sentinel.id)) + Request("ping", [], sentinel.id), Success(SuccessResult(sentinel.result)) + ) == Success(SuccessResponse(sentinel.result, sentinel.id)) def test_to_response_ErrorResult(): assert ( to_response( Request("ping", [], sentinel.id), - Left( + Failure( ErrorResult( code=sentinel.code, message=sentinel.message, data=sentinel.data ) ), ) - ) == Left( + ) == Failure( ErrorResponse(sentinel.code, sentinel.message, sentinel.data, sentinel.id) ) @@ -102,11 +96,11 @@ def test_to_response_ErrorResult(): def test_to_response_InvalidParams(): assert to_response( Request("ping", [], sentinel.id), InvalidParams(sentinel.data) - ) == Left(ErrorResponse(-32602, "Invalid params", sentinel.data, sentinel.id)) + ) == Failure(ErrorResponse(-32602, "Invalid params", sentinel.data, sentinel.id)) def test_to_response_InvalidParams_no_data(): - assert to_response(Request("ping", [], sentinel.id), InvalidParams()) == Left( + assert to_response(Request("ping", [], sentinel.id), InvalidParams()) == Failure( ErrorResponse(-32602, "Invalid params", NODATA, sentinel.id) ) @@ -139,11 +133,13 @@ def test_extract_kwargs(): def test_validate_result_no_arguments(): f = lambda: None - assert validate_args(Request("f", [], NOID), NOCONTEXT, f) == Right(f) + assert validate_args(Request("f", [], NOID), NOCONTEXT, f) == Success(f) def test_validate_result_no_arguments_too_many_positionals(): - assert validate_args(Request("f", ["foo"], NOID), NOCONTEXT, lambda: None) == Left( + assert validate_args( + Request("f", ["foo"], NOID), NOCONTEXT, lambda: None + ) == Failure( ErrorResult( code=ERROR_INVALID_PARAMS, message="Invalid params", @@ -154,13 +150,13 @@ def test_validate_result_no_arguments_too_many_positionals(): def test_validate_result_positionals(): f = lambda x: None - assert validate_args(Request("f", [1], NOID), NOCONTEXT, f) == Right(f) + assert validate_args(Request("f", [1], NOID), NOCONTEXT, f) == Success(f) def test_validate_result_positionals_not_passed(): assert validate_args( Request("f", {"foo": "bar"}, NOID), NOCONTEXT, lambda x: None - ) == Left( + ) == Failure( ErrorResult( ERROR_INVALID_PARAMS, "Invalid params", "missing a required argument: 'x'" ) @@ -169,7 +165,7 @@ def test_validate_result_positionals_not_passed(): def test_validate_result_keywords(): f = lambda **kwargs: None - assert validate_args(Request("f", {"foo": "bar"}, NOID), NOCONTEXT, f) == Right(f) + assert validate_args(Request("f", {"foo": "bar"}, NOID), NOCONTEXT, f) == Success(f) def test_validate_result_object_method(): @@ -178,21 +174,23 @@ def foo(self, one, two): return "bar" f = FooClass().foo - assert validate_args(Request("f", ["one", "two"], NOID), NOCONTEXT, f) == Right(f) + assert validate_args(Request("f", ["one", "two"], NOID), NOCONTEXT, f) == Success(f) # call def test_call(): - assert call(Request("ping", [], 1), NOCONTEXT, ping) == Right(SuccessResult("pong")) + assert call(Request("ping", [], 1), NOCONTEXT, ping) == Success( + SuccessResult("pong") + ) def test_call_raising_jsonrpcerror(): def method(): raise JsonRpcError(code=1, message="foo", data=NODATA) - assert call(Request("ping", [], 1), NOCONTEXT, method) == Left( + assert call(Request("ping", [], 1), NOCONTEXT, method) == Failure( ErrorResult(1, "foo") ) @@ -201,7 +199,7 @@ def test_call_raising_exception(): def method(): raise ValueError("foo") - assert call(Request("ping", [], 1), NOCONTEXT, method) == Left( + assert call(Request("ping", [], 1), NOCONTEXT, method) == Failure( ErrorResult(ERROR_INTERNAL_ERROR, "Internal error", "foo") ) @@ -210,11 +208,11 @@ def method(): def test_validate_args(): - assert validate_args(Request("ping", [], 1), NOCONTEXT, ping) == Right(ping) + assert validate_args(Request("ping", [], 1), NOCONTEXT, ping) == Success(ping) def test_validate_args(): - assert validate_args(Request("ping", ["foo"], 1), NOCONTEXT, ping) == Left( + assert validate_args(Request("ping", ["foo"], 1), NOCONTEXT, ping) == Failure( ErrorResult( ERROR_INVALID_PARAMS, "Invalid params", "too many positional arguments" ) @@ -225,11 +223,11 @@ def test_validate_args(): def test_get_method(): - assert get_method({"ping": ping}, "ping") == Right(ping) + assert get_method({"ping": ping}, "ping") == Success(ping) def test_get_method(): - assert get_method({"ping": ping}, "non-existant") == Left( + assert get_method({"ping": ping}, "non-existant") == Failure( ErrorResult(ERROR_METHOD_NOT_FOUND, "Method not found", "non-existant") ) @@ -241,14 +239,14 @@ def test_dispatch_request(): request = Request("ping", [], 1) assert dispatch_request({"ping": ping}, NOCONTEXT, request) == ( request, - Right(SuccessResult("pong")), + Success(SuccessResult("pong")), ) def test_dispatch_request_with_context(): def ping_with_context(context: Any): assert context is sentinel.context - return Success() + return Ok() dispatch_request( {"ping_with_context": ping_with_context}, @@ -281,15 +279,12 @@ def test_not_notification_false(): def test_dispatch_deserialized(): - assert ( - dispatch_deserialized( - methods={"ping": ping}, - context=NOCONTEXT, - post_process=identity, - deserialized={"jsonrpc": "2.0", "method": "ping", "id": 1}, - ) - == Right(SuccessResponse("pong", 1)) - ) + assert dispatch_deserialized( + methods={"ping": ping}, + context=NOCONTEXT, + post_process=identity, + deserialized={"jsonrpc": "2.0", "method": "ping", "id": 1}, + ) == Success(SuccessResponse("pong", 1)) # validate_request @@ -297,11 +292,11 @@ def test_dispatch_deserialized(): def test_validate_request(): request = {"jsonrpc": "2.0", "method": "ping"} - assert validate_request(default_validator, request) == Right(request) + assert validate_request(default_validator, request) == Success(request) def test_validate_request_invalid(): - assert validate_request(default_validator, {"jsonrpc": "2.0"}) == Left( + assert validate_request(default_validator, {"jsonrpc": "2.0"}) == Failure( ErrorResponse( ERROR_INVALID_REQUEST, "Invalid request", @@ -318,7 +313,7 @@ def test_dispatch_request(): request = Request("ping", [], 1) assert dispatch_request({"ping": ping}, NOCONTEXT, request) == ( request, - Right(SuccessResult("pong")), + Success(SuccessResult("pong")), ) @@ -326,17 +321,14 @@ def test_dispatch_request(): def test_dispatch_to_response_pure(): - assert ( - dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"ping": ping}, - request='{"jsonrpc": "2.0", "method": "ping", "id": 1}', - ) - == Right(SuccessResponse("pong", 1)) - ) + assert dispatch_to_response_pure( + deserializer=default_deserializer, + validator=default_validator, + post_process=identity, + context=NOCONTEXT, + methods={"ping": ping}, + request='{"jsonrpc": "2.0", "method": "ping", "id": 1}', + ) == Success(SuccessResponse("pong", 1)) def test_dispatch_to_response_pure_parse_error(): @@ -348,7 +340,7 @@ def test_dispatch_to_response_pure_parse_error(): context=NOCONTEXT, methods={"ping": ping}, request="{", - ) == Left( + ) == Failure( ErrorResponse( ERROR_PARSE_ERROR, "Parse error", @@ -369,7 +361,7 @@ def test_dispatch_to_response_pure_invalid_request(): context=NOCONTEXT, methods={"ping": ping}, request="{}", - ) == Left( + ) == Failure( ErrorResponse( ERROR_INVALID_REQUEST, "Invalid request", @@ -387,14 +379,14 @@ def test_dispatch_to_response_pure_method_not_found(): context=NOCONTEXT, methods={}, request='{"jsonrpc": "2.0", "method": "non_existant", "id": 1}', - ) == Left( + ) == Failure( ErrorResponse(ERROR_METHOD_NOT_FOUND, "Method not found", "non_existant", 1) ) def test_dispatch_to_response_pure_invalid_params_auto(): def foo(colour: str, size: str): - return Success() + return Ok() assert dispatch_to_response_pure( deserializer=default_deserializer, @@ -403,7 +395,7 @@ def foo(colour: str, size: str): context=NOCONTEXT, methods={"foo": foo}, request='{"jsonrpc": "2.0", "method": "foo", "params": {"colour":"blue"}, "id": 1}', - ) == Left( + ) == Failure( ErrorResponse( ERROR_INVALID_PARAMS, "Invalid params", @@ -418,52 +410,43 @@ def foo(colour: str) -> Result: if colour not in ("orange", "red", "yellow"): return InvalidParams() - assert ( - dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"foo": foo}, - request='{"jsonrpc": "2.0", "method": "foo", "params": ["blue"], "id": 1}', - ) - == Left(ErrorResponse(ERROR_INVALID_PARAMS, "Invalid params", NODATA, 1)) - ) + assert dispatch_to_response_pure( + deserializer=default_deserializer, + validator=default_validator, + post_process=identity, + context=NOCONTEXT, + methods={"foo": foo}, + request='{"jsonrpc": "2.0", "method": "foo", "params": ["blue"], "id": 1}', + ) == Failure(ErrorResponse(ERROR_INVALID_PARAMS, "Invalid params", NODATA, 1)) def test_dispatch_to_response_pure_internal_error(): def foo(): raise ValueError("foo") - assert ( - dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"foo": foo}, - request='{"jsonrpc": "2.0", "method": "foo", "id": 1}', - ) - == Left(ErrorResponse(ERROR_INTERNAL_ERROR, "Internal error", "foo", 1)) - ) + assert dispatch_to_response_pure( + deserializer=default_deserializer, + validator=default_validator, + post_process=identity, + context=NOCONTEXT, + methods={"foo": foo}, + request='{"jsonrpc": "2.0", "method": "foo", "id": 1}', + ) == Failure(ErrorResponse(ERROR_INTERNAL_ERROR, "Internal error", "foo", 1)) @patch("jsonrpcserver.dispatcher.dispatch_request", side_effect=ValueError("foo")) def test_dispatch_to_response_pure_server_error(*_): def foo(): - return Success() + return Ok() - assert ( - dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"foo": foo}, - request='{"jsonrpc": "2.0", "method": "foo", "id": 1}', - ) - == Left(ErrorResponse(ERROR_SERVER_ERROR, "Server error", "foo", None)) - ) + assert dispatch_to_response_pure( + deserializer=default_deserializer, + validator=default_validator, + post_process=identity, + context=NOCONTEXT, + methods={"foo": foo}, + request='{"jsonrpc": "2.0", "method": "foo", "id": 1}', + ) == Failure(ErrorResponse(ERROR_SERVER_ERROR, "Server error", "foo", None)) def test_dispatch_to_response_pure_invalid_result(): @@ -479,7 +462,7 @@ def not_a_result(): context=NOCONTEXT, methods={"not_a_result": not_a_result}, request='{"jsonrpc": "2.0", "method": "not_a_result", "id": 1}', - ) == Left( + ) == Failure( ErrorResponse( ERROR_INTERNAL_ERROR, "Internal error", @@ -495,17 +478,14 @@ def test_dispatch_to_response_pure_raising_exception(): def raise_exception(): raise JsonRpcError(code=0, message="foo", data="bar") - assert ( - dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"raise_exception": raise_exception}, - request='{"jsonrpc": "2.0", "method": "raise_exception", "id": 1}', - ) - == Left(ErrorResponse(0, "foo", "bar", 1)) - ) + assert dispatch_to_response_pure( + deserializer=default_deserializer, + validator=default_validator, + post_process=identity, + context=NOCONTEXT, + methods={"raise_exception": raise_exception}, + request='{"jsonrpc": "2.0", "method": "raise_exception", "id": 1}', + ) == Failure(ErrorResponse(0, "foo", "bar", 1)) # dispatch_to_response_pure -- Notifications @@ -534,7 +514,7 @@ def test_dispatch_to_response_pure_notification_parse_error(): context=NOCONTEXT, methods={"ping": ping}, request="{", - ) == Left( + ) == Failure( ErrorResponse( ERROR_PARSE_ERROR, "Parse error", @@ -553,7 +533,7 @@ def test_dispatch_to_response_pure_notification_invalid_request(): context=NOCONTEXT, methods={"ping": ping}, request="{}", - ) == Left( + ) == Failure( ErrorResponse( ERROR_INVALID_REQUEST, "Invalid request", @@ -579,7 +559,7 @@ def test_dispatch_to_response_pure_notification_method_not_found(): def test_dispatch_to_response_pure_notification_invalid_params_auto(): def foo(colour: str, size: str): - return Success() + return Ok() assert ( dispatch_to_response_pure( @@ -632,19 +612,16 @@ def foo(bar): @patch("jsonrpcserver.dispatcher.dispatch_request", side_effect=ValueError("foo")) def test_dispatch_to_response_pure_notification_server_error(*_): def foo(): - return Success() + return Ok() - assert ( - dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"foo": foo}, - request='{"jsonrpc": "2.0", "method": "foo"}', - ) - == Left(ErrorResponse(ERROR_SERVER_ERROR, "Server error", "foo", None)) - ) + assert dispatch_to_response_pure( + deserializer=default_deserializer, + validator=default_validator, + post_process=identity, + context=NOCONTEXT, + methods={"foo": foo}, + request='{"jsonrpc": "2.0", "method": "foo"}', + ) == Failure(ErrorResponse(ERROR_SERVER_ERROR, "Server error", "foo", None)) def test_dispatch_to_response_pure_notification_invalid_result(): @@ -692,16 +669,16 @@ def test_dispatch_to_response(): response = dispatch_to_response( '{"jsonrpc": "2.0", "method": "ping", "id": 1}', {"ping": ping} ) - assert response == Right(SuccessResponse("pong", 1)) + assert response == Success(SuccessResponse("pong", 1)) def test_dispatch_to_response_with_global_methods(): @method def ping(): - return Success("pong") + return Ok("pong") response = dispatch_to_response('{"jsonrpc": "2.0", "method": "ping", "id": 1}') - assert response == Right(SuccessResponse("pong", 1)) + assert response == Success(SuccessResponse("pong", 1)) # The remaining tests are direct from the examples in the specification @@ -709,7 +686,7 @@ def ping(): def test_examples_positionals(): def subtract(minuend, subtrahend): - return Success(minuend - subtrahend) + return Ok(minuend - subtrahend) response = dispatch_to_response_pure( methods={"subtract": subtract}, @@ -719,7 +696,7 @@ def subtract(minuend, subtrahend): deserializer=default_deserializer, request='{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}', ) - assert response == Right(SuccessResponse(19, 1)) + assert response == Success(SuccessResponse(19, 1)) # Second example response = dispatch_to_response_pure( @@ -730,12 +707,12 @@ def subtract(minuend, subtrahend): deserializer=default_deserializer, request='{"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}', ) - assert response == Right(SuccessResponse(-19, 2)) + assert response == Success(SuccessResponse(-19, 2)) def test_examples_nameds(): def subtract(**kwargs): - return Success(kwargs["minuend"] - kwargs["subtrahend"]) + return Ok(kwargs["minuend"] - kwargs["subtrahend"]) response = dispatch_to_response_pure( methods={"subtract": subtract}, @@ -745,7 +722,7 @@ def subtract(**kwargs): deserializer=default_deserializer, request='{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}', ) - assert response == Right(SuccessResponse(19, 3)) + assert response == Success(SuccessResponse(19, 3)) # Second example response = dispatch_to_response_pure( @@ -756,7 +733,7 @@ def subtract(**kwargs): deserializer=default_deserializer, request='{"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}', ) - assert response == Right(SuccessResponse(19, 4)) + assert response == Success(SuccessResponse(19, 4)) def test_examples_notification(): @@ -791,7 +768,7 @@ def test_examples_invalid_json(): deserializer=default_deserializer, request='[{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, {"jsonrpc": "2.0", "method"]', ) - assert response == Left( + assert response == Failure( ErrorResponse( ERROR_PARSE_ERROR, "Parse error", @@ -811,7 +788,7 @@ def test_examples_empty_array(): post_process=identity, deserializer=default_deserializer, ) - assert response == Left( + assert response == Failure( ErrorResponse( ERROR_INVALID_REQUEST, "Invalid request", @@ -834,7 +811,7 @@ def test_examples_invalid_jsonrpc_batch(): methods={"ping": ping}, request="[1]", ) - assert response == Left( + assert response == Failure( ErrorResponse( ERROR_INVALID_REQUEST, "Invalid request", @@ -857,7 +834,7 @@ def test_examples_multiple_invalid_jsonrpc(): methods={"ping": ping}, request="[1, 2, 3]", ) - assert response == Left( + assert response == Failure( ErrorResponse( ERROR_INVALID_REQUEST, "Invalid request", @@ -878,10 +855,10 @@ def test_examples_mixed_requests_and_notifications(): {"foo": "boo"}, """ methods = { - "sum": lambda *args: Right(SuccessResult(sum(args))), - "notify_hello": lambda *args: Right(SuccessResult(19)), - "subtract": lambda *args: Right(SuccessResult(args[0] - sum(args[1:]))), - "get_data": lambda: Right(SuccessResult(["hello", 5])), + "sum": lambda *args: Success(SuccessResult(sum(args))), + "notify_hello": lambda *args: Success(SuccessResult(19)), + "subtract": lambda *args: Success(SuccessResult(args[0] - sum(args[1:]))), + "get_data": lambda: Success(SuccessResult(["hello", 5])), } requests = json.dumps( [ @@ -906,13 +883,13 @@ def test_examples_mixed_requests_and_notifications(): request=requests, ) expected = [ - Right( + Success( SuccessResponse(result=7, id="1") ), # {"jsonrpc": "2.0", "result": 7, "id": "1"}, - Right( + Success( SuccessResponse(result=19, id="2") ), # {"jsonrpc": "2.0", "result": 19, "id": "2"}, - Left( + Failure( ErrorResponse( code=-32601, message="Method not found", data="foo.get", id="5" ) @@ -922,7 +899,7 @@ def test_examples_mixed_requests_and_notifications(): # "error": {"code": -32601, "message": "Method not found", "data": "foo.get"}, # "id": "5", # }, - Right( + Success( SuccessResponse(result=["hello", 5], id="9") ), # {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}, ] diff --git a/tests/test_main.py b/tests/test_main.py index 81bbfc7..f36e71a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,4 +1,4 @@ -from oslash.either import Right +from returns.result import Success from jsonrpcserver.main import ( dispatch_to_response, @@ -6,17 +6,17 @@ dispatch_to_json, ) from jsonrpcserver.response import SuccessResponse -from jsonrpcserver.result import Result, Success +from jsonrpcserver.result import Result, Ok def ping() -> Result: - return Success("pong") + return Ok("pong") def test_dispatch_to_response(): assert dispatch_to_response( '{"jsonrpc": "2.0", "method": "ping", "id": 1}', {"ping": ping} - ) == Right(SuccessResponse("pong", 1)) + ) == Success(SuccessResponse("pong", 1)) def test_dispatch_to_serializable(): diff --git a/tests/test_response.py b/tests/test_response.py index d84feee..28f67b5 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -1,6 +1,6 @@ from unittest.mock import sentinel -from oslash.either import Left, Right +from returns.result import Failure, Success from jsonrpcserver.response import ( ErrorResponse, @@ -62,7 +62,7 @@ def test_ServerErrorResponse(): def test_to_serializable(): - assert to_serializable(Right(SuccessResponse(sentinel.result, sentinel.id))) == { + assert to_serializable(Success(SuccessResponse(sentinel.result, sentinel.id))) == { "jsonrpc": "2.0", "result": sentinel.result, "id": sentinel.id, @@ -74,7 +74,7 @@ def test_to_serializable_None(): def test_to_serializable_SuccessResponse(): - assert to_serializable(Right(SuccessResponse(sentinel.result, sentinel.id))) == { + assert to_serializable(Success(SuccessResponse(sentinel.result, sentinel.id))) == { "jsonrpc": "2.0", "result": sentinel.result, "id": sentinel.id, @@ -83,7 +83,9 @@ def test_to_serializable_SuccessResponse(): def test_to_serializable_ErrorResponse(): assert to_serializable( - Left(ErrorResponse(sentinel.code, sentinel.message, sentinel.data, sentinel.id)) + Failure( + ErrorResponse(sentinel.code, sentinel.message, sentinel.data, sentinel.id) + ) ) == { "jsonrpc": "2.0", "error": { @@ -96,7 +98,9 @@ def test_to_serializable_ErrorResponse(): def test_to_serializable_list(): - assert to_serializable([Right(SuccessResponse(sentinel.result, sentinel.id))]) == [ + assert to_serializable( + [Success(SuccessResponse(sentinel.result, sentinel.id))] + ) == [ { "jsonrpc": "2.0", "result": sentinel.result, diff --git a/tests/test_result.py b/tests/test_result.py index 52d483e..6903254 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -1,13 +1,13 @@ from unittest.mock import sentinel -from oslash.either import Left, Right +from returns.result import Failure, Success from jsonrpcserver.result import ( + Ok, Error, ErrorResult, InvalidParamsResult, NODATA, - Success, SuccessResult, ) @@ -55,9 +55,9 @@ def test_InvalidParamsResult_with_data(): assert result.data == sentinel.data -def test_Success(): - assert Success() == Right(SuccessResult(None)) +def test_Ok(): + assert Ok(None) == Success(SuccessResult(None)) def test_Error(): - assert Error(1, "foo", None) == Left(ErrorResult(1, "foo", None)) + assert Error(1, "foo", None) == Failure(ErrorResult(1, "foo", None)) From c1426f6c0ab8ed24307debc521b99058bd173e67 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Sat, 14 May 2022 18:04:37 +1000 Subject: [PATCH 02/33] Fix github action --- .github/workflows/code-quality.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 3ce4de1..3934369 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -10,7 +10,7 @@ jobs: with: python-version: 3.x - run: pip install --upgrade pip - - run: pip install black==22.3.0 pylint==v3.0.0a3 mypy==v0.950 types-setuptools + - run: pip install black<23 pylint==v3.0.0a3 mypy==v0.950 types-setuptools returns<1 - run: black --diff --check $(git ls-files -- '*.py' ':!:tests/*' ':!:docs/*' ':!:examples/*') - run: pylint --disable=all --enable=unused-import $(git ls-files -- '*.py' ':!:tests/*' ':!:docs/*' ':!:examples/*') - run: mypy --strict $(git ls-files -- '*.py' ':!:tests/*' ':!:docs/*' ':!:examples/*') From d806553b3dfaa064c637605a5cb820a93964c31a Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Sat, 14 May 2022 18:06:55 +1000 Subject: [PATCH 03/33] Fix github action --- .github/workflows/code-quality.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 3934369..aded782 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -10,7 +10,7 @@ jobs: with: python-version: 3.x - run: pip install --upgrade pip - - run: pip install black<23 pylint==v3.0.0a3 mypy==v0.950 types-setuptools returns<1 + - run: pip install "black<23" pylint==v3.0.0a3 mypy==v0.950 types-setuptools "returns<1" - run: black --diff --check $(git ls-files -- '*.py' ':!:tests/*' ':!:docs/*' ':!:examples/*') - run: pylint --disable=all --enable=unused-import $(git ls-files -- '*.py' ':!:tests/*' ':!:docs/*' ':!:examples/*') - run: mypy --strict $(git ls-files -- '*.py' ':!:tests/*' ':!:docs/*' ':!:examples/*') From 0ede79e949df363a4a225ab2d4bab5c8c004d7b4 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Tue, 17 May 2022 10:47:08 +1000 Subject: [PATCH 04/33] Add comment to docs --- docs/installation.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/installation.md b/docs/installation.md index caf8468..d2f3680 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -27,3 +27,6 @@ Test the server: $ curl -X POST http://localhost:5000 -d '{"jsonrpc": "2.0", "method": "ping", "id": 1}' {"jsonrpc": "2.0", "result": "pong", "id": 1} ``` + +`serve` is good for serving methods in development, but for production use +`dispatch` instead. From a8546709aa5042be71b29c019107dfac7f1f487c Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Thu, 19 May 2022 08:28:29 +1000 Subject: [PATCH 05/33] Prepare version 6.0.0 --- CHANGELOG.md | 21 +++++++++++++++++++++ setup.py | 2 +- tox.ini | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c413734..e703f1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # jsonrpcserver Change Log +## 6.0.0 (May 18, 2022) + +A small release but incrementing the major release number due to a breaking +change for async use. + +Breaking changes: + +- Decorate async JSON-RPC methods with `@async_method` instead of `@method`. + The reason for this change is due to the typing of the decorator, async + functions return a different type (`Awaitable`) to other functions. + +Other changes: + +- Internally, replaced the Oslash dependency with + [Returns](https://github.com/dry-python/returns). Because Oslash is not meant + for production use. +- Use `Ok` instead of `Success` when returning a response. This is to avoid + confusion with the Returns library now used internally which has it's own + `Success` class. This is not a breaking change, `Success` will still work for + now, however `Ok` is recommended. + ## 5.0.7 (Mar 10, 2022) - Upgrade to jsonschema 4. diff --git a/setup.py b/setup.py index cec2012..0e40d27 100644 --- a/setup.py +++ b/setup.py @@ -38,5 +38,5 @@ zip_safe=False, packages=["jsonrpcserver"], url="https://github.com/explodinglabs/jsonrpcserver", - version="5.0.7", + version="6.0.0", ) diff --git a/tox.ini b/tox.ini index d8673ad..bd427c4 100644 --- a/tox.ini +++ b/tox.ini @@ -11,5 +11,5 @@ setenv = PYTHONDONTWRITEBYTECODE=1 deps = pytest pytest-asyncio -commands = pytest tests +commands = pytest tests --asyncio-mode=strict install_command=pip install --trusted-host=pypi.org --trusted-host=files.pythonhosted.org {opts} {packages} From 8808a436b02fd88008a0b2601535d915667f74c8 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Fri, 20 May 2022 12:33:26 +1000 Subject: [PATCH 06/33] Adjust docstring --- jsonrpcserver/dispatcher.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jsonrpcserver/dispatcher.py b/jsonrpcserver/dispatcher.py index f4ab864..b5071c2 100644 --- a/jsonrpcserver/dispatcher.py +++ b/jsonrpcserver/dispatcher.py @@ -173,9 +173,9 @@ def dispatch_request( ) -> Tuple[Request, Result[SuccessResult, ErrorResult]]: """Get the method, validates the arguments and calls the method. - Returns: A tuple containing the Result of the method, along with the original - Request. We need the ids from the original request to remove notifications - before responding, and create a Response. + Returns: A tuple containing the original Request, and the Result of the method call. + We need the ids from the original request to remove notifications before + responding, and create a Response. """ return ( request, From 03f80d0563c3058f15ff9a28b7d2c1c5b0102fd7 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Tue, 24 May 2022 10:34:32 +1000 Subject: [PATCH 07/33] Remove errant comma --- jsonrpcserver/dispatcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonrpcserver/dispatcher.py b/jsonrpcserver/dispatcher.py index b5071c2..b262a42 100644 --- a/jsonrpcserver/dispatcher.py +++ b/jsonrpcserver/dispatcher.py @@ -205,7 +205,7 @@ def dispatch_deserialized( context: Any, post_process: Callable[[Response], Response], deserialized: Deserialized, -) -> Union[Response, List[Response], None,]: +) -> Union[Response, List[Response], None]: """This is simply continuing the pipeline from dispatch_to_response_pure. It exists only to be an abstraction, otherwise that function is doing too much. It continues on from the request string having been parsed and validated. From 609b990aac277104b7216907f3d55651474eb14f Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Wed, 21 Sep 2022 16:59:07 +1000 Subject: [PATCH 08/33] Adjust github workflow --- .github/workflows/code-quality.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 5c4e749..638f726 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -10,7 +10,7 @@ jobs: with: python-version: 3.x - run: pip install --upgrade pip - - run: pip install "black<23" pylint==v3.0.0a3 mypy==v0.902 types-setuptools + - run: pip install "black<23" pylint==v3.0.0a3 mypy==v0.902 types-setuptools "returns<1" - run: black --diff --check $(git ls-files -- '*.py' ':!:tests/*' ':!:docs/*' ':!:examples/*') - run: pylint --disable=all --enable=unused-import $(git ls-files -- '*.py' ':!:tests/*' ':!:docs/*' ':!:examples/*') - run: mypy --strict $(git ls-files -- '*.py' ':!:tests/*' ':!:docs/*' ':!:examples/*') From bae441c0eb347297f1857b395ca1c792bf65c4df Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Wed, 21 Sep 2022 17:10:23 +1000 Subject: [PATCH 09/33] Upgrade mypy in github workflow --- .github/workflows/code-quality.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 638f726..5539c73 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -10,7 +10,7 @@ jobs: with: python-version: 3.x - run: pip install --upgrade pip - - run: pip install "black<23" pylint==v3.0.0a3 mypy==v0.902 types-setuptools "returns<1" + - run: pip install "black<23" pylint==v3.0.0a3 mypy==v0.971 types-setuptools "returns<1" - run: black --diff --check $(git ls-files -- '*.py' ':!:tests/*' ':!:docs/*' ':!:examples/*') - run: pylint --disable=all --enable=unused-import $(git ls-files -- '*.py' ':!:tests/*' ':!:docs/*' ':!:examples/*') - run: mypy --strict $(git ls-files -- '*.py' ':!:tests/*' ':!:docs/*' ':!:examples/*') From 866a4e2aa94e1cd160095891e6b4d335e8280781 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Mon, 10 Oct 2022 12:07:16 +1100 Subject: [PATCH 10/33] Enable all Pylint errors --- .github/workflows/code-quality.yml | 10 +- .pre-commit-config.yaml | 41 ++- examples/aiohttp_server.py | 5 +- examples/aiozmq_server.py | 10 +- examples/asyncio_server.py | 8 +- examples/django_server.py | 11 +- examples/fastapi_server.py | 7 +- examples/flask_server.py | 5 +- examples/http_server.py | 10 +- examples/jsonrpcserver_server.py | 5 + examples/sanic_server.py | 7 +- examples/socketio_server.py | 11 +- examples/tornado_server.py | 15 +- examples/websockets_server.py | 9 +- examples/werkzeug_server.py | 11 +- examples/zeromq_server.py | 4 +- jsonrpcserver/__init__.py | 38 +-- jsonrpcserver/async_dispatcher.py | 17 +- jsonrpcserver/async_main.py | 3 + jsonrpcserver/dispatcher.py | 18 +- jsonrpcserver/exceptions.py | 9 +- jsonrpcserver/main.py | 8 +- jsonrpcserver/methods.py | 6 +- jsonrpcserver/request.py | 6 +- jsonrpcserver/response.py | 49 ++-- jsonrpcserver/result.py | 10 +- jsonrpcserver/sentinels.py | 6 + jsonrpcserver/server.py | 9 +- jsonrpcserver/utils.py | 16 +- setup.py | 4 +- tests/test_async_dispatcher.py | 33 +-- tests/test_async_main.py | 13 +- tests/test_async_request.py | 0 tests/test_dispatcher.py | 412 +++++++++++++++-------------- tests/test_main.py | 13 +- tests/test_methods.py | 18 +- tests/test_request.py | 7 +- tests/test_response.py | 31 ++- tests/test_result.py | 25 +- tests/test_sentinels.py | 5 +- tests/test_server.py | 7 +- tests/test_status.py | 0 42 files changed, 552 insertions(+), 380 deletions(-) delete mode 100644 tests/test_async_request.py delete mode 100644 tests/test_status.py diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 5539c73..9eef871 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -8,9 +8,9 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: - python-version: 3.x + python-version: 3.8 - run: pip install --upgrade pip - - run: pip install "black<23" pylint==v3.0.0a3 mypy==v0.971 types-setuptools "returns<1" - - run: black --diff --check $(git ls-files -- '*.py' ':!:tests/*' ':!:docs/*' ':!:examples/*') - - run: pylint --disable=all --enable=unused-import $(git ls-files -- '*.py' ':!:tests/*' ':!:docs/*' ':!:examples/*') - - run: mypy --strict $(git ls-files -- '*.py' ':!:tests/*' ':!:docs/*' ':!:examples/*') + - run: pip install types-setuptools "black<23" "pylint<3" "mypy<1" "jsonschema<5" pytest "returns<1" "aiohttp<4" "aiozmq<1" "django<5" "fastapi<1" "flask<3" "flask-socketio<5.3.1" "pyzmq" "sanic" "tornado<7" "uvicorn<1" "websockets<11" + - run: black --diff --check $(git ls-files -- '*.py' ':!:docs/*') + - run: pylint $(git ls-files -- '*.py' ':!:docs/*') + - run: mypy --strict $(git ls-files -- '*.py' ':!:docs/*') diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2e9f301..1c4424a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,6 @@ +default_language_version: + python: python3.8 +exclude: (^docs) fail_fast: true repos: - repo: https://github.com/ambv/black @@ -6,16 +9,44 @@ repos: - id: black args: [--diff, --check] - - repo: https://github.com/pre-commit/mirrors-pylint - rev: v3.0.0a5 + - repo: https://github.com/PyCQA/pylint + rev: v2.15.3 hooks: - id: pylint - args: [--disable=all, --enable=unused-import] + args: [--ignored-modules=returns] + additional_dependencies: + - returns<1 + - aiohttp<4 + - aiozmq<1 + - django<5 + - fastapi<1 + - flask<3 + - flask-socketio<5.3.1 + - jsonschema<5 + - pytest + - pyzmq + - sanic + - tornado<7 + - uvicorn<1 + - websockets<11 - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.971 hooks: - id: mypy - exclude: (^tests|^examples|^docs) args: [--strict] - additional_dependencies: ['types-setuptools', 'returns'] + additional_dependencies: + - returns<1 + - aiohttp<4 + - aiozmq<1 + - django<5 + - fastapi<1 + - flask<3 + - flask-socketio<5.3.1 + - pytest + - pyzmq + - sanic + - tornado<7 + - types-setuptools + - uvicorn<1 + - websockets<11 diff --git a/examples/aiohttp_server.py b/examples/aiohttp_server.py index ab05312..22523bf 100644 --- a/examples/aiohttp_server.py +++ b/examples/aiohttp_server.py @@ -1,13 +1,16 @@ +"""AioHTTP server""" from aiohttp import web from jsonrpcserver import async_dispatch, async_method, Ok, Result @async_method async def ping() -> Result: + """JSON-RPC method""" return Ok("pong") -async def handle(request): +async def handle(request: web.Request) -> web.Response: + """Handle aiohttp request""" return web.Response( text=await async_dispatch(await request.text()), content_type="application/json" ) diff --git a/examples/aiozmq_server.py b/examples/aiozmq_server.py index 8d3e723..dff855a 100644 --- a/examples/aiozmq_server.py +++ b/examples/aiozmq_server.py @@ -1,15 +1,19 @@ -from jsonrpcserver import async_dispatch, async_method, Ok, Result -import aiozmq +"""AioZMQ server""" import asyncio + +import aiozmq # type: ignore import zmq +from jsonrpcserver import async_dispatch, async_method, Ok, Result @async_method async def ping() -> Result: + """JSON-RPC method""" return Ok("pong") -async def main(): +async def main() -> None: + """Handle AioZMQ request""" rep = await aiozmq.create_zmq_stream(zmq.REP, bind="tcp://*:5000") while True: request = (await rep.read())[0].decode() diff --git a/examples/asyncio_server.py b/examples/asyncio_server.py index 950cf6e..d83b6cd 100644 --- a/examples/asyncio_server.py +++ b/examples/asyncio_server.py @@ -1,4 +1,4 @@ -"""Demonstrates processing a batch of 100 requests asynchronously""" +"""Demonstrates processing a batch of 100 requests asynchronously with asyncio.""" import asyncio import json @@ -7,12 +7,14 @@ @async_method async def sleep_() -> Result: + """JSON-RPC method""" await asyncio.sleep(1) return Ok() -async def handle(request: str) -> None: - print(await async_dispatch(request)) +async def handle(req: str) -> None: + """Handle asyncio event""" + print(await async_dispatch(req)) if __name__ == "__main__": diff --git a/examples/django_server.py b/examples/django_server.py index 2ff2654..5a80362 100644 --- a/examples/django_server.py +++ b/examples/django_server.py @@ -1,15 +1,18 @@ -from django.http import HttpResponse -from django.views.decorators.csrf import csrf_exempt +"""Django server""" +from django.http import HttpRequest, HttpResponse # type: ignore +from django.views.decorators.csrf import csrf_exempt # type: ignore from jsonrpcserver import dispatch, method, Ok, Result @method def ping() -> Result: + """JSON-RPC method""" return Ok("pong") -@csrf_exempt -def jsonrpc(request): +@csrf_exempt # type: ignore +def jsonrpc(request: HttpRequest) -> HttpResponse: + """Handle Django request""" return HttpResponse( dispatch(request.body.decode()), content_type="application/json" ) diff --git a/examples/fastapi_server.py b/examples/fastapi_server.py index 81954a4..bd61cf6 100644 --- a/examples/fastapi_server.py +++ b/examples/fastapi_server.py @@ -1,17 +1,20 @@ +"""FastAPI server""" from fastapi import FastAPI, Request, Response from jsonrpcserver import dispatch, method, Ok, Result -import uvicorn +import uvicorn # type: ignore app = FastAPI() @method def ping() -> Result: + """JSON-RPC method""" return Ok("pong") @app.post("/") -async def index(request: Request): +async def index(request: Request) -> Response: + """Handle FastAPI request""" return Response(dispatch(await request.body())) diff --git a/examples/flask_server.py b/examples/flask_server.py index 79a6793..32044b0 100644 --- a/examples/flask_server.py +++ b/examples/flask_server.py @@ -1,3 +1,4 @@ +"""Flask server""" from flask import Flask, Response, request from jsonrpcserver import dispatch, method, Ok, Result @@ -6,11 +7,13 @@ @method def ping() -> Result: + """JSON-RPC method""" return Ok("pong") @app.route("/", methods=["POST"]) -def index(): +def index() -> Response: + """Handle Flask request""" return Response( dispatch(request.get_data().decode()), content_type="application/json" ) diff --git a/examples/http_server.py b/examples/http_server.py index ca1b00a..047cf6b 100644 --- a/examples/http_server.py +++ b/examples/http_server.py @@ -1,3 +1,7 @@ +"""HTTPServer server + +Demonstrates using Python's builtin http.server module to serve JSON-RPC. +""" from http.server import BaseHTTPRequestHandler, HTTPServer from jsonrpcserver import dispatch, method, Ok, Result @@ -5,11 +9,15 @@ @method def ping() -> Result: + """JSON-RPC method""" return Ok("pong") class TestHttpServer(BaseHTTPRequestHandler): - def do_POST(self): + """HTTPServer request handler""" + + def do_POST(self) -> None: # pylint: disable=invalid-name + """POST handler""" # Process request request = self.rfile.read(int(self.headers["Content-Length"])).decode() response = dispatch(request) diff --git a/examples/jsonrpcserver_server.py b/examples/jsonrpcserver_server.py index 60b18f2..c0471bb 100644 --- a/examples/jsonrpcserver_server.py +++ b/examples/jsonrpcserver_server.py @@ -1,8 +1,13 @@ +"""Jsonrpcserver server. + +Uses jsonrpcserver's built-in "serve" function. +""" from jsonrpcserver import method, serve, Ok, Result @method def ping() -> Result: + """JSON-RPC method""" return Ok("pong") diff --git a/examples/sanic_server.py b/examples/sanic_server.py index 666917d..180d900 100644 --- a/examples/sanic_server.py +++ b/examples/sanic_server.py @@ -1,6 +1,7 @@ +"""Sanic server""" from sanic import Sanic from sanic.request import Request -from sanic.response import json +from sanic.response import HTTPResponse, json from jsonrpcserver import dispatch_to_serializable, method, Ok, Result app = Sanic("JSON-RPC app") @@ -8,11 +9,13 @@ @method def ping() -> Result: + """JSON-RPC method""" return Ok("pong") @app.route("/", methods=["POST"]) -async def test(request: Request): +async def test(request: Request) -> HTTPResponse: + """Handle Sanic request""" return json(dispatch_to_serializable(request.body)) diff --git a/examples/socketio_server.py b/examples/socketio_server.py index 72b699b..1a5c2c7 100644 --- a/examples/socketio_server.py +++ b/examples/socketio_server.py @@ -1,5 +1,6 @@ -from flask import Flask -from flask_socketio import SocketIO, send +"""SocketIO server""" +from flask import Flask, Request +from flask_socketio import SocketIO, send # type: ignore from jsonrpcserver import dispatch, method, Ok, Result app = Flask(__name__) @@ -8,11 +9,13 @@ @method def ping() -> Result: + """JSON-RPC method""" return Ok("pong") -@socketio.on("message") -def handle_message(request): +@socketio.on("message") # type: ignore +def handle_message(request: Request) -> None: + """Handle SocketIO request""" if response := dispatch(request): send(response, json=True) diff --git a/examples/tornado_server.py b/examples/tornado_server.py index 02c565c..b31473f 100644 --- a/examples/tornado_server.py +++ b/examples/tornado_server.py @@ -1,18 +1,29 @@ -from jsonrpcserver import async_dispatch, async_method, Ok, Result +"""Tornado server""" +from typing import Awaitable, Optional + from tornado import ioloop, web +from jsonrpcserver import async_dispatch, async_method, Ok, Result @async_method async def ping() -> Result: + """JSON-RPC method""" return Ok("pong") class MainHandler(web.RequestHandler): + """Handle Tornado request""" + async def post(self) -> None: + """Post""" request = self.request.body.decode() - if response := await async_dispatch(request): + response = await async_dispatch(request) + if response: self.write(response) + def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]: + pass + app = web.Application([(r"/", MainHandler)]) diff --git a/examples/websockets_server.py b/examples/websockets_server.py index 3b0680c..33098e4 100644 --- a/examples/websockets_server.py +++ b/examples/websockets_server.py @@ -1,19 +1,22 @@ +"""Websockets server""" import asyncio +from websockets.server import WebSocketServerProtocol, serve from jsonrpcserver import async_dispatch, async_method, Ok, Result -import websockets @async_method async def ping() -> Result: + """JSON-RPC method""" return Ok("pong") -async def main(websocket, path): +async def main(websocket: WebSocketServerProtocol, _: str) -> None: + """Handle Websocket message""" if response := await async_dispatch(await websocket.recv()): await websocket.send(response) -start_server = websockets.serve(main, "localhost", 5000) +start_server = serve(main, "localhost", 5000) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever() diff --git a/examples/werkzeug_server.py b/examples/werkzeug_server.py index 79819e6..f2c242b 100644 --- a/examples/werkzeug_server.py +++ b/examples/werkzeug_server.py @@ -1,18 +1,19 @@ -from jsonrpcserver import method, Result, Ok, dispatch +"""Werkzeug server""" from werkzeug.serving import run_simple from werkzeug.wrappers import Request, Response +from jsonrpcserver import method, Result, Ok, dispatch @method def ping() -> Result: + """JSON-RPC method""" return Ok("pong") @Request.application -def application(request): - return Response( - dispatch(request.get_data().decode()), 200, mimetype="application/json" - ) +def application(request: Request) -> Response: + """Handle Werkzeug request""" + return Response(dispatch(request.data.decode()), 200, mimetype="application/json") if __name__ == "__main__": diff --git a/examples/zeromq_server.py b/examples/zeromq_server.py index 7473342..328667a 100644 --- a/examples/zeromq_server.py +++ b/examples/zeromq_server.py @@ -1,11 +1,13 @@ -from jsonrpcserver import dispatch, method, Ok, Result +"""ZeroMQ server""" import zmq +from jsonrpcserver import dispatch, method, Ok, Result socket = zmq.Context().socket(zmq.REP) @method def ping() -> Result: + """JSON-RPC method""" return Ok("pong") diff --git a/jsonrpcserver/__init__.py b/jsonrpcserver/__init__.py index 0c2589c..69c66d5 100644 --- a/jsonrpcserver/__init__.py +++ b/jsonrpcserver/__init__.py @@ -1,4 +1,22 @@ -"""Use __all__ so mypy considers these re-exported.""" +"""Jsonrpcserver""" +from returns.result import Result as R + +from .async_main import ( + dispatch as async_dispatch, + dispatch_to_response as async_dispatch_to_response, + dispatch_to_serializable as async_dispatch_to_serializable, +) +from .async_methods import method as async_method +from .exceptions import JsonRpcError +from .main import dispatch, dispatch_to_response, dispatch_to_serializable +from .methods import method +from .result import Error, ErrorResult, InvalidParams, Ok, SuccessResult +from .server import serve + +Success = Ok # For backward compatibility - version 5 used Success instead of Ok +Result = R[SuccessResult, ErrorResult] + + __all__ = [ "Error", "InvalidParams", @@ -16,21 +34,3 @@ "method", "serve", ] - -from returns.result import Result as R - -from .async_main import ( - dispatch as async_dispatch, - dispatch_to_response as async_dispatch_to_response, - dispatch_to_serializable as async_dispatch_to_serializable, -) -from .async_methods import method as async_method -from .exceptions import JsonRpcError -from .main import dispatch, dispatch_to_response, dispatch_to_serializable -from .methods import method -from .result import Error, InvalidParams, Ok, SuccessResult, ErrorResult -from .server import serve as serve - -# For backward compatibility -Result = R[SuccessResult, ErrorResult] -Success = Ok diff --git a/jsonrpcserver/async_dispatcher.py b/jsonrpcserver/async_dispatcher.py index 5bacb1e..0c5e518 100644 --- a/jsonrpcserver/async_dispatcher.py +++ b/jsonrpcserver/async_dispatcher.py @@ -1,5 +1,4 @@ """Async version of dispatcher.py""" - from functools import partial from inspect import signature from itertools import starmap @@ -36,6 +35,8 @@ logger = logging.getLogger(__name__) +# pylint: disable=missing-function-docstring,duplicate-code + async def call( request: Request, context: Any, method: Method @@ -47,8 +48,9 @@ async def call( validate_result(result) except JsonRpcError as exc: return Failure(ErrorResult(code=exc.code, message=exc.message, data=exc.data)) - except Exception as exc: # Other error inside method - Internal error - logging.exception(exc) + except Exception as exc: # pylint: disable=broad-except + # Other error inside method - Internal error + logger.exception(exc) return Failure(InternalErrorResult(str(exc))) return result @@ -130,9 +132,12 @@ async def dispatch_to_response_pure( post_process(result) if isinstance(result, Failure) else await dispatch_deserialized( - methods, context, post_process, result.unwrap() + methods, + context, + post_process, + result.unwrap(), ) ) - except Exception as exc: - logging.exception(exc) + except Exception as exc: # pylint: disable=broad-except + logger.exception(exc) return post_process(Failure(ServerErrorResponse(str(exc), None))) diff --git a/jsonrpcserver/async_main.py b/jsonrpcserver/async_main.py index 061e110..fe5f749 100644 --- a/jsonrpcserver/async_main.py +++ b/jsonrpcserver/async_main.py @@ -11,6 +11,9 @@ from .utils import identity +# pylint: disable=missing-function-docstring,duplicate-code + + async def dispatch_to_response( request: str, methods: Optional[Methods] = None, diff --git a/jsonrpcserver/dispatcher.py b/jsonrpcserver/dispatcher.py index 8f54cdf..c2c1bd1 100644 --- a/jsonrpcserver/dispatcher.py +++ b/jsonrpcserver/dispatcher.py @@ -1,6 +1,7 @@ """Dispatcher - does the hard work of this library: parses, validates and dispatches requests, providing responses. """ +# pylint: disable=protected-access from functools import partial from inspect import signature from itertools import starmap @@ -37,7 +38,7 @@ def extract_list( is_batch: bool, responses: Iterable[Response] -) -> Union[Response, List[Response], None,]: +) -> Union[Response, List[Response], None]: """This is the inverse of make_list. Here we extract a response back out of the list if it wasn't a batch request originally. Also applies a JSON-RPC rule: we do not respond to batches of notifications. @@ -58,11 +59,10 @@ def extract_list( if len(response_list) == 0: return None # For batches containing at least one non-notification, return the list - elif is_batch: + if is_batch: return response_list # For single requests, extract it back from the list (there will be only one). - else: - return response_list[0] + return response_list[0] def to_response( @@ -139,8 +139,8 @@ def call( except JsonRpcError as exc: return Failure(ErrorResult(code=exc.code, message=exc.message, data=exc.data)) # Any other uncaught exception inside method - internal error. - except Exception as exc: - logging.exception(exc) + except Exception as exc: # pylint: disable=broad-except + logger.exception(exc) return Failure(InternalErrorResult(str(exc))) return result @@ -237,7 +237,7 @@ def validate_request( # Since the validator is unknown, the specific exception that will be raised is also # unknown. Any exception raised we assume the request is invalid and return an # "invalid request" response. - except Exception as exc: + except Exception: # pylint: disable=broad-except return Failure(InvalidRequestResponse("The request failed schema validation")) return Success(request) @@ -254,7 +254,7 @@ def deserialize_request( # Since the deserializer is unknown, the specific exception that will be raised is # also unknown. Any exception raised we assume the request is invalid, return a # parse error response. - except Exception as exc: + except Exception as exc: # pylint: disable=broad-except return Failure(ParseErrorResponse(str(exc))) @@ -283,7 +283,7 @@ def dispatch_to_response_pure( if isinstance(result, Failure) else dispatch_deserialized(methods, context, post_process, result.unwrap()) ) - except Exception as exc: + except Exception as exc: # pylint: disable=broad-except # There was an error with the jsonrpcserver library. logging.exception(exc) return post_process(Failure(ServerErrorResponse(str(exc), None))) diff --git a/jsonrpcserver/exceptions.py b/jsonrpcserver/exceptions.py index a8e8bcd..b2e2afb 100644 --- a/jsonrpcserver/exceptions.py +++ b/jsonrpcserver/exceptions.py @@ -1,10 +1,13 @@ -"""A JsonRpcError exception can be raised from inside a method, as an alternative way to -return an error response. See https://github.com/explodinglabs/jsonrpcserver/discussions/158 -""" +"""Exceptions""" from typing import Any from .sentinels import NODATA class JsonRpcError(Exception): + """A JsonRpcError exception can be raised from inside a method, as an alternate way + to return an error response. See + https://github.com/explodinglabs/jsonrpcserver/discussions/158 + """ + def __init__(self, code: int, message: str, data: Any = NODATA): self.code, self.message, self.data = (code, message, data) diff --git a/jsonrpcserver/main.py b/jsonrpcserver/main.py index ac1855f..bbf824b 100644 --- a/jsonrpcserver/main.py +++ b/jsonrpcserver/main.py @@ -9,15 +9,15 @@ - dispatch_to_json/dispatch: Returns a JSON-RPC response string (or an empty string for notifications). """ +from importlib.resources import read_text from typing import Any, Callable, Dict, List, Optional, Union, cast import json from jsonschema.validators import validator_for # type: ignore -import importlib.resources from .dispatcher import dispatch_to_response_pure, Deserialized from .methods import Methods, global_methods -from .response import Response, to_serializable_one +from .response import Response, to_dict from .sentinels import NOCONTEXT from .utils import identity @@ -26,7 +26,7 @@ # Prepare the jsonschema validator. This is global so it loads only once, not every # time dispatch is called. -schema = json.loads(importlib.resources.read_text(__package__, "request-schema.json")) +schema = json.loads(read_text(__package__, "request-schema.json")) klass = validator_for(schema) klass.check_schema(schema) default_validator = klass(schema).validate @@ -84,7 +84,7 @@ def dispatch_to_serializable( """ return cast( Union[Dict[str, Any], List[Dict[str, Any]], None], - dispatch_to_response(*args, post_process=to_serializable_one, **kwargs), + dispatch_to_response(*args, post_process=to_dict, **kwargs), ) diff --git a/jsonrpcserver/methods.py b/jsonrpcserver/methods.py index 9ba30d8..81b0a36 100644 --- a/jsonrpcserver/methods.py +++ b/jsonrpcserver/methods.py @@ -19,11 +19,13 @@ Method = Callable[..., Result[SuccessResult, ErrorResult]] Methods = Dict[str, Method] -global_methods: Methods = dict() + +global_methods: Methods = {} def method( - f: Optional[Method] = None, name: Optional[str] = None + f: Optional[Method] = None, # pylint: disable=invalid-name + name: Optional[str] = None, ) -> Callable[..., Any]: """A decorator to add a function into jsonrpcserver's internal global_methods dict. The global_methods dict will be used by default unless a methods argument is passed diff --git a/jsonrpcserver/request.py b/jsonrpcserver/request.py index 958c36e..37f88c1 100644 --- a/jsonrpcserver/request.py +++ b/jsonrpcserver/request.py @@ -1,12 +1,14 @@ """A simple namedtuple to hold a request. -After parsing the request string, we put the requests (which are dicts) into these -Request namedtuples because they're nicer to work with. +After parsing a request string, we put the (dict) requests into these Request +namedtuples, simply because they're nicer to work with. """ from typing import Any, Dict, List, NamedTuple, Union class Request(NamedTuple): + """JSON-RPC Request""" + method: str params: Union[List[Any], Dict[str, Any]] id: Any # Use NOID for a Notification. diff --git a/jsonrpcserver/response.py b/jsonrpcserver/response.py index e802bad..9c6e4f1 100644 --- a/jsonrpcserver/response.py +++ b/jsonrpcserver/response.py @@ -18,18 +18,16 @@ class SuccessResponse(NamedTuple): - """ - It would be nice to subclass Success here, adding only id. But it's not possible to - easily subclass NamedTuples in Python 3.6. (I believe it can be done in 3.8.) + """It would be nice to subclass Success here, adding only id. But it's not possible + to easily subclass NamedTuples in Python 3.6. (I believe it can be done in 3.8.) """ - result: str + result: Any id: Any class ErrorResponse(NamedTuple): - """ - It would be nice to subclass Error here, adding only id. But it's not possible to + """It would be nice to subclass Error here, adding only id. But it's not possible to easily subclass NamedTuples in Python 3.6. (I believe it can be done in 3.8.) """ @@ -42,8 +40,9 @@ class ErrorResponse(NamedTuple): Response = Result[SuccessResponse, ErrorResponse] -def ParseErrorResponse(data: Any) -> ErrorResponse: - """ +def ParseErrorResponse(data: Any) -> ErrorResponse: # pylint: disable=invalid-name + """An ErrorResponse with most attributes already populated. + From the spec: "This (id) member is REQUIRED. It MUST be the same as the value of the id member in the Request Object. If there was an error in detecting the id in the Request object (e.g. Parse error/Invalid Request), it MUST be Null." @@ -51,8 +50,9 @@ def ParseErrorResponse(data: Any) -> ErrorResponse: return ErrorResponse(ERROR_PARSE_ERROR, "Parse error", data, None) -def InvalidRequestResponse(data: Any) -> ErrorResponse: - """ +def InvalidRequestResponse(data: Any) -> ErrorResponse: # pylint: disable=invalid-name + """An ErrorResponse with most attributes already populated. + From the spec: "This (id) member is REQUIRED. It MUST be the same as the value of the id member in the Request Object. If there was an error in detecting the id in the Request object (e.g. Parse error/Invalid Request), it MUST be Null." @@ -61,14 +61,19 @@ def InvalidRequestResponse(data: Any) -> ErrorResponse: def MethodNotFoundResponse(data: Any, id: Any) -> ErrorResponse: + """An ErrorResponse with some attributes already populated.""" + # pylint: disable=invalid-name,redefined-builtin return ErrorResponse(ERROR_METHOD_NOT_FOUND, "Method not found", data, id) def ServerErrorResponse(data: Any, id: Any) -> ErrorResponse: + """An ErrorResponse with some attributes already populated.""" + # pylint: disable=invalid-name,redefined-builtin return ErrorResponse(ERROR_SERVER_ERROR, "Server error", data, id) -def serialize_error(response: ErrorResponse) -> Dict[str, Any]: +def to_error_dict(response: ErrorResponse) -> Dict[str, Any]: + """From ErrorResponse object to dict""" return { "jsonrpc": "2.0", "error": { @@ -81,22 +86,26 @@ def serialize_error(response: ErrorResponse) -> Dict[str, Any]: } -def serialize_success(response: SuccessResponse) -> Dict[str, Any]: +def to_success_dict(response: SuccessResponse) -> Dict[str, Any]: + """From SuccessResponse object to dict""" return {"jsonrpc": "2.0", "result": response.result, "id": response.id} -def to_serializable_one(response: Response) -> Union[Deserialized, None]: +def to_dict(response: Response) -> Dict[str, Any]: + """Serialize either an error or success response object to dict""" return ( - serialize_error(response.failure()) + to_error_dict(response.failure()) if isinstance(response, Failure) - else serialize_success(response.unwrap()) + else to_success_dict(response.unwrap()) ) -def to_serializable(response: Response) -> Union[Deserialized, None]: +def to_serializable( + response: Union[Response, List[Response], None] +) -> Union[Deserialized, None]: + """Serialize a response object (or list of them), to a dict, or list of them.""" if response is None: return None - elif isinstance(response, List): - return [to_serializable_one(r) for r in response] - else: - return to_serializable_one(response) + if isinstance(response, List): + return [to_dict(r) for r in response] + return to_dict(response) diff --git a/jsonrpcserver/result.py b/jsonrpcserver/result.py index 2943fd9..e7f4056 100644 --- a/jsonrpcserver/result.py +++ b/jsonrpcserver/result.py @@ -8,11 +8,13 @@ """ from typing import Any, NamedTuple -from returns.result import Failure, Result, Success +from returns.result import Failure, Success from .codes import ERROR_INVALID_PARAMS, ERROR_METHOD_NOT_FOUND, ERROR_INTERNAL_ERROR from .sentinels import NODATA +# pylint: disable=missing-class-docstring,missing-function-docstring,invalid-name + class SuccessResult(NamedTuple): result: Any = None @@ -48,15 +50,15 @@ def InvalidParamsResult(data: Any = NODATA) -> ErrorResult: # Helpers (the public functions) -def Ok(*args: Any, **kwargs: Any) -> Result[SuccessResult, ErrorResult]: +def Ok(*args: Any, **kwargs: Any) -> Result: return Success(SuccessResult(*args, **kwargs)) -def Error(*args: Any, **kwargs: Any) -> Result[SuccessResult, ErrorResult]: +def Error(*args: Any, **kwargs: Any) -> Result: return Failure(ErrorResult(*args, **kwargs)) -def InvalidParams(*args: Any, **kwargs: Any) -> Result[SuccessResult, ErrorResult]: +def InvalidParams(*args: Any, **kwargs: Any) -> Result: """InvalidParams is a shortcut to save you from having to pass the Invalid Params JSON-RPC code to Error. """ diff --git a/jsonrpcserver/sentinels.py b/jsonrpcserver/sentinels.py index 22cd64e..0ed0cca 100644 --- a/jsonrpcserver/sentinels.py +++ b/jsonrpcserver/sentinels.py @@ -7,6 +7,12 @@ class Sentinel: + """Use this class to create a unique object. + + Has a nicer repr than `object()`. + """ + + # pylint: disable=too-few-public-methods def __init__(self, name: str): self.name = name diff --git a/jsonrpcserver/server.py b/jsonrpcserver/server.py index bab5f91..643b6a7 100644 --- a/jsonrpcserver/server.py +++ b/jsonrpcserver/server.py @@ -1,3 +1,6 @@ +"""A simple development server for serving JSON-RPC requests using Python's builtin +http.server module. +""" import logging from http.server import BaseHTTPRequestHandler, HTTPServer @@ -5,7 +8,10 @@ class RequestHandler(BaseHTTPRequestHandler): - def do_POST(self) -> None: + """Handle HTTP requests""" + + def do_POST(self) -> None: # pylint: disable=invalid-name + """Handle POST request""" response = dispatch( self.rfile.read(int(str(self.headers["Content-Length"]))).decode() ) @@ -17,5 +23,6 @@ def do_POST(self) -> None: def serve(name: str = "", port: int = 5000) -> None: + """A simple function to serve HTTP requests""" logging.info(" * Listening on port %s", port) HTTPServer((name, port), RequestHandler).serve_forever() diff --git a/jsonrpcserver/utils.py b/jsonrpcserver/utils.py index cd61e91..4ee5a2c 100644 --- a/jsonrpcserver/utils.py +++ b/jsonrpcserver/utils.py @@ -1,16 +1,20 @@ +"""Utility functions""" from functools import reduce from typing import Any, Callable, List +# pylint: disable=invalid-name -identity = lambda x: x +def identity(x: Any) -> Any: + """Returns the argument.""" + return x -def compose(*fs: Callable[..., Any]) -> Callable[..., Any]: - def compose2(f: Callable[..., Any], g: Callable[..., Any]) -> Callable[..., Any]: - return lambda *a, **kw: f(g(*a, **kw)) - return reduce(compose2, fs) +def compose(*funcs: Callable[..., Any]) -> Callable[..., Any]: + """Compose two or more functions producing a single composite function.""" + return reduce(lambda f, g: lambda *a, **kw: f(g(*a, **kw)), funcs) def make_list(x: Any) -> List[Any]: - return [x] if not isinstance(x, list) else x + """Puts a value into a list if it's not already.""" + return x if isinstance(x, list) else [x] diff --git a/setup.py b/setup.py index d357cd2..945255f 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ """setup.py""" from setuptools import setup -with open("README.md") as f: +with open("README.md", encoding="utf-8") as f: README = f.read() setup( @@ -25,7 +25,7 @@ "websockets", "werkzeug", ], - "test": [ + "qa": [ "pytest", "pytest-asyncio", "pytest-cov", diff --git a/tests/test_async_dispatcher.py b/tests/test_async_dispatcher.py index 8c898ba..1333b9b 100644 --- a/tests/test_async_dispatcher.py +++ b/tests/test_async_dispatcher.py @@ -1,4 +1,5 @@ -from unittest.mock import patch +"""Test async_dispatcher.py""" +from unittest.mock import Mock, patch import pytest from returns.result import Failure, Success @@ -9,49 +10,51 @@ dispatch_request, dispatch_to_response_pure, ) -from jsonrpcserver.async_main import default_deserializer, default_validator +from jsonrpcserver.main import default_deserializer, default_validator from jsonrpcserver.codes import ERROR_INTERNAL_ERROR, ERROR_SERVER_ERROR from jsonrpcserver.exceptions import JsonRpcError from jsonrpcserver.request import Request from jsonrpcserver.response import ErrorResponse, SuccessResponse -from jsonrpcserver.result import ErrorResult, Result, Ok, SuccessResult +from jsonrpcserver.result import ErrorResult, Ok, Result, SuccessResult from jsonrpcserver.sentinels import NOCONTEXT, NODATA from jsonrpcserver.utils import identity +# pylint: disable=missing-function-docstring,duplicate-code + async def ping() -> Result: return Ok("pong") @pytest.mark.asyncio -async def test_call(): +async def test_call() -> None: assert await call(Request("ping", [], 1), NOCONTEXT, ping) == Success( SuccessResult("pong") ) @pytest.mark.asyncio -async def test_call_raising_jsonrpcerror(): - def method(): +async def test_call_raising_jsonrpcerror() -> None: + async def method_() -> Result: raise JsonRpcError(code=1, message="foo", data=NODATA) - assert await call(Request("ping", [], 1), NOCONTEXT, method) == Failure( + assert await call(Request("ping", [], 1), NOCONTEXT, method_) == Failure( ErrorResult(1, "foo") ) @pytest.mark.asyncio -async def test_call_raising_exception(): - def method(): +async def test_call_raising_exception() -> None: + async def method_() -> Result: raise ValueError("foo") - assert await call(Request("ping", [], 1), NOCONTEXT, method) == Failure( + assert await call(Request("ping", [], 1), NOCONTEXT, method_) == Failure( ErrorResult(ERROR_INTERNAL_ERROR, "Internal error", "foo") ) @pytest.mark.asyncio -async def test_dispatch_request(): +async def test_dispatch_request() -> None: request = Request("ping", [], 1) assert await dispatch_request({"ping": ping}, NOCONTEXT, request) == ( request, @@ -60,7 +63,7 @@ async def test_dispatch_request(): @pytest.mark.asyncio -async def test_dispatch_deserialized(): +async def test_dispatch_deserialized() -> None: assert await dispatch_deserialized( {"ping": ping}, NOCONTEXT, @@ -70,7 +73,7 @@ async def test_dispatch_deserialized(): @pytest.mark.asyncio -async def test_dispatch_to_response_pure_success(): +async def test_dispatch_to_response_pure_success() -> None: assert await dispatch_to_response_pure( deserializer=default_deserializer, validator=default_validator, @@ -83,8 +86,8 @@ async def test_dispatch_to_response_pure_success(): @patch("jsonrpcserver.async_dispatcher.dispatch_request", side_effect=ValueError("foo")) @pytest.mark.asyncio -async def test_dispatch_to_response_pure_server_error(*_): - async def foo(): +async def test_dispatch_to_response_pure_server_error(_: Mock) -> None: + async def foo() -> Result: return Ok() assert await dispatch_to_response_pure( diff --git a/tests/test_async_main.py b/tests/test_async_main.py index 13645ae..c88893c 100644 --- a/tests/test_async_main.py +++ b/tests/test_async_main.py @@ -1,3 +1,4 @@ +"""Test async_main.py""" import pytest from returns.result import Success @@ -8,7 +9,9 @@ dispatch_to_json, ) from jsonrpcserver.response import SuccessResponse -from jsonrpcserver.result import Result, Ok +from jsonrpcserver.result import Ok, Result + +# pylint: disable=missing-function-docstring async def ping() -> Result: @@ -16,21 +19,21 @@ async def ping() -> Result: @pytest.mark.asyncio -async def test_dispatch_to_response(): +async def test_dispatch_to_response() -> None: assert await dispatch_to_response( '{"jsonrpc": "2.0", "method": "ping", "id": 1}', {"ping": ping} ) == Success(SuccessResponse("pong", 1)) @pytest.mark.asyncio -async def test_dispatch_to_serializable(): +async def test_dispatch_to_serializable() -> None: assert await dispatch_to_serializable( '{"jsonrpc": "2.0", "method": "ping", "id": 1}', {"ping": ping} ) == {"jsonrpc": "2.0", "result": "pong", "id": 1} @pytest.mark.asyncio -async def test_dispatch_to_json(): +async def test_dispatch_to_json() -> None: assert ( await dispatch_to_json( '{"jsonrpc": "2.0", "method": "ping", "id": 1}', {"ping": ping} @@ -40,7 +43,7 @@ async def test_dispatch_to_json(): @pytest.mark.asyncio -async def test_dispatch_to_json_notification(): +async def test_dispatch_to_json_notification() -> None: assert ( await dispatch_to_json('{"jsonrpc": "2.0", "method": "ping"}', {"ping": ping}) == "" diff --git a/tests/test_async_request.py b/tests/test_async_request.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index b094bdf..8715439 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -1,6 +1,9 @@ -"""TODO: Add tests for dispatch_requests (non-pure version)""" -from typing import Any -from unittest.mock import patch, sentinel +"""Test dispatcher.py + +TODO: Add tests for dispatch_requests (non-pure version) +""" +from typing import Any, Callable, Dict +from unittest.mock import Mock, patch, sentinel import json import pytest @@ -35,14 +38,17 @@ default_deserializer, default_validator, dispatch_to_response, + dispatch, ) from jsonrpcserver.methods import method from jsonrpcserver.request import Request from jsonrpcserver.response import ErrorResponse, SuccessResponse -from jsonrpcserver.result import ErrorResult, InvalidParams, Ok, SuccessResult, NODATA -from jsonrpcserver.sentinels import NOCONTEXT, NOID +from jsonrpcserver.result import ErrorResult, InvalidParams, Ok, Result, SuccessResult +from jsonrpcserver.sentinels import NOCONTEXT, NODATA, NOID from jsonrpcserver.utils import identity +# pylint: disable=missing-function-docstring,missing-class-docstring,too-few-public-methods,unnecessary-lambda-assignment,invalid-name,disallowed-name + def ping() -> Result: return Ok("pong") @@ -51,34 +57,36 @@ def ping() -> Result: # extract_list -def test_extract_list(): - assert extract_list(False, [SuccessResponse("foo", 1)]) == SuccessResponse("foo", 1) +def test_extract_list() -> None: + assert extract_list(False, [Success(SuccessResponse("foo", 1))]) == Success( + SuccessResponse("foo", 1) + ) -def test_extract_list_notification(): - assert extract_list(False, [None]) == None +def test_extract_list_notification() -> None: + assert extract_list(False, []) is None -def test_extract_list_batch(): - assert extract_list(True, [SuccessResponse("foo", 1)]) == [ - SuccessResponse("foo", 1) +def test_extract_list_batch() -> None: + assert extract_list(True, [Success(SuccessResponse("foo", 1))]) == [ + Success(SuccessResponse("foo", 1)) ] -def test_extract_list_batch_all_notifications(): - assert extract_list(True, []) == None +def test_extract_list_batch_all_notifications() -> None: + assert extract_list(True, []) is None # to_response -def test_to_response_SuccessResult(): +def test_to_response_SuccessResult() -> None: assert to_response( Request("ping", [], sentinel.id), Success(SuccessResult(sentinel.result)) ) == Success(SuccessResponse(sentinel.result, sentinel.id)) -def test_to_response_ErrorResult(): +def test_to_response_ErrorResult() -> None: assert ( to_response( Request("ping", [], sentinel.id), @@ -93,53 +101,58 @@ def test_to_response_ErrorResult(): ) -def test_to_response_InvalidParams(): +def test_to_response_InvalidParams() -> None: assert to_response( Request("ping", [], sentinel.id), InvalidParams(sentinel.data) ) == Failure(ErrorResponse(-32602, "Invalid params", sentinel.data, sentinel.id)) -def test_to_response_InvalidParams_no_data(): +def test_to_response_InvalidParams_no_data() -> None: assert to_response(Request("ping", [], sentinel.id), InvalidParams()) == Failure( ErrorResponse(-32602, "Invalid params", NODATA, sentinel.id) ) -def test_to_response_notification(): +def test_to_response_notification() -> None: with pytest.raises(AssertionError): - to_response(Request("ping", [], NOID), SuccessResult(result=sentinel.result)) + to_response( + Request("ping", [], NOID), Success(SuccessResult(result=sentinel.result)) + ) # extract_args -def test_extract_args(): +def test_extract_args() -> None: assert extract_args(Request("ping", [], NOID), NOCONTEXT) == [] -def test_extract_args_with_context(): +def test_extract_args_with_context() -> None: assert extract_args(Request("ping", ["bar"], NOID), "foo") == ["foo", "bar"] # extract_kwargs -def test_extract_kwargs(): +def test_extract_kwargs() -> None: assert extract_kwargs(Request("ping", {"foo": "bar"}, NOID)) == {"foo": "bar"} # validate_result -def test_validate_result_no_arguments(): - f = lambda: None +def test_validate_result_no_arguments() -> None: + def f() -> Result: + return Ok() + assert validate_args(Request("f", [], NOID), NOCONTEXT, f) == Success(f) -def test_validate_result_no_arguments_too_many_positionals(): - assert validate_args( - Request("f", ["foo"], NOID), NOCONTEXT, lambda: None - ) == Failure( +def test_validate_result_no_arguments_too_many_positionals() -> None: + def f() -> Result: + return Ok() + + assert validate_args(Request("f", ["foo"], NOID), NOCONTEXT, f) == Failure( ErrorResult( code=ERROR_INVALID_PARAMS, message="Invalid params", @@ -148,58 +161,63 @@ def test_validate_result_no_arguments_too_many_positionals(): ) -def test_validate_result_positionals(): - f = lambda x: None +def test_validate_result_positionals() -> None: + def f(x: int) -> Result: + return Ok() + assert validate_args(Request("f", [1], NOID), NOCONTEXT, f) == Success(f) -def test_validate_result_positionals_not_passed(): - assert validate_args( - Request("f", {"foo": "bar"}, NOID), NOCONTEXT, lambda x: None - ) == Failure( +def test_validate_result_positionals_not_passed() -> None: + def f(x: str) -> Result: + return Ok() + + assert validate_args(Request("f", {"foo": "bar"}, NOID), NOCONTEXT, f) == Failure( ErrorResult( ERROR_INVALID_PARAMS, "Invalid params", "missing a required argument: 'x'" ) ) -def test_validate_result_keywords(): - f = lambda **kwargs: None +def test_validate_result_keywords() -> None: + def f(**kwargs: str) -> Result: + return Ok() + assert validate_args(Request("f", {"foo": "bar"}, NOID), NOCONTEXT, f) == Success(f) -def test_validate_result_object_method(): +def test_validate_result_object_method() -> None: class FooClass: - def foo(self, one, two): - return "bar" + def f(self, *_: str) -> Result: + return Ok() - f = FooClass().foo - assert validate_args(Request("f", ["one", "two"], NOID), NOCONTEXT, f) == Success(f) + g = FooClass().f + assert validate_args(Request("g", ["one", "two"], NOID), NOCONTEXT, g) == Success(g) # call -def test_call(): +def test_call() -> None: assert call(Request("ping", [], 1), NOCONTEXT, ping) == Success( SuccessResult("pong") ) -def test_call_raising_jsonrpcerror(): - def method(): +def test_call_raising_jsonrpcerror() -> None: + def method_() -> Result: raise JsonRpcError(code=1, message="foo", data=NODATA) - assert call(Request("ping", [], 1), NOCONTEXT, method) == Failure( + assert call(Request("ping", [], 1), NOCONTEXT, method_) == Failure( ErrorResult(1, "foo") ) -def test_call_raising_exception(): - def method(): +def test_call_raising_exception() -> None: + def method_() -> Result: raise ValueError("foo") - assert call(Request("ping", [], 1), NOCONTEXT, method) == Failure( + assert call(Request("ping", [], 1), NOCONTEXT, method_) == Failure( ErrorResult(ERROR_INTERNAL_ERROR, "Internal error", "foo") ) @@ -207,35 +225,55 @@ def method(): # validate_args -def test_validate_args(): - assert validate_args(Request("ping", [], 1), NOCONTEXT, ping) == Success(ping) - - -def test_validate_args(): - assert validate_args(Request("ping", ["foo"], 1), NOCONTEXT, ping) == Failure( - ErrorResult( - ERROR_INVALID_PARAMS, "Invalid params", "too many positional arguments" - ) - ) +@pytest.mark.parametrize( + "argument,value", + [ + ( + validate_args(Request("ping", [], 1), NOCONTEXT, ping), + Success(ping), + ), + ( + validate_args(Request("ping", ["foo"], 1), NOCONTEXT, ping), + Failure( + ErrorResult( + ERROR_INVALID_PARAMS, + "Invalid params", + "too many positional arguments", + ) + ), + ), + ], +) +def test_validate_args(argument: Result, value: Result) -> None: + assert argument == value # get_method -def test_get_method(): - assert get_method({"ping": ping}, "ping") == Success(ping) - - -def test_get_method(): - assert get_method({"ping": ping}, "non-existant") == Failure( - ErrorResult(ERROR_METHOD_NOT_FOUND, "Method not found", "non-existant") - ) +@pytest.mark.parametrize( + "argument,value", + [ + ( + get_method({"ping": ping}, "ping"), + Success(ping), + ), + ( + get_method({"ping": ping}, "non-existant"), + Failure( + ErrorResult(ERROR_METHOD_NOT_FOUND, "Method not found", "non-existant") + ), + ), + ], +) +def test_get_method(argument: Result, value: Result) -> None: + assert argument == value # dispatch_request -def test_dispatch_request(): +def test_dispatch_request() -> None: request = Request("ping", [], 1) assert dispatch_request({"ping": ping}, NOCONTEXT, request) == ( request, @@ -243,8 +281,8 @@ def test_dispatch_request(): ) -def test_dispatch_request_with_context(): - def ping_with_context(context: Any): +def test_dispatch_request_with_context() -> None: + def ping_with_context(context: Any) -> Result: assert context is sentinel.context return Ok() @@ -259,7 +297,7 @@ def ping_with_context(context: Any): # create_request -def test_create_request(): +def test_create_request() -> None: request = create_request({"jsonrpc": "2.0", "method": "ping"}) assert isinstance(request, Request) @@ -267,18 +305,18 @@ def test_create_request(): # not_notification -def test_not_notification(): - assert not_notification((Request("ping", [], 1), SuccessResult("pong"))) == True +def test_not_notification() -> None: + assert not_notification((Request("ping", [], 1), SuccessResult("pong"))) is True -def test_not_notification_false(): - assert not_notification((Request("ping", [], NOID), SuccessResult("pong"))) == False +def test_not_notification_false() -> None: + assert not_notification((Request("ping", [], NOID), SuccessResult("pong"))) is False # dispatch_deserialized -def test_dispatch_deserialized(): +def test_dispatch_deserialized() -> None: assert dispatch_deserialized( methods={"ping": ping}, context=NOCONTEXT, @@ -290,12 +328,12 @@ def test_dispatch_deserialized(): # validate_request -def test_validate_request(): +def test_validate_request() -> None: request = {"jsonrpc": "2.0", "method": "ping"} assert validate_request(default_validator, request) == Success(request) -def test_validate_request_invalid(): +def test_validate_request_invalid() -> None: assert validate_request(default_validator, {"jsonrpc": "2.0"}) == Failure( ErrorResponse( ERROR_INVALID_REQUEST, @@ -306,21 +344,10 @@ def test_validate_request_invalid(): ) -# dispatch_request - - -def test_dispatch_request(): - request = Request("ping", [], 1) - assert dispatch_request({"ping": ping}, NOCONTEXT, request) == ( - request, - Success(SuccessResult("pong")), - ) - - # dispatch_to_response_pure -def test_dispatch_to_response_pure(): +def test_dispatch_to_response_pure() -> None: assert dispatch_to_response_pure( deserializer=default_deserializer, validator=default_validator, @@ -331,7 +358,7 @@ def test_dispatch_to_response_pure(): ) == Success(SuccessResponse("pong", 1)) -def test_dispatch_to_response_pure_parse_error(): +def test_dispatch_to_response_pure_parse_error() -> None: """Unable to parse, must return an error""" assert dispatch_to_response_pure( deserializer=default_deserializer, @@ -350,7 +377,7 @@ def test_dispatch_to_response_pure_parse_error(): ) -def test_dispatch_to_response_pure_invalid_request(): +def test_dispatch_to_response_pure_invalid_request() -> None: """Invalid JSON-RPC, must return an error. (impossible to determine if notification). """ @@ -371,7 +398,7 @@ def test_dispatch_to_response_pure_invalid_request(): ) -def test_dispatch_to_response_pure_method_not_found(): +def test_dispatch_to_response_pure_method_not_found() -> None: assert dispatch_to_response_pure( deserializer=default_deserializer, validator=default_validator, @@ -384,8 +411,8 @@ def test_dispatch_to_response_pure_method_not_found(): ) -def test_dispatch_to_response_pure_invalid_params_auto(): - def foo(colour: str, size: str): +def test_dispatch_to_response_pure_invalid_params_auto() -> None: + def f(colour: str, size: str) -> Result: # pylint: disable=unused-argument return Ok() assert dispatch_to_response_pure( @@ -393,8 +420,8 @@ def foo(colour: str, size: str): validator=default_validator, post_process=identity, context=NOCONTEXT, - methods={"foo": foo}, - request='{"jsonrpc": "2.0", "method": "foo", "params": {"colour":"blue"}, "id": 1}', + methods={"f": f}, + request='{"jsonrpc": "2.0", "method": "f", "params": {"colour":"blue"}, "id": 1}', ) == Failure( ErrorResponse( ERROR_INVALID_PARAMS, @@ -405,10 +432,11 @@ def foo(colour: str, size: str): ) -def test_dispatch_to_response_pure_invalid_params_explicitly_returned(): +def test_dispatch_to_response_pure_invalid_params_explicitly_returned() -> None: def foo(colour: str) -> Result: if colour not in ("orange", "red", "yellow"): return InvalidParams() + return Ok() assert dispatch_to_response_pure( deserializer=default_deserializer, @@ -420,8 +448,8 @@ def foo(colour: str) -> Result: ) == Failure(ErrorResponse(ERROR_INVALID_PARAMS, "Invalid params", NODATA, 1)) -def test_dispatch_to_response_pure_internal_error(): - def foo(): +def test_dispatch_to_response_pure_internal_error() -> None: + def foo() -> Result: raise ValueError("foo") assert dispatch_to_response_pure( @@ -435,8 +463,8 @@ def foo(): @patch("jsonrpcserver.dispatcher.dispatch_request", side_effect=ValueError("foo")) -def test_dispatch_to_response_pure_server_error(*_): - def foo(): +def test_dispatch_to_response_pure_server_error(*_: Mock) -> None: + def foo() -> Result: return Ok() assert dispatch_to_response_pure( @@ -449,11 +477,11 @@ def foo(): ) == Failure(ErrorResponse(ERROR_SERVER_ERROR, "Server error", "foo", None)) -def test_dispatch_to_response_pure_invalid_result(): +def test_dispatch_to_response_pure_invalid_result() -> None: """Methods should return a Result, otherwise we get an Internal Error response.""" - def not_a_result(): - return None + def not_a_result() -> Result: + return None # type: ignore assert dispatch_to_response_pure( deserializer=default_deserializer, @@ -472,10 +500,10 @@ def not_a_result(): ) -def test_dispatch_to_response_pure_raising_exception(): +def test_dispatch_to_response_pure_raising_exception() -> None: """Allow raising an exception to return an error.""" - def raise_exception(): + def raise_exception() -> Result: raise JsonRpcError(code=0, message="foo", data="bar") assert dispatch_to_response_pure( @@ -491,7 +519,7 @@ def raise_exception(): # dispatch_to_response_pure -- Notifications -def test_dispatch_to_response_pure_notification(): +def test_dispatch_to_response_pure_notification() -> None: assert ( dispatch_to_response_pure( deserializer=default_deserializer, @@ -501,11 +529,11 @@ def test_dispatch_to_response_pure_notification(): methods={"ping": ping}, request='{"jsonrpc": "2.0", "method": "ping"}', ) - == None + is None ) -def test_dispatch_to_response_pure_notification_parse_error(): +def test_dispatch_to_response_pure_notification_parse_error() -> None: """Unable to parse, must return an error""" assert dispatch_to_response_pure( deserializer=default_deserializer, @@ -524,7 +552,7 @@ def test_dispatch_to_response_pure_notification_parse_error(): ) -def test_dispatch_to_response_pure_notification_invalid_request(): +def test_dispatch_to_response_pure_notification_invalid_request() -> None: """Invalid JSON-RPC, must return an error. (impossible to determine if notification)""" assert dispatch_to_response_pure( deserializer=default_deserializer, @@ -543,7 +571,7 @@ def test_dispatch_to_response_pure_notification_invalid_request(): ) -def test_dispatch_to_response_pure_notification_method_not_found(): +def test_dispatch_to_response_pure_notification_method_not_found() -> None: assert ( dispatch_to_response_pure( deserializer=default_deserializer, @@ -553,12 +581,12 @@ def test_dispatch_to_response_pure_notification_method_not_found(): methods={}, request='{"jsonrpc": "2.0", "method": "non_existant"}', ) - == None + is None ) -def test_dispatch_to_response_pure_notification_invalid_params_auto(): - def foo(colour: str, size: str): +def test_dispatch_to_response_pure_notification_invalid_params_auto() -> None: + def foo(colour: str, size: str) -> Result: # pylint: disable=unused-argument return Ok() assert ( @@ -570,14 +598,15 @@ def foo(colour: str, size: str): methods={"foo": foo}, request='{"jsonrpc": "2.0", "method": "foo", "params": {"colour":"blue"}}', ) - == None + is None ) -def test_dispatch_to_response_pure_invalid_params_notification_explicitly_returned(): +def test_dispatch_to_response_pure_invalid_params_notification_explicitly_returned() -> None: def foo(colour: str) -> Result: if colour not in ("orange", "red", "yellow"): return InvalidParams() + return Ok() assert ( dispatch_to_response_pure( @@ -588,12 +617,12 @@ def foo(colour: str) -> Result: methods={"foo": foo}, request='{"jsonrpc": "2.0", "method": "foo", "params": ["blue"]}', ) - == None + is None ) -def test_dispatch_to_response_pure_notification_internal_error(): - def foo(bar): +def test_dispatch_to_response_pure_notification_internal_error() -> None: + def foo(bar: str) -> Result: raise ValueError assert ( @@ -605,13 +634,13 @@ def foo(bar): methods={"foo": foo}, request='{"jsonrpc": "2.0", "method": "foo"}', ) - == None + is None ) @patch("jsonrpcserver.dispatcher.dispatch_request", side_effect=ValueError("foo")) -def test_dispatch_to_response_pure_notification_server_error(*_): - def foo(): +def test_dispatch_to_response_pure_notification_server_error(*_: Mock) -> None: + def foo() -> Result: return Ok() assert dispatch_to_response_pure( @@ -624,11 +653,11 @@ def foo(): ) == Failure(ErrorResponse(ERROR_SERVER_ERROR, "Server error", "foo", None)) -def test_dispatch_to_response_pure_notification_invalid_result(): +def test_dispatch_to_response_pure_notification_invalid_result() -> None: """Methods should return a Result, otherwise we get an Internal Error response.""" - def not_a_result(): - return None + def not_a_result() -> Result: + return None # type: ignore assert ( dispatch_to_response_pure( @@ -639,14 +668,14 @@ def not_a_result(): methods={"not_a_result": not_a_result}, request='{"jsonrpc": "2.0", "method": "not_a_result"}', ) - == None + is None ) -def test_dispatch_to_response_pure_notification_raising_exception(): +def test_dispatch_to_response_pure_notification_raising_exception() -> None: """Allow raising an exception to return an error.""" - def raise_exception(): + def raise_exception() -> Result: raise JsonRpcError(code=0, message="foo", data="bar") assert ( @@ -658,23 +687,23 @@ def raise_exception(): methods={"raise_exception": raise_exception}, request='{"jsonrpc": "2.0", "method": "raise_exception"}', ) - == None + is None ) # dispatch_to_response -def test_dispatch_to_response(): +def test_dispatch_to_response() -> None: response = dispatch_to_response( '{"jsonrpc": "2.0", "method": "ping", "id": 1}', {"ping": ping} ) assert response == Success(SuccessResponse("pong", 1)) -def test_dispatch_to_response_with_global_methods(): +def test_dispatch_to_response_with_global_methods() -> None: @method - def ping(): + def ping() -> Result: # pylint: disable=redefined-outer-name return Ok("pong") response = dispatch_to_response('{"jsonrpc": "2.0", "method": "ping", "id": 1}') @@ -684,8 +713,8 @@ def ping(): # The remaining tests are direct from the examples in the specification -def test_examples_positionals(): - def subtract(minuend, subtrahend): +def test_examples_positionals() -> None: + def subtract(minuend: int, subtrahend: int) -> Result: return Ok(minuend - subtrahend) response = dispatch_to_response_pure( @@ -710,8 +739,8 @@ def subtract(minuend, subtrahend): assert response == Success(SuccessResponse(-19, 2)) -def test_examples_nameds(): - def subtract(**kwargs): +def test_examples_nameds() -> None: + def subtract(**kwargs: int) -> Result: return Ok(kwargs["minuend"] - kwargs["subtrahend"]) response = dispatch_to_response_pure( @@ -720,7 +749,10 @@ def subtract(**kwargs): validator=default_validator, post_process=identity, deserializer=default_deserializer, - request='{"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}', + request=( + '{"jsonrpc": "2.0", "method": "subtract", ' + '"params": {"subtrahend": 23, "minuend": 42}, "id": 3}' + ), ) assert response == Success(SuccessResponse(19, 3)) @@ -731,14 +763,20 @@ def subtract(**kwargs): validator=default_validator, post_process=identity, deserializer=default_deserializer, - request='{"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}', + request=( + '{"jsonrpc": "2.0", "method": "subtract", ' + '"params": {"minuend": 42, "subtrahend": 23}, "id": 4}' + ), ) assert response == Success(SuccessResponse(19, 4)) -def test_examples_notification(): +def test_examples_notification() -> None: + def f() -> Result: + return Ok() + response = dispatch_to_response_pure( - methods={"update": lambda: None, "foobar": lambda: None}, + methods={"update": f, "foobar": f}, context=NOCONTEXT, validator=default_validator, post_process=identity, @@ -749,7 +787,7 @@ def test_examples_notification(): # Second example response = dispatch_to_response_pure( - methods={"update": lambda: None, "foobar": lambda: None}, + methods={"update": f, "foobar": f}, context=NOCONTEXT, validator=default_validator, post_process=identity, @@ -759,14 +797,17 @@ def test_examples_notification(): assert response is None -def test_examples_invalid_json(): +def test_examples_invalid_json() -> None: response = dispatch_to_response_pure( methods={"ping": ping}, context=NOCONTEXT, validator=default_validator, post_process=identity, deserializer=default_deserializer, - request='[{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, {"jsonrpc": "2.0", "method"]', + request=( + '[{"jsonrpc": "2.0", "method": "sum", ' + '"params": [1,2,4], "id": "1"}, {"jsonrpc": "2.0", "method"]' + ), ) assert response == Failure( ErrorResponse( @@ -778,7 +819,7 @@ def test_examples_invalid_json(): ) -def test_examples_empty_array(): +def test_examples_empty_array() -> None: # This is an invalid JSON-RPC request, should return an error. response = dispatch_to_response_pure( request="[]", @@ -798,7 +839,7 @@ def test_examples_empty_array(): ) -def test_examples_invalid_jsonrpc_batch(): +def test_examples_invalid_jsonrpc_batch() -> None: """ We break the spec here, by not validating each request in the batch individually. The examples are expecting a batch response full of error responses. @@ -821,7 +862,7 @@ def test_examples_invalid_jsonrpc_batch(): ) -def test_examples_multiple_invalid_jsonrpc(): +def test_examples_multiple_invalid_jsonrpc() -> None: """ We break the spec here, by not validating each request in the batch individually. The examples are expecting a batch response full of error responses. @@ -844,65 +885,40 @@ def test_examples_multiple_invalid_jsonrpc(): ) -def test_examples_mixed_requests_and_notifications(): - """ - We break the spec here. The examples put an invalid jsonrpc request in the - mix here. but it's removed to test the rest, because we're not validating - each request individually. Any invalid jsonrpc will respond with a single - error message. +def test_examples_mixed_requests_and_notifications() -> None: + """We break the spec here. The examples put an invalid jsonrpc request in the mix + here, but it's removed to test the rest, because we're not validating each request + individually. Any invalid jsonrpc will respond with a single error message. The spec example includes this which invalidates the entire request: {"foo": "boo"}, """ methods = { - "sum": lambda *args: Success(SuccessResult(sum(args))), - "notify_hello": lambda *args: Success(SuccessResult(19)), - "subtract": lambda *args: Success(SuccessResult(args[0] - sum(args[1:]))), - "get_data": lambda: Success(SuccessResult(["hello", 5])), + "sum": lambda *args: Ok(sum(args)), + "notify_hello": lambda *args: Ok(19), + "subtract": lambda *args: Ok(args[0] - sum(args[1:])), + "get_data": lambda: Ok(["hello", 5]), } - requests = json.dumps( - [ - {"jsonrpc": "2.0", "method": "sum", "params": [1, 2, 4], "id": "1"}, - {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}, - {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": "2"}, - { - "jsonrpc": "2.0", - "method": "foo.get", - "params": {"name": "myself"}, - "id": "5", - }, - {"jsonrpc": "2.0", "method": "get_data", "id": "9"}, - ] - ) - response = dispatch_to_response_pure( + response = dispatch( deserializer=default_deserializer, validator=default_validator, - post_process=identity, context=NOCONTEXT, methods=methods, - request=requests, - ) - expected = [ - Success( - SuccessResponse(result=7, id="1") - ), # {"jsonrpc": "2.0", "result": 7, "id": "1"}, - Success( - SuccessResponse(result=19, id="2") - ), # {"jsonrpc": "2.0", "result": 19, "id": "2"}, - Failure( - ErrorResponse( - code=-32601, message="Method not found", data="foo.get", id="5" - ) - ), - # { - # "jsonrpc": "2.0", - # "error": {"code": -32601, "message": "Method not found", "data": "foo.get"}, - # "id": "5", - # }, - Success( - SuccessResponse(result=["hello", 5], id="9") - ), # {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}, + request="""[ + {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, + {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}, + {"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"}, + {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"}, + {"jsonrpc": "2.0", "method": "get_data", "id": "9"} + ]""", + ) + assert json.loads(response) == [ + {"jsonrpc": "2.0", "result": 7, "id": "1"}, + {"jsonrpc": "2.0", "result": 19, "id": "2"}, + { + "jsonrpc": "2.0", + "error": {"code": -32601, "message": "Method not found", "data": "foo.get"}, + "id": "5", + }, + {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}, ] - # assert isinstance(response, Iterable) - for r in response: - assert r in expected diff --git a/tests/test_main.py b/tests/test_main.py index f36e71a..f80d365 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,3 +1,4 @@ +"""Test main.py""" from returns.result import Success from jsonrpcserver.main import ( @@ -6,26 +7,28 @@ dispatch_to_json, ) from jsonrpcserver.response import SuccessResponse -from jsonrpcserver.result import Result, Ok +from jsonrpcserver.result import Ok, Result + +# pylint: disable=missing-function-docstring def ping() -> Result: return Ok("pong") -def test_dispatch_to_response(): +def test_dispatch_to_response() -> None: assert dispatch_to_response( '{"jsonrpc": "2.0", "method": "ping", "id": 1}', {"ping": ping} ) == Success(SuccessResponse("pong", 1)) -def test_dispatch_to_serializable(): +def test_dispatch_to_serializable() -> None: assert dispatch_to_serializable( '{"jsonrpc": "2.0", "method": "ping", "id": 1}', {"ping": ping} ) == {"jsonrpc": "2.0", "result": "pong", "id": 1} -def test_dispatch_to_json(): +def test_dispatch_to_json() -> None: assert ( dispatch_to_json( '{"jsonrpc": "2.0", "method": "ping", "id": 1}', {"ping": ping} @@ -34,7 +37,7 @@ def test_dispatch_to_json(): ) -def test_dispatch_to_json_notification(): +def test_dispatch_to_json_notification() -> None: assert ( dispatch_to_json('{"jsonrpc": "2.0", "method": "ping"}', {"ping": ping}) == "" ) diff --git a/tests/test_methods.py b/tests/test_methods.py index 08717b5..38555a3 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -1,17 +1,21 @@ +"""Test methods.py""" from jsonrpcserver.methods import global_methods, method +from jsonrpcserver.result import Result +# pylint: disable=missing-function-docstring -def test_decorator(): + +def test_decorator() -> None: @method - def foo(): + def func() -> Result: pass - assert callable(global_methods["foo"]) + assert callable(global_methods["func"]) -def test_decorator_custom_name(): - @method(name="baz") - def bar(): +def test_decorator_custom_name() -> None: + @method(name="new_name") + def name() -> None: pass - assert callable(global_methods["baz"]) + assert callable(global_methods["new_name"]) diff --git a/tests/test_request.py b/tests/test_request.py index d46fc6a..114a34d 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -1,11 +1,14 @@ +"""Test request.py""" from jsonrpcserver.request import Request +# pylint: disable=missing-function-docstring -def test_request(): + +def test_request() -> None: assert Request(method="foo", params=[], id=1).method == "foo" -def test_request_invalid(): +def test_request_invalid() -> None: # Should never happen, because the incoming request string is passed through the # jsonrpc schema before creating a Request pass diff --git a/tests/test_response.py b/tests/test_response.py index 28f67b5..5771ded 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -1,3 +1,4 @@ +"""Test response.py""" from unittest.mock import sentinel from returns.result import Failure, Success @@ -12,14 +13,16 @@ to_serializable, ) +# pylint: disable=missing-function-docstring,invalid-name,duplicate-code -def test_SuccessResponse(): + +def test_SuccessResponse() -> None: response = SuccessResponse(sentinel.result, sentinel.id) assert response.result == sentinel.result assert response.id == sentinel.id -def test_ErrorResponse(): +def test_ErrorResponse() -> None: response = ErrorResponse( sentinel.code, sentinel.message, sentinel.data, sentinel.id ) @@ -29,23 +32,23 @@ def test_ErrorResponse(): assert response.id is sentinel.id -def test_ParseErrorResponse(): +def test_ParseErrorResponse() -> None: response = ParseErrorResponse(sentinel.data) assert response.code == -32700 assert response.message == "Parse error" assert response.data == sentinel.data - assert response.id == None + assert response.id is None -def test_InvalidRequestResponse(): +def test_InvalidRequestResponse() -> None: response = InvalidRequestResponse(sentinel.data) assert response.code == -32600 assert response.message == "Invalid request" assert response.data == sentinel.data - assert response.id == None + assert response.id is None -def test_MethodNotFoundResponse(): +def test_MethodNotFoundResponse() -> None: response = MethodNotFoundResponse(sentinel.data, sentinel.id) assert response.code == -32601 assert response.message == "Method not found" @@ -53,7 +56,7 @@ def test_MethodNotFoundResponse(): assert response.id == sentinel.id -def test_ServerErrorResponse(): +def test_ServerErrorResponse() -> None: response = ServerErrorResponse(sentinel.data, sentinel.id) assert response.code == -32000 assert response.message == "Server error" @@ -61,7 +64,7 @@ def test_ServerErrorResponse(): assert response.id == sentinel.id -def test_to_serializable(): +def test_to_serializable() -> None: assert to_serializable(Success(SuccessResponse(sentinel.result, sentinel.id))) == { "jsonrpc": "2.0", "result": sentinel.result, @@ -69,11 +72,11 @@ def test_to_serializable(): } -def test_to_serializable_None(): - assert to_serializable(None) == None +def test_to_serializable_None() -> None: + assert to_serializable(None) is None -def test_to_serializable_SuccessResponse(): +def test_to_serializable_SuccessResponse() -> None: assert to_serializable(Success(SuccessResponse(sentinel.result, sentinel.id))) == { "jsonrpc": "2.0", "result": sentinel.result, @@ -81,7 +84,7 @@ def test_to_serializable_SuccessResponse(): } -def test_to_serializable_ErrorResponse(): +def test_to_serializable_ErrorResponse() -> None: assert to_serializable( Failure( ErrorResponse(sentinel.code, sentinel.message, sentinel.data, sentinel.id) @@ -97,7 +100,7 @@ def test_to_serializable_ErrorResponse(): } -def test_to_serializable_list(): +def test_to_serializable_list() -> None: assert to_serializable( [Success(SuccessResponse(sentinel.result, sentinel.id))] ) == [ diff --git a/tests/test_result.py b/tests/test_result.py index 6903254..dc02117 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -1,3 +1,4 @@ +"""Test result.py""" from unittest.mock import sentinel from returns.result import Failure, Success @@ -7,57 +8,59 @@ Error, ErrorResult, InvalidParamsResult, - NODATA, SuccessResult, ) +from jsonrpcserver.sentinels import NODATA +# pylint: disable=missing-function-docstring,invalid-name -def test_SuccessResult(): + +def test_SuccessResult() -> None: assert SuccessResult(None).result is None -def test_SuccessResult_repr(): +def test_SuccessResult_repr() -> None: assert repr(SuccessResult(None)) == "SuccessResult(None)" -def test_ErrorResult(): +def test_ErrorResult() -> None: result = ErrorResult(sentinel.code, sentinel.message) assert result.code == sentinel.code assert result.message == sentinel.message assert result.data == NODATA -def test_ErrorResult_repr(): +def test_ErrorResult_repr() -> None: assert ( repr(ErrorResult(1, "foo", None)) == "ErrorResult(code=1, message='foo', data=None)" ) -def test_ErrorResult_with_data(): +def test_ErrorResult_with_data() -> None: result = ErrorResult(sentinel.code, sentinel.message, sentinel.data) assert result.code == sentinel.code assert result.message == sentinel.message assert result.data == sentinel.data -def test_InvalidParamsResult(): +def test_InvalidParamsResult() -> None: result = InvalidParamsResult(sentinel.data) assert result.code == -32602 assert result.message == "Invalid params" assert result.data == sentinel.data -def test_InvalidParamsResult_with_data(): +def test_InvalidParamsResult_with_data() -> None: result = InvalidParamsResult(sentinel.data) assert result.code == -32602 assert result.message == "Invalid params" assert result.data == sentinel.data -def test_Ok(): - assert Ok(None) == Success(SuccessResult(None)) +def test_Ok() -> None: + assert Ok() == Success(SuccessResult(None)) -def test_Error(): +def test_Error() -> None: assert Error(1, "foo", None) == Failure(ErrorResult(1, "foo", None)) diff --git a/tests/test_sentinels.py b/tests/test_sentinels.py index 95b1036..c5c8f02 100644 --- a/tests/test_sentinels.py +++ b/tests/test_sentinels.py @@ -1,5 +1,8 @@ +"""Test sentinels.py""" from jsonrpcserver.sentinels import Sentinel +# pylint: disable=missing-function-docstring -def test_Sentinel(): + +def test_sentinel() -> None: assert repr(Sentinel("foo")) == "" diff --git a/tests/test_server.py b/tests/test_server.py index ee6f794..ad37266 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,8 +1,11 @@ -from unittest.mock import patch +"""Test server.py""" +from unittest.mock import Mock, patch from jsonrpcserver.server import serve +# pylint: disable=missing-function-docstring + @patch("jsonrpcserver.server.HTTPServer") -def test_serve(*_): +def test_serve(*_: Mock) -> None: serve() diff --git a/tests/test_status.py b/tests/test_status.py deleted file mode 100644 index e69de29..0000000 From f6294ee31c985db2c1d4ac0bb220db1cffc10168 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Mon, 10 Oct 2022 12:37:45 +1100 Subject: [PATCH 11/33] Fix not re-exported error --- tests/test_methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_methods.py b/tests/test_methods.py index f85ad9e..1b075bf 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -1,5 +1,5 @@ """Test methods.py""" -from jsonrpcserver import Result +from jsonrpcserver.result import Result from jsonrpcserver.methods import global_methods, method # pylint: disable=missing-function-docstring From ab30150cfe8c02d332e0d64558f73bb183d25722 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Thu, 10 Nov 2022 10:01:34 +1100 Subject: [PATCH 12/33] Upgrade pylint --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0f29dd..224c525 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: args: [--diff, --check] - repo: https://github.com/PyCQA/pylint - rev: v2.15.3 + rev: v2.15.5 hooks: - id: pylint stages: [manual] diff --git a/CHANGELOG.md b/CHANGELOG.md index 54bb948..745f977 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,8 @@ Other changes: for production use. - Use `Ok` instead of `Success` when returning a response. This is to avoid confusion with the Returns library now used internally which has it's own - `Success` class. This is not a breaking change, `Success` will still work for - now, however `Ok` is recommended. + `Success` class. This is not a breaking change. `Success` will still work for + now, but use `Ok` instead. ## 5.0.9 (Sep 15, 2022) From 4f510b1be877988ae82c50e81d602fa4fdc2bbb0 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Thu, 10 Nov 2022 10:13:01 +1100 Subject: [PATCH 13/33] Pylint fixes --- examples/fastapi_server.py | 2 +- jsonrpcserver/async_methods.py | 13 +++++++------ tests/test_async_dispatcher.py | 8 ++++---- tests/test_dispatcher.py | 8 ++++---- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/examples/fastapi_server.py b/examples/fastapi_server.py index bd61cf6..3a1e40d 100644 --- a/examples/fastapi_server.py +++ b/examples/fastapi_server.py @@ -1,7 +1,7 @@ """FastAPI server""" from fastapi import FastAPI, Request, Response -from jsonrpcserver import dispatch, method, Ok, Result import uvicorn # type: ignore +from jsonrpcserver import dispatch, method, Ok, Result app = FastAPI() diff --git a/jsonrpcserver/async_methods.py b/jsonrpcserver/async_methods.py index 3364bd2..6de95a4 100644 --- a/jsonrpcserver/async_methods.py +++ b/jsonrpcserver/async_methods.py @@ -1,3 +1,4 @@ +"""Async methods""" from typing import Any, Awaitable, Callable, Dict, Optional, cast from returns.result import Result @@ -6,11 +7,11 @@ Method = Callable[..., Awaitable[Result[SuccessResult, ErrorResult]]] Methods = Dict[str, Method] -global_methods: Methods = dict() +global_methods: Methods = {} def method( - f: Optional[Method] = None, name: Optional[str] = None + func: Optional[Method] = None, name: Optional[str] = None ) -> Callable[..., Awaitable[Any]]: """A decorator to add a function into jsonrpcserver's internal global_methods dict. The global_methods dict will be used by default unless a methods argument is passed @@ -23,9 +24,9 @@ def foo(): ... """ - def decorator(func: Method) -> Method: + def decorator(func_: Method) -> Method: nonlocal name - global_methods[name or func.__name__] = func - return func + global_methods[name or func_.__name__] = func_ + return func_ - return decorator(f) if callable(f) else cast(Method, decorator) + return decorator(func) if callable(func) else cast(Method, decorator) diff --git a/tests/test_async_dispatcher.py b/tests/test_async_dispatcher.py index 1333b9b..3a0c8da 100644 --- a/tests/test_async_dispatcher.py +++ b/tests/test_async_dispatcher.py @@ -87,7 +87,7 @@ async def test_dispatch_to_response_pure_success() -> None: @patch("jsonrpcserver.async_dispatcher.dispatch_request", side_effect=ValueError("foo")) @pytest.mark.asyncio async def test_dispatch_to_response_pure_server_error(_: Mock) -> None: - async def foo() -> Result: + async def ping() -> Result: return Ok() assert await dispatch_to_response_pure( @@ -95,6 +95,6 @@ async def foo() -> Result: validator=default_validator, post_process=identity, context=NOCONTEXT, - methods={"foo": foo}, - request='{"jsonrpc": "2.0", "method": "foo", "id": 1}', - ) == Failure(ErrorResponse(ERROR_SERVER_ERROR, "Server error", "foo", None)) + methods={"ping": ping}, + request='{"jsonrpc": "2.0", "method": "ping", "id": 1}', + ) == Failure(ErrorResponse(ERROR_SERVER_ERROR, "Server error", "ping", None)) diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 3c68550..760db1f 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -2,7 +2,7 @@ TODO: Add tests for dispatch_requests (non-pure version) """ -from typing import Any, Callable, Dict +from typing import Any from unittest.mock import Mock, patch, sentinel import json import pytest @@ -167,14 +167,14 @@ def f() -> Result: def test_validate_result_positionals() -> None: - def f(x: int) -> Result: + def f(_: int) -> Result: return Ok() assert validate_args(Request("f", [1], NOID), NOCONTEXT, f) == Success(f) def test_validate_result_positionals_not_passed() -> None: - def f(x: str) -> Result: + def f(_: str) -> Result: return Ok() assert validate_args(Request("f", {"foo": "bar"}, NOID), NOCONTEXT, f) == Failure( @@ -185,7 +185,7 @@ def f(x: str) -> Result: def test_validate_result_keywords() -> None: - def f(**kwargs: str) -> Result: + def f(**_: str) -> Result: return Ok() assert validate_args(Request("f", {"foo": "bar"}, NOID), NOCONTEXT, f) == Success(f) From 73ba6a37692d18bc41ed69c74def8a870232174c Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Thu, 10 Nov 2022 10:33:49 +1100 Subject: [PATCH 14/33] Update changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 745f977..737b24a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,8 @@ Other changes: for production use. - Use `Ok` instead of `Success` when returning a response. This is to avoid confusion with the Returns library now used internally which has it's own - `Success` class. This is not a breaking change. `Success` will still work for - now, but use `Ok` instead. + `Success` class. It also matches Jsonrpcclient's `Ok` type. This is not a + breaking change, `Success` will still work for now. But use `Ok` instead. ## 5.0.9 (Sep 15, 2022) From 0e6752249adeb7af69e31e4b8ab33b70ef2362d5 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Wed, 16 Nov 2022 12:01:21 +1100 Subject: [PATCH 15/33] Fix some tests --- tests/test_async_dispatcher.py | 2 +- tests/test_dispatcher.py | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/test_async_dispatcher.py b/tests/test_async_dispatcher.py index 3a0c8da..e70d71a 100644 --- a/tests/test_async_dispatcher.py +++ b/tests/test_async_dispatcher.py @@ -97,4 +97,4 @@ async def ping() -> Result: context=NOCONTEXT, methods={"ping": ping}, request='{"jsonrpc": "2.0", "method": "ping", "id": 1}', - ) == Failure(ErrorResponse(ERROR_SERVER_ERROR, "Server error", "ping", None)) + ) == Failure(ErrorResponse(ERROR_SERVER_ERROR, "Server error", "foo", None)) diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 760db1f..9583e52 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -167,19 +167,25 @@ def f() -> Result: def test_validate_result_positionals() -> None: - def f(_: int) -> Result: + def ping_(_: int) -> Result: return Ok() - assert validate_args(Request("f", [1], NOID), NOCONTEXT, f) == Success(f) + assert validate_args(Request("ping_", [1], NOID), NOCONTEXT, ping_) == Success( + ping_ + ) def test_validate_result_positionals_not_passed() -> None: - def f(_: str) -> Result: + def ping_(name: str) -> Result: return Ok() - assert validate_args(Request("f", {"foo": "bar"}, NOID), NOCONTEXT, f) == Failure( + assert validate_args( + Request("ping_", {"foo": "bar"}, NOID), NOCONTEXT, ping_ + ) == Failure( ErrorResult( - ERROR_INVALID_PARAMS, "Invalid params", "missing a required argument: 'x'" + ERROR_INVALID_PARAMS, + "Invalid params", + "missing a required argument: 'name'", ) ) From f4e8761c7673f80d198b3f4dd4c5df2289ca0c15 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Sun, 26 Feb 2023 22:28:21 +1100 Subject: [PATCH 16/33] Fix parameters --- .pre-commit-config.yaml | 26 +- docs/dispatch.md | 32 ++- examples/fastapi_server.py | 2 +- jsonrpcserver/async_main.py | 4 +- jsonrpcserver/dispatcher.py | 45 +-- jsonrpcserver/main.py | 43 +-- tests/test_async_dispatcher.py | 6 +- tests/test_dispatcher.py | 482 +++++++++++++++++---------------- tests/test_main.py | 17 +- 9 files changed, 353 insertions(+), 304 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 224c525..7a536a3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,26 +9,14 @@ repos: - id: black args: [--diff, --check] - - repo: https://github.com/PyCQA/pylint - rev: v2.15.5 + - repo: local hooks: - - id: pylint - stages: [manual] - additional_dependencies: - - returns<1 - - aiohttp<4 - - aiozmq<1 - - django<5 - - fastapi<1 - - flask<3 - - flask-socketio<5.3.1 - - jsonschema<5 - - pytest - - pyzmq - - sanic - - tornado<7 - - uvicorn<1 - - websockets<11 + - id: pylint + name: pylint + entry: pylint + language: system + types: [python] + require_serial: true - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.971 diff --git a/docs/dispatch.md b/docs/dispatch.md index 1e743ec..2278b47 100644 --- a/docs/dispatch.md +++ b/docs/dispatch.md @@ -12,6 +12,9 @@ and gives a JSON-RPC response. ## Optional parameters +The `dispatch` function has some optional parameters that allow you to +customise how it works. + ### methods This lets you specify the methods to dispatch to. It's an alternative to using @@ -43,12 +46,32 @@ def greet(context, name): ### deserializer -A function that parses the request string. Default is `json.loads`. +A function that parses the JSON request string. Default is `json.loads`. ```python dispatch(request, deserializer=ujson.loads) ``` +### jsonrpc_validator + +A function that validates the request once the JSON string has been parsed. The +function should raise an exception (any exception) if the request doesn't match +the JSON-RPC spec (https://www.jsonrpc.org/specification). Default is +`default_jsonrpc_validator` which uses Jsonschema to validate requests against +a schema. + +To disable JSON-RPC validation, pass `jsonrpc_validator=lambda _: None`, which +will improve performance because this validation takes around half the dispatch +time. + +### args_validator + +A function that validates a request's parameters against the signature of the +Python function that will be called for it. Note this should not validate the +_values_ of the parameters, it should simply ensure the parameters match the +Python function's signature. For reference, see the `validate_args` function in +`dispatcher.py`, which is the default `args_validator`. + ### serializer A function that serializes the response string. Default is `json.dumps`. @@ -56,10 +79,3 @@ A function that serializes the response string. Default is `json.dumps`. ```python dispatch(request, serializer=ujson.dumps) ``` - -### validator - -A function that validates the request once the json has been parsed. The -function should raise an exception (any exception) if the request doesn't match -the JSON-RPC spec. Default is `default_validator` which validates the request -against a schema. diff --git a/examples/fastapi_server.py b/examples/fastapi_server.py index 3a1e40d..c02b216 100644 --- a/examples/fastapi_server.py +++ b/examples/fastapi_server.py @@ -1,6 +1,6 @@ """FastAPI server""" from fastapi import FastAPI, Request, Response -import uvicorn # type: ignore +import uvicorn from jsonrpcserver import dispatch, method, Ok, Result app = FastAPI() diff --git a/jsonrpcserver/async_main.py b/jsonrpcserver/async_main.py index fe5f749..39c0f35 100644 --- a/jsonrpcserver/async_main.py +++ b/jsonrpcserver/async_main.py @@ -5,7 +5,7 @@ from .async_dispatcher import dispatch_to_response_pure from .async_methods import Methods, global_methods from .dispatcher import Deserialized -from .main import default_validator, default_deserializer +from .main import default_jsonrpc_validator, default_deserializer from .response import Response, to_serializable from .sentinels import NOCONTEXT from .utils import identity @@ -20,7 +20,7 @@ async def dispatch_to_response( *, context: Any = NOCONTEXT, deserializer: Callable[[str], Deserialized] = default_deserializer, - validator: Callable[[Deserialized], Deserialized] = default_validator, + validator: Callable[[Deserialized], Deserialized] = default_jsonrpc_validator, post_process: Callable[[Response], Any] = identity, ) -> Union[Response, Iterable[Response], None]: return await dispatch_to_response_pure( diff --git a/jsonrpcserver/dispatcher.py b/jsonrpcserver/dispatcher.py index c2c1bd1..060d434 100644 --- a/jsonrpcserver/dispatcher.py +++ b/jsonrpcserver/dispatcher.py @@ -31,6 +31,7 @@ from .sentinels import NOCONTEXT, NOID from .utils import compose, make_list +ArgsValidator = Callable[[Any, Request, Method], Result[Method, ErrorResult]] Deserialized = Union[Dict[str, Any], List[Dict[str, Any]]] logger = logging.getLogger(__name__) @@ -120,7 +121,9 @@ def validate_result(result: Result[SuccessResult, ErrorResult]) -> None: def call( - request: Request, context: Any, method: Method + request: Request, + context: Any, + method: Method, ) -> Result[SuccessResult, ErrorResult]: """Call the method. @@ -146,7 +149,9 @@ def call( def validate_args( - request: Request, context: Any, func: Method + request: Request, + context: Any, + func: Method, ) -> Result[Method, ErrorResult]: """Ensure the method can be called with the arguments given. @@ -171,7 +176,10 @@ def get_method(methods: Methods, method_name: str) -> Result[Method, ErrorResult def dispatch_request( - methods: Methods, context: Any, request: Request + args_validator: ArgsValidator, + methods: Methods, + context: Any, + request: Request, ) -> Tuple[Request, Result[SuccessResult, ErrorResult]]: """Get the method, validates the arguments and calls the method. @@ -182,7 +190,7 @@ def dispatch_request( return ( request, get_method(methods, request.method) - .bind(partial(validate_args, request, context)) + .bind(partial(args_validator, request, context)) .bind(partial(call, request, context)), ) @@ -203,9 +211,10 @@ def not_notification(request_result: Any) -> bool: def dispatch_deserialized( + args_validator: ArgsValidator, + post_process: Callable[[Response], Response], methods: Methods, context: Any, - post_process: Callable[[Response], Response], deserialized: Deserialized, ) -> Union[Response, List[Response], None]: """This is simply continuing the pipeline from dispatch_to_response_pure. It exists @@ -216,7 +225,9 @@ def dispatch_deserialized( applied to the Response(s). """ results = map( - compose(partial(dispatch_request, methods, context), create_request), + compose( + partial(dispatch_request, args_validator, methods, context), create_request + ), make_list(deserialized), ) responses = starmap(to_response, filter(not_notification, results)) @@ -224,7 +235,7 @@ def dispatch_deserialized( def validate_request( - validator: Callable[[Deserialized], Deserialized], request: Deserialized + jsonrpc_validator: Callable[[Deserialized], Deserialized], request: Deserialized ) -> Result[Deserialized, ErrorResponse]: """Validate the request against a JSON-RPC schema. @@ -233,9 +244,9 @@ def validate_request( Returns: Either the same request passed in or an Invalid request response. """ try: - validator(request) + jsonrpc_validator(request) # Since the validator is unknown, the specific exception that will be raised is also - # unknown. Any exception raised we assume the request is invalid and return an + # unknown. Any exception raised we assume the request is invalid and return an # "invalid request" response. except Exception: # pylint: disable=broad-except return Failure(InvalidRequestResponse("The request failed schema validation")) @@ -259,29 +270,31 @@ def deserialize_request( def dispatch_to_response_pure( - *, + args_validator: ArgsValidator, deserializer: Callable[[str], Deserialized], - validator: Callable[[Deserialized], Deserialized], + jsonrpc_validator: Callable[[Deserialized], Deserialized], + post_process: Callable[[Response], Response], methods: Methods, context: Any, - post_process: Callable[[Response], Response], request: str, ) -> Union[Response, List[Response], None]: """A function from JSON-RPC request string to Response namedtuple(s), (yet to be serialized to json). Returns: A single Response, a list of Responses, or None. None is given for - notifications or batches of notifications, to indicate that we should not - respond. + notifications or batches of notifications, to indicate that we should + not respond. """ try: result = deserialize_request(deserializer, request).bind( - partial(validate_request, validator) + partial(validate_request, jsonrpc_validator) ) return ( post_process(result) if isinstance(result, Failure) - else dispatch_deserialized(methods, context, post_process, result.unwrap()) + else dispatch_deserialized( + args_validator, post_process, methods, context, result.unwrap() + ) ) except Exception as exc: # pylint: disable=broad-except # There was an error with the jsonrpcserver library. diff --git a/jsonrpcserver/main.py b/jsonrpcserver/main.py index bbf824b..914c187 100644 --- a/jsonrpcserver/main.py +++ b/jsonrpcserver/main.py @@ -15,30 +15,38 @@ from jsonschema.validators import validator_for # type: ignore -from .dispatcher import dispatch_to_response_pure, Deserialized +from .dispatcher import ( + ArgsValidator, + Deserialized, + dispatch_to_response_pure, + validate_args, +) from .methods import Methods, global_methods from .response import Response, to_dict from .sentinels import NOCONTEXT from .utils import identity +default_args_validator = validate_args default_deserializer = json.loads -# Prepare the jsonschema validator. This is global so it loads only once, not every -# time dispatch is called. +# Prepare the jsonschema validator. This is global so it loads only once, not every time +# dispatch is called. schema = json.loads(read_text(__package__, "request-schema.json")) klass = validator_for(schema) klass.check_schema(schema) -default_validator = klass(schema).validate +default_jsonrpc_validator = klass(schema).validate def dispatch_to_response( request: str, - methods: Optional[Methods] = None, - *, + methods: Methods = global_methods, context: Any = NOCONTEXT, + args_validator: ArgsValidator = default_args_validator, deserializer: Callable[[str], Deserialized] = json.loads, - validator: Callable[[Deserialized], Deserialized] = default_validator, + jsonrpc_validator: Callable[ + [Deserialized], Deserialized + ] = default_jsonrpc_validator, post_process: Callable[[Response], Any] = identity, ) -> Union[Response, List[Response], None]: """Takes a JSON-RPC request string and dispatches it to method(s), giving Response @@ -54,9 +62,11 @@ def dispatch_to_response( populated with the @method decorator. context: If given, will be passed as the first argument to methods. deserializer: Function that deserializes the request string. - validator: Function that validates the JSON-RPC request. The function should - raise an exception if the request is invalid. To disable validation, pass - lambda _: None. + args_validator: Function that validates that the parameters in the request match + the Python function being called. + jsonrpc_validator: Function that validates the JSON-RPC request. The function + should raise an exception if the request is invalid. To disable validation, + pass lambda _: None. post_process: Function that will be applied to Responses. Returns: @@ -67,12 +77,13 @@ def dispatch_to_response( '{"jsonrpc": "2.0", "result": "pong", "id": 1}' """ return dispatch_to_response_pure( - deserializer=deserializer, - validator=validator, - post_process=post_process, - context=context, - methods=global_methods if methods is None else methods, - request=request, + args_validator, + deserializer, + jsonrpc_validator, + post_process, + methods, + context, + request, ) diff --git a/tests/test_async_dispatcher.py b/tests/test_async_dispatcher.py index e70d71a..4030ca3 100644 --- a/tests/test_async_dispatcher.py +++ b/tests/test_async_dispatcher.py @@ -10,7 +10,7 @@ dispatch_request, dispatch_to_response_pure, ) -from jsonrpcserver.main import default_deserializer, default_validator +from jsonrpcserver.main import default_deserializer, default_jsonrpc_validator from jsonrpcserver.codes import ERROR_INTERNAL_ERROR, ERROR_SERVER_ERROR from jsonrpcserver.exceptions import JsonRpcError from jsonrpcserver.request import Request @@ -76,7 +76,7 @@ async def test_dispatch_deserialized() -> None: async def test_dispatch_to_response_pure_success() -> None: assert await dispatch_to_response_pure( deserializer=default_deserializer, - validator=default_validator, + validator=default_jsonrpc_validator, post_process=identity, context=NOCONTEXT, methods={"ping": ping}, @@ -92,7 +92,7 @@ async def ping() -> Result: assert await dispatch_to_response_pure( deserializer=default_deserializer, - validator=default_validator, + validator=default_jsonrpc_validator, post_process=identity, context=NOCONTEXT, methods={"ping": ping}, diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 9583e52..1511d50 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -33,12 +33,7 @@ validate_request, ) from jsonrpcserver.exceptions import JsonRpcError -from jsonrpcserver.main import ( - default_deserializer, - default_validator, - dispatch_to_response, - dispatch, -) +from jsonrpcserver.main import default_jsonrpc_validator from jsonrpcserver.methods import method from jsonrpcserver.request import Request from jsonrpcserver.response import ErrorResponse, SuccessResponse @@ -143,17 +138,17 @@ def test_extract_kwargs() -> None: assert extract_kwargs(Request("ping", {"foo": "bar"}, NOID)) == {"foo": "bar"} -# validate_result +# validate_args -def test_validate_result_no_arguments() -> None: +def test_validate_args_result_no_arguments() -> None: def f() -> Result: return Ok() assert validate_args(Request("f", [], NOID), NOCONTEXT, f) == Success(f) -def test_validate_result_no_arguments_too_many_positionals() -> None: +def test_validate_args_result_no_arguments_too_many_positionals() -> None: def f() -> Result: return Ok() @@ -166,7 +161,7 @@ def f() -> Result: ) -def test_validate_result_positionals() -> None: +def test_validate_args_positionals() -> None: def ping_(_: int) -> Result: return Ok() @@ -175,7 +170,7 @@ def ping_(_: int) -> Result: ) -def test_validate_result_positionals_not_passed() -> None: +def test_validate_args_positionals_not_passed() -> None: def ping_(name: str) -> Result: return Ok() @@ -190,14 +185,14 @@ def ping_(name: str) -> Result: ) -def test_validate_result_keywords() -> None: +def test_validate_args_keywords() -> None: def f(**_: str) -> Result: return Ok() assert validate_args(Request("f", {"foo": "bar"}, NOID), NOCONTEXT, f) == Success(f) -def test_validate_result_object_method() -> None: +def test_validate_args_object_method() -> None: class FooClass: def f(self, *_: str) -> Result: return Ok() @@ -240,11 +235,11 @@ def method_() -> Result: "argument,value", [ ( - validate_args(Request("ping", [], 1), NOCONTEXT, ping), + Request("ping", [], 1), Success(ping), ), ( - validate_args(Request("ping", ["foo"], 1), NOCONTEXT, ping), + Request("ping", ["foo"], 1), Failure( ErrorResult( ERROR_INVALID_PARAMS, @@ -255,8 +250,8 @@ def method_() -> Result: ), ], ) -def test_validate_args(argument: Result, value: Result) -> None: - assert argument == value +def test_validate_args(argument: Request, value: Result) -> None: + assert validate_args(argument, NOCONTEXT, ping) == value # get_method @@ -286,7 +281,7 @@ def test_get_method(argument: Result, value: Result) -> None: def test_dispatch_request() -> None: request = Request("ping", [], 1) - assert dispatch_request({"ping": ping}, NOCONTEXT, request) == ( + assert dispatch_request(validate_args, {"ping": ping}, NOCONTEXT, request) == ( request, Success(SuccessResult("pong")), ) @@ -298,6 +293,7 @@ def ping_with_context(context: Any) -> Result: return Ok() dispatch_request( + validate_args, {"ping_with_context": ping_with_context}, sentinel.context, Request("ping_with_context", [], 1), @@ -329,10 +325,11 @@ def test_not_notification_false() -> None: def test_dispatch_deserialized() -> None: assert dispatch_deserialized( - methods={"ping": ping}, - context=NOCONTEXT, - post_process=identity, - deserialized={"jsonrpc": "2.0", "method": "ping", "id": 1}, + validate_args, + identity, + {"ping": ping}, + NOCONTEXT, + {"jsonrpc": "2.0", "method": "ping", "id": 1}, ) == Success(SuccessResponse("pong", 1)) @@ -341,11 +338,11 @@ def test_dispatch_deserialized() -> None: def test_validate_request() -> None: request = {"jsonrpc": "2.0", "method": "ping"} - assert validate_request(default_validator, request) == Success(request) + assert validate_request(default_jsonrpc_validator, request) == Success(request) def test_validate_request_invalid() -> None: - assert validate_request(default_validator, {"jsonrpc": "2.0"}) == Failure( + assert validate_request(default_jsonrpc_validator, {"jsonrpc": "2.0"}) == Failure( ErrorResponse( ERROR_INVALID_REQUEST, "Invalid request", @@ -360,24 +357,26 @@ def test_validate_request_invalid() -> None: def test_dispatch_to_response_pure() -> None: assert dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"ping": ping}, - request='{"jsonrpc": "2.0", "method": "ping", "id": 1}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"ping": ping}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "ping", "id": 1}', ) == Success(SuccessResponse("pong", 1)) def test_dispatch_to_response_pure_parse_error() -> None: """Unable to parse, must return an error""" assert dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"ping": ping}, - request="{", + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"ping": ping}, + NOCONTEXT, + "{", ) == Failure( ErrorResponse( ERROR_PARSE_ERROR, @@ -393,12 +392,13 @@ def test_dispatch_to_response_pure_invalid_request() -> None: notification). """ assert dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"ping": ping}, - request="{}", + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"ping": ping}, + NOCONTEXT, + "{}", ) == Failure( ErrorResponse( ERROR_INVALID_REQUEST, @@ -411,12 +411,13 @@ def test_dispatch_to_response_pure_invalid_request() -> None: def test_dispatch_to_response_pure_method_not_found() -> None: assert dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={}, - request='{"jsonrpc": "2.0", "method": "non_existant", "id": 1}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "non_existant", "id": 1}', ) == Failure( ErrorResponse(ERROR_METHOD_NOT_FOUND, "Method not found", "non_existant", 1) ) @@ -427,12 +428,13 @@ def f(colour: str, size: str) -> Result: # pylint: disable=unused-argument return Ok() assert dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"f": f}, - request='{"jsonrpc": "2.0", "method": "f", "params": {"colour":"blue"}, "id": 1}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"f": f}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "f", "params": {"colour":"blue"}, "id": 1}', ) == Failure( ErrorResponse( ERROR_INVALID_PARAMS, @@ -450,12 +452,13 @@ def foo(colour: str) -> Result: return Ok() assert dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"foo": foo}, - request='{"jsonrpc": "2.0", "method": "foo", "params": ["blue"], "id": 1}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"foo": foo}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "foo", "params": ["blue"], "id": 1}', ) == Failure(ErrorResponse(ERROR_INVALID_PARAMS, "Invalid params", NODATA, 1)) @@ -464,12 +467,13 @@ def foo() -> Result: raise ValueError("foo") assert dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"foo": foo}, - request='{"jsonrpc": "2.0", "method": "foo", "id": 1}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"foo": foo}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "foo", "id": 1}', ) == Failure(ErrorResponse(ERROR_INTERNAL_ERROR, "Internal error", "foo", 1)) @@ -479,12 +483,13 @@ def foo() -> Result: return Ok() assert dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"foo": foo}, - request='{"jsonrpc": "2.0", "method": "foo", "id": 1}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"foo": foo}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "foo", "id": 1}', ) == Failure(ErrorResponse(ERROR_SERVER_ERROR, "Server error", "foo", None)) @@ -495,12 +500,13 @@ def not_a_result() -> Result: return None # type: ignore assert dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"not_a_result": not_a_result}, - request='{"jsonrpc": "2.0", "method": "not_a_result", "id": 1}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"not_a_result": not_a_result}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "not_a_result", "id": 1}', ) == Failure( ErrorResponse( ERROR_INTERNAL_ERROR, @@ -518,12 +524,13 @@ def raise_exception() -> Result: raise JsonRpcError(code=0, message="foo", data="bar") assert dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"raise_exception": raise_exception}, - request='{"jsonrpc": "2.0", "method": "raise_exception", "id": 1}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"raise_exception": raise_exception}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "raise_exception", "id": 1}', ) == Failure(ErrorResponse(0, "foo", "bar", 1)) @@ -533,12 +540,13 @@ def raise_exception() -> Result: def test_dispatch_to_response_pure_notification() -> None: assert ( dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"ping": ping}, - request='{"jsonrpc": "2.0", "method": "ping"}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"ping": ping}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "ping"}', ) is None ) @@ -547,12 +555,13 @@ def test_dispatch_to_response_pure_notification() -> None: def test_dispatch_to_response_pure_notification_parse_error() -> None: """Unable to parse, must return an error""" assert dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"ping": ping}, - request="{", + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"ping": ping}, + NOCONTEXT, + "{", ) == Failure( ErrorResponse( ERROR_PARSE_ERROR, @@ -566,12 +575,13 @@ def test_dispatch_to_response_pure_notification_parse_error() -> None: def test_dispatch_to_response_pure_notification_invalid_request() -> None: """Invalid JSON-RPC, must return an error. (impossible to determine if notification)""" assert dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"ping": ping}, - request="{}", + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"ping": ping}, + NOCONTEXT, + "{}", ) == Failure( ErrorResponse( ERROR_INVALID_REQUEST, @@ -585,12 +595,13 @@ def test_dispatch_to_response_pure_notification_invalid_request() -> None: def test_dispatch_to_response_pure_notification_method_not_found() -> None: assert ( dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={}, - request='{"jsonrpc": "2.0", "method": "non_existant"}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "non_existant"}', ) is None ) @@ -602,12 +613,13 @@ def foo(colour: str, size: str) -> Result: # pylint: disable=unused-argument assert ( dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"foo": foo}, - request='{"jsonrpc": "2.0", "method": "foo", "params": {"colour":"blue"}}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"foo": foo}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "foo", "params": {"colour":"blue"}}', ) is None ) @@ -621,12 +633,13 @@ def foo(colour: str) -> Result: assert ( dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"foo": foo}, - request='{"jsonrpc": "2.0", "method": "foo", "params": ["blue"]}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"foo": foo}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "foo", "params": ["blue"]}', ) is None ) @@ -638,12 +651,13 @@ def foo(bar: str) -> Result: assert ( dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"foo": foo}, - request='{"jsonrpc": "2.0", "method": "foo"}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"foo": foo}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "foo"}', ) is None ) @@ -655,12 +669,13 @@ def foo() -> Result: return Ok() assert dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"foo": foo}, - request='{"jsonrpc": "2.0", "method": "foo"}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"foo": foo}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "foo"}', ) == Failure(ErrorResponse(ERROR_SERVER_ERROR, "Server error", "foo", None)) @@ -672,12 +687,13 @@ def not_a_result() -> Result: assert ( dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"not_a_result": not_a_result}, - request='{"jsonrpc": "2.0", "method": "not_a_result"}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"not_a_result": not_a_result}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "not_a_result"}', ) is None ) @@ -691,36 +707,18 @@ def raise_exception() -> Result: assert ( dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"raise_exception": raise_exception}, - request='{"jsonrpc": "2.0", "method": "raise_exception"}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"raise_exception": raise_exception}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "raise_exception"}', ) is None ) -# dispatch_to_response - - -def test_dispatch_to_response() -> None: - response = dispatch_to_response( - '{"jsonrpc": "2.0", "method": "ping", "id": 1}', {"ping": ping} - ) - assert response == Success(SuccessResponse("pong", 1)) - - -def test_dispatch_to_response_with_global_methods() -> None: - @method - def ping() -> Result: # pylint: disable=redefined-outer-name - return Ok("pong") - - response = dispatch_to_response('{"jsonrpc": "2.0", "method": "ping", "id": 1}') - assert response == Success(SuccessResponse("pong", 1)) - - # The remaining tests are direct from the examples in the specification @@ -729,23 +727,25 @@ def subtract(minuend: int, subtrahend: int) -> Result: return Ok(minuend - subtrahend) response = dispatch_to_response_pure( - methods={"subtract": subtract}, - context=NOCONTEXT, - validator=default_validator, - post_process=identity, - deserializer=default_deserializer, - request='{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"subtract": subtract}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}', ) assert response == Success(SuccessResponse(19, 1)) # Second example response = dispatch_to_response_pure( - methods={"subtract": subtract}, - context=NOCONTEXT, - validator=default_validator, - post_process=identity, - deserializer=default_deserializer, - request='{"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"subtract": subtract}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}', ) assert response == Success(SuccessResponse(-19, 2)) @@ -755,12 +755,13 @@ def subtract(**kwargs: int) -> Result: return Ok(kwargs["minuend"] - kwargs["subtrahend"]) response = dispatch_to_response_pure( - methods={"subtract": subtract}, - context=NOCONTEXT, - validator=default_validator, - post_process=identity, - deserializer=default_deserializer, - request=( + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"subtract": subtract}, + NOCONTEXT, + ( '{"jsonrpc": "2.0", "method": "subtract", ' '"params": {"subtrahend": 23, "minuend": 42}, "id": 3}' ), @@ -769,12 +770,13 @@ def subtract(**kwargs: int) -> Result: # Second example response = dispatch_to_response_pure( - methods={"subtract": subtract}, - context=NOCONTEXT, - validator=default_validator, - post_process=identity, - deserializer=default_deserializer, - request=( + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"subtract": subtract}, + NOCONTEXT, + ( '{"jsonrpc": "2.0", "method": "subtract", ' '"params": {"minuend": 42, "subtrahend": 23}, "id": 4}' ), @@ -787,35 +789,38 @@ def f() -> Result: return Ok() response = dispatch_to_response_pure( - methods={"update": f, "foobar": f}, - context=NOCONTEXT, - validator=default_validator, - post_process=identity, - deserializer=default_deserializer, - request='{"jsonrpc": "2.0", "method": "update", "params": [1, 2, 3, 4, 5]}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"update": f, "foobar": f}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "update", "params": [1, 2, 3, 4, 5]}', ) assert response is None # Second example response = dispatch_to_response_pure( - methods={"update": f, "foobar": f}, - context=NOCONTEXT, - validator=default_validator, - post_process=identity, - deserializer=default_deserializer, - request='{"jsonrpc": "2.0", "method": "foobar"}', + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"update": f, "foobar": f}, + NOCONTEXT, + '{"jsonrpc": "2.0", "method": "foobar"}', ) assert response is None def test_examples_invalid_json() -> None: response = dispatch_to_response_pure( - methods={"ping": ping}, - context=NOCONTEXT, - validator=default_validator, - post_process=identity, - deserializer=default_deserializer, - request=( + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"ping": ping}, + NOCONTEXT, + ( '[{"jsonrpc": "2.0", "method": "sum", ' '"params": [1,2,4], "id": "1"}, {"jsonrpc": "2.0", "method"]' ), @@ -833,12 +838,13 @@ def test_examples_invalid_json() -> None: def test_examples_empty_array() -> None: # This is an invalid JSON-RPC request, should return an error. response = dispatch_to_response_pure( - request="[]", - methods={"ping": ping}, - context=NOCONTEXT, - validator=default_validator, - post_process=identity, - deserializer=default_deserializer, + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"ping": ping}, + NOCONTEXT, + "[]", ) assert response == Failure( ErrorResponse( @@ -856,12 +862,13 @@ def test_examples_invalid_jsonrpc_batch() -> None: The examples are expecting a batch response full of error responses. """ response = dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"ping": ping}, - request="[1]", + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"ping": ping}, + NOCONTEXT, + "[1]", ) assert response == Failure( ErrorResponse( @@ -879,12 +886,13 @@ def test_examples_multiple_invalid_jsonrpc() -> None: The examples are expecting a batch response full of error responses. """ response = dispatch_to_response_pure( - deserializer=default_deserializer, - validator=default_validator, - post_process=identity, - context=NOCONTEXT, - methods={"ping": ping}, - request="[1, 2, 3]", + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + {"ping": ping}, + NOCONTEXT, + "[1, 2, 3]", ) assert response == Failure( ErrorResponse( @@ -910,12 +918,14 @@ def test_examples_mixed_requests_and_notifications() -> None: "subtract": lambda *args: Ok(args[0] - sum(args[1:])), "get_data": lambda: Ok(["hello", 5]), } - response = dispatch( - deserializer=default_deserializer, - validator=default_validator, - context=NOCONTEXT, - methods=methods, - request="""[ + response = dispatch_to_response_pure( + validate_args, + json.loads, + default_jsonrpc_validator, + identity, + methods, + NOCONTEXT, + """[ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}, {"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"}, @@ -923,13 +933,9 @@ def test_examples_mixed_requests_and_notifications() -> None: {"jsonrpc": "2.0", "method": "get_data", "id": "9"} ]""", ) - assert json.loads(response) == [ - {"jsonrpc": "2.0", "result": 7, "id": "1"}, - {"jsonrpc": "2.0", "result": 19, "id": "2"}, - { - "jsonrpc": "2.0", - "error": {"code": -32601, "message": "Method not found", "data": "foo.get"}, - "id": "5", - }, - {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}, + assert response == [ + Success(SuccessResponse(7, id="1")), + Success(SuccessResponse(19, id="2")), + Failure(ErrorResponse(-32601, "Method not found", "foo.get", id="5")), + Success(SuccessResponse(["hello", 5], id="9")), ] diff --git a/tests/test_main.py b/tests/test_main.py index 594e4ef..ad6d284 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -2,10 +2,16 @@ from returns.result import Success from jsonrpcserver.main import ( + default_args_validator, + default_deserializer, + default_jsonrpc_validator, + dispatch, + dispatch_to_json, + dispatch_to_response, dispatch_to_response, dispatch_to_serializable, - dispatch_to_json, ) +from jsonrpcserver.methods import method from jsonrpcserver.response import SuccessResponse from jsonrpcserver.result import Ok, Result @@ -24,6 +30,15 @@ def test_dispatch_to_response() -> None: ) == Success(SuccessResponse("pong", 1)) +def test_dispatch_to_response_with_global_methods() -> None: + @method + def ping() -> Result: # pylint: disable=redefined-outer-name + return Ok("pong") + + response = dispatch_to_response('{"jsonrpc": "2.0", "method": "ping", "id": 1}') + assert response == Success(SuccessResponse("pong", 1)) + + def test_dispatch_to_serializable() -> None: assert dispatch_to_serializable( '{"jsonrpc": "2.0", "method": "ping", "id": 1}', {"ping": ping} From 558215bc5bd3c2434821fbbf5a197ce28e35c590 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Sun, 26 Feb 2023 22:32:25 +1100 Subject: [PATCH 17/33] Fixes to satisfy ruff --- jsonrpcserver/main.py | 2 +- jsonrpcserver/result.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/jsonrpcserver/main.py b/jsonrpcserver/main.py index 914c187..5baf23d 100644 --- a/jsonrpcserver/main.py +++ b/jsonrpcserver/main.py @@ -10,7 +10,7 @@ notifications). """ from importlib.resources import read_text -from typing import Any, Callable, Dict, List, Optional, Union, cast +from typing import Any, Callable, Dict, List, Union, cast import json from jsonschema.validators import validator_for # type: ignore diff --git a/jsonrpcserver/result.py b/jsonrpcserver/result.py index 6f689b1..cba8107 100644 --- a/jsonrpcserver/result.py +++ b/jsonrpcserver/result.py @@ -29,7 +29,10 @@ class ErrorResult(NamedTuple): data: Any = NODATA # The spec says this value may be omitted def __repr__(self) -> str: - return f"ErrorResult(code={self.code!r}, message={self.message!r}, data={self.data!r})" + return ( + f"ErrorResult(code={self.code!r}, message={self.message!r}, " + "data={self.data!r})" + ) Result = R[SuccessResult, ErrorResult] From 0a88d8fcb291d4f7ac5b9dca96d2fe875b88a2e7 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Fri, 3 Mar 2023 17:32:37 +1100 Subject: [PATCH 18/33] Replace pylint with ruff --- .pre-commit-config.yaml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7a536a3..55063c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,14 +9,10 @@ repos: - id: black args: [--diff, --check] - - repo: local + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.253 hooks: - - id: pylint - name: pylint - entry: pylint - language: system - types: [python] - require_serial: true + - id: ruff - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.971 From 65f798c2e63015301647e13673d2458229af1a03 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Fri, 3 Mar 2023 17:32:55 +1100 Subject: [PATCH 19/33] Adjustments to satisfy ruff and mypy --- jsonrpcserver/main.py | 5 +++-- tests/test_dispatcher.py | 31 +++++++++++++++++++++---------- tests/test_main.py | 5 ----- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/jsonrpcserver/main.py b/jsonrpcserver/main.py index 5baf23d..66ba720 100644 --- a/jsonrpcserver/main.py +++ b/jsonrpcserver/main.py @@ -93,9 +93,10 @@ def dispatch_to_serializable( """Takes a JSON-RPC request string and dispatches it to method(s), giving responses as dicts (or None). """ + kwargs.setdefault("post_process", to_dict) return cast( Union[Dict[str, Any], List[Dict[str, Any]], None], - dispatch_to_response(*args, post_process=to_dict, **kwargs), + dispatch_to_response(*args, **kwargs), ) @@ -117,7 +118,7 @@ def dispatch_to_json( The rest: Passed through to dispatch_to_serializable. """ response = dispatch_to_serializable(*args, **kwargs) - # Better to respond with the empty string instead of json "null", because "null" is + # Better to respond with an empty string instead of json "null", because "null" is # an invalid JSON-RPC response. return "" if response is None else serializer(response) diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 1511d50..014a8a7 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -2,7 +2,7 @@ TODO: Add tests for dispatch_requests (non-pure version) """ -from typing import Any +from typing import Any, Dict from unittest.mock import Mock, patch, sentinel import json import pytest @@ -34,7 +34,7 @@ ) from jsonrpcserver.exceptions import JsonRpcError from jsonrpcserver.main import default_jsonrpc_validator -from jsonrpcserver.methods import method +from jsonrpcserver.methods import Method from jsonrpcserver.request import Request from jsonrpcserver.response import ErrorResponse, SuccessResponse from jsonrpcserver.result import ( @@ -47,8 +47,6 @@ from jsonrpcserver.sentinels import NOCONTEXT, NODATA, NOID from jsonrpcserver.utils import identity -# pylint: disable=missing-function-docstring,missing-class-docstring,too-few-public-methods,unnecessary-lambda-assignment,invalid-name,disallowed-name - def ping() -> Result: return Ok("pong") @@ -381,7 +379,10 @@ def test_dispatch_to_response_pure_parse_error() -> None: ErrorResponse( ERROR_PARSE_ERROR, "Parse error", - "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", + ( + "Expecting property name enclosed in double quotes: " + "line 1 column 2 (char 1)" + ), None, ) ) @@ -566,14 +567,19 @@ def test_dispatch_to_response_pure_notification_parse_error() -> None: ErrorResponse( ERROR_PARSE_ERROR, "Parse error", - "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", + ( + "Expecting property name enclosed in double quotes: " + "line 1 column 2 (char 1)" + ), None, ) ) def test_dispatch_to_response_pure_notification_invalid_request() -> None: - """Invalid JSON-RPC, must return an error. (impossible to determine if notification)""" + """Invalid JSON-RPC, must return an error. (impossible to determine if + notification) + """ assert dispatch_to_response_pure( validate_args, json.loads, @@ -625,7 +631,7 @@ def foo(colour: str, size: str) -> Result: # pylint: disable=unused-argument ) -def test_dispatch_to_response_pure_invalid_params_notification_explicitly_returned() -> None: +def test_dispatch_to_response_pure_invalid_params_notification_returned() -> None: def foo(colour: str) -> Result: if colour not in ("orange", "red", "yellow"): return InvalidParams() @@ -912,7 +918,7 @@ def test_examples_mixed_requests_and_notifications() -> None: The spec example includes this which invalidates the entire request: {"foo": "boo"}, """ - methods = { + methods: Dict[str, Method] = { "sum": lambda *args: Ok(sum(args)), "notify_hello": lambda *args: Ok(19), "subtract": lambda *args: Ok(args[0] - sum(args[1:])), @@ -929,7 +935,12 @@ def test_examples_mixed_requests_and_notifications() -> None: {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}, {"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"}, - {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"}, + { + "jsonrpc": "2.0", + "method": "foo.get", + "params": {"name": "myself"}, + "id": "5" + }, {"jsonrpc": "2.0", "method": "get_data", "id": "9"} ]""", ) diff --git a/tests/test_main.py b/tests/test_main.py index ad6d284..de1e5ee 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -2,13 +2,8 @@ from returns.result import Success from jsonrpcserver.main import ( - default_args_validator, - default_deserializer, - default_jsonrpc_validator, - dispatch, dispatch_to_json, dispatch_to_response, - dispatch_to_response, dispatch_to_serializable, ) from jsonrpcserver.methods import method From 843fdcd73fa98149c0c665b46f4c80becab79532 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Wed, 10 May 2023 16:55:26 +1000 Subject: [PATCH 20/33] Fix a repr --- jsonrpcserver/result.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonrpcserver/result.py b/jsonrpcserver/result.py index cba8107..a35315f 100644 --- a/jsonrpcserver/result.py +++ b/jsonrpcserver/result.py @@ -31,7 +31,7 @@ class ErrorResult(NamedTuple): def __repr__(self) -> str: return ( f"ErrorResult(code={self.code!r}, message={self.message!r}, " - "data={self.data!r})" + f"data={self.data!r})" ) From ae90cdf697c599028503857d459d6273fd672217 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Wed, 10 May 2023 17:14:05 +1000 Subject: [PATCH 21/33] Use ruff in github actions --- .github/workflows/code-quality.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 9eef871..347e62f 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -10,7 +10,7 @@ jobs: with: python-version: 3.8 - run: pip install --upgrade pip - - run: pip install types-setuptools "black<23" "pylint<3" "mypy<1" "jsonschema<5" pytest "returns<1" "aiohttp<4" "aiozmq<1" "django<5" "fastapi<1" "flask<3" "flask-socketio<5.3.1" "pyzmq" "sanic" "tornado<7" "uvicorn<1" "websockets<11" + - run: pip install types-setuptools "black<23" ruff "mypy<1" "jsonschema<5" pytest "returns<1" "aiohttp<4" "aiozmq<1" "django<5" "fastapi<1" "flask<3" "flask-socketio<5.3.1" "pyzmq" "sanic" "tornado<7" "uvicorn<1" "websockets<11" - run: black --diff --check $(git ls-files -- '*.py' ':!:docs/*') - - run: pylint $(git ls-files -- '*.py' ':!:docs/*') + - run: ruff $(git ls-files -- '*.py' ':!:docs/*') - run: mypy --strict $(git ls-files -- '*.py' ':!:docs/*') From 6db0f90e257cf601f36de447c114931a22a94c82 Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Wed, 10 May 2023 17:53:22 +1000 Subject: [PATCH 22/33] Fix type error --- .github/workflows/code-quality.yml | 2 +- .pre-commit-config.yaml | 2 +- tests/test_methods.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 347e62f..2c22310 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -10,7 +10,7 @@ jobs: with: python-version: 3.8 - run: pip install --upgrade pip - - run: pip install types-setuptools "black<23" ruff "mypy<1" "jsonschema<5" pytest "returns<1" "aiohttp<4" "aiozmq<1" "django<5" "fastapi<1" "flask<3" "flask-socketio<5.3.1" "pyzmq" "sanic" "tornado<7" "uvicorn<1" "websockets<11" + - run: pip install types-setuptools "black<23" ruff "mypy<2" "jsonschema<5" pytest "returns<1" "aiohttp<4" "aiozmq<1" "django<5" "fastapi<1" "flask<3" "flask-socketio<5.3.1" "pyzmq" "sanic" "tornado<7" "uvicorn<1" "websockets<11" - run: black --diff --check $(git ls-files -- '*.py' ':!:docs/*') - run: ruff $(git ls-files -- '*.py' ':!:docs/*') - run: mypy --strict $(git ls-files -- '*.py' ':!:docs/*') diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 55063c4..2aaf075 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: ruff - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.971 + rev: v1.2.0 hooks: - id: mypy args: [--strict] diff --git a/tests/test_methods.py b/tests/test_methods.py index 1b075bf..0ff2c15 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -1,5 +1,5 @@ """Test methods.py""" -from jsonrpcserver.result import Result +from jsonrpcserver.result import Ok, Result from jsonrpcserver.methods import global_methods, method # pylint: disable=missing-function-docstring @@ -10,7 +10,7 @@ def test_decorator() -> None: @method def func() -> Result: - pass + return Ok() assert callable(global_methods["func"]) From e7287b571b65c7e89d5fb41b8da6e53e6bcecb3d Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Wed, 10 May 2023 17:56:15 +1000 Subject: [PATCH 23/33] Upgrade ruff --- .pre-commit-config.yaml | 2 +- tests/test_methods.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2aaf075..a71d9a4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: args: [--diff, --check] - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.253 + rev: v0.0.265 hooks: - id: ruff diff --git a/tests/test_methods.py b/tests/test_methods.py index 0ff2c15..cd62ae3 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -4,8 +4,6 @@ # pylint: disable=missing-function-docstring -# pylint: disable=missing-function-docstring - def test_decorator() -> None: @method From 75cee0e2b8ad617043f9832ac59be5a79bbb948e Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Wed, 24 May 2023 12:31:18 +1000 Subject: [PATCH 24/33] Replace setup.py with pyproject.toml Closes #232 --- pyproject.toml | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 47 ----------------------------------------------- 2 files changed, 50 insertions(+), 47 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..91b82cd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,50 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +authors = [ + {name = "Beau Barker", email = "beau@explodinglabs.com"} +] +classifiers = [ + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11" +] +description = "Process JSON-RPC requests" +license = {file = "LICENSE"} +name = "jsonrpcserver" +readme = {file = "README.md", content-type = "text/markdown"} +requires-python = ">=3.8" +version = "6.0.0" + +[project.urls] +homepage = "https://www.jsonrpcserver.com" +repository = "https://github.com/explodinglabs/jsonrpcserver" + +[project.optional-dependencies] +qa = [ + "pytest", + "pytest-asyncio", + "pytest-cov", + "tox", +] +examples = [ + "aiohttp", + "aiozmq", + "flask", + "flask-socketio", + "gmqtt", + "pyzmq", + "tornado", + "websockets", + "werkzeug", +] + +[tool.setuptools] +include-package-data = true +packages = [ + "jsonrpcserver" +] +zip-safe = false diff --git a/setup.py b/setup.py deleted file mode 100644 index 945255f..0000000 --- a/setup.py +++ /dev/null @@ -1,47 +0,0 @@ -"""setup.py""" -from setuptools import setup - -with open("README.md", encoding="utf-8") as f: - README = f.read() - -setup( - author="Beau Barker", - author_email="beau@explodinglabs.com", - classifiers=[ - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - ], - description="Process JSON-RPC requests", - extras_require={ - "examples": [ - "aiohttp", - "aiozmq", - "flask", - "flask-socketio", - "gmqtt", - "pyzmq", - "tornado", - "websockets", - "werkzeug", - ], - "qa": [ - "pytest", - "pytest-asyncio", - "pytest-cov", - "tox", - ], - }, - include_package_data=True, - install_requires=["jsonschema<5", "returns<1"], - license="MIT", - long_description=README, - long_description_content_type="text/markdown", - name="jsonrpcserver", - packages=["jsonrpcserver"], - url="https://github.com/explodinglabs/jsonrpcserver", - version="6.0.0", - # Be PEP 561 compliant - # https://mypy.readthedocs.io/en/stable/installed_packages.html#making-pep-561-compatible-packages - zip_safe=False, -) From 9a1fe9f295741c110a71fef3911cd4e0e1e793af Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Wed, 31 May 2023 11:13:46 +1000 Subject: [PATCH 25/33] Always stop the server when exiting serve() Closes #264 --- jsonrpcserver/server.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/jsonrpcserver/server.py b/jsonrpcserver/server.py index 643b6a7..ec0ddb8 100644 --- a/jsonrpcserver/server.py +++ b/jsonrpcserver/server.py @@ -25,4 +25,11 @@ def do_POST(self) -> None: # pylint: disable=invalid-name def serve(name: str = "", port: int = 5000) -> None: """A simple function to serve HTTP requests""" logging.info(" * Listening on port %s", port) - HTTPServer((name, port), RequestHandler).serve_forever() + try: + httpd = HTTPServer((name, port), RequestHandler) + httpd.serve_forever() + except KeyboardInterrupt: + pass + except Exception: + httpd.shutdown() + raise From ffe83747769c33660aa5b3323ac0b31435301685 Mon Sep 17 00:00:00 2001 From: Beau Date: Mon, 29 Jul 2024 15:57:19 +1000 Subject: [PATCH 26/33] Replace black and isort with ruff (#278) Closes #277 --- .github/workflows/code-quality.yml | 4 ++-- .pre-commit-config.yaml | 16 ++++++++-------- docs/conf.py | 3 ++- examples/aiohttp_server.py | 4 +++- examples/aiozmq_server.py | 4 +++- examples/asyncio_server.py | 3 ++- examples/django_server.py | 4 +++- examples/fastapi_server.py | 6 ++++-- examples/flask_server.py | 4 +++- examples/http_server.py | 3 ++- examples/jsonrpcserver_server.py | 3 ++- examples/sanic_server.py | 4 +++- examples/socketio_server.py | 4 +++- examples/tornado_server.py | 4 +++- examples/websockets_server.py | 4 +++- examples/werkzeug_server.py | 4 +++- examples/zeromq_server.py | 4 +++- jsonrpcserver/__init__.py | 5 +++++ jsonrpcserver/async_dispatcher.py | 9 +++++---- jsonrpcserver/async_main.py | 4 ++-- jsonrpcserver/async_methods.py | 1 + jsonrpcserver/dispatcher.py | 7 ++++--- jsonrpcserver/exceptions.py | 2 ++ jsonrpcserver/main.py | 4 ++-- jsonrpcserver/methods.py | 1 + jsonrpcserver/request.py | 1 + jsonrpcserver/response.py | 5 +++-- jsonrpcserver/result.py | 6 ++++-- jsonrpcserver/server.py | 1 + jsonrpcserver/utils.py | 1 + pyproject.toml | 1 + tests/test_async_dispatcher.py | 5 +++-- tests/test_async_main.py | 4 ++-- tests/test_dispatcher.py | 7 ++++--- tests/test_main.py | 1 + tests/test_methods.py | 3 ++- tests/test_request.py | 1 + tests/test_response.py | 1 + tests/test_result.py | 3 ++- tests/test_sentinels.py | 1 + tests/test_server.py | 1 + 41 files changed, 103 insertions(+), 50 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 2c22310..abc349c 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -11,6 +11,6 @@ jobs: python-version: 3.8 - run: pip install --upgrade pip - run: pip install types-setuptools "black<23" ruff "mypy<2" "jsonschema<5" pytest "returns<1" "aiohttp<4" "aiozmq<1" "django<5" "fastapi<1" "flask<3" "flask-socketio<5.3.1" "pyzmq" "sanic" "tornado<7" "uvicorn<1" "websockets<11" - - run: black --diff --check $(git ls-files -- '*.py' ':!:docs/*') - - run: ruff $(git ls-files -- '*.py' ':!:docs/*') + - run: ruff check --select I $(git ls-files -- '*.py' ':!:docs/*') + - run: ruff format --check $(git ls-files -- '*.py' ':!:docs/*') - run: mypy --strict $(git ls-files -- '*.py' ':!:docs/*') diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a71d9a4..f884896 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,16 +3,16 @@ default_language_version: exclude: (^docs) fail_fast: true repos: - - repo: https://github.com/ambv/black - rev: 22.8.0 - hooks: - - id: black - args: [--diff, --check] - - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.265 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.4.1 hooks: - id: ruff + name: lint with ruff + - id: ruff + name: sort imports with ruff + args: [--select, I, --fix] + - id: ruff-format + name: format with ruff - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.2.0 diff --git a/docs/conf.py b/docs/conf.py index f876374..218ef00 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -3,6 +3,7 @@ # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +from typing import List # -- Path setup -------------------------------------------------------------- @@ -38,7 +39,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = [] +exclude_patterns: List[str] = [] # -- Options for HTML output ------------------------------------------------- diff --git a/examples/aiohttp_server.py b/examples/aiohttp_server.py index 22523bf..6626a32 100644 --- a/examples/aiohttp_server.py +++ b/examples/aiohttp_server.py @@ -1,6 +1,8 @@ """AioHTTP server""" + from aiohttp import web -from jsonrpcserver import async_dispatch, async_method, Ok, Result + +from jsonrpcserver import Ok, Result, async_dispatch, async_method @async_method diff --git a/examples/aiozmq_server.py b/examples/aiozmq_server.py index dff855a..545bceb 100644 --- a/examples/aiozmq_server.py +++ b/examples/aiozmq_server.py @@ -1,9 +1,11 @@ """AioZMQ server""" + import asyncio import aiozmq # type: ignore import zmq -from jsonrpcserver import async_dispatch, async_method, Ok, Result + +from jsonrpcserver import Ok, Result, async_dispatch, async_method @async_method diff --git a/examples/asyncio_server.py b/examples/asyncio_server.py index d83b6cd..4352403 100644 --- a/examples/asyncio_server.py +++ b/examples/asyncio_server.py @@ -1,8 +1,9 @@ """Demonstrates processing a batch of 100 requests asynchronously with asyncio.""" + import asyncio import json -from jsonrpcserver import async_dispatch, async_method, Ok, Result +from jsonrpcserver import Ok, Result, async_dispatch, async_method @async_method diff --git a/examples/django_server.py b/examples/django_server.py index 5a80362..86f77fd 100644 --- a/examples/django_server.py +++ b/examples/django_server.py @@ -1,7 +1,9 @@ """Django server""" + from django.http import HttpRequest, HttpResponse # type: ignore from django.views.decorators.csrf import csrf_exempt # type: ignore -from jsonrpcserver import dispatch, method, Ok, Result + +from jsonrpcserver import Ok, Result, dispatch, method @method diff --git a/examples/fastapi_server.py b/examples/fastapi_server.py index c02b216..858ba07 100644 --- a/examples/fastapi_server.py +++ b/examples/fastapi_server.py @@ -1,7 +1,9 @@ """FastAPI server""" -from fastapi import FastAPI, Request, Response + import uvicorn -from jsonrpcserver import dispatch, method, Ok, Result +from fastapi import FastAPI, Request, Response + +from jsonrpcserver import Ok, Result, dispatch, method app = FastAPI() diff --git a/examples/flask_server.py b/examples/flask_server.py index 32044b0..071b110 100644 --- a/examples/flask_server.py +++ b/examples/flask_server.py @@ -1,6 +1,8 @@ """Flask server""" + from flask import Flask, Response, request -from jsonrpcserver import dispatch, method, Ok, Result + +from jsonrpcserver import Ok, Result, dispatch, method app = Flask(__name__) diff --git a/examples/http_server.py b/examples/http_server.py index 047cf6b..5b81a2a 100644 --- a/examples/http_server.py +++ b/examples/http_server.py @@ -2,9 +2,10 @@ Demonstrates using Python's builtin http.server module to serve JSON-RPC. """ + from http.server import BaseHTTPRequestHandler, HTTPServer -from jsonrpcserver import dispatch, method, Ok, Result +from jsonrpcserver import Ok, Result, dispatch, method @method diff --git a/examples/jsonrpcserver_server.py b/examples/jsonrpcserver_server.py index c0471bb..55f7db6 100644 --- a/examples/jsonrpcserver_server.py +++ b/examples/jsonrpcserver_server.py @@ -2,7 +2,8 @@ Uses jsonrpcserver's built-in "serve" function. """ -from jsonrpcserver import method, serve, Ok, Result + +from jsonrpcserver import Ok, Result, method, serve @method diff --git a/examples/sanic_server.py b/examples/sanic_server.py index 180d900..be80c26 100644 --- a/examples/sanic_server.py +++ b/examples/sanic_server.py @@ -1,8 +1,10 @@ """Sanic server""" + from sanic import Sanic from sanic.request import Request from sanic.response import HTTPResponse, json -from jsonrpcserver import dispatch_to_serializable, method, Ok, Result + +from jsonrpcserver import Ok, Result, dispatch_to_serializable, method app = Sanic("JSON-RPC app") diff --git a/examples/socketio_server.py b/examples/socketio_server.py index 1a5c2c7..7dd4b94 100644 --- a/examples/socketio_server.py +++ b/examples/socketio_server.py @@ -1,7 +1,9 @@ """SocketIO server""" + from flask import Flask, Request from flask_socketio import SocketIO, send # type: ignore -from jsonrpcserver import dispatch, method, Ok, Result + +from jsonrpcserver import Ok, Result, dispatch, method app = Flask(__name__) socketio = SocketIO(app) diff --git a/examples/tornado_server.py b/examples/tornado_server.py index b31473f..3a027a4 100644 --- a/examples/tornado_server.py +++ b/examples/tornado_server.py @@ -1,8 +1,10 @@ """Tornado server""" + from typing import Awaitable, Optional from tornado import ioloop, web -from jsonrpcserver import async_dispatch, async_method, Ok, Result + +from jsonrpcserver import Ok, Result, async_dispatch, async_method @async_method diff --git a/examples/websockets_server.py b/examples/websockets_server.py index 33098e4..ed8812e 100644 --- a/examples/websockets_server.py +++ b/examples/websockets_server.py @@ -1,8 +1,10 @@ """Websockets server""" + import asyncio from websockets.server import WebSocketServerProtocol, serve -from jsonrpcserver import async_dispatch, async_method, Ok, Result + +from jsonrpcserver import Ok, Result, async_dispatch, async_method @async_method diff --git a/examples/werkzeug_server.py b/examples/werkzeug_server.py index f2c242b..dcc8590 100644 --- a/examples/werkzeug_server.py +++ b/examples/werkzeug_server.py @@ -1,7 +1,9 @@ """Werkzeug server""" + from werkzeug.serving import run_simple from werkzeug.wrappers import Request, Response -from jsonrpcserver import method, Result, Ok, dispatch + +from jsonrpcserver import Ok, Result, dispatch, method @method diff --git a/examples/zeromq_server.py b/examples/zeromq_server.py index 328667a..d88368c 100644 --- a/examples/zeromq_server.py +++ b/examples/zeromq_server.py @@ -1,6 +1,8 @@ """ZeroMQ server""" + import zmq -from jsonrpcserver import dispatch, method, Ok, Result + +from jsonrpcserver import Ok, Result, dispatch, method socket = zmq.Context().socket(zmq.REP) diff --git a/jsonrpcserver/__init__.py b/jsonrpcserver/__init__.py index 69c66d5..8136ba0 100644 --- a/jsonrpcserver/__init__.py +++ b/jsonrpcserver/__init__.py @@ -1,9 +1,14 @@ """Jsonrpcserver""" + from returns.result import Result as R from .async_main import ( dispatch as async_dispatch, +) +from .async_main import ( dispatch_to_response as async_dispatch_to_response, +) +from .async_main import ( dispatch_to_serializable as async_dispatch_to_serializable, ) from .async_methods import method as async_method diff --git a/jsonrpcserver/async_dispatcher.py b/jsonrpcserver/async_dispatcher.py index 0c5e518..49ba167 100644 --- a/jsonrpcserver/async_dispatcher.py +++ b/jsonrpcserver/async_dispatcher.py @@ -1,13 +1,15 @@ """Async version of dispatcher.py""" + +import asyncio +import logging from functools import partial from inspect import signature from itertools import starmap from typing import Any, Callable, Iterable, Tuple, Union -import asyncio -import logging from returns.result import Failure, Result, Success +from .async_methods import Method, Methods from .dispatcher import ( Deserialized, create_request, @@ -21,8 +23,8 @@ validate_result, ) from .exceptions import JsonRpcError -from .async_methods import Method, Methods from .request import Request +from .response import Response, ServerErrorResponse from .result import ( ErrorResult, InternalErrorResult, @@ -30,7 +32,6 @@ MethodNotFoundResult, SuccessResult, ) -from .response import Response, ServerErrorResponse from .utils import make_list logger = logging.getLogger(__name__) diff --git a/jsonrpcserver/async_main.py b/jsonrpcserver/async_main.py index 39c0f35..d34801c 100644 --- a/jsonrpcserver/async_main.py +++ b/jsonrpcserver/async_main.py @@ -1,16 +1,16 @@ """Async version of main.py. The public async functions.""" + import json from typing import Any, Callable, Dict, Iterable, List, Optional, Union, cast from .async_dispatcher import dispatch_to_response_pure from .async_methods import Methods, global_methods from .dispatcher import Deserialized -from .main import default_jsonrpc_validator, default_deserializer +from .main import default_deserializer, default_jsonrpc_validator from .response import Response, to_serializable from .sentinels import NOCONTEXT from .utils import identity - # pylint: disable=missing-function-docstring,duplicate-code diff --git a/jsonrpcserver/async_methods.py b/jsonrpcserver/async_methods.py index 6de95a4..63edfc1 100644 --- a/jsonrpcserver/async_methods.py +++ b/jsonrpcserver/async_methods.py @@ -1,4 +1,5 @@ """Async methods""" + from typing import Any, Awaitable, Callable, Dict, Optional, cast from returns.result import Result diff --git a/jsonrpcserver/dispatcher.py b/jsonrpcserver/dispatcher.py index 060d434..2893012 100644 --- a/jsonrpcserver/dispatcher.py +++ b/jsonrpcserver/dispatcher.py @@ -1,23 +1,24 @@ """Dispatcher - does the hard work of this library: parses, validates and dispatches requests, providing responses. """ + # pylint: disable=protected-access +import logging from functools import partial from inspect import signature from itertools import starmap from typing import Any, Callable, Dict, Iterable, List, Tuple, Union -import logging -from returns.result import Result, Failure, Success +from returns.result import Failure, Result, Success from .exceptions import JsonRpcError from .methods import Method, Methods from .request import Request from .response import ( - Response, ErrorResponse, InvalidRequestResponse, ParseErrorResponse, + Response, ServerErrorResponse, SuccessResponse, ) diff --git a/jsonrpcserver/exceptions.py b/jsonrpcserver/exceptions.py index b2e2afb..0314e44 100644 --- a/jsonrpcserver/exceptions.py +++ b/jsonrpcserver/exceptions.py @@ -1,5 +1,7 @@ """Exceptions""" + from typing import Any + from .sentinels import NODATA diff --git a/jsonrpcserver/main.py b/jsonrpcserver/main.py index 66ba720..19070b2 100644 --- a/jsonrpcserver/main.py +++ b/jsonrpcserver/main.py @@ -9,9 +9,10 @@ - dispatch_to_json/dispatch: Returns a JSON-RPC response string (or an empty string for notifications). """ + +import json from importlib.resources import read_text from typing import Any, Callable, Dict, List, Union, cast -import json from jsonschema.validators import validator_for # type: ignore @@ -26,7 +27,6 @@ from .sentinels import NOCONTEXT from .utils import identity - default_args_validator = validate_args default_deserializer = json.loads diff --git a/jsonrpcserver/methods.py b/jsonrpcserver/methods.py index 81b0a36..c946a58 100644 --- a/jsonrpcserver/methods.py +++ b/jsonrpcserver/methods.py @@ -11,6 +11,7 @@ Methods can take either positional or named arguments, but not both. This is a limitation of JSON-RPC. """ + from typing import Any, Callable, Dict, Optional, cast from returns.result import Result diff --git a/jsonrpcserver/request.py b/jsonrpcserver/request.py index 37f88c1..d82043e 100644 --- a/jsonrpcserver/request.py +++ b/jsonrpcserver/request.py @@ -3,6 +3,7 @@ After parsing a request string, we put the (dict) requests into these Request namedtuples, simply because they're nicer to work with. """ + from typing import Any, Dict, List, NamedTuple, Union diff --git a/jsonrpcserver/response.py b/jsonrpcserver/response.py index 9c6e4f1..3113d05 100644 --- a/jsonrpcserver/response.py +++ b/jsonrpcserver/response.py @@ -2,9 +2,10 @@ https://www.jsonrpc.org/specification#response_object """ + from typing import Any, Dict, List, NamedTuple, Union -from returns.result import Result, Failure +from returns.result import Failure, Result from .codes import ( ERROR_INVALID_REQUEST, @@ -101,7 +102,7 @@ def to_dict(response: Response) -> Dict[str, Any]: def to_serializable( - response: Union[Response, List[Response], None] + response: Union[Response, List[Response], None], ) -> Union[Deserialized, None]: """Serialize a response object (or list of them), to a dict, or list of them.""" if response is None: diff --git a/jsonrpcserver/result.py b/jsonrpcserver/result.py index a35315f..b193880 100644 --- a/jsonrpcserver/result.py +++ b/jsonrpcserver/result.py @@ -6,11 +6,13 @@ The public functions are Success, Error and InvalidParams. """ + from typing import Any, NamedTuple -from returns.result import Failure, Result as R, Success +from returns.result import Failure, Success +from returns.result import Result as R -from .codes import ERROR_INVALID_PARAMS, ERROR_METHOD_NOT_FOUND, ERROR_INTERNAL_ERROR +from .codes import ERROR_INTERNAL_ERROR, ERROR_INVALID_PARAMS, ERROR_METHOD_NOT_FOUND from .sentinels import NODATA # pylint: disable=missing-class-docstring,missing-function-docstring,invalid-name diff --git a/jsonrpcserver/server.py b/jsonrpcserver/server.py index ec0ddb8..e783082 100644 --- a/jsonrpcserver/server.py +++ b/jsonrpcserver/server.py @@ -1,6 +1,7 @@ """A simple development server for serving JSON-RPC requests using Python's builtin http.server module. """ + import logging from http.server import BaseHTTPRequestHandler, HTTPServer diff --git a/jsonrpcserver/utils.py b/jsonrpcserver/utils.py index 4ee5a2c..a259799 100644 --- a/jsonrpcserver/utils.py +++ b/jsonrpcserver/utils.py @@ -1,4 +1,5 @@ """Utility functions""" + from functools import reduce from typing import Any, Callable, List diff --git a/pyproject.toml b/pyproject.toml index 91b82cd..5e4171f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ examples = [ "flask-socketio", "gmqtt", "pyzmq", + "sanic", "tornado", "websockets", "werkzeug", diff --git a/tests/test_async_dispatcher.py b/tests/test_async_dispatcher.py index 4030ca3..b1afbfd 100644 --- a/tests/test_async_dispatcher.py +++ b/tests/test_async_dispatcher.py @@ -1,7 +1,8 @@ """Test async_dispatcher.py""" + from unittest.mock import Mock, patch -import pytest +import pytest from returns.result import Failure, Success from jsonrpcserver.async_dispatcher import ( @@ -10,9 +11,9 @@ dispatch_request, dispatch_to_response_pure, ) -from jsonrpcserver.main import default_deserializer, default_jsonrpc_validator from jsonrpcserver.codes import ERROR_INTERNAL_ERROR, ERROR_SERVER_ERROR from jsonrpcserver.exceptions import JsonRpcError +from jsonrpcserver.main import default_deserializer, default_jsonrpc_validator from jsonrpcserver.request import Request from jsonrpcserver.response import ErrorResponse, SuccessResponse from jsonrpcserver.result import ErrorResult, Ok, Result, SuccessResult diff --git a/tests/test_async_main.py b/tests/test_async_main.py index acfb56b..7686a8e 100644 --- a/tests/test_async_main.py +++ b/tests/test_async_main.py @@ -1,12 +1,12 @@ """Test async_main.py""" -import pytest +import pytest from returns.result import Success from jsonrpcserver.async_main import ( + dispatch_to_json, dispatch_to_response, dispatch_to_serializable, - dispatch_to_json, ) from jsonrpcserver.response import SuccessResponse from jsonrpcserver.result import Ok, Result diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 014a8a7..26fe49a 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -2,11 +2,12 @@ TODO: Add tests for dispatch_requests (non-pure version) """ + +import json from typing import Any, Dict from unittest.mock import Mock, patch, sentinel -import json -import pytest +import pytest from returns.result import Failure, Success from jsonrpcserver.codes import ( @@ -23,9 +24,9 @@ dispatch_deserialized, dispatch_request, dispatch_to_response_pure, - extract_list, extract_args, extract_kwargs, + extract_list, get_method, not_notification, to_response, diff --git a/tests/test_main.py b/tests/test_main.py index de1e5ee..fae47ce 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,4 +1,5 @@ """Test main.py""" + from returns.result import Success from jsonrpcserver.main import ( diff --git a/tests/test_methods.py b/tests/test_methods.py index cd62ae3..4459e7a 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -1,6 +1,7 @@ """Test methods.py""" -from jsonrpcserver.result import Ok, Result + from jsonrpcserver.methods import global_methods, method +from jsonrpcserver.result import Ok, Result # pylint: disable=missing-function-docstring diff --git a/tests/test_request.py b/tests/test_request.py index 114a34d..67e803b 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -1,4 +1,5 @@ """Test request.py""" + from jsonrpcserver.request import Request # pylint: disable=missing-function-docstring diff --git a/tests/test_response.py b/tests/test_response.py index 5771ded..7829349 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -1,4 +1,5 @@ """Test response.py""" + from unittest.mock import sentinel from returns.result import Failure, Success diff --git a/tests/test_result.py b/tests/test_result.py index dc02117..797263c 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -1,13 +1,14 @@ """Test result.py""" + from unittest.mock import sentinel from returns.result import Failure, Success from jsonrpcserver.result import ( - Ok, Error, ErrorResult, InvalidParamsResult, + Ok, SuccessResult, ) from jsonrpcserver.sentinels import NODATA diff --git a/tests/test_sentinels.py b/tests/test_sentinels.py index c5c8f02..b1e5ffe 100644 --- a/tests/test_sentinels.py +++ b/tests/test_sentinels.py @@ -1,4 +1,5 @@ """Test sentinels.py""" + from jsonrpcserver.sentinels import Sentinel # pylint: disable=missing-function-docstring diff --git a/tests/test_server.py b/tests/test_server.py index ad37266..46c2b51 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,4 +1,5 @@ """Test server.py""" + from unittest.mock import Mock, patch from jsonrpcserver.server import serve From be3467579aaf2d5d60161889a8730e60804d571a Mon Sep 17 00:00:00 2001 From: Beau Date: Wed, 31 Jul 2024 10:11:32 +1000 Subject: [PATCH 27/33] Move documentation to Github wiki (#280) --- CHANGELOG.md | 19 +++-- README.md | 6 -- docs/Makefile | 177 ------------------------------------------- docs/async.md | 43 ----------- docs/conf.py | 62 --------------- docs/dispatch.md | 81 -------------------- docs/examples.md | 100 ------------------------ docs/faq.md | 41 ---------- docs/index.md | 23 ------ docs/installation.md | 32 -------- docs/logo.png | Bin 17627 -> 0 bytes docs/methods.md | 69 ----------------- pyproject.toml | 6 ++ 13 files changed, 15 insertions(+), 644 deletions(-) delete mode 100644 docs/Makefile delete mode 100644 docs/async.md delete mode 100644 docs/conf.py delete mode 100644 docs/dispatch.md delete mode 100644 docs/examples.md delete mode 100644 docs/faq.md delete mode 100644 docs/index.md delete mode 100644 docs/installation.md delete mode 100644 docs/logo.png delete mode 100644 docs/methods.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 737b24a..036af4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,19 +7,18 @@ change for async use. Breaking changes: -- Decorate async JSON-RPC methods with `@async_method` instead of `@method`. - The reason for this change is due to the typing of the decorator, async - functions return a different type (`Awaitable`) to other functions. +- Async methods should be decorated with `@async_method` instead of `@method`. The + reason is due to the _type_ of the decorated function - async functions have a + different type to other functions (`Awaitable`). Other changes: -- Internally, replaced the Oslash dependency with - [Returns](https://github.com/dry-python/returns). Because Oslash is not meant - for production use. -- Use `Ok` instead of `Success` when returning a response. This is to avoid - confusion with the Returns library now used internally which has it's own - `Success` class. It also matches Jsonrpcclient's `Ok` type. This is not a - breaking change, `Success` will still work for now. But use `Ok` instead. +- Replaced the Oslash dependency with [Returns](https://github.com/dry-python/returns). + Oslash is not meant for production use, and doesn't work in Python 3.12. +- Use `Ok` instead of `Success` when returning a response. This is to avoid confusion + with the Returns library's `Success` class. It also matches Jsonrpcclient's `Ok` type. + This is not a breaking change, `Success` will still work for now. +- Docs moved to https://github.com/explodinglabs/jsonrpcserver/wiki ## 5.0.9 (Sep 15, 2022) diff --git a/README.md b/README.md index d556c04..86f5770 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,6 @@ from jsonrpcserver import method, serve, Ok, Result def ping() -> Result: return Ok("pong") -if __name__ == "__main__": - serve() -``` - -Or use `dispatch` instead of `serve`: -```python response = dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}') # => '{"jsonrpc": "2.0", "result": "pong", "id": 1}' ``` diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index fa0c384..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,177 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/jsonrpcserver.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/jsonrpcserver.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/jsonrpcserver" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/jsonrpcserver" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/async.md b/docs/async.md deleted file mode 100644 index e52fde8..0000000 --- a/docs/async.md +++ /dev/null @@ -1,43 +0,0 @@ -# Async - -Async dispatch is supported. - -```python -from jsonrpcserver import async_dispatch, async_method, Ok, Result - -@async_method -async def ping() -> Result: - return Ok("pong") - -await async_dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}') -``` - -Some reasons to use this: - -- Use it with an asynchronous protocol like sockets or message queues. -- `await` long-running functions from your method. -- Batch requests are dispatched concurrently. - -## Notifications - -Notifications are requests without an `id`. We should not respond to -notifications, so jsonrpcserver gives an empty string to signify there is *no -response*. - -```python ->>> await async_dispatch('{"jsonrpc": "2.0", "method": "ping"}') -'' -``` - -If the response is an empty string, don't send it. - -```python -if response := dispatch(request): - send(response) -``` - -```{note} -A synchronous protocol like HTTP requires a response no matter what, so we can -send back the empty string. However with async protocols, we have the choice of -responding or not. -``` diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 218ef00..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,62 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html -from typing import List - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- Project information ----------------------------------------------------- - -project = "jsonrpcserver" -copyright = "2021, Beau Barker" -author = "Beau Barker" - -# The full version, including alpha/beta/rc tags -release = "5.0.0" - - -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = ["sphinx_rtd_theme", "myst_parser"] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns: List[str] = [] - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = "sphinx_rtd_theme" -html_theme_options = { - "analytics_id": "G-G05775CD6C", # UA-81795603-3 - "display_version": True, -} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] - -source_suffix = [".rst", ".md"] -html_show_sourcelink = False diff --git a/docs/dispatch.md b/docs/dispatch.md deleted file mode 100644 index 2278b47..0000000 --- a/docs/dispatch.md +++ /dev/null @@ -1,81 +0,0 @@ -# Dispatch - -The `dispatch` function takes a JSON-RPC request, calls the appropriate method -and gives a JSON-RPC response. - -```python ->>> dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}') -'{"jsonrpc": "2.0", "result": "pong", "id": 1}' -``` - -[See how dispatch is used in different frameworks.](examples) - -## Optional parameters - -The `dispatch` function has some optional parameters that allow you to -customise how it works. - -### methods - -This lets you specify the methods to dispatch to. It's an alternative to using -the `@method` decorator. The value should be a dict mapping function names to -functions. - -```python -def ping(): - return Ok("pong") - -dispatch(request, methods={"ping": ping}) -``` - -Default is `global_methods`, which is an internal dict populated by the -`@method` decorator. - -### context - -If specified, this will be the first argument to all methods. - -```python -@method -def greet(context, name): - return Ok(context + " " + name) - ->>> dispatch('{"jsonrpc": "2.0", "method": "greet", "params": ["Beau"], "id": 1}', context="Hello") -'{"jsonrpc": "2.0", "result": "Hello Beau", "id": 1}' -``` - -### deserializer - -A function that parses the JSON request string. Default is `json.loads`. - -```python -dispatch(request, deserializer=ujson.loads) -``` - -### jsonrpc_validator - -A function that validates the request once the JSON string has been parsed. The -function should raise an exception (any exception) if the request doesn't match -the JSON-RPC spec (https://www.jsonrpc.org/specification). Default is -`default_jsonrpc_validator` which uses Jsonschema to validate requests against -a schema. - -To disable JSON-RPC validation, pass `jsonrpc_validator=lambda _: None`, which -will improve performance because this validation takes around half the dispatch -time. - -### args_validator - -A function that validates a request's parameters against the signature of the -Python function that will be called for it. Note this should not validate the -_values_ of the parameters, it should simply ensure the parameters match the -Python function's signature. For reference, see the `validate_args` function in -`dispatcher.py`, which is the default `args_validator`. - -### serializer - -A function that serializes the response string. Default is `json.dumps`. - -```python -dispatch(request, serializer=ujson.dumps) -``` diff --git a/docs/examples.md b/docs/examples.md deleted file mode 100644 index cfa8c22..0000000 --- a/docs/examples.md +++ /dev/null @@ -1,100 +0,0 @@ -# Examples - -```{contents} -``` - -## aiohttp - -```{literalinclude} ../examples/aiohttp_server.py -``` - -See [blog post](https://composed.blog/jsonrpc/aiohttp). - -## Django - -Create a `views.py`: - -```{literalinclude} ../examples/django_server.py -``` - -See [blog post](https://composed.blog/jsonrpc/django). - -## FastAPI - -```{literalinclude} ../examples/fastapi_server.py -``` - -See [blog post](https://composed.blog/jsonrpc/fastapi). - -## Flask - -```{literalinclude} ../examples/flask_server.py -``` - -See [blog post](https://composed.blog/jsonrpc/flask). - -## http.server - -Using Python's built-in -[http.server](https://docs.python.org/3/library/http.server.html) module. - -```{literalinclude} ../examples/http_server.py -``` - -See [blog post](https://composed.blog/jsonrpc/httpserver). - -## jsonrpcserver - -Using jsonrpcserver's built-in `serve` method. - -```{literalinclude} ../examples/jsonrpcserver_server.py -``` - -## Sanic - -```{literalinclude} ../examples/sanic_server.py -``` - -See [blog post](https://composed.blog/jsonrpc/sanic). - -## Socket.IO - -```{literalinclude} ../examples/socketio_server.py -``` - -See [blog post](https://composed.blog/jsonrpc/flask-socketio). - -## Tornado - -```{literalinclude} ../examples/tornado_server.py -``` - -See [blog post](https://composed.blog/jsonrpc/tornado). - -## Websockets - -```{literalinclude} ../examples/websockets_server.py -``` - -See [blog post](https://composed.blog/jsonrpc/websockets). - -## Werkzeug - -```{literalinclude} ../examples/werkzeug_server.py -``` - -See [blog post](https://composed.blog/jsonrpc/werkzeug). - -## ZeroMQ - -```{literalinclude} ../examples/zeromq_server.py -``` - -See [blog post](https://composed.blog/jsonrpc/zeromq). - -## ZeroMQ (asynchronous) - -```{literalinclude} ../examples/aiozmq_server.py -``` - -See [blog post](https://composed.blog/jsonrpc/zeromq-async). diff --git a/docs/faq.md b/docs/faq.md deleted file mode 100644 index f928e24..0000000 --- a/docs/faq.md +++ /dev/null @@ -1,41 +0,0 @@ -# FAQ - -## How to disable schema validation? - -Validating requests is costly - roughly 40% of dispatching time is spent on schema validation. -If you know the incoming requests are valid, you can disable the validation for better -performance. - -```python -dispatch(request, validator=lambda _: None) -``` - -## Which HTTP status code to respond with? - -I suggest: - -```python -200 if response else 204 -``` - -If the request was a notification, `dispatch` will give you an empty string. So -since there's no http body, use status code 204 - no content. - -## How to rename a method - -Use `@method(name="new_name")`. - -Or use the dispatch function's [methods -parameter](https://www.jsonrpcserver.com/en/latest/dispatch.html#methods). - -## How to get the response in other forms? - -Instead of `dispatch`, use: - -- `dispatch_to_serializable` to get the response as a dict. -- `dispatch_to_response` to get the response as a namedtuple (either a - `SuccessResponse` or `ErrorResponse`, these are defined in - [response.py](https://github.com/explodinglabs/jsonrpcserver/blob/main/jsonrpcserver/response.py)). - -For these functions, if the request was a batch, you'll get a list of -responses. If the request was a notification, you'll get `None`. diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 12a7251..0000000 --- a/docs/index.md +++ /dev/null @@ -1,23 +0,0 @@ -```{warning} -This is the documentation for version 5, released August 16, 2021. [Read about -the changes in version 5](https://composed.blog/jsonrpcserver-5-changes). -``` - -# Process incoming JSON-RPC requests in Python | jsonrpcserver Documentation - -![jsonrpcserver](/logo.png) - -Process incoming JSON-RPC requests in Python. - -```{toctree} ---- -maxdepth: 3 -caption: Contents ---- -installation -methods -dispatch -async -faq -examples -``` diff --git a/docs/installation.md b/docs/installation.md deleted file mode 100644 index d2f3680..0000000 --- a/docs/installation.md +++ /dev/null @@ -1,32 +0,0 @@ -# Quickstart - -Create a `server.py`: - -```python -from jsonrpcserver import method, serve, Ok - -@method -def ping(): - return Ok("pong") - -if __name__ == "__main__": - serve() -``` - -Start the server: - -```sh -$ pip install jsonrpcserver -$ python server.py - * Listening on port 5000 -``` - -Test the server: - -```sh -$ curl -X POST http://localhost:5000 -d '{"jsonrpc": "2.0", "method": "ping", "id": 1}' -{"jsonrpc": "2.0", "result": "pong", "id": 1} -``` - -`serve` is good for serving methods in development, but for production use -`dispatch` instead. diff --git a/docs/logo.png b/docs/logo.png deleted file mode 100644 index d7024dca11a882b994b4ca9aee8549294e39dbe4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17627 zcmagFWmH_j(kMFU3_eI8xNC3;?jC|e2=4CgGPt`0_uv-X-Gh6u0TSHZd7Mw)z5niB zt97@Q^s1_^j!;sNMnfh*1^@tPvN94X007MOd)@$q_#Pj93$T8#2(}a#SCSPMCv$MJ zH?y=c1pw$0d=ms^`bCHa^;HKMK8T`3x{N4gGloaYH9#AM8VGhM`vdhmjtZ0G#sq~4~Mll_d62zGeEwcS@{YZdTEAf(KZ`f zlJ19gJ*I>0f)Ceb)Pjm)hy3E`{#)_SKze;Cv`980XNUJc2yD=4N@% zu*1G9tAtNQ45PM@T~IEWrfu@z5#f56jmVVjMu-m><;VQ{Y$hpAokI~zS1$D+He?!d zg=^lZV>;n|iHGR?XN-NxG8A)|A~il3gs+~g6dFQTMLn#?*G*N*N^{47JFL$znZ;A|0i2CpKz0V9$!g#5n`u`KSRS`_oI&dU!`vR_=@#@c(?zZ*4~h;1bLGTl~rC|>HqJt zi{M<2eTgkxf^cruVE3P4O%jpzaft@mNvu;>40Jusc=JvtZz*g-vZfo=cQ zP?jdI1oZn~Ko*UoZ6D~^40+?f_E?l!)L*WerN>$#({-qOK%jNtV>Ooi-#yHN?ErLP zE&(TfbB6qWtc=9`0n4@kK-Vwi1hh=dj2rr3gd^pSJ z7l0{W;2PGrVJ0K?g2pG5U!AC6-vZAtJD&X`mnivqbx-THB%Nt$Gye-h^F;H9s;7|# zeoL(~?H3HLtUy4;hiVRmkduSAfW$aTu~ zC?3qZlPOdO`ADnB4t`$V;J@K^K#H>eMbdK7ky7LrdwR`Y%N4SGd`#H&{oPGKo1Ou8 z@A`kkK~1%SDN?nPwOQMMw=JzM>|{Y4F?>)$!apu$Ai+y1PG4Jt;m6#Z)VC=B*H@@ifU*COxT5(>@J|IX|xN{z)>x)ps-HRh+*4l&xjN`ew)z1M3E> z9S&OxH*1mIh#wc?Oc6F7DVrmJgX~#{-13l z?Q7`?qFq9pZGeavCS7jOl1-Ol#xhia`k&B+fX=es?0*g!CPShVA1gIST;Shc=QCUm zavANt9;UVSM!Y=a-F%B)j=oGgD*#7FreAd5(VlPKr8KpS{ZFkOx0KdL_rvl6y!4X` z_hT~`6;0jMNU5PL*QVz1OtRBb#*h&o-diQFm1W)@`fvockah_4R|+a#eyQK(;C_x% zX$M_Q*LDw2lQeN+_xOpXhLgm#okk(S8#?V?MVX$wZ+EJX8RetlUwzemOSz5i#@J%| zn|H3%at&5+?UIIpXZHGdKY+X^+s{>_&A+D_y!ohnMRI{I;xi&okgx5hn?3)QMitPe zW$#g^vBc?odv_Cyadt%OZ9j{glh#ncOIsEH_LhwNwy02qzgNLUwWY1j;@*gE6Ctlh z2Qbkx^xtOaJI z-Ap5g(*I}(+(*N#ElpbuaH~~p1vHVYi|C-AyOIQdwhwtr)!3d@Rm1u>9>k6b=Osvx z1g{haw4*lAxcrkruoiQCY)p*J)gQF0KJQ;DUaFRR>FzD0x^X=vtr`=Pg6Fiu3Zo0m z8*em-(SJ$Q-ovd4nG0^Tze$!AF!%)V>Zc9=G8R($YVuV{y8?#wq7x~(J_6NVbu@mX zj$5f2CykC`}sHDge0*^@+29S_dX4~y_~(?;|*^L~T>_0Hl)oJ z=ueOfBkbr`I3D!*qp(^_?){<2e&$3Of@g(ObQK+tQFGAaOCEJfo}61z&ph@CgrCQO z_746RhsbPJ9s~@%fK>m1(b~4fDiOcsAI1%Gu|r8bg$2Rf+>2s-6ZzgY%t{*_Ep93F z#;ibZ8&Ppk3k=iHD=gc-_b1C6SI~V~)1=7_Z_xf3z(YJ!$~!|%3(t9BkBk1{P7rc_ zylE=vA!Y=ukU%-Lk@4T=Ye6K2vlb;Ws0;Yc$aX~z%EKfH91fcP$prGd61>OoTY`6& z<;)l7IZ@O@db$WQu+@hDUIiG2(RopaM^=J?dV)w;`uN=_Ip2U18_x;y;GfY_yTorm z0`yN|JdXI0Bd3Dr(b)_yU#p34`Dy!vEC7{*CnCqU7QIIO?(~2hpuTQQ4~87L<3)b3 zq*;L|w9?XDVz@|JD3IJk&jT(YJ!^0A_5o$=#&kfD6lmh z7=~LkFEh0{%e{t>os}a4LKjp<&sJq22O`ig1X&p$krV>GCF?Nc2qc{fW3`tb+mTyk zGVc>|;;CG33*aFN7^Fzm7X`#ffLEGHxBgXNc06U{NaW6TYfT-edQ*M)(7MvmTHD`5;rcn>0tdB4oSk}{NF#&TPsQLs@a$7q}xH`TG9p8Zc4NIe7CU^FzyRAOMUYT;emz@Q6mT6 zEOGD-c%u=RQ`fwoS@KCC^rpqw;A9O`M*%&24#}t)655vs0o;!R+w4zzt$RhPf z{|4*t&((2-N6u+YvVRCy@EZnm@MX-82qs`s6B>F0vA%q8Aez&^yK-Dvscm8jwH`pq z!`MXHbdSQ!y5SAhZ-x`q( z*LS+V1XL0UXEra1psx#f)%0nlW9YKUndm25e6E}n#sAJHUU9Y_x&x}|O(O**Zm8h? z^nHFHD$4e4vm?=Ra)$0N0r53-BYQ|Iw*22zLF3#}sJ0!aSXFfmazOlP{7KoJEgnBm zOSFrt@p2iZ!&hPGHCxY1GkMDe3%pQH*vOrKfL+zB->C~;W`~z=o&wOKnmASY1=@BD z%M(XPCEVmD>2Q4(6+O-zdiInH^?$Vmjo&TuBSK31hm7u}(Ip*b%vRDHZch;8{76*U z#O|MRldq)LoqCAp1Rs5d(WmE;w3jAl6!Lba;Bz9$YA+S*cT;|zl0$V zw-*jMT)4M6f{7X2M)l;rYu{U>a-;u)iOvi1;F?8-N!AK?Sx=G%ZRS~$4)j>j;U2Ac zdz(q*%Y>)vAO})mj92SiMdk!4dKp8K**&4i2J#@2X7(}I!}E6_|0M0uX9{>5!ncx} zjPwBlPOrOU+D}z@9`6wLTv(?s6L$udK`J}0;>QaK=I+Tq$~J|IT0^SMZHlWmxP zuTk*NiEj;PPCIq$LL`%pB|RGz(wws<-^gI4a|@yS&NzJ<#3IlzYS8nYt2*IehZ_qB z%J-q4>j{Y`?x4gw%d${0^nEaou6xAZx31XprtF>(%&$|HC+pZS2=etf&8AHd2)A&8 z?xc%=AA(MyVqn!3CL-n-^4@f>13Gc^bQAHp{|m8L+=Tit$|5np4}7X>7=>-$N*f%O zQ8W2Y!4*`%lc$5!C-6Kew}6ZBBS{V>pMlNgHoTDD{p?S9sV#pRh%=80J3BKaoFy$< znDajPeK7>DgnG$Rtn6gDY-h49q38++U8s{=0Y~@b4ygN4ZLeqK*CRyKj3(Gw+ioM; z_n#S8R_G0=MR$44$(-`wd&BnT&ln8sMMrs$N7{^xI47p~6b}wYI?i>zx4TQOTdJ-U zMeP{8ZK{uS+zU7$o-&b06a1=a$7)mn2D*39f4Xs6$FJnhji@YJ3UqE&G5&~*67M*P z?-27bG~-PO{Q5-^25;RJTl_y+ z1ok#d_(Ai3?c1A1T93?hS2!(qi zbl-B5A+7KRG||$F1qWo5ZK8QNV!qy*Faguxh3J!*wHW6uK0lcx+h2cmnE$SEB1~%7 z+Yw1WN3DD;E-6O*O~}{ zYHq7_m0GqwEuUtCt^Z_{O7A>Un2)xW|?NLMEbqcv+SAKb> znqab*ft}tPek?K8k)fqE9ZmiKw+HCQ8@?8|pnZZ=>tV9D9SuWk{VaCYIJC9EhNLh` z=NXr2J6?+Q@C(RGhE$J$5MhSnEAKlj(gW=18oz|s9U~4$+*X)(x^N(~be4|hPI2Xt zUg~*cHEB=6+peKG@#vcX}xs$$kad*hXczpx#pQk=^|phG4gG#^%q! z*g^zHI#z?A)=}WKrX*WtUfA&v@nZg<%X-5ZP5xOC$mTarzs-l=3nQI(-yAG0>jRwk{llJz#@prBw{HwM&!$$g`pS-_;X)SWzzk9aaoz|+zP_xsZj~vU5%4$?YZ#2 z8M2dd6l(Ul^Iz*uxa`BZD_>Is#dq;b`nY!9s3r{-`wnL2tvF)D!2?&KQa2Kl@*4e# z*$V<~I^P4b%unH%6!5qktY9$a+g%Xo=wPiE>tWgkVXHN`M@6|7UprTbBKf%c`0pTr zeV=dMxyeQE3x~SZ-{TdygSZX&L#PDb?6D5i^FEC+MCp_pX5FHEba`$lGW>lZ=%4y8 zDn=8ZnoU7nkGiOpk#kD5UbrpS4Y=ujS>w!zymi_~{GFplke%14!dNL3)biWgH!dl^ zDhRvu8;Xm=uvcqDI+K$R?ca4b@vAuVXZ%`u-eO0_RN4M5g5~a&Q_`|>sdY-0h+WF3 zgjK^)Q|-IreDHk+fdm6Oq^1l8P5MXI+5X0d$+9%g*h0xHBT8HzVXKA58VBdw zgJ;Jo+}q1p`s!jLtc%gHcZ~92ZT>`BEIpI>LiZWg1Gy%~Cg2H^SbH+bxi$Xox|2@$ z^jE1-V*$yJTcJ*9@dRmpyT2}SR~J;)LXz0diKOd8O34P!Q34U^h)X%qND;t017PP?R904 z8uBzh+^9wd@Z!aW?RD1-^^~<{Pe}@km7)ZJ1N6Iq9ZSHd4FClEeHHh!U9*jgTAQmO zfw;q_-+8-r{L@0`SAN}KCD1~yPsDVjaBtc-*e#HZ89}8WAEo&$f&=UF&~v+eZ?C&w zWQ45qmEp#-#m~|YB9B`O-)StZapYx|CrSHqa0_tq>iY2d9NRK$NHf?}8x(BuXVB6M z6|F8FT-=V3S7Ua-K3kdJ7Rbl-x@)v8CY6z3t<3%Hd{9)dRsIC{@gy6T&bl}E0!$9dLAD(5|-E%tu&slR!De$*_v zCOCafGy)K8a$jO5nsz(I@+w~cOc$|@=Vc^RuO1z8=P%+@u@wD@wkI(sr%3DyKo`!% zi))tS%=gUVlZ|COuY|8qIja5xLF|@QJ&D z#kGJY^b-RXJD~K_?XOF5O3Y*hU$EF35{l)L4!pt{2MBh|yaO_wWm?`*mEk~2^vS$; zrMO<`h1V0loN&=Da|Y$QQWwL|JAHou>1~(kYk=Lv1dMGGTAN@-ij#t1n0zQaOxLiG z*w%Jh`rp$YS9h{sHqD27W0PG-#2S96nUkf$&hsFTbFa?R&!nVQU0Pt*QV^Hw=@65U zr9erREZ~T6{GA=Yl6+r(S4N)=5%zz!#ELjJ?|C&tUj7;@vp$FwxyBX0(M=U@Z@h^* zgn|9DtR|P0LkmX^?eTh_)~E7Z?UeKYj>}umOGqQUuE_6N}Saf0hG~_Ac66IUNDk_w37GUrF^$;aI z0syHBCkf7{=_AKWVVWDnzHxy-z!D9%rSuIu=T8}Vrtb?Q|L-eb1@EF^Kbc0QrM_Jf)Y!(5hPRv{FYlo^qvU2(}=SK4I&6Js4g zfGVg4Le}Tn2uexrZ9ia1Uc1Aw5p2&hn1wHLJ&B&ZGo$>~uz^jj+$h)mBqpAa;C_Xh zjP@2+34`p{gw^;Xw(ntz{HdkaCLkvlC|AQA+WF_QH8=k;!+cN^tuo~9fn~<67iEOR zsgg~kWYoJOd2eg86S#na*MGNQ^sYGduonQ?hvFnpmz5nF=%a6)Zh=btH>S!vIQ=uY z#ie?egbc5~C8!pakasZ++U7{ax5kxHmYG$>N{WMHnKWq@H%Tu!5hthQ`P!*6ymQo8 z+HOn}iQ66hHJg5Ni0$Sm#6h0vn2%vCVj{Amewh61*zwm4|JfCVnsNj*r}O-|ji|9+ zNOE!^jqF{R2B{N?t@Gy&{yBoJbzu(dZ25loea`DvByCJTgQ~@Wc$Jjow}Mwh_&bJ9 zmLoC{Hv8Xhroi7w158a6P4rmNc_|eGJB75+mKJ)i&Vh-`6e?Pi4Y3L~pW_RjhaB5K zwyQ3_bP?S~N%d_z9lk2`uIwl&@iO0tq$a>v*HnXyEN9cP$iczpJ7)8fPq@93noeZSW(0Y= z`wjy7R}f=A+ZGI+7hGY|F$JB~RF%H`bDv*(L=K;Wg}=9`@c>wg#~OCUd7pSC$8>z- zDhCAonzMruzBBwA^NNS8f;siQ9EC%(WnH4=*i7$eD!ZP4T}7&lr~kU+?H zluI-$Mn!al-nc{kpn?f4Ai1V|uhGx*28frN-J~HUagzU#Wu z?3wIPrp`XaPdc!MGuD1*sx>q1%}5Dpu+xp_zoa_%6ZTnkFR<#pJAXd^L}85{35Xtd z=w1A}I?qX**(7=MG@TTFD?pOm@l4a97`~c;9Q7u)u-&Z`Vm`f_G#t-;Nu}mn&-yJ; z#E8kgpC5xvMWdvf6+OV&!5@C(h5nHU0pY80weOa%iJq@$6pr~A@3$re;-uXm|4qMS zp*x1avuC+8wIt|hF3ziRwid)_5@!XxM?(3M&Eo#k2MFH%=9)NH{3mFKz*I6O}?`A(C~0N5}d<` z{!>>Z?+VMCfu-Hd=`8iSR(B-OJgDdBmOjBZj^{S|p|CfDlwlcGA!pu1XEmWUzZlfo z@jURVa&eDZ)cuB4d2>U|NhHbM zV)S+LVZz`)F|&8`&q@h5dl>pq7o2JM^RL1L?jm`;Bv*A4w_}c3NSCB66XuEkA;P_x z59MH)2k@bz9im7oJu$&ybhg0FQUch&>{tMT!P$gcc#QWt zA}Fuc#H@ip=)1F1A~GC9_Zwy}%eT=nA21h0P2{>3IfK~N}`3oFqHF?SBytPQMu%*Q9Sik=p%Ui7O<0Pyd z7&a}+le7(tO!?~&Ko?@8xNl*t-E(!A7OKKNS{anm!Nak5OxcCu_3{I2Pj&56)Iwc! zRNIoR)!#U*?rr@pZd?d*huuo!waF)T)TKr8=W+CjI?gUUDCVoI-;9 z%Umh=6ZmW)o=(XZzW~34u=Ea7fC0qSXtUb`__k5NhYljcw+{0_`Z|{ z8^SWCEON)_80n1`P#*YD>^Cr*>@@xRsJ=?%d8!~gGJc5%drvIMJvkv{XENV?*ertp}3H9o+4fF)4G90y!3VufODdwW`IV zFzZGzjaq0dbHPieNn?^h(F`N>SbwrUKW}pTBROLu41b_BH0F_`(9sBkPIf92qrq!q zkXC-{Ke+wldL#)6Ut6P{Y`+%7o{oXE%`n_aVW@SSb~ukvCHvmN_1U>6#dLaXhbY6* zvG%F8GfEmWxc49zqg@q}##88#dbcGrptx;++BQFOBw~u>_({>_3+zc-90Me=*V6Ju zOQp;$miY=|%tMq+;9Vy9{~-k5)tV+{*&?gg^Jich_W6kZ)OUL}x3bHD=r(U+yq`|; zKc(^4+j@8(d4lD}w9{<2!7(EfnX@+#e+5)MtBIv+GC^H0cnAVgil5e&b(y3LQ*~98%U&t;6U1# zh*qVzta-l}`*iU3tsRA0-*25t%u3$5m&=qfSR~-U`ygo&$1^%&*z!v(I($e~3zKTV zyYzBA!%E&(AOHaOk;4VpF}mW?eF{>-kB_h_jTV&<%MjFiU0&dLw`=N>ZSYhVaIVKF zThIkh7s&s{lv~8spk)lA>nu`6a)Gqpd%ke^_iUh44{y-shM8N>nRGc#5k;Lt*u~Rx zY1j%r6Vh=tSx$6&*BCX>ItqOd5uCxld2I}t7`i@1(yo4`RQ2_~gKVB5B1pEBX9&tz zvP(IHD#l*F{?c(fB{csrb|h|H*zZj0g@uau@Es{f{X9yY?#bxG*g2h!Mx5yBa)Qj! zEYj_o&Ud0)Dd|8ktV7l-vwC<_r2LUTrvSfuulkz8-QAxoXGE)0&!_{oIWISJEsQJM zO-s6?eCp|<-j5eH$eRXa-#;WF!@koz1HJQ~7X4%@0TA_QTBFJB0ooCLC#GJ5teJKG zwIMq*A)YOHCGEt)jTB6MB@uII;PB@+A-K~TT=L5!1$iMy!As?PXbc%qn35&081M`D z(Hdx>&IH~S?H2%}!sLUXXplvC2pP)?YiY0@oQfn^HJ}gV>v8WxpCf~K>J1)s1>eE0_RXA-u`(9?sNw6b z;nEQ?I{5q{I0AFhzt?b8r5Wq+*Ai1vk%b(yT)^viymzHNu|RfwuI8`iXPzk` z7|A4NkynH47*!CYij35?VlmI*9;t2kh`}x_ko#0*P+jp*u3kX^bH#UsF9E^rIM%LH z7Bf%o)``PRjc$tY(vN6v&mTEGEGCaF2?F)h{&fmh5swJbd0zy~G2ZWOB z0>Y6#u9-v6sYTnb0jG?NB4M@tn}$WHW3M>Vgn4FMvHyFJK?P zb0Ja&;TPmCyj|Ix#G?!pL-PjVmU9h+@@ju{btJDP*0)qc-2|>zHJ2=(y3tONz*D~knjrJ9Mb95WM5PS6tvkPmXPgIz$zJ|^=@DitmOSb^pL)bMtrs?( z1gi<(Ghfw4P4QxTPHmPx@Q&A3@890Pe9g`bT;O`wo`-WbXyWB-C=m`yL&I};Pvq%6 znrTzr3Lk3{7)*cj+kx9&Ut^Db2D&eEI$?Od`0JIC&zgwy@=BjCwRhP}Nqup=eD7eg zG-4C?)Kk&2Q#+daZ<~dN^dl+1t6g2}ukS6H*-Lb3nEn*Z z7RQa)^DBEE$$=IQSe&d&>&|V~^Pl*eu3Z{BZ=ELo1xo>Gz@jA~91efWS4RAIy#{;8 z(s3$#uAn=qch`&$VB$-;W_5Bk^>wOm%bmt)GJBNcZvF1iyjMVRDQJ*XXpP5E?cc(A zI*FWl{t=5k`4dF8{B~%+_1cFOL=aiZJX1Ew3u(pZG+o1ERsa}LD4$=d{UzZ5037Bu z&Wz3~?0Y%Rq^Dy{$7U?L;95)SZcO4lIspg_sc#Um(BzMGS`Gt$qY_+?0m)t5zyxCN zUX50W0f=e>My?&!BvuHMm{xBjxeY!*#FAbNg}Y~FNu-&D0pVVas@O=<@puXAu+W== zL#?17{-QgHD#=d2qn99a`a7YHv^E^*|hqz4(f~6gXE+f&%RK+%;5Gi?d35j@_Oc-WTF! zvxfriH#!JSHH2s|vTlT(d?S^fdnV{g&oa_h8d3Q4*^oATpOjyQ8RlQ_2`PYMV9iGC z24)kTZ*&M)O-$uq1S8U>z7xq(&*JNI@gHydIf`n|z*J#@o* z7hndPdT^V>5Jo(|+xOek{J=#57;ibgd4Msq3CUilv1?84XC;N)jS){PfUq{QM z$NmLympmT7uIf*jg1$2)rnRI%_hgb&nvQ}1ZM!xxd9(g_iHIiQHH@Bk_S5}1;5wFg z+VFV=r1p(M=^M;hY<6kZ3$NH_HAmCeqC%#Kjv#vAZk-}RlZ`lT|H`RX*GA0P|#byzOkY@RN^sMc?91P_I;vY)m@&YN|N=2g+l)Q z0Tj&AQjxq$SXV;s!o!;t=n~bYMzEEMSa{!*Suk_DO!k3t6g4&dT?65i$lg(PV&^xh zJ%y3|HKY>-QWSx&gCJ25zYG%WVOR9+E824{^1=}}Sxq-vi1XineF?B<{7RY;hh8k{ zpA%p|Cfxo7WIKYsC@V&C&gWz!cqa-D%L&C;Phvxr9|nDJ&{&2A5DIx|_Y9 z6^dMw-R$dsmQdP{ko`p`0DWy0OI$FVpo0-vIQvAvQhx*~VKtci`txiLZ9i_!&leJQ zYRA}PGi~0vIat@6$7@Qo;$e0mj` zBYKLBnu5_;y7fM406B>(7JaqXaIcUbE8n_T(9a(1*r;z?T1Wc0C>%fFL~b5qpcUXR-OYFSkX(R@TbSo2vy}eLcYL zJga}XOrL)`WD}+=4J#~Lb9fUt#R`VX$5rBr8<04)E8k6QFQ1_-?m*pzYsaNtJl@$L zYhp;pa#JLy^9lU;9G5&%MeqYmjbF_|9|`>6=Z{1|V*lkquU)wnCEI)vR?Ys;@7Io- zE9UJWm?y1f(@*ywyfYN+Bb?L})NFH+_W$JBv5MI*Z=wRSNjibI$H%+){8PqL*>Mpc zpo29I4!s4qnNz<8h5MhjHNK`};$Gtv7o)1RV)r*zsC6e|9%y~Hmdhu8zIR^HT=rg6 z`)O2^Jo<|)&t;M}rWPikVGuKqw_3faFLRQemrnCD}*n6Fd?rnAn}B!-w`xubfZMkK<#? zk84K+%3MLP@dFZSiFwPl_Sr>NI_wE(2^z~LDBCKsU2s5UjwkEEJNLC$u!sMZ+hO@? z3qF^-RGKCGX=jUOufX=rR!^s*+*_9ow_Eq4Sv_~g3jZBPaQA9_Q7yJ4kW){g*a;tF zSAwG7MVyy{tC8M&F@7}$x~O}2`M7(xeAT;Vwv<#ACkWTJ8Do;59s$&v;Z1Pv`zm4% z@D2@SrpR*ep5A={ZXHhh&~mBQYzBxcc2cwOFr&EXxp1uUyK}F4h&8${drlQX{nvZ< zrRalbBCyR@IC0ogooMHOs6-A^-*rtkc*~8ok{N6sor!$!)OR~c27spYqt7HmXz zSGwT5rFA}J7k?$>({T=|PTE$%3JDQg<~xx+Cmb_*IPtr6K7Keaq$%r;UpPX`bl4x| zmiV?&`kM+CX(oA2f)#IEQha9*I;l(Vz2RU>9EhS}>32Fk!IEdPbvN;omKvoyZ0})8 zQWSv=4BFp*TuiEVoqmB3R0gZjPX6K>eaqIKq@<1?xnonciL(g->9@N(sQs68O zAZoMZbL(QS9~JHN>ZdstI70;SS4dde6%ojU|B7}3@ZhuB-=hKpp}A$_ubytFh*sk4 z2%fV`5TrXOw0V=ORVVqS0UpTQA^khZu}6!qZ~O@q{L)+WiQ zno7iOAC{(T9`VK=$`5L-UT96|T@tvz3H`!<&Vhnmi%42g@wR{f$myle!2M)GPRK04^XZ!>*fc`feh$hdnQJUOj1!&@ts3JF)bF zm;dL_d|cR`^1~DRF{!U$Fxo%1E<_JCy(iAwZmtU1D1J&k(k8(NB7rGinlqf?7cmi~ z_XBA8bxjWX0--TLmwc${Hr|WS05rl_TBSCs5~)RQgl9bu_2_gdDWF>Qns3(Xx_dCvf(oEvfPy^SV?M&6aZ8pm_j}SnER75Bg8nGANXid*v%P{!> z?>7*AwVTQ&M4yLMl-k4eA%gZ(f!z&^jmi$DAKbd1VBBIdD2}?Y#Hi>GLzgz{xDA%Z z?R)f7+medCetl$hyNTvAcn(X_0t#Y`Vme=sGv?7hGo z`&r{MRQqa$^tBWfy44K<0DibAK$tHiMcl-GEypF zGX7xgl&4hGIc{^E3p{xmGpJt%s^zZ5`GrqtY(^h89;trD^_Qi0>%2g}&Bo70!2+8U zs(;S?&fH#%z8mptoPIfR$TS@X2Sp_(?5Qly_`rrYQz~5I?bAHpdKRBrH)V@Z>i3y}SqXV41SD(}yh?aD0es@*g? z*xR<-`}Rld`1w-9L;*hzJx!6bqn^&86zfUPADGj+2{|p3w^jREHb5d00+e90;)c=1 z?NKjNWwt?Nn5PuVJr;bw%GZURr1(akvaQ=kDG>#+fF=Sm2w>QE_f0| zkYS-x=%_}OZ>HvNx0$~NbB*cyph!0BMnzM$_iwvM-7RTV8AZv_2RnRBFaSe>is{l{kObzs)!-ec8ZIbuMgTi@X-!wu zbXo;raSH2xf=TsZ-}%HnqPCe&Py@a3%x>b<)}a!rwV*!MFm`qn>%9^fa*3?Q{D{eQ z*HQN57hFp%Fqn?GKf*{K)S6fg@23r^Oj7*Hta`b_(&jZXzVge$WGm`<=+eZ21s_X3b+IP$Sishj=IJ_^G(DV!KGjlk_WLF!il z1cIyD=l5V{CTWXt=3svVfkLgpc6DsvxMQ}#*(P?=?MY8@YY#6m0A#@m>LnFvDMHP! zY6m~~)svf#dVR6~o8=#RnV})Zmr_$DKB3D|+#_COIdW`X{XTx=QuB6l>@1HE7%6=5V=Cr)yr83i1r^qdKyjYc7y)|W?f6;G+j-h@|Fo7L72S^xq`H%VA}h z=nP>@%Tpfm?4D>1 z$@oNt$EY+zVHOqpe6s{qqBKLQeEzGsD2~A3iq4qF<;Nlf(+rv@f*o1aa{wPP18e}L zN|#9kYhLp&z0(`))f%S#wppnyH~@RWC-&TyIq(m-fcHDnwnDRSHMzti#Uk&|I~4J| zO5Jj5X~Yg?Z^{FBW=;w?ufw9M&gLFf%*2Y1Uezq^kZ@GZv%WkMBUvhb+g5v|9J%RK zg|jX&qKkl_V#teiEr?`sQCA~BP$?+TOZ>_kcEF~|rovJp=gZPBn8r#d-m07R8FLRv zf|WH=tIoNUx5?QwP%EroJr|5s0woVven>eF9wCyhG#PI&ktk{}+HybeBh;1Cr{9Ii ztNU6KZ^gYd{b9s!PrcUVF#J6CNVMPZyO&GI%g8CzDZ1I}hWS-~q2(~9NOSf;3wD^` zK?VlGl%X7*70za?Xp)mPB7mUmphp00?H!8e8A*k^l_&{TdU%i*KvbX7YWwRMV^I=< zUHm~7mdX+zuQgmwB`y!FT+5U4xJ{PoBHS)F1E6ICVNPLFKXRDb?Bti{^<`vJ@|i23 z*kEBGssOPl+M^_1EhDaHh6B#iY zhA9c-_eWysY$Lc}1pS=iY%x}qrTb>PKE6MsQE{3GUy2$z#hNIMu#jBfd-oznL)UJg zyc1Gq{ixPoiY3m_XO^5S@_D1mBJ1t3tXR!Pg3S25QAC4TE^k9qu1jTEDUEM()`A85 zYbk>X){0CFD~qXxEZhRQ^rL4ES+|6|f^j&0CJMt5`kcfHYdqKV7`l?AQbI0^pg}J? z;z}KMX-#28Kx_t2?b%Z!;(6>baExc7d-=fdh)Q~%WH)CmTvsyi5Iw~O z!|%ufZ`}&enE>%^?=^CWioklfJB73I`91UgEE7z4UJln>U`R0r6T3{N+&Yj!PPddB zp`>I+fdRETg^cthwTv*?$;X9LpoMa|WGnL~L?EKsif;3Ofj|PNq@toanJPU4E%I=i z7(Kh{SAMLztsG)hbb_2?bDTgUB$Pv|Tu)bu~V0^3?kS@Af zxLc-?Z)_WTsr1r3p@2#m`j~8_UYMR7%?>dGL%-xAHRS{&(#)iXC9hiiNvk1jTeyv} zqJn28^Vv{08PT~6D&qpcNl+QgX5kQCu0*dNJ@zq1!kCHljV_Z*_lwwd2`|#MFrNCC zZkoyx6-`qOQgH#L3dw($^zLvmZj3}SvDiiPX#iY_hFiKlo)PW_rO%gNCX-YcCAPC@ zsPVthR8`^mNMiEHejhDnOh|9yDD1S6G$AG~pj;Y+#c9hjhEbq2xuUT1N-#izTF7x=FT1K1U@nZpzAIuZ&-tADnyXw;;;BwVeFW6wtH z)#sa!6B$Y8BcB^mdH)i?rU#IaX2%Ir{##=7KZ0wGSNbheQ0oeR_#xhFF3yWmLvTz{ zJz0#OogKFV8xB*D{IMES`1CtilnNkXYL<3*-GB|zLB5rV;KM?}gS&pr`h}(6FnbiS zYMlHFEwV=nYmbv;Z656t8yS$1U$it-Sp(J?S6b}ES!>GPglKdhnXGC@$wi7>&_w1F z77hE1*Rx94S{omxsWM%Xae*07!nn|eJ%14Un-JZF2(xnu{BG5W!1Xt{{WVQE)H@3e zin~zx^UnK5DvCS_>KmkoxwfJ*j~99xT!;oX$_iaJ16-H#k6GI+q)aDkUWVX-;!W)2 zY>VF^B#PQ-BdW7$nL8pxO~DW3zrHHlv88`(9ItsBmvr{>po*9-Ynv-Y)(f79|MCb@ z@{N^ZAePbGgB&_%ph<=X6NA*#V#1wJ`<@bhBwBj?lbh)Q(nDV3g-($dHx! zQqquoAsB=h^F!sd97aWwH^Q*fRVBHMt4AwzA=Xw(PD?v{o$mjbMU`aT4`n?GFg;8a zGc6bplrMsBka^e z*abxS+67_poI+^o>%&z(Ha~A~`5L8!7@ZkiE2Htg9y-&Jp|E+Gr0xH0Y$6#vCCdgD z@YxkAQ@F&crq)Q9K49=IP46(~x8$S4{!{0 zQKjcS;o>iG0GDb-^Y8603Zsp|B4E=qU(lJcv&kKk_1TMBY@$gaMFAZ2Lx*r@Xe0xO zctI)h?`#N_C4!M`rplma{^7!CM!;9K*>pC<$ZSEY%DU(fI7@gJ&*emC)P;ioZ$(R$ zG#&Qd-)*EJ==(F%EA!$m{phppfwq_I`QMmo9Q!uiqrziKx7pT3PD^!nCN=cDKXhY5 zZUEPtTbm~OB+FLbUUsy5*`vldI}OV#X;Z727dI(##OdpNo3mt{u#Tqd%`evlDrzVC z2(@XcrEOWH!YN$3#AfR@{}o*IP8zdzw0dct3iRBdD%!ZYOEa*FPc>j!$Ru0OJIx28 zq!&mm6@KgNDzbZ7M*9l|GhoFqUsWqS`>SSpgGjskO1+<+p(0DpcLtY-M9y^!wOuvc z)O5o#u0LLG|4Qon1kO4%+`V=pT}fzhsbxZV@wU>B4H?@v_C4pkda^s@^oluWGPXvA z6>aiyV0RGz%UXWE_+e+yfgKDCOtZn~0=&<&@RwqHw1Iifxy`Mtj5P`!uhxq%cy=pY z)X%Tm-%Ra#V9yH4V6hKszkYEm`&|@yY^U}9=%f%1dqq#p-g)0OCR!44hjOah`m6FhgSD{s$Yi^!JK z2+W@_dy=8%i(WzX2G$c(t9F`4*stRF9h9JMRO}>Kxxo8->b~{BaDUSHsaW9X+C?t= zSN=(7sA6VSvu$J0SOeTTx%B(Q2e-r;PoHAhc$;yysDg}!JR>iQVBu~-O>Ostvu;1; z{c_pk78ifYPbBEVq^%mVn*KZ2D(||zOK95_jqF){t0b&O2HS=(;ei<=iozq`02-uz+t!T0QISLCh9>wjM(R{Z2jx4~4a zX3v!V4Qm&!l8nh>m=XNYB*-ABaqtvQsVZGRPN3OwV2!PC{xWt~$(696{G!U6yQ diff --git a/docs/methods.md b/docs/methods.md deleted file mode 100644 index cba2406..0000000 --- a/docs/methods.md +++ /dev/null @@ -1,69 +0,0 @@ -# Methods - -Methods are functions that can be called by a JSON-RPC request. To write one, -decorate a function with `@method`: - -```python -from jsonrpcserver import method, Error, Ok, Result - -@method -def ping() -> Result: - return Ok("pong") -``` - -If you don't need to respond with any value simply `return Ok()`. - -## Responses - -Methods return either `Ok` or `Error`. These are the [JSON-RPC response -objects](https://www.jsonrpc.org/specification#response_object) (excluding the -`jsonrpc` and `id` parts). `Error` takes a code, message, and optionally -'data'. - -```python -@method -def test() -> Result: - return Error(1, "There was a problem") -``` - -```{note} -Alternatively, raise a `JsonRpcError`, which takes the same arguments as `Error`. -``` - -## Parameters - -Methods can accept arguments. - -```python -@method -def hello(name: str) -> Result: - return Ok("Hello " + name) -``` - -Testing it: - -```sh -$ curl -X POST http://localhost:5000 -d '{"jsonrpc": "2.0", "method": "hello", "params": ["Beau"], "id": 1}' -{"jsonrpc": "2.0", "result": "Hello Beau", "id": 1} -``` - -## Invalid params - -A common error response is *invalid params*. -The JSON-RPC error code for this is **-32602**. A shortcut, *InvalidParams*, is -included so you don't need to remember that. - -```python -from jsonrpcserver import dispatch, method, InvalidParams, Ok, Result - -@method -def within_range(num: int) -> Result: - if num not in range(1, 5): - return InvalidParams("Value must be 1-5") - return Ok() -``` - -This is the same as saying -```python -return Error(-32602, "Invalid params", "Value must be 1-5") -``` diff --git a/pyproject.toml b/pyproject.toml index 5e4171f..ffb3378 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,3 +49,9 @@ packages = [ "jsonrpcserver" ] zip-safe = false + +[tool.pytest.ini_options] +addopts = '--doctest-glob="*.md"' +testpaths = [ + "tests", +] From 389959d190fa880732bef7b2b113993c5b741b18 Mon Sep 17 00:00:00 2001 From: Beau Date: Wed, 31 Jul 2024 13:33:25 +1000 Subject: [PATCH 28/33] Update readme (#281) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 86f5770..caa0aa5 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,6 @@ response = dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}') [Watch a video on how to use it.](https://www.youtube.com/watch?v=3_BMmgJaFHQ) -Full documentation is at [jsonrpcserver.com](https://www.jsonrpcserver.com/). +Full documentation is in the [wiki](https://github.com/explodinglabs/jsonrpcserver/wiki). See also: [jsonrpcclient](https://github.com/explodinglabs/jsonrpcclient) From 8c23c46e3af2887a26e528c3e3a02c3ba1b6f5ce Mon Sep 17 00:00:00 2001 From: Beau Date: Fri, 16 Aug 2024 11:59:45 +1000 Subject: [PATCH 29/33] Replace readthedocs with mkdocs (#282) * Add docs back * Replace readthedocs with mkdocs --- README.md | 4 +- docs/async.md | 41 ++++++++++++++++++++ docs/dispatch.md | 84 ++++++++++++++++++++++++++++++++++++++++ docs/faq.md | 39 +++++++++++++++++++ docs/index.md | 37 ++++++++++++++++++ docs/methods.md | 70 +++++++++++++++++++++++++++++++++ jsonrpcserver/server.py | 18 +++------ logo.png | Bin 0 -> 17627 bytes mkdocs.yml | 49 +++++++++++++++++++++++ 9 files changed, 327 insertions(+), 15 deletions(-) create mode 100644 docs/async.md create mode 100644 docs/dispatch.md create mode 100644 docs/faq.md create mode 100644 docs/index.md create mode 100644 docs/methods.md create mode 100644 logo.png create mode 100644 mkdocs.yml diff --git a/README.md b/README.md index caa0aa5..42618e8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ jsonrpcserver ![PyPI](https://img.shields.io/pypi/v/jsonrpcserver.svg) @@ -16,7 +16,7 @@ pip install jsonrpcserver ``` ```python -from jsonrpcserver import method, serve, Ok, Result +from jsonrpcserver import method, Result, Ok @method def ping() -> Result: diff --git a/docs/async.md b/docs/async.md new file mode 100644 index 0000000..5954d07 --- /dev/null +++ b/docs/async.md @@ -0,0 +1,41 @@ +Async dispatch is supported. + +```python +from jsonrpcserver import async_dispatch, async_method, Ok, Result + +@async_method +async def ping() -> Result: + return Ok("pong") + +await async_dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}') +``` + +Some reasons to use this: + +- Use it with an asynchronous protocol like sockets or message queues. +- `await` long-running functions from your method. +- Batch requests are dispatched concurrently. + +## Notifications + +Notifications are requests without an `id`. We should not respond to +notifications, so jsonrpcserver gives an empty string to signify there is *no +response*. + +```python +>>> await async_dispatch('{"jsonrpc": "2.0", "method": "ping"}') +'' +``` + +If the response is an empty string, don't send it. + +```python +if response := dispatch(request): + send(response) +``` + +```{note} +A synchronous protocol like HTTP requires a response no matter what, so we can +send back the empty string. However with async protocols, we have the choice of +responding or not. +``` diff --git a/docs/dispatch.md b/docs/dispatch.md new file mode 100644 index 0000000..719ea9c --- /dev/null +++ b/docs/dispatch.md @@ -0,0 +1,84 @@ +# Dispatch + +The `dispatch` function processes a JSON-RPC request, attempting to call the method(s) +and gives a JSON-RPC response. + +```python +>>> dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}') +'{"jsonrpc": "2.0", "result": "pong", "id": 1}' +``` + +It's a pure function; it will always give you a JSON-RPC response. No exceptions will be +raised. + +[See how dispatch is used in different frameworks.](examples) + +## Optional parameters + +The `dispatch` function takes a request as its argument, and also has some optional +parameters that allow you to customise how it works. + +### methods + +This lets you specify the methods to dispatch to. It's an alternative to using +the `@method` decorator. The value should be a dict mapping function names to +functions. + +```python +def ping(): + return Ok("pong") + +dispatch(request, methods={"ping": ping}) +``` + +Default is `global_methods`, which is an internal dict populated by the +`@method` decorator. + +### context + +If specified, this will be the first argument to all methods. + +```python +@method +def greet(context, name): + return Ok(f"Hello {context}") + +>>> dispatch('{"jsonrpc": "2.0", "method": "greet", "params": ["Beau"], "id": 1}', context="Beau") +'{"jsonrpc": "2.0", "result": "Hello Beau", "id": 1}' +``` + +### deserializer + +A function that parses the JSON request string. Default is `json.loads`. + +```python +dispatch(request, deserializer=ujson.loads) +``` + +### jsonrpc_validator + +A function that validates the request once the JSON string has been parsed. The +function should raise an exception (any exception) if the request doesn't match +the JSON-RPC spec (https://www.jsonrpc.org/specification). Default is +`default_jsonrpc_validator` which uses Jsonschema to validate requests against +a schema. + +To disable JSON-RPC validation, pass `jsonrpc_validator=lambda _: None`, which +will improve performance because this validation takes around half the dispatch +time. + +### args_validator + +A function that validates a request's parameters against the signature of the +Python function that will be called for it. Note this should not validate the +_values_ of the parameters, it should simply ensure the parameters match the +Python function's signature. For reference, see the `validate_args` function in +`dispatcher.py`, which is the default `args_validator`. + +### serializer + +A function that serializes the response string. Default is `json.dumps`. + +```python +dispatch(request, serializer=ujson.dumps) +``` diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..98bf4e7 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,39 @@ +## How to disable schema validation? + +Validating requests is costly - roughly 40% of dispatching time is spent on schema validation. +If you know the incoming requests are valid, you can disable the validation for better +performance. + +```python +dispatch(request, validator=lambda _: None) +``` + +## Which HTTP status code to respond with? + +I suggest: + +```python +200 if response else 204 +``` + +If the request was a notification, `dispatch` will give you an empty string. So +since there's no http body, use status code 204 - no content. + +## How to rename a method + +Use `@method(name="new_name")`. + +Or use the dispatch function's [methods +parameter](https://www.jsonrpcserver.com/en/latest/dispatch.html#methods). + +## How to get the response in other forms? + +Instead of `dispatch`, use: + +- `dispatch_to_serializable` to get the response as a dict. +- `dispatch_to_response` to get the response as a namedtuple (either a + `SuccessResponse` or `ErrorResponse`, these are defined in + [response.py](https://github.com/explodinglabs/jsonrpcserver/blob/main/jsonrpcserver/response.py)). + +For these functions, if the request was a batch, you'll get a list of +responses. If the request was a notification, you'll get `None`. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..3612046 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,37 @@ +# Jsonrpcserver + +Jsonrpcserver processes JSON-RPC requests. + +## Quickstart + +Install jsonrpcserver: +```python +pip install jsonrpcserver +``` + +Create a `server.py`: + +```python +from jsonrpcserver import method, serve, Ok + +@method +def ping(): + return Ok("pong") + +if __name__ == "__main__": + serve() +``` + +Start the server: +```sh +$ python server.py +``` + +Send a request: +```sh +$ curl -X POST http://localhost:5000 -d '{"jsonrpc": "2.0", "method": "ping", "id": 1}' +{"jsonrpc": "2.0", "result": "pong", "id": 1} +``` + +`serve` starts a basic development server. Do not use it in a production deployment. Use +a production WSGI server instead, with jsonrpcserver's [dispatch](dispatch) function. diff --git a/docs/methods.md b/docs/methods.md new file mode 100644 index 0000000..28457f8 --- /dev/null +++ b/docs/methods.md @@ -0,0 +1,70 @@ +# Methods + +Methods are functions that can be called by a JSON-RPC request. + +## Writing methods + +To write a method, decorate a function with `@method`: + +```python +from jsonrpcserver import method, Error, Ok, Result + +@method +def ping() -> Result: + return Ok("pong") +``` + +If you don't need to respond with any value simply `return Ok()`. + +## Responses + +Methods return either `Ok` or `Error`. These are the [JSON-RPC response +objects](https://www.jsonrpc.org/specification#response_object) (excluding the +`jsonrpc` and `id` parts). `Error` takes a code, message, and optionally +'data'. + +```python +@method +def test() -> Result: + return Error(1, "There was a problem") +``` + +Alternatively, raise a `JsonRpcError`, which takes the same arguments as `Error`. + +## Parameters + +Methods can accept arguments. + +```python +@method +def hello(name: str) -> Result: + return Ok("Hello " + name) +``` + +Testing it: + +```sh +$ curl -X POST http://localhost:5000 -d '{"jsonrpc": "2.0", "method": "hello", "params": ["Beau"], "id": 1}' +{"jsonrpc": "2.0", "result": "Hello Beau", "id": 1} +``` + +## Invalid params + +A common error response is *invalid params*. +The JSON-RPC error code for this is **-32602**. A shortcut, *InvalidParams*, is +included so you don't need to remember that. + +```python +from jsonrpcserver import dispatch, method, InvalidParams, Ok, Result + +@method +def within_range(num: int) -> Result: + if num not in range(1, 5): + return InvalidParams("Value must be 1-5") + return Ok() +``` + +This is the same as saying +```python +return Error(-32602, "Invalid params", "Value must be 1-5") +``` diff --git a/jsonrpcserver/server.py b/jsonrpcserver/server.py index e783082..d0c54fb 100644 --- a/jsonrpcserver/server.py +++ b/jsonrpcserver/server.py @@ -2,35 +2,27 @@ http.server module. """ -import logging from http.server import BaseHTTPRequestHandler, HTTPServer from .main import dispatch class RequestHandler(BaseHTTPRequestHandler): - """Handle HTTP requests""" - def do_POST(self) -> None: # pylint: disable=invalid-name - """Handle POST request""" - response = dispatch( - self.rfile.read(int(str(self.headers["Content-Length"]))).decode() - ) + request = self.rfile.read(int(str(self.headers["Content-Length"]))).decode() + response = dispatch(request) if response is not None: self.send_response(200) self.send_header("Content-type", "application/json") self.end_headers() - self.wfile.write(str(response).encode()) + self.wfile.write(response.encode()) def serve(name: str = "", port: int = 5000) -> None: - """A simple function to serve HTTP requests""" - logging.info(" * Listening on port %s", port) + httpd = HTTPServer((name, port), RequestHandler) try: - httpd = HTTPServer((name, port), RequestHandler) httpd.serve_forever() except KeyboardInterrupt: pass - except Exception: + finally: httpd.shutdown() - raise diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d7024dca11a882b994b4ca9aee8549294e39dbe4 GIT binary patch literal 17627 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 literal 0 HcmV?d00001 diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..57eb2b9 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,49 @@ +markdown_extensions: + - pymdownx.highlight: + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.details + - pymdownx.superfences + - pymdownx.mark +nav: + - Home: 'index.md' + - 'methods.md' + - 'dispatch.md' + - 'async.md' + - 'faq.md' + - 'examples.md' +repo_name: jsonrpcserver +repo_url: https://github.com/explodinglabs/jsonrpcserver +site_author: Exploding Labs +site_description: Welcome to the documentation for Jsonrcpcserver. +site_name: Jsonrpcserver +site_url: https://www.jsonrpcserver.com/ +theme: + features: + - content.code.copy + - navigation.footer + - navigation.tabs + - toc.integrate + name: material + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to system preference +extra: + version: + provider: mike From 1d9153e1a6a1b757fa3d118019565c91991ad581 Mon Sep 17 00:00:00 2001 From: Beau Date: Sat, 17 Aug 2024 20:40:44 +1000 Subject: [PATCH 30/33] Remove pylint pragmas (#283) --- examples/http_server.py | 2 +- jsonrpcserver/async_dispatcher.py | 6 ++---- jsonrpcserver/async_main.py | 2 -- jsonrpcserver/dispatcher.py | 9 ++++----- jsonrpcserver/methods.py | 2 +- jsonrpcserver/response.py | 6 ++---- jsonrpcserver/result.py | 2 -- jsonrpcserver/sentinels.py | 1 - jsonrpcserver/server.py | 5 ++++- jsonrpcserver/utils.py | 2 -- tests/test_async_dispatcher.py | 2 -- tests/test_async_main.py | 4 ---- tests/test_dispatcher.py | 4 ++-- tests/test_main.py | 6 +----- tests/test_methods.py | 2 -- tests/test_request.py | 2 -- tests/test_response.py | 2 -- tests/test_result.py | 2 -- tests/test_sentinels.py | 2 -- tests/test_server.py | 2 -- 20 files changed, 17 insertions(+), 48 deletions(-) diff --git a/examples/http_server.py b/examples/http_server.py index 5b81a2a..59c7bd6 100644 --- a/examples/http_server.py +++ b/examples/http_server.py @@ -17,7 +17,7 @@ def ping() -> Result: class TestHttpServer(BaseHTTPRequestHandler): """HTTPServer request handler""" - def do_POST(self) -> None: # pylint: disable=invalid-name + def do_POST(self) -> None: """POST handler""" # Process request request = self.rfile.read(int(self.headers["Content-Length"])).decode() diff --git a/jsonrpcserver/async_dispatcher.py b/jsonrpcserver/async_dispatcher.py index 49ba167..bfd47a0 100644 --- a/jsonrpcserver/async_dispatcher.py +++ b/jsonrpcserver/async_dispatcher.py @@ -36,8 +36,6 @@ logger = logging.getLogger(__name__) -# pylint: disable=missing-function-docstring,duplicate-code - async def call( request: Request, context: Any, method: Method @@ -49,7 +47,7 @@ async def call( validate_result(result) except JsonRpcError as exc: return Failure(ErrorResult(code=exc.code, message=exc.message, data=exc.data)) - except Exception as exc: # pylint: disable=broad-except + except Exception as exc: # Other error inside method - Internal error logger.exception(exc) return Failure(InternalErrorResult(str(exc))) @@ -139,6 +137,6 @@ async def dispatch_to_response_pure( result.unwrap(), ) ) - except Exception as exc: # pylint: disable=broad-except + except Exception as exc: logger.exception(exc) return post_process(Failure(ServerErrorResponse(str(exc), None))) diff --git a/jsonrpcserver/async_main.py b/jsonrpcserver/async_main.py index d34801c..4ff3773 100644 --- a/jsonrpcserver/async_main.py +++ b/jsonrpcserver/async_main.py @@ -11,8 +11,6 @@ from .sentinels import NOCONTEXT from .utils import identity -# pylint: disable=missing-function-docstring,duplicate-code - async def dispatch_to_response( request: str, diff --git a/jsonrpcserver/dispatcher.py b/jsonrpcserver/dispatcher.py index 2893012..467869a 100644 --- a/jsonrpcserver/dispatcher.py +++ b/jsonrpcserver/dispatcher.py @@ -2,7 +2,6 @@ requests, providing responses. """ -# pylint: disable=protected-access import logging from functools import partial from inspect import signature @@ -143,7 +142,7 @@ def call( except JsonRpcError as exc: return Failure(ErrorResult(code=exc.code, message=exc.message, data=exc.data)) # Any other uncaught exception inside method - internal error. - except Exception as exc: # pylint: disable=broad-except + except Exception as exc: logger.exception(exc) return Failure(InternalErrorResult(str(exc))) return result @@ -249,7 +248,7 @@ def validate_request( # Since the validator is unknown, the specific exception that will be raised is also # unknown. Any exception raised we assume the request is invalid and return an # "invalid request" response. - except Exception: # pylint: disable=broad-except + except Exception: return Failure(InvalidRequestResponse("The request failed schema validation")) return Success(request) @@ -266,7 +265,7 @@ def deserialize_request( # Since the deserializer is unknown, the specific exception that will be raised is # also unknown. Any exception raised we assume the request is invalid, return a # parse error response. - except Exception as exc: # pylint: disable=broad-except + except Exception as exc: return Failure(ParseErrorResponse(str(exc))) @@ -297,7 +296,7 @@ def dispatch_to_response_pure( args_validator, post_process, methods, context, result.unwrap() ) ) - except Exception as exc: # pylint: disable=broad-except + except Exception as exc: # There was an error with the jsonrpcserver library. logging.exception(exc) return post_process(Failure(ServerErrorResponse(str(exc), None))) diff --git a/jsonrpcserver/methods.py b/jsonrpcserver/methods.py index c946a58..6a24930 100644 --- a/jsonrpcserver/methods.py +++ b/jsonrpcserver/methods.py @@ -25,7 +25,7 @@ def method( - f: Optional[Method] = None, # pylint: disable=invalid-name + f: Optional[Method] = None, name: Optional[str] = None, ) -> Callable[..., Any]: """A decorator to add a function into jsonrpcserver's internal global_methods dict. diff --git a/jsonrpcserver/response.py b/jsonrpcserver/response.py index 3113d05..2702b0d 100644 --- a/jsonrpcserver/response.py +++ b/jsonrpcserver/response.py @@ -41,7 +41,7 @@ class ErrorResponse(NamedTuple): Response = Result[SuccessResponse, ErrorResponse] -def ParseErrorResponse(data: Any) -> ErrorResponse: # pylint: disable=invalid-name +def ParseErrorResponse(data: Any) -> ErrorResponse: """An ErrorResponse with most attributes already populated. From the spec: "This (id) member is REQUIRED. It MUST be the same as the value of @@ -51,7 +51,7 @@ def ParseErrorResponse(data: Any) -> ErrorResponse: # pylint: disable=invalid-n return ErrorResponse(ERROR_PARSE_ERROR, "Parse error", data, None) -def InvalidRequestResponse(data: Any) -> ErrorResponse: # pylint: disable=invalid-name +def InvalidRequestResponse(data: Any) -> ErrorResponse: """An ErrorResponse with most attributes already populated. From the spec: "This (id) member is REQUIRED. It MUST be the same as the value of @@ -63,13 +63,11 @@ def InvalidRequestResponse(data: Any) -> ErrorResponse: # pylint: disable=inval def MethodNotFoundResponse(data: Any, id: Any) -> ErrorResponse: """An ErrorResponse with some attributes already populated.""" - # pylint: disable=invalid-name,redefined-builtin return ErrorResponse(ERROR_METHOD_NOT_FOUND, "Method not found", data, id) def ServerErrorResponse(data: Any, id: Any) -> ErrorResponse: """An ErrorResponse with some attributes already populated.""" - # pylint: disable=invalid-name,redefined-builtin return ErrorResponse(ERROR_SERVER_ERROR, "Server error", data, id) diff --git a/jsonrpcserver/result.py b/jsonrpcserver/result.py index b193880..37740c2 100644 --- a/jsonrpcserver/result.py +++ b/jsonrpcserver/result.py @@ -15,8 +15,6 @@ from .codes import ERROR_INTERNAL_ERROR, ERROR_INVALID_PARAMS, ERROR_METHOD_NOT_FOUND from .sentinels import NODATA -# pylint: disable=missing-class-docstring,missing-function-docstring,invalid-name - class SuccessResult(NamedTuple): result: Any = None diff --git a/jsonrpcserver/sentinels.py b/jsonrpcserver/sentinels.py index 0ed0cca..46ca287 100644 --- a/jsonrpcserver/sentinels.py +++ b/jsonrpcserver/sentinels.py @@ -12,7 +12,6 @@ class Sentinel: Has a nicer repr than `object()`. """ - # pylint: disable=too-few-public-methods def __init__(self, name: str): self.name = name diff --git a/jsonrpcserver/server.py b/jsonrpcserver/server.py index d0c54fb..c0977c2 100644 --- a/jsonrpcserver/server.py +++ b/jsonrpcserver/server.py @@ -8,7 +8,10 @@ class RequestHandler(BaseHTTPRequestHandler): - def do_POST(self) -> None: # pylint: disable=invalid-name + """Handle HTTP requests""" + + def do_POST(self) -> None: + """Handle POST request""" request = self.rfile.read(int(str(self.headers["Content-Length"]))).decode() response = dispatch(request) if response is not None: diff --git a/jsonrpcserver/utils.py b/jsonrpcserver/utils.py index a259799..ec47b32 100644 --- a/jsonrpcserver/utils.py +++ b/jsonrpcserver/utils.py @@ -3,8 +3,6 @@ from functools import reduce from typing import Any, Callable, List -# pylint: disable=invalid-name - def identity(x: Any) -> Any: """Returns the argument.""" diff --git a/tests/test_async_dispatcher.py b/tests/test_async_dispatcher.py index b1afbfd..78253c2 100644 --- a/tests/test_async_dispatcher.py +++ b/tests/test_async_dispatcher.py @@ -20,8 +20,6 @@ from jsonrpcserver.sentinels import NOCONTEXT, NODATA from jsonrpcserver.utils import identity -# pylint: disable=missing-function-docstring,duplicate-code - async def ping() -> Result: return Ok("pong") diff --git a/tests/test_async_main.py b/tests/test_async_main.py index 7686a8e..f70e83f 100644 --- a/tests/test_async_main.py +++ b/tests/test_async_main.py @@ -11,10 +11,6 @@ from jsonrpcserver.response import SuccessResponse from jsonrpcserver.result import Ok, Result -# pylint: disable=missing-function-docstring - -# pylint: disable=missing-function-docstring - async def ping() -> Result: return Ok("pong") diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 26fe49a..a58885a 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -426,7 +426,7 @@ def test_dispatch_to_response_pure_method_not_found() -> None: def test_dispatch_to_response_pure_invalid_params_auto() -> None: - def f(colour: str, size: str) -> Result: # pylint: disable=unused-argument + def f(colour: str, size: str) -> Result: return Ok() assert dispatch_to_response_pure( @@ -615,7 +615,7 @@ def test_dispatch_to_response_pure_notification_method_not_found() -> None: def test_dispatch_to_response_pure_notification_invalid_params_auto() -> None: - def foo(colour: str, size: str) -> Result: # pylint: disable=unused-argument + def foo(colour: str, size: str) -> Result: return Ok() assert ( diff --git a/tests/test_main.py b/tests/test_main.py index fae47ce..52c936b 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -11,10 +11,6 @@ from jsonrpcserver.response import SuccessResponse from jsonrpcserver.result import Ok, Result -# pylint: disable=missing-function-docstring - -# pylint: disable=missing-function-docstring - def ping() -> Result: return Ok("pong") @@ -28,7 +24,7 @@ def test_dispatch_to_response() -> None: def test_dispatch_to_response_with_global_methods() -> None: @method - def ping() -> Result: # pylint: disable=redefined-outer-name + def ping() -> Result: return Ok("pong") response = dispatch_to_response('{"jsonrpc": "2.0", "method": "ping", "id": 1}') diff --git a/tests/test_methods.py b/tests/test_methods.py index 4459e7a..901b781 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -3,8 +3,6 @@ from jsonrpcserver.methods import global_methods, method from jsonrpcserver.result import Ok, Result -# pylint: disable=missing-function-docstring - def test_decorator() -> None: @method diff --git a/tests/test_request.py b/tests/test_request.py index 67e803b..728cc9a 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -2,8 +2,6 @@ from jsonrpcserver.request import Request -# pylint: disable=missing-function-docstring - def test_request() -> None: assert Request(method="foo", params=[], id=1).method == "foo" diff --git a/tests/test_response.py b/tests/test_response.py index 7829349..a0da382 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -14,8 +14,6 @@ to_serializable, ) -# pylint: disable=missing-function-docstring,invalid-name,duplicate-code - def test_SuccessResponse() -> None: response = SuccessResponse(sentinel.result, sentinel.id) diff --git a/tests/test_result.py b/tests/test_result.py index 797263c..3cb2f9b 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -13,8 +13,6 @@ ) from jsonrpcserver.sentinels import NODATA -# pylint: disable=missing-function-docstring,invalid-name - def test_SuccessResult() -> None: assert SuccessResult(None).result is None diff --git a/tests/test_sentinels.py b/tests/test_sentinels.py index b1e5ffe..555460a 100644 --- a/tests/test_sentinels.py +++ b/tests/test_sentinels.py @@ -2,8 +2,6 @@ from jsonrpcserver.sentinels import Sentinel -# pylint: disable=missing-function-docstring - def test_sentinel() -> None: assert repr(Sentinel("foo")) == "" diff --git a/tests/test_server.py b/tests/test_server.py index 46c2b51..94753cf 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -4,8 +4,6 @@ from jsonrpcserver.server import serve -# pylint: disable=missing-function-docstring - @patch("jsonrpcserver.server.HTTPServer") def test_serve(*_: Mock) -> None: From 6efc17269b57b0cd31756c751154988c2af7826a Mon Sep 17 00:00:00 2001 From: Beau Date: Wed, 28 Aug 2024 11:19:15 +1000 Subject: [PATCH 31/33] Move request-schema.json to a .py file (#284) --- jsonrpcserver/main.py | 9 +++-- ...{request-schema.json => request_schema.py} | 35 ++++++++----------- 2 files changed, 18 insertions(+), 26 deletions(-) rename jsonrpcserver/{request-schema.json => request_schema.py} (51%) diff --git a/jsonrpcserver/main.py b/jsonrpcserver/main.py index 19070b2..70fdd18 100644 --- a/jsonrpcserver/main.py +++ b/jsonrpcserver/main.py @@ -11,7 +11,6 @@ """ import json -from importlib.resources import read_text from typing import Any, Callable, Dict, List, Union, cast from jsonschema.validators import validator_for # type: ignore @@ -23,6 +22,7 @@ validate_args, ) from .methods import Methods, global_methods +from .request_schema import REQUEST_SCHEMA from .response import Response, to_dict from .sentinels import NOCONTEXT from .utils import identity @@ -32,10 +32,9 @@ # Prepare the jsonschema validator. This is global so it loads only once, not every time # dispatch is called. -schema = json.loads(read_text(__package__, "request-schema.json")) -klass = validator_for(schema) -klass.check_schema(schema) -default_jsonrpc_validator = klass(schema).validate +klass = validator_for(REQUEST_SCHEMA) +klass.check_schema(REQUEST_SCHEMA) +default_jsonrpc_validator = klass(REQUEST_SCHEMA).validate def dispatch_to_response( diff --git a/jsonrpcserver/request-schema.json b/jsonrpcserver/request_schema.py similarity index 51% rename from jsonrpcserver/request-schema.json rename to jsonrpcserver/request_schema.py index 52bb147..fc163e4 100644 --- a/jsonrpcserver/request-schema.json +++ b/jsonrpcserver/request_schema.py @@ -1,39 +1,32 @@ -{ +REQUEST_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "A JSON RPC 2.0 request", "oneOf": [ - { - "description": "An individual request", - "$ref": "#/definitions/request" - }, + {"description": "An individual request", "$ref": "#/definitions/request"}, { "description": "An array of requests", "type": "array", - "items": { "$ref": "#/definitions/request" }, - "minItems": 1 - } + "items": {"$ref": "#/definitions/request"}, + "minItems": 1, + }, ], "definitions": { "request": { "type": "object", - "required": [ "jsonrpc", "method" ], + "required": ["jsonrpc", "method"], "properties": { - "jsonrpc": { "enum": [ "2.0" ] }, - "method": { - "type": "string" - }, + "jsonrpc": {"enum": ["2.0"]}, + "method": {"type": "string"}, "id": { - "type": [ "string", "number", "null" ], + "type": ["string", "number", "null"], "note": [ "While allowed, null should be avoided: http://www.jsonrpc.org/specification#id1", - "While allowed, a number with a fractional part should be avoided: http://www.jsonrpc.org/specification#id2" - ] + "While allowed, a number with a fractional part should be avoided: http://www.jsonrpc.org/specification#id2", + ], }, - "params": { - "type": [ "array", "object" ] - } + "params": {"type": ["array", "object"]}, }, - "additionalProperties": false + "additionalProperties": False, } - } + }, } From 5c745ce6ade5f6772276b66e7e0a575b289dffce Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Fri, 15 Nov 2024 10:26:14 +1100 Subject: [PATCH 32/33] Add jsonschema dependency --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index ffb3378..cd8e31b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ classifiers = [ "Programming Language :: Python :: 3.11" ] description = "Process JSON-RPC requests" +dependencies = ["returns", "jsonschema"] license = {file = "LICENSE"} name = "jsonrpcserver" readme = {file = "README.md", content-type = "text/markdown"} From c2b807e0c7554cc85d133a67bd4878bb7a382ecc Mon Sep 17 00:00:00 2001 From: Beau Barker Date: Sun, 23 Mar 2025 10:33:01 +1100 Subject: [PATCH 33/33] Add license badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 42618e8..a295785 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ ![Code Quality](https://github.com/explodinglabs/jsonrpcserver/actions/workflows/code-quality.yml/badge.svg) ![Coverage Status](https://coveralls.io/repos/github/explodinglabs/jsonrpcserver/badge.svg?branch=main) ![Downloads](https://img.shields.io/pypi/dw/jsonrpcserver) +![License](https://img.shields.io/pypi/l/jsonrpcserver.svg) Process incoming JSON-RPC requests in Python.