jsonrpcserver 5.0.10__tar.gz → 5.2.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.2.0}/CHANGELOG.md +34 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/PKG-INFO +3 -3
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/README.md +2 -2
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/RELEASING.md +6 -4
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/errors.md +11 -8
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/methods.md +12 -5
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/migration.md +10 -6
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/roadmap.md +2 -1
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/jsonrpcserver/__init__.py +1 -1
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/jsonrpcserver/async_dispatcher.py +6 -4
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/jsonrpcserver/dispatcher.py +44 -13
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/jsonrpcserver/main.py +39 -2
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/jsonrpcserver/methods.py +2 -1
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/jsonrpcserver/server.py +17 -5
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/requirements/examples.txt +3 -3
- jsonrpcserver-5.2.0/requirements/lint.txt +7 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/test_async_dispatcher.py +14 -6
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/test_batch_limit.py +3 -1
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/test_dispatcher.py +42 -13
- jsonrpcserver-5.2.0/tests/test_future_warnings.py +81 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/test_robustness.py +9 -4
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/test_server.py +3 -2
- jsonrpcserver-5.0.10/requirements/lint.txt +0 -7
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/CODE_OF_CONDUCT.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/CONTRIBUTING.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/LICENSE +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/SECURITY.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/assets/a11y.js +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/assets/apple-touch-icon.png +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/assets/copy-without-prompts.js +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/assets/extra.css +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/assets/favicon.png +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/assets/logo.png +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/assets/social-card.png +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/async.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/batches.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/changelog.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/context.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/contributing.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/dispatch.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/examples/aiohttp_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/examples/check_examples.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/examples/django_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/examples/fastapi_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/examples/flask_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/examples/http_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/examples/quickstart.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/examples/sanic_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/examples/socketio_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/examples/tornado_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/examples/websockets_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/examples/werkzeug_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/examples/zeromq_async_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/examples/zeromq_server.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/examples.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/faq.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/frameworks/aiohttp.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/frameworks/django.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/frameworks/fastapi.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/frameworks/flask.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/frameworks/http-server.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/frameworks/sanic.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/frameworks/socketio.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/frameworks/tornado.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/frameworks/websockets.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/frameworks/werkzeug.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/frameworks/zeromq.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/index.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/license.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/reference.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/security-policy.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/security.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/testing.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/threads.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/typing.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/docs/validation.md +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/jsonrpcserver/async_main.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/jsonrpcserver/codes.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/jsonrpcserver/exceptions.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/jsonrpcserver/py.typed +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/jsonrpcserver/request-schema.json +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/jsonrpcserver/request.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/jsonrpcserver/response.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/jsonrpcserver/result.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/jsonrpcserver/sentinels.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/jsonrpcserver/utils.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/mkdocs.yml +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/overrides/404.html +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/overrides/main.html +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/pyproject.toml +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/requirements/build.txt +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/requirements/docs-constraints.txt +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/requirements/docs.txt +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/requirements/site.txt +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/requirements/test.txt +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/__init__.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/doc_examples.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/test_async_main.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/test_docs.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/test_docstrings.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/test_main.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/test_methods.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/test_request.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/test_response.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/test_result.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/test_sentinels.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/test_threading.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/test_typing.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/test_version.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tests/test_warnings.py +0 -0
- {jsonrpcserver-5.0.10 → jsonrpcserver-5.2.0}/tox.ini +0 -0
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.2.0 (2026-11-06)
|
|
4
|
+
|
|
5
|
+
Warnings about what 6.0 changes, so an upgrade doesn't surprise you. Nothing
|
|
6
|
+
else changes. Each is a `FutureWarning`, which Python shows by default.
|
|
7
|
+
|
|
8
|
+
- `serve()` without a host warns. From 6.0 it listens on localhost only.
|
|
9
|
+
`serve("", port)` keeps every network interface, quietly.
|
|
10
|
+
- A request with `NaN`, `Infinity`, `-Infinity` or a number too big for a float
|
|
11
|
+
warns. 6.0 rejects them with a Parse error. `deserializer=json.loads` accepts
|
|
12
|
+
them quietly. `dispatch_to_response` now uses the same default deserializer
|
|
13
|
+
as the other dispatch functions.
|
|
14
|
+
- A batch of more than 100 requests without `max_batch_size` warns. 6.0 limits
|
|
15
|
+
batches to 100 by default. Passing `max_batch_size` turns the warning off.
|
|
16
|
+
|
|
17
|
+
The [6.0 plan](https://github.com/bensynapse/jsonrpcserver/blob/main/plans/6.0.md)
|
|
18
|
+
lists everything else that changes, such as results and responses without
|
|
19
|
+
oslash.
|
|
20
|
+
|
|
21
|
+
## 5.1.0 (2026-11-06)
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- A method can return a plain value. `return "pong"` sends the same response as
|
|
26
|
+
`return Success("pong")`, as in 4.x (#285). Before, a plain value gave the
|
|
27
|
+
client an Internal error. `Success` and `Error` work as before.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- A method that returns an oslash `Left` or `Right` not made by `Success` or
|
|
32
|
+
`Error`, or a bare `SuccessResult` or `ErrorResult`, still gets an Internal
|
|
33
|
+
error. Its log line and `InvalidResultError` message now say "Return a value,
|
|
34
|
+
Success(value) or Error(code, message)", and the log line links the Methods
|
|
35
|
+
page.
|
|
36
|
+
|
|
3
37
|
## 5.0.10 (2026-11-06)
|
|
4
38
|
|
|
5
39
|
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.2.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,10 @@ 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
|
-
|
|
31
|
+
warn_if_over_6_0_batch_limit,
|
|
31
32
|
)
|
|
32
33
|
from .exceptions import JsonRpcError
|
|
33
34
|
from .methods import AnyMethod, MethodsArgument
|
|
@@ -51,7 +52,8 @@ async def call(
|
|
|
51
52
|
# Plain (sync) methods work too. Their result is used as it is.
|
|
52
53
|
if isawaitable(result):
|
|
53
54
|
result = await result
|
|
54
|
-
|
|
55
|
+
# A plain value becomes Success(value) (5.1.0).
|
|
56
|
+
result = to_result(result)
|
|
55
57
|
except JsonRpcError as exc:
|
|
56
58
|
return Left(ErrorResult(code=exc.code, message=exc.message, data=exc.data))
|
|
57
59
|
except InvalidResultError as exc:
|
|
@@ -61,8 +63,7 @@ async def call(
|
|
|
61
63
|
# Other error inside method - Internal error
|
|
62
64
|
logger.exception("Method %r raised an exception", request.method)
|
|
63
65
|
return Left(InternalErrorResult(exception_data(exc, debug)))
|
|
64
|
-
|
|
65
|
-
return cast(Result, result)
|
|
66
|
+
return result
|
|
66
67
|
|
|
67
68
|
|
|
68
69
|
async def dispatch_request(
|
|
@@ -161,6 +162,7 @@ async def dispatch_to_response_pure(
|
|
|
161
162
|
return post_process(Left(parsed._error))
|
|
162
163
|
deserialized = unwrap(parsed)
|
|
163
164
|
if isinstance(deserialized, list) and deserialized:
|
|
165
|
+
warn_if_over_6_0_batch_limit(len(deserialized), max_batch_size)
|
|
164
166
|
if max_batch_size is not None and len(deserialized) > max_batch_size:
|
|
165
167
|
return post_process(
|
|
166
168
|
Left(BatchTooLargeResponse(len(deserialized), max_batch_size))
|
|
@@ -4,6 +4,7 @@ requests, providing responses.
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
import reprlib
|
|
7
|
+
import warnings
|
|
7
8
|
from functools import partial
|
|
8
9
|
from inspect import iscoroutine, signature
|
|
9
10
|
from itertools import starmap
|
|
@@ -32,6 +33,7 @@ from .result import (
|
|
|
32
33
|
InvalidParamsResult,
|
|
33
34
|
MethodNotFoundResult,
|
|
34
35
|
Result,
|
|
36
|
+
Success,
|
|
35
37
|
SuccessResult,
|
|
36
38
|
)
|
|
37
39
|
from .sentinels import NOCONTEXT, NODATA, NOID, Sentinel
|
|
@@ -114,12 +116,13 @@ def extract_kwargs(request: Request) -> Dict[str, Any]:
|
|
|
114
116
|
|
|
115
117
|
|
|
116
118
|
class InvalidResultError(AssertionError):
|
|
117
|
-
"""A method returned
|
|
119
|
+
"""A method returned an oslash `Left` or `Right`, or a result object, that isn't a
|
|
120
|
+
Result made by Success(...) or Error(...)."""
|
|
118
121
|
|
|
119
122
|
def __init__(self, result: object):
|
|
120
123
|
super().__init__(
|
|
121
124
|
f"The method did not return a valid Result (returned {result!r}). "
|
|
122
|
-
"Return Success(value) or Error(code, message)."
|
|
125
|
+
"Return a value, Success(value) or Error(code, message)."
|
|
123
126
|
)
|
|
124
127
|
self.result = result
|
|
125
128
|
|
|
@@ -143,19 +146,36 @@ def validate_result(result: object) -> None:
|
|
|
143
146
|
raise InvalidResultError(returned)
|
|
144
147
|
|
|
145
148
|
|
|
149
|
+
def to_result(result: object) -> Result:
|
|
150
|
+
"""The Result for what a method returned.
|
|
151
|
+
|
|
152
|
+
A plain value becomes `Success(value)`, so `return "pong"` and
|
|
153
|
+
`return Success("pong")` send the same response. A Result made by `Success` or
|
|
154
|
+
`Error` is used as it is. An oslash `Left` or `Right` that holds something else,
|
|
155
|
+
or a bare `SuccessResult` or `ErrorResult`, raises InvalidResultError, because
|
|
156
|
+
it is nearly always a mistake, not a value meant for the client.
|
|
157
|
+
|
|
158
|
+
New in 5.1.0. Before it, any plain value gave the client an Internal error.
|
|
159
|
+
"""
|
|
160
|
+
returned = result # Not narrowed: oslash's Left and Right are generic.
|
|
161
|
+
if isinstance(result, (Left, Right, SuccessResult, ErrorResult)):
|
|
162
|
+
validate_result(returned)
|
|
163
|
+
return cast(Result, returned)
|
|
164
|
+
return Success(result)
|
|
165
|
+
|
|
166
|
+
|
|
146
167
|
def log_invalid_result(
|
|
147
168
|
method_name: str, exc: InvalidResultError, log: logging.Logger
|
|
148
169
|
) -> None:
|
|
149
|
-
"""Log a method that returned
|
|
170
|
+
"""Log a method that returned an invalid Result, without a traceback.
|
|
150
171
|
|
|
151
172
|
The traceback would only point into jsonrpcserver, so the message says what to
|
|
152
173
|
change instead.
|
|
153
174
|
"""
|
|
154
175
|
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/",
|
|
176
|
+
"Method %r returned %s, which is not a valid Result, so the client got an "
|
|
177
|
+
"Internal error. Return a value, Success(value) or Error(code, message): "
|
|
178
|
+
"https://bensynapse.github.io/jsonrpcserver/methods/",
|
|
159
179
|
method_name,
|
|
160
180
|
reprlib.repr(exc.result),
|
|
161
181
|
)
|
|
@@ -188,10 +208,10 @@ def call(
|
|
|
188
208
|
f"Method {request.method!r} is async. Use async_dispatch to call "
|
|
189
209
|
"async methods."
|
|
190
210
|
)
|
|
191
|
-
#
|
|
192
|
-
#
|
|
193
|
-
# method.
|
|
194
|
-
|
|
211
|
+
# A plain value becomes Success(value). to_result raises InvalidResultError,
|
|
212
|
+
# an AssertionError, for an invalid Result, which gets an Internal error
|
|
213
|
+
# because it's a problem in the method.
|
|
214
|
+
result = to_result(result)
|
|
195
215
|
# Raising JsonRpcError inside the method is an alternative way of returning an error
|
|
196
216
|
# response.
|
|
197
217
|
except JsonRpcError as exc:
|
|
@@ -203,8 +223,7 @@ def call(
|
|
|
203
223
|
except Exception as exc:
|
|
204
224
|
logger.exception("Method %r raised an exception", request.method)
|
|
205
225
|
return Left(InternalErrorResult(exception_data(exc, debug)))
|
|
206
|
-
|
|
207
|
-
return cast(Result, result)
|
|
226
|
+
return result
|
|
208
227
|
|
|
209
228
|
|
|
210
229
|
def validate_args(
|
|
@@ -369,6 +388,17 @@ def check_max_batch_size(max_batch_size: object) -> None:
|
|
|
369
388
|
)
|
|
370
389
|
|
|
371
390
|
|
|
391
|
+
def warn_if_over_6_0_batch_limit(size: int, max_batch_size: Optional[int]) -> None:
|
|
392
|
+
"""Warn when a batch would get an error under 6.0's default limit of 100."""
|
|
393
|
+
if max_batch_size is None and size > 100:
|
|
394
|
+
warnings.warn(
|
|
395
|
+
f"This batch has {size} requests. 6.0 limits batches to 100 by default. "
|
|
396
|
+
"Pass max_batch_size=None to keep no limit, or set the limit you want.",
|
|
397
|
+
FutureWarning,
|
|
398
|
+
stacklevel=2,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
372
402
|
def member_id(member: Any) -> Any:
|
|
373
403
|
"""The id of a batch member, or None if it has none we can use.
|
|
374
404
|
|
|
@@ -435,6 +465,7 @@ def dispatch_to_response_pure(
|
|
|
435
465
|
return post_process(Left(parsed._error))
|
|
436
466
|
deserialized = unwrap(parsed)
|
|
437
467
|
if isinstance(deserialized, list) and deserialized:
|
|
468
|
+
warn_if_over_6_0_batch_limit(len(deserialized), max_batch_size)
|
|
438
469
|
if max_batch_size is not None and len(deserialized) > max_batch_size:
|
|
439
470
|
return post_process(
|
|
440
471
|
Left(BatchTooLargeResponse(len(deserialized), max_batch_size))
|
|
@@ -12,7 +12,9 @@ They differ only in the form of the response:
|
|
|
12
12
|
|
|
13
13
|
import json
|
|
14
14
|
import logging
|
|
15
|
+
import math
|
|
15
16
|
import pkgutil
|
|
17
|
+
import warnings
|
|
16
18
|
from typing import Any, Callable, Dict, List, Optional, Union, cast
|
|
17
19
|
|
|
18
20
|
from jsonschema.validators import validator_for
|
|
@@ -41,7 +43,42 @@ from .utils import identity
|
|
|
41
43
|
|
|
42
44
|
logger = logging.getLogger(__name__)
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
_CONSTANTS = {"NaN": math.nan, "Infinity": math.inf, "-Infinity": -math.inf}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _future_constant(constant: str) -> float:
|
|
50
|
+
warnings.warn(
|
|
51
|
+
f"{constant} in a request isn't valid JSON. 5.x accepts it, but 6.0 will "
|
|
52
|
+
"reject it with a Parse error.",
|
|
53
|
+
FutureWarning,
|
|
54
|
+
stacklevel=2,
|
|
55
|
+
)
|
|
56
|
+
return _CONSTANTS[constant]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _future_float(text: str) -> float:
|
|
60
|
+
number = float(text)
|
|
61
|
+
if math.isinf(number):
|
|
62
|
+
warnings.warn(
|
|
63
|
+
f"{text} in a request is too big for a float. 5.x reads it as infinity, "
|
|
64
|
+
"but 6.0 will reject it with a Parse error.",
|
|
65
|
+
FutureWarning,
|
|
66
|
+
stacklevel=2,
|
|
67
|
+
)
|
|
68
|
+
return number
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def default_deserializer(request: str) -> Any:
|
|
72
|
+
"""json.loads, which warns about input 6.0 will reject.
|
|
73
|
+
|
|
74
|
+
`NaN`, `Infinity`, `-Infinity` and numbers too big for a float are accepted, as
|
|
75
|
+
in every 5.x release, with a `FutureWarning`: 6.0's default rejects them with a
|
|
76
|
+
Parse error. Pass `deserializer=json.loads` to accept them without a warning.
|
|
77
|
+
New in 5.2.0.
|
|
78
|
+
"""
|
|
79
|
+
return json.loads(
|
|
80
|
+
request, parse_constant=_future_constant, parse_float=_future_float
|
|
81
|
+
)
|
|
45
82
|
|
|
46
83
|
|
|
47
84
|
def default_serializer(response: Any) -> str:
|
|
@@ -121,7 +158,7 @@ def dispatch_to_response(
|
|
|
121
158
|
methods: Optional[MethodsArgument] = None,
|
|
122
159
|
*,
|
|
123
160
|
context: Any = NOCONTEXT,
|
|
124
|
-
deserializer: Callable[[str], Deserialized] =
|
|
161
|
+
deserializer: Callable[[str], Deserialized] = default_deserializer,
|
|
125
162
|
validator: Callable[[Deserialized], object] = default_validator,
|
|
126
163
|
post_process: Callable[[Response], Any] = identity,
|
|
127
164
|
debug: bool = False,
|
|
@@ -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.
|
|
@@ -7,8 +7,9 @@ server or framework.
|
|
|
7
7
|
import json
|
|
8
8
|
import logging
|
|
9
9
|
import sys
|
|
10
|
+
import warnings
|
|
10
11
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
11
|
-
from typing import Any
|
|
12
|
+
from typing import Any, Optional
|
|
12
13
|
|
|
13
14
|
from .main import dispatch
|
|
14
15
|
from .response import ParseErrorResponse, to_error_dict
|
|
@@ -71,7 +72,7 @@ def listening_message(name: str, port: int) -> str:
|
|
|
71
72
|
return f" * Listening on http://{name}:{port}/. This is a development server."
|
|
72
73
|
|
|
73
74
|
|
|
74
|
-
def serve(name: str =
|
|
75
|
+
def serve(name: Optional[str] = None, port: int = 5000) -> None:
|
|
75
76
|
"""Serve the methods registered with `@method` over HTTP. For development only.
|
|
76
77
|
|
|
77
78
|
It answers POST requests on any path with `dispatch`, sends 204 No Content for a
|
|
@@ -87,9 +88,11 @@ def serve(name: str = "", port: int = 5000) -> None:
|
|
|
87
88
|
production.
|
|
88
89
|
|
|
89
90
|
Args:
|
|
90
|
-
name: The host name or address to listen on.
|
|
91
|
-
every network interface, so other machines can connect
|
|
92
|
-
|
|
91
|
+
name: The host name or address to listen on. Without one, it listens on
|
|
92
|
+
every network interface, so other machines can connect, and gives a
|
|
93
|
+
`FutureWarning` (5.2.0): 6.0 listens on localhost by default. Pass ""
|
|
94
|
+
to keep every interface, or "localhost" to accept only local
|
|
95
|
+
connections.
|
|
93
96
|
port: The port to listen on.
|
|
94
97
|
|
|
95
98
|
Example:
|
|
@@ -103,6 +106,15 @@ def serve(name: str = "", port: int = 5000) -> None:
|
|
|
103
106
|
serve("localhost", 8000)
|
|
104
107
|
```
|
|
105
108
|
"""
|
|
109
|
+
if name is None:
|
|
110
|
+
warnings.warn(
|
|
111
|
+
"serve() without a host listens on every network interface. From 6.0 it "
|
|
112
|
+
'will listen on localhost only. Pass serve("", port) to keep listening '
|
|
113
|
+
'on every interface, or serve("localhost", port).',
|
|
114
|
+
FutureWarning,
|
|
115
|
+
stacklevel=2,
|
|
116
|
+
)
|
|
117
|
+
name = ""
|
|
106
118
|
httpd = ThreadingHTTPServer((name, port), RequestHandler)
|
|
107
119
|
try:
|
|
108
120
|
message = listening_message(name, port)
|
|
@@ -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
|
|
@@ -47,7 +47,9 @@ def reset_calls() -> None:
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
def test_no_limit_by_default() -> None:
|
|
50
|
-
|
|
50
|
+
"""5.x has no limit. From 5.2.0 a batch over 6.0's default of 100 warns."""
|
|
51
|
+
with pytest.warns(FutureWarning, match="6.0 limits batches to 100 by default"):
|
|
52
|
+
assert len(json.loads(dispatch(batch(500), {"ping": ping}))) == 500
|
|
51
53
|
|
|
52
54
|
|
|
53
55
|
def test_at_the_limit() -> None:
|
|
@@ -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
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""5.2.0 warns about the 5.x behaviour that 6.0 changes (plans/6.0.md, D12)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import warnings
|
|
5
|
+
from unittest.mock import Mock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from jsonrpcserver import Result, Success, dispatch, serve
|
|
10
|
+
|
|
11
|
+
METHODS = {"ping": lambda: Success("pong")}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def take_one(x: float) -> Result:
|
|
15
|
+
return Success()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def take_two(a: float, b: float) -> Result:
|
|
19
|
+
return Success()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def batch(size: int) -> str:
|
|
23
|
+
return json.dumps(
|
|
24
|
+
[{"jsonrpc": "2.0", "method": "ping", "id": n} for n in range(size)]
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.mark.parametrize("constant", ["NaN", "Infinity", "-Infinity"])
|
|
29
|
+
def test_non_json_constants_warn(constant: str) -> None:
|
|
30
|
+
request = (
|
|
31
|
+
'{"jsonrpc": "2.0", "method": "ping", "params": [' + constant + '], "id": 1}'
|
|
32
|
+
)
|
|
33
|
+
with pytest.warns(FutureWarning, match=f"{constant} in a request isn't valid JSON"):
|
|
34
|
+
dispatch(request, {"ping": take_one})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_json_loads_accepts_them_quietly() -> None:
|
|
38
|
+
request = '{"jsonrpc": "2.0", "method": "ping", "params": [NaN], "id": 1}'
|
|
39
|
+
with warnings.catch_warnings():
|
|
40
|
+
warnings.simplefilter("error")
|
|
41
|
+
dispatch(request, {"ping": take_one}, deserializer=json.loads)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_ordinary_numbers_dont_warn() -> None:
|
|
45
|
+
request = '{"jsonrpc": "2.0", "method": "ping", "params": [1.5, 1e300], "id": 1}'
|
|
46
|
+
with warnings.catch_warnings():
|
|
47
|
+
warnings.simplefilter("error")
|
|
48
|
+
dispatch(request, {"ping": take_two})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_batch_of_100_doesnt_warn() -> None:
|
|
52
|
+
with warnings.catch_warnings():
|
|
53
|
+
warnings.simplefilter("error")
|
|
54
|
+
dispatch(batch(100), METHODS)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_batch_of_101_warns() -> None:
|
|
58
|
+
with pytest.warns(FutureWarning, match="This batch has 101 requests"):
|
|
59
|
+
dispatch(batch(101), METHODS)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.mark.asyncio
|
|
63
|
+
async def test_async_batch_of_101_warns() -> None:
|
|
64
|
+
from jsonrpcserver import async_dispatch
|
|
65
|
+
|
|
66
|
+
with pytest.warns(FutureWarning, match="This batch has 101 requests"):
|
|
67
|
+
await async_dispatch(batch(101), METHODS)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_explicit_limit_doesnt_warn() -> None:
|
|
71
|
+
with warnings.catch_warnings():
|
|
72
|
+
warnings.simplefilter("error")
|
|
73
|
+
dispatch(batch(101), METHODS, max_batch_size=200)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@patch("jsonrpcserver.server.ThreadingHTTPServer")
|
|
77
|
+
def test_serve_with_a_host_doesnt_warn(server: Mock) -> None:
|
|
78
|
+
with warnings.catch_warnings():
|
|
79
|
+
warnings.simplefilter("error")
|
|
80
|
+
serve("", 5000)
|
|
81
|
+
serve("localhost", 5000)
|
|
@@ -134,7 +134,10 @@ def test_nan_allowed_with_plain_json_dumps() -> None:
|
|
|
134
134
|
|
|
135
135
|
|
|
136
136
|
def test_infinite_id_gets_null_id() -> None:
|
|
137
|
-
|
|
137
|
+
with pytest.warns(FutureWarning, match="too big for a float"):
|
|
138
|
+
response = dispatch(
|
|
139
|
+
'{"jsonrpc": "2.0", "method": "ping", "id": 1e400}', METHODS
|
|
140
|
+
)
|
|
138
141
|
assert json.loads(response) == {
|
|
139
142
|
"jsonrpc": "2.0",
|
|
140
143
|
"error": INTERNAL_ERROR,
|
|
@@ -158,10 +161,11 @@ def test_serializer_that_always_fails() -> None:
|
|
|
158
161
|
|
|
159
162
|
|
|
160
163
|
def test_builtin_without_signature() -> None:
|
|
161
|
-
"""max has no signature inspect can read. It used to collapse the batch.
|
|
164
|
+
"""max has no signature inspect can read. It used to collapse the batch. From
|
|
165
|
+
5.1.0 its plain return value is the result."""
|
|
162
166
|
batch = [{**request("max", 1), "params": [1, 2]}, request("ping", 2)]
|
|
163
167
|
assert json.loads(dispatch(json.dumps(batch), METHODS)) == [
|
|
164
|
-
{"jsonrpc": "2.0", "
|
|
168
|
+
{"jsonrpc": "2.0", "result": 2, "id": 1},
|
|
165
169
|
{"jsonrpc": "2.0", "result": "pong", "id": 2},
|
|
166
170
|
]
|
|
167
171
|
|
|
@@ -290,9 +294,10 @@ def test_invalid_result_under_optimize_flag() -> None:
|
|
|
290
294
|
"import json, logging\n"
|
|
291
295
|
"logging.disable(logging.CRITICAL)\n"
|
|
292
296
|
"from jsonrpcserver import dispatch, Success\n"
|
|
297
|
+
"from oslash.either import Right\n"
|
|
293
298
|
"batch = [{'jsonrpc': '2.0', 'method': 'none', 'id': 1},"
|
|
294
299
|
" {'jsonrpc': '2.0', 'method': 'ping', 'id': 2}]\n"
|
|
295
|
-
"methods = {'none': lambda: None, 'ping': lambda: Success('pong')}\n"
|
|
300
|
+
"methods = {'none': lambda: Right(None), 'ping': lambda: Success('pong')}\n"
|
|
296
301
|
"print(dispatch(json.dumps(batch), methods))\n"
|
|
297
302
|
)
|
|
298
303
|
output = subprocess.run(
|
|
@@ -18,7 +18,8 @@ from jsonrpcserver.server import RequestHandler, listening_message, serve
|
|
|
18
18
|
|
|
19
19
|
@patch("jsonrpcserver.server.ThreadingHTTPServer")
|
|
20
20
|
def test_serve(server: Mock) -> None:
|
|
21
|
-
|
|
21
|
+
with pytest.warns(FutureWarning, match="From 6.0 it will listen on localhost"):
|
|
22
|
+
serve()
|
|
22
23
|
server.assert_called_once_with(("", 5000), RequestHandler)
|
|
23
24
|
server.return_value.serve_forever.assert_called_once_with()
|
|
24
25
|
server.return_value.server_close.assert_called_once_with()
|
|
@@ -34,7 +35,7 @@ def test_serve_name_and_port(server: Mock) -> None:
|
|
|
34
35
|
def test_serve_closes_on_interrupt(server: Mock) -> None:
|
|
35
36
|
server.return_value.serve_forever.side_effect = KeyboardInterrupt
|
|
36
37
|
with pytest.raises(KeyboardInterrupt):
|
|
37
|
-
serve()
|
|
38
|
+
serve("", 5000)
|
|
38
39
|
server.return_value.server_close.assert_called_once_with()
|
|
39
40
|
|
|
40
41
|
|
|
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
|