jsonrpcserver 5.0.10__tar.gz → 5.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/CHANGELOG.md +16 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/PKG-INFO +3 -3
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/README.md +2 -2
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/RELEASING.md +6 -4
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/errors.md +11 -8
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/methods.md +12 -5
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/migration.md +10 -6
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/roadmap.md +2 -1
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/jsonrpcserver/__init__.py +1 -1
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/jsonrpcserver/async_dispatcher.py +4 -4
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/jsonrpcserver/dispatcher.py +31 -13
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/jsonrpcserver/methods.py +2 -1
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/requirements/examples.txt +3 -3
- jsonrpcserver-5.1.0/requirements/lint.txt +7 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/test_async_dispatcher.py +14 -6
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/test_dispatcher.py +42 -13
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/test_robustness.py +5 -3
- jsonrpcserver-5.0.10/requirements/lint.txt +0 -7
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/CODE_OF_CONDUCT.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/CONTRIBUTING.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/LICENSE +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/SECURITY.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/assets/a11y.js +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/assets/apple-touch-icon.png +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/assets/copy-without-prompts.js +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/assets/extra.css +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/assets/favicon.png +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/assets/logo.png +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/assets/social-card.png +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/async.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/batches.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/changelog.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/context.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/contributing.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/dispatch.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/examples/aiohttp_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/examples/check_examples.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/examples/django_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/examples/fastapi_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/examples/flask_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/examples/http_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/examples/quickstart.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/examples/sanic_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/examples/socketio_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/examples/tornado_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/examples/websockets_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/examples/werkzeug_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/examples/zeromq_async_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/examples/zeromq_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/examples.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/faq.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/frameworks/aiohttp.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/frameworks/django.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/frameworks/fastapi.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/frameworks/flask.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/frameworks/http-server.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/frameworks/sanic.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/frameworks/socketio.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/frameworks/tornado.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/frameworks/websockets.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/frameworks/werkzeug.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/frameworks/zeromq.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/index.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/license.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/reference.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/security-policy.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/security.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/testing.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/threads.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/typing.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/docs/validation.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/jsonrpcserver/async_main.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/jsonrpcserver/codes.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/jsonrpcserver/exceptions.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/jsonrpcserver/main.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/jsonrpcserver/py.typed +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/jsonrpcserver/request-schema.json +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/jsonrpcserver/request.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/jsonrpcserver/response.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/jsonrpcserver/result.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/jsonrpcserver/sentinels.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/jsonrpcserver/server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/jsonrpcserver/utils.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/mkdocs.yml +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/overrides/404.html +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/overrides/main.html +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/pyproject.toml +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/requirements/build.txt +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/requirements/docs-constraints.txt +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/requirements/docs.txt +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/requirements/site.txt +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/requirements/test.txt +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/__init__.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/doc_examples.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/test_async_main.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/test_batch_limit.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/test_docs.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/test_docstrings.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/test_main.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/test_methods.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/test_request.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/test_response.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/test_result.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/test_sentinels.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/test_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/test_threading.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/test_typing.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/test_version.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tests/test_warnings.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.1.0}/tox.ini +0 -0
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.1.0 (2026-11-06)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- A method can return a plain value. `return "pong"` sends the same response as
|
|
8
|
+
`return Success("pong")`, as in 4.x (#285). Before, a plain value gave the
|
|
9
|
+
client an Internal error. `Success` and `Error` work as before.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- A method that returns an oslash `Left` or `Right` not made by `Success` or
|
|
14
|
+
`Error`, or a bare `SuccessResult` or `ErrorResult`, still gets an Internal
|
|
15
|
+
error. Its log line and `InvalidResultError` message now say "Return a value,
|
|
16
|
+
Success(value) or Error(code, message)", and the log line links the Methods
|
|
17
|
+
page.
|
|
18
|
+
|
|
3
19
|
## 5.0.10 (2026-11-06)
|
|
4
20
|
|
|
5
21
|
The first release since the project moved to
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: jsonrpcserver
|
|
3
|
-
Version: 5.0
|
|
3
|
+
Version: 5.1.0
|
|
4
4
|
Summary: Process incoming JSON-RPC 2.0 requests in Python
|
|
5
5
|
Keywords: json-rpc,jsonrpc,json-rpc-2.0,rpc,server
|
|
6
6
|
Author-email: Beau Barker <beau@explodinglabs.com>
|
|
@@ -143,8 +143,8 @@ explains why.
|
|
|
143
143
|
framework examples and the
|
|
144
144
|
[API reference](https://bensynapse.github.io/jsonrpcserver/reference/).
|
|
145
145
|
- [Migrating from 4.x](https://bensynapse.github.io/jsonrpcserver/migration/):
|
|
146
|
-
methods
|
|
147
|
-
|
|
146
|
+
methods return `Success(value)` or `Error(...)`. From 5.1.0 a method can also
|
|
147
|
+
return a plain value, as in 4.x.
|
|
148
148
|
- [Contributing](https://github.com/bensynapse/jsonrpcserver/blob/main/CONTRIBUTING.md)
|
|
149
149
|
and the
|
|
150
150
|
[security policy](https://github.com/bensynapse/jsonrpcserver/blob/main/SECURITY.md).
|
|
@@ -101,8 +101,8 @@ explains why.
|
|
|
101
101
|
framework examples and the
|
|
102
102
|
[API reference](https://bensynapse.github.io/jsonrpcserver/reference/).
|
|
103
103
|
- [Migrating from 4.x](https://bensynapse.github.io/jsonrpcserver/migration/):
|
|
104
|
-
methods
|
|
105
|
-
|
|
104
|
+
methods return `Success(value)` or `Error(...)`. From 5.1.0 a method can also
|
|
105
|
+
return a plain value, as in 4.x.
|
|
106
106
|
- [Contributing](https://github.com/bensynapse/jsonrpcserver/blob/main/CONTRIBUTING.md)
|
|
107
107
|
and the
|
|
108
108
|
[security policy](https://github.com/bensynapse/jsonrpcserver/blob/main/SECURITY.md).
|
|
@@ -35,7 +35,8 @@ Settings → Environments → pypi.
|
|
|
35
35
|
yet": `## 5.0.10 (2026-10-20)`.
|
|
36
36
|
- The `unreleased` and `pypi_version` lines under `extra` in mkdocs.yml are
|
|
37
37
|
deleted. They show the "not on PyPI yet" banner on every docs page.
|
|
38
|
-
|
|
38
|
+
The docs deploy when you push the tag, so merging this changes nothing
|
|
39
|
+
users see until then.
|
|
39
40
|
- The paragraph under "Install" in README.md that says the version isn't
|
|
40
41
|
released yet is deleted. The README becomes the PyPI page.
|
|
41
42
|
- For 5.0.10 only: shorten the "If you are on 5.0.9" section of
|
|
@@ -89,7 +90,8 @@ whose version reached PyPI.
|
|
|
89
90
|
|
|
90
91
|
## Docs
|
|
91
92
|
|
|
92
|
-
The docs site deploys
|
|
93
|
-
so it
|
|
94
|
-
|
|
93
|
+
The docs site deploys from `.github/workflows/docs.yml` when a release tag is
|
|
94
|
+
pushed, so it always describes the version on PyPI. To publish a docs-only fix
|
|
95
|
+
between releases, run the Docs workflow by hand on main. If main is ahead of
|
|
96
|
+
PyPI by then, set `unreleased` in mkdocs.yml first, so a banner says so.
|
|
95
97
|
The docs also mark features with "New in" or "Changed in" notes.
|
|
@@ -153,19 +153,22 @@ logging.basicConfig(
|
|
|
153
153
|
To change only jsonrpcserver's messages, set the level or add a handler on
|
|
154
154
|
the parent logger: `logging.getLogger("jsonrpcserver")`.
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
A method can return a plain value, a `Success` or an `Error`. What it can't
|
|
157
|
+
return is a result object that `Success` or `Error` didn't make, such as a bare
|
|
158
|
+
`SuccessResult`. This is what that looks like in the log:
|
|
158
159
|
|
|
159
160
|
```python
|
|
160
161
|
import sys
|
|
161
162
|
|
|
163
|
+
from jsonrpcserver.result import SuccessResult
|
|
164
|
+
|
|
162
165
|
handler = logging.StreamHandler(sys.stdout)
|
|
163
166
|
handler.setFormatter(logging.Formatter("%(name)s %(levelname)s: %(message)s"))
|
|
164
167
|
logging.getLogger("jsonrpcserver").addHandler(handler)
|
|
165
168
|
|
|
166
169
|
|
|
167
|
-
def ping() ->
|
|
168
|
-
return "pong" # Should be Success("pong")
|
|
170
|
+
def ping() -> SuccessResult:
|
|
171
|
+
return SuccessResult("pong") # Should be Success("pong"), or just "pong"
|
|
169
172
|
|
|
170
173
|
|
|
171
174
|
dispatch('{"jsonrpc": "2.0", "method": "ping", "id": 1}', {"ping": ping})
|
|
@@ -173,11 +176,11 @@ logging.getLogger("jsonrpcserver").removeHandler(handler)
|
|
|
173
176
|
```
|
|
174
177
|
|
|
175
178
|
```text title="Output"
|
|
176
|
-
jsonrpcserver.dispatcher ERROR: Method 'ping' returned 'pong', which is not a Result, so the client got an Internal error. Return Success(value) or Error(code, message)
|
|
179
|
+
jsonrpcserver.dispatcher ERROR: Method 'ping' returned SuccessResult('pong'), which is not a valid Result, so the client got an Internal error. Return a value, Success(value) or Error(code, message): https://bensynapse.github.io/jsonrpcserver/methods/
|
|
177
180
|
```
|
|
178
181
|
|
|
179
|
-
!!! info "New in 5.0.10"
|
|
182
|
+
!!! info "New in 5.0.10, changed in 5.1.0"
|
|
180
183
|
The `jsonrpcserver.main` messages and this one. Before 5.0.10, a
|
|
181
|
-
serializer failure raised out of `dispatch`.
|
|
184
|
+
serializer failure raised out of `dispatch`. An invalid return value was
|
|
182
185
|
logged with a traceback, and its explanation was also sent to the client
|
|
183
|
-
in `data`.
|
|
186
|
+
in `data`. Before 5.1.0, a plain return value was invalid too.
|
|
@@ -47,11 +47,18 @@ A method returns `Success` or `Error`. They are the `result` and `error` parts
|
|
|
47
47
|
of a [JSON-RPC response](https://www.jsonrpc.org/specification#response_object).
|
|
48
48
|
jsonrpcserver adds the `jsonrpc` and `id` parts.
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
A method can also return its value directly. `return "pong"` sends the same
|
|
51
|
+
response as `return Success("pong")`:
|
|
52
|
+
|
|
53
|
+
```pycon
|
|
54
|
+
>>> dispatch('{"jsonrpc": "2.0", "method": "count", "id": 1}', {"count": lambda: 3})
|
|
55
|
+
'{"jsonrpc": "2.0", "result": 3, "id": 1}'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
!!! info "New in 5.1.0"
|
|
59
|
+
Plain return values. From 5.0.0 to 5.0.10, `return "pong"` sent the client
|
|
60
|
+
a -32603 "Internal error". If your code has to run on those versions too,
|
|
61
|
+
return `Success(value)`. See [Migration](migration.md).
|
|
55
62
|
|
|
56
63
|
`Success` takes the result value. If there's nothing to return, call it with
|
|
57
64
|
no argument and the result is `null`:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Upgrade jsonrpcserver from 4.x to 5.x,
|
|
2
|
+
description: Upgrade jsonrpcserver from 4.x to 5.x, from 5.0.9 to 5.0.10, and to 5.1.0. Side-by-side examples, removed options, and plain return values, which work again from 5.1.0.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# Migration
|
|
@@ -9,9 +9,10 @@ description: Upgrade jsonrpcserver from 4.x to 5.x, and from 5.0.9 to 5.0.10. Si
|
|
|
9
9
|
Version 5 changed how methods report their result. Most other code needs only
|
|
10
10
|
small changes.
|
|
11
11
|
|
|
12
|
-
!!! danger "Old methods fail with no details"
|
|
13
|
-
In 4.x a method returned its result directly. In 5.x it must return
|
|
14
|
-
`Success(result)`.
|
|
12
|
+
!!! danger "Old methods fail with no details before 5.1.0"
|
|
13
|
+
In 4.x a method returned its result directly. In 5.0.x it must return
|
|
14
|
+
`Success(result)`. From 5.1.0 a plain return value works again, and
|
|
15
|
+
becomes `Success(value)`. On 5.0.x, a 4.x method such as
|
|
15
16
|
|
|
16
17
|
<!-- skip: 4.x code -->
|
|
17
18
|
```python
|
|
@@ -28,6 +29,9 @@ small changes.
|
|
|
28
29
|
Method 'ping' returned 'pong', which is not a Result, so the client got an Internal error. Return Success(value) or Error(code, message). ...
|
|
29
30
|
```
|
|
30
31
|
|
|
32
|
+
That's the 5.0.10 message. Upgrading to 5.1.0 fixes these methods
|
|
33
|
+
without a code change.
|
|
34
|
+
|
|
31
35
|
If you don't see that line, configure logging (see
|
|
32
36
|
[Errors and logging](errors.md#logging)). Then search your code for
|
|
33
37
|
`return` statements in methods. mypy and pyright catch them too, once
|
|
@@ -85,7 +89,7 @@ print(response, 200 if response else 204)
|
|
|
85
89
|
|
|
86
90
|
| 4.x | 5.x |
|
|
87
91
|
|---|---|
|
|
88
|
-
| `return value` | `return Success(value)` |
|
|
92
|
+
| `return value` | `return Success(value)`. From 5.1.0 `return value` works too |
|
|
89
93
|
| `raise ApiError(message, code=1, data=...)` | `return Error(code, message, data)`, or `raise JsonRpcError(code, message, data)`. Note the order: code first. |
|
|
90
94
|
| `raise InvalidParamsError(...)` | `return InvalidParams(data)` |
|
|
91
95
|
| `raise MethodNotFoundError` | there's no equivalent. jsonrpcserver sends "Method not found" itself |
|
|
@@ -103,7 +107,7 @@ print(response, 200 if response else 204)
|
|
|
103
107
|
Code written for 4.x that 5.x can't run gives clear errors in most cases. The
|
|
104
108
|
removed keywords raise `TypeError`, and `response.wanted` raises
|
|
105
109
|
`AttributeError: 'str' object has no attribute 'wanted'`. The plain return
|
|
106
|
-
value above is the one that fails quietly, and `str(response)` still works but
|
|
110
|
+
value above is the one that fails quietly before 5.1.0, and `str(response)` still works but
|
|
107
111
|
is no longer needed.
|
|
108
112
|
|
|
109
113
|
The 4.x documentation is no longer online. The [changelog](changelog.md)
|
|
@@ -6,6 +6,8 @@ description: The plan for jsonrpcserver 6.0. Results without oslash, safer defau
|
|
|
6
6
|
|
|
7
7
|
This is the plan for the next major version. Nothing here is released yet, and
|
|
8
8
|
it may change. 5.x keeps its current API, and gets bug and security fixes.
|
|
9
|
+
5.1.0 took one idea from this list early: a method can return a plain value
|
|
10
|
+
(#285).
|
|
9
11
|
|
|
10
12
|
Discussion happens in the
|
|
11
13
|
[issues](https://github.com/bensynapse/jsonrpcserver/issues) and
|
|
@@ -51,7 +53,6 @@ five request fields would be faster and lighter.
|
|
|
51
53
|
|
|
52
54
|
**Ideas under discussion.**
|
|
53
55
|
|
|
54
|
-
- Let a method return a plain value instead of `Success(value)` (#285).
|
|
55
56
|
- Middleware around method calls (#126). Decorators on your own functions
|
|
56
57
|
already cover most uses.
|
|
57
58
|
- OpenRPC schema generation (#156), probably as a separate package.
|
|
@@ -25,9 +25,9 @@ from .dispatcher import (
|
|
|
25
25
|
member_id,
|
|
26
26
|
not_notification,
|
|
27
27
|
to_response,
|
|
28
|
+
to_result,
|
|
28
29
|
validate_args,
|
|
29
30
|
validate_request,
|
|
30
|
-
validate_result,
|
|
31
31
|
)
|
|
32
32
|
from .exceptions import JsonRpcError
|
|
33
33
|
from .methods import AnyMethod, MethodsArgument
|
|
@@ -51,7 +51,8 @@ async def call(
|
|
|
51
51
|
# Plain (sync) methods work too. Their result is used as it is.
|
|
52
52
|
if isawaitable(result):
|
|
53
53
|
result = await result
|
|
54
|
-
|
|
54
|
+
# A plain value becomes Success(value) (5.1.0).
|
|
55
|
+
result = to_result(result)
|
|
55
56
|
except JsonRpcError as exc:
|
|
56
57
|
return Left(ErrorResult(code=exc.code, message=exc.message, data=exc.data))
|
|
57
58
|
except InvalidResultError as exc:
|
|
@@ -61,8 +62,7 @@ async def call(
|
|
|
61
62
|
# Other error inside method - Internal error
|
|
62
63
|
logger.exception("Method %r raised an exception", request.method)
|
|
63
64
|
return Left(InternalErrorResult(exception_data(exc, debug)))
|
|
64
|
-
|
|
65
|
-
return cast(Result, result)
|
|
65
|
+
return result
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
async def dispatch_request(
|
|
@@ -32,6 +32,7 @@ from .result import (
|
|
|
32
32
|
InvalidParamsResult,
|
|
33
33
|
MethodNotFoundResult,
|
|
34
34
|
Result,
|
|
35
|
+
Success,
|
|
35
36
|
SuccessResult,
|
|
36
37
|
)
|
|
37
38
|
from .sentinels import NOCONTEXT, NODATA, NOID, Sentinel
|
|
@@ -114,12 +115,13 @@ def extract_kwargs(request: Request) -> Dict[str, Any]:
|
|
|
114
115
|
|
|
115
116
|
|
|
116
117
|
class InvalidResultError(AssertionError):
|
|
117
|
-
"""A method returned
|
|
118
|
+
"""A method returned an oslash `Left` or `Right`, or a result object, that isn't a
|
|
119
|
+
Result made by Success(...) or Error(...)."""
|
|
118
120
|
|
|
119
121
|
def __init__(self, result: object):
|
|
120
122
|
super().__init__(
|
|
121
123
|
f"The method did not return a valid Result (returned {result!r}). "
|
|
122
|
-
"Return Success(value) or Error(code, message)."
|
|
124
|
+
"Return a value, Success(value) or Error(code, message)."
|
|
123
125
|
)
|
|
124
126
|
self.result = result
|
|
125
127
|
|
|
@@ -143,19 +145,36 @@ def validate_result(result: object) -> None:
|
|
|
143
145
|
raise InvalidResultError(returned)
|
|
144
146
|
|
|
145
147
|
|
|
148
|
+
def to_result(result: object) -> Result:
|
|
149
|
+
"""The Result for what a method returned.
|
|
150
|
+
|
|
151
|
+
A plain value becomes `Success(value)`, so `return "pong"` and
|
|
152
|
+
`return Success("pong")` send the same response. A Result made by `Success` or
|
|
153
|
+
`Error` is used as it is. An oslash `Left` or `Right` that holds something else,
|
|
154
|
+
or a bare `SuccessResult` or `ErrorResult`, raises InvalidResultError, because
|
|
155
|
+
it is nearly always a mistake, not a value meant for the client.
|
|
156
|
+
|
|
157
|
+
New in 5.1.0. Before it, any plain value gave the client an Internal error.
|
|
158
|
+
"""
|
|
159
|
+
returned = result # Not narrowed: oslash's Left and Right are generic.
|
|
160
|
+
if isinstance(result, (Left, Right, SuccessResult, ErrorResult)):
|
|
161
|
+
validate_result(returned)
|
|
162
|
+
return cast(Result, returned)
|
|
163
|
+
return Success(result)
|
|
164
|
+
|
|
165
|
+
|
|
146
166
|
def log_invalid_result(
|
|
147
167
|
method_name: str, exc: InvalidResultError, log: logging.Logger
|
|
148
168
|
) -> None:
|
|
149
|
-
"""Log a method that returned
|
|
169
|
+
"""Log a method that returned an invalid Result, without a traceback.
|
|
150
170
|
|
|
151
171
|
The traceback would only point into jsonrpcserver, so the message says what to
|
|
152
172
|
change instead.
|
|
153
173
|
"""
|
|
154
174
|
log.error(
|
|
155
|
-
"Method %r returned %s, which is not a Result, so the client got an "
|
|
156
|
-
"Internal error. Return Success(value) or Error(code, message)
|
|
157
|
-
"
|
|
158
|
-
"https://bensynapse.github.io/jsonrpcserver/migration/",
|
|
175
|
+
"Method %r returned %s, which is not a valid Result, so the client got an "
|
|
176
|
+
"Internal error. Return a value, Success(value) or Error(code, message): "
|
|
177
|
+
"https://bensynapse.github.io/jsonrpcserver/methods/",
|
|
159
178
|
method_name,
|
|
160
179
|
reprlib.repr(exc.result),
|
|
161
180
|
)
|
|
@@ -188,10 +207,10 @@ def call(
|
|
|
188
207
|
f"Method {request.method!r} is async. Use async_dispatch to call "
|
|
189
208
|
"async methods."
|
|
190
209
|
)
|
|
191
|
-
#
|
|
192
|
-
#
|
|
193
|
-
# method.
|
|
194
|
-
|
|
210
|
+
# A plain value becomes Success(value). to_result raises InvalidResultError,
|
|
211
|
+
# an AssertionError, for an invalid Result, which gets an Internal error
|
|
212
|
+
# because it's a problem in the method.
|
|
213
|
+
result = to_result(result)
|
|
195
214
|
# Raising JsonRpcError inside the method is an alternative way of returning an error
|
|
196
215
|
# response.
|
|
197
216
|
except JsonRpcError as exc:
|
|
@@ -203,8 +222,7 @@ def call(
|
|
|
203
222
|
except Exception as exc:
|
|
204
223
|
logger.exception("Method %r raised an exception", request.method)
|
|
205
224
|
return Left(InternalErrorResult(exception_data(exc, debug)))
|
|
206
|
-
|
|
207
|
-
return cast(Result, result)
|
|
225
|
+
return result
|
|
208
226
|
|
|
209
227
|
|
|
210
228
|
def validate_args(
|
|
@@ -7,7 +7,8 @@ your own dict instead, pass it as the methods argument:
|
|
|
7
7
|
dispatch(request) # the functions registered with @method
|
|
8
8
|
dispatch(request, methods={"ping": ping}) # only the functions in this dict
|
|
9
9
|
|
|
10
|
-
Either way, a method returns Success(...) or Error(...)
|
|
10
|
+
Either way, a method returns Success(...) or Error(...). From 5.1.0 it can also
|
|
11
|
+
return a plain value, which becomes Success(value).
|
|
11
12
|
|
|
12
13
|
A request's params are either a list (positional arguments) or an object (named
|
|
13
14
|
arguments), not both. That's a JSON-RPC rule.
|
|
@@ -3,19 +3,19 @@ aiohttp==3.14.5
|
|
|
3
3
|
django==6.1.2
|
|
4
4
|
fastapi==0.162.0
|
|
5
5
|
flask==3.1.3
|
|
6
|
-
flask-socketio==5.7.
|
|
6
|
+
flask-socketio==5.7.2
|
|
7
7
|
# FastAPI's test client, used on the Context page.
|
|
8
8
|
httpx2==2.16.0
|
|
9
9
|
# The quickstart calls the server with jsonrpcclient and requests.
|
|
10
10
|
jsonrpcclient==4.0.3
|
|
11
11
|
# The FAQ's orjson example.
|
|
12
12
|
orjson==3.13.0
|
|
13
|
-
python-socketio[client]==5.
|
|
13
|
+
python-socketio[client]==5.18.0
|
|
14
14
|
pyzmq==27.2.0
|
|
15
15
|
requests==2.34.2
|
|
16
16
|
sanic==25.12.1
|
|
17
17
|
tornado==6.6
|
|
18
18
|
ujson==6.0.0
|
|
19
19
|
uvicorn==0.54.0
|
|
20
|
-
websockets==17.
|
|
20
|
+
websockets==17.4
|
|
21
21
|
werkzeug==3.1.9
|
|
@@ -95,21 +95,29 @@ async def test_call_raising_exception_is_logged(
|
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
@pytest.mark.asyncio
|
|
98
|
-
async def
|
|
99
|
-
caplog: pytest.LogCaptureFixture,
|
|
100
|
-
) -> None:
|
|
98
|
+
async def test_plain_return_value_becomes_success() -> None:
|
|
101
99
|
async def method() -> str:
|
|
102
100
|
return "pong"
|
|
103
101
|
|
|
102
|
+
assert await call(Request("ping", [], 1), NOCONTEXT, method) == Right(
|
|
103
|
+
SuccessResult("pong")
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.asyncio
|
|
108
|
+
async def test_invalid_result_is_logged_with_a_hint(
|
|
109
|
+
caplog: pytest.LogCaptureFixture,
|
|
110
|
+
) -> None:
|
|
111
|
+
async def method() -> object:
|
|
112
|
+
return Right[str, Any]("pong")
|
|
113
|
+
|
|
104
114
|
assert await call(Request("ping", [], 1), NOCONTEXT, method) == Left(
|
|
105
115
|
ErrorResult(ERROR_INTERNAL_ERROR, "Internal error")
|
|
106
116
|
)
|
|
107
117
|
(record,) = caplog.records
|
|
108
118
|
assert record.name == "jsonrpcserver.async_dispatcher"
|
|
109
119
|
assert record.exc_info is None
|
|
110
|
-
assert record.getMessage()
|
|
111
|
-
"Method 'ping' returned 'pong', which is not a Result"
|
|
112
|
-
)
|
|
120
|
+
assert "which is not a valid Result" in record.getMessage()
|
|
113
121
|
|
|
114
122
|
|
|
115
123
|
@pytest.mark.asyncio
|
|
@@ -49,6 +49,7 @@ from jsonrpcserver.response import (
|
|
|
49
49
|
)
|
|
50
50
|
from jsonrpcserver.result import (
|
|
51
51
|
ErrorResult,
|
|
52
|
+
InternalErrorResult,
|
|
52
53
|
InvalidParams,
|
|
53
54
|
Result,
|
|
54
55
|
Success,
|
|
@@ -512,10 +513,11 @@ def test_dispatch_to_response_pure_server_error(
|
|
|
512
513
|
|
|
513
514
|
|
|
514
515
|
def test_dispatch_to_response_pure_invalid_result() -> None:
|
|
515
|
-
"""
|
|
516
|
+
"""A bare SuccessResult, not wrapped by Success(), gets an Internal Error
|
|
517
|
+
response. A plain value is fine from 5.1.0."""
|
|
516
518
|
|
|
517
|
-
def not_a_result() ->
|
|
518
|
-
return None
|
|
519
|
+
def not_a_result() -> object:
|
|
520
|
+
return SuccessResult(None)
|
|
519
521
|
|
|
520
522
|
assert dispatch_to_response_pure(
|
|
521
523
|
deserializer=default_deserializer,
|
|
@@ -529,21 +531,36 @@ def test_dispatch_to_response_pure_invalid_result() -> None:
|
|
|
529
531
|
ErrorResponse(
|
|
530
532
|
ERROR_INTERNAL_ERROR,
|
|
531
533
|
"Internal error",
|
|
532
|
-
"The method did not return a valid Result
|
|
533
|
-
"
|
|
534
|
+
"The method did not return a valid Result "
|
|
535
|
+
"(returned SuccessResult(None)). "
|
|
536
|
+
"Return a value, Success(value) or Error(code, message).",
|
|
534
537
|
1,
|
|
535
538
|
)
|
|
536
539
|
)
|
|
537
540
|
|
|
538
541
|
|
|
539
|
-
|
|
542
|
+
@pytest.mark.parametrize("value", ["pong", None, 0, [1, 2], {"a": 1}, False])
|
|
543
|
+
def test_plain_return_value_becomes_success(value: object) -> None:
|
|
544
|
+
"""A method can return its value directly (5.1.0, #285)."""
|
|
545
|
+
response = dispatch_to_response_pure(
|
|
546
|
+
deserializer=default_deserializer,
|
|
547
|
+
validator=default_validator,
|
|
548
|
+
post_process=identity,
|
|
549
|
+
context=NOCONTEXT,
|
|
550
|
+
methods={"ping": lambda: value},
|
|
551
|
+
request='{"jsonrpc": "2.0", "method": "ping", "id": 1}',
|
|
552
|
+
)
|
|
553
|
+
assert response == Right(SuccessResponse(value, 1))
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def test_invalid_result_is_logged_with_a_hint(
|
|
540
557
|
caplog: pytest.LogCaptureFixture,
|
|
541
558
|
) -> None:
|
|
542
|
-
"""
|
|
543
|
-
not a traceback into jsonrpcserver."""
|
|
559
|
+
"""An oslash Right that doesn't hold a SuccessResult is a mistake, not a value. It
|
|
560
|
+
gets a log line saying what to do, not a traceback into jsonrpcserver."""
|
|
544
561
|
|
|
545
|
-
def ping() ->
|
|
546
|
-
return "pong"
|
|
562
|
+
def ping() -> object:
|
|
563
|
+
return Right[str, Any]("pong")
|
|
547
564
|
|
|
548
565
|
response = dispatch_to_response_pure(
|
|
549
566
|
deserializer=default_deserializer,
|
|
@@ -559,9 +576,21 @@ def test_plain_return_value_is_logged_with_a_hint(
|
|
|
559
576
|
(record,) = caplog.records
|
|
560
577
|
assert record.levelname == "ERROR"
|
|
561
578
|
assert record.exc_info is None
|
|
562
|
-
assert
|
|
563
|
-
|
|
564
|
-
|
|
579
|
+
assert "which is not a valid Result, so the client got an Internal error. " in (
|
|
580
|
+
record.getMessage()
|
|
581
|
+
)
|
|
582
|
+
assert "Return a value, Success(value) or Error(code, message)" in (
|
|
583
|
+
record.getMessage()
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
@pytest.mark.parametrize(
|
|
588
|
+
"returned",
|
|
589
|
+
[SuccessResult("pong"), ErrorResult(1, "x"), Left[Any, str]("oops")],
|
|
590
|
+
)
|
|
591
|
+
def test_bare_result_objects_are_invalid(returned: object) -> None:
|
|
592
|
+
assert call(Request("ping", [], 1), NOCONTEXT, lambda: returned) == Left(
|
|
593
|
+
InternalErrorResult(NODATA)
|
|
565
594
|
)
|
|
566
595
|
|
|
567
596
|
|
|
@@ -158,10 +158,11 @@ def test_serializer_that_always_fails() -> None:
|
|
|
158
158
|
|
|
159
159
|
|
|
160
160
|
def test_builtin_without_signature() -> None:
|
|
161
|
-
"""max has no signature inspect can read. It used to collapse the batch.
|
|
161
|
+
"""max has no signature inspect can read. It used to collapse the batch. From
|
|
162
|
+
5.1.0 its plain return value is the result."""
|
|
162
163
|
batch = [{**request("max", 1), "params": [1, 2]}, request("ping", 2)]
|
|
163
164
|
assert json.loads(dispatch(json.dumps(batch), METHODS)) == [
|
|
164
|
-
{"jsonrpc": "2.0", "
|
|
165
|
+
{"jsonrpc": "2.0", "result": 2, "id": 1},
|
|
165
166
|
{"jsonrpc": "2.0", "result": "pong", "id": 2},
|
|
166
167
|
]
|
|
167
168
|
|
|
@@ -290,9 +291,10 @@ def test_invalid_result_under_optimize_flag() -> None:
|
|
|
290
291
|
"import json, logging\n"
|
|
291
292
|
"logging.disable(logging.CRITICAL)\n"
|
|
292
293
|
"from jsonrpcserver import dispatch, Success\n"
|
|
294
|
+
"from oslash.either import Right\n"
|
|
293
295
|
"batch = [{'jsonrpc': '2.0', 'method': 'none', 'id': 1},"
|
|
294
296
|
" {'jsonrpc': '2.0', 'method': 'ping', 'id': 2}]\n"
|
|
295
|
-
"methods = {'none': lambda: None, 'ping': lambda: Success('pong')}\n"
|
|
297
|
+
"methods = {'none': lambda: Right(None), 'ping': lambda: Success('pong')}\n"
|
|
296
298
|
"print(dispatch(json.dumps(batch), methods))\n"
|
|
297
299
|
)
|
|
298
300
|
output = subprocess.run(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|