redis-lua-py 0.11.0__tar.gz → 0.13.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.
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/.github/workflows/ci.yml +3 -0
- redis_lua_py-0.13.0/.release-please-manifest.json +3 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/CHANGELOG.md +14 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/PKG-INFO +2 -2
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/README.md +1 -1
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/examples.md +2 -2
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/guide/build-time.md +7 -1
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/guide/generated-lua.md +1 -1
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/guide/keys-and-arguments.md +17 -8
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/guide/redis-functions.md +1 -1
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/installation.md +27 -1
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/quickstart.md +1 -1
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/reference/lua-vs-python.md +10 -2
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/reference/supported-subset.md +7 -4
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/pyproject.toml +5 -1
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/__init__.py +1 -1
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/_compile/analysis.py +27 -1
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/_compile/base.py +15 -3
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/_compile/calls.py +12 -2
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/_compile/helpers.py +9 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/_compile/scope.py +47 -4
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/_compile/statements.py +1 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/_compile/tables.py +17 -2
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/_portable.py +11 -6
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/codegen.py +1 -1
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/generated_scripts.py +8 -8
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_binary.py +2 -1
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_codegen.py +117 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_compile.py +21 -1
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_errors.py +22 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_expressions.py +14 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_semantics.py +29 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_strings_and_indexing.py +63 -0
- redis_lua_py-0.11.0/.release-please-manifest.json +0 -3
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/.github/assets/README.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/.github/assets/banner-dark.svg +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/.github/assets/banner-light.svg +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/.github/assets/generate.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/.github/assets/logo.svg +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/.github/assets/logomark.svg +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/.github/assets/social-preview.png +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/.github/assets/social-preview.svg +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/.github/workflows/docs.yml +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/.github/workflows/pr-title.yml +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/.github/workflows/release.yml +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/.gitignore +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/.python-version +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/CONTRIBUTING.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/LICENSE +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/assets/logo.svg +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/contributing.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/development.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/guide/async.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/guide/binary-values.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/guide/binding-a-client.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/guide/calling-redis-commands.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/guide/constants.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/guide/return-values.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/guide/testing.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/index.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/reference/api.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/reference/errors.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/docs/stylesheets/extra.css +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/release-please-config.json +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/scripts/generate_commands.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/__main__.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/_command_stubs.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/_commands.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/_compile/__init__.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/_compile/control.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/_compile/expressions.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/_compile/source.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/_library.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/_lua.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/_runtime.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/_script.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/errors.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/src/redis_lua_py/py.typed +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/codegen_scripts.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/conftest.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/module_with_client.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/module_without_import.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_binding.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_booleans_and_conversions.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_commands.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_constants.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_control_flow.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_coredis.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_execute.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_functions.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_mistranslations.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_namespace.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_table_stakes.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/tests/test_typing.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/uv.lock +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.13.0}/zensical.toml +0 -0
|
@@ -32,6 +32,9 @@ jobs:
|
|
|
32
32
|
with:
|
|
33
33
|
enable-cache: true
|
|
34
34
|
- run: uv sync --dev --python ${{ matrix.python-version }}
|
|
35
|
+
# Generated modules must still run on 3.9, which libraries vendoring
|
|
36
|
+
# them may support; tests/test_codegen.py runs one there when it can.
|
|
37
|
+
- run: uv python install 3.9
|
|
35
38
|
- run: uv run pytest
|
|
36
39
|
|
|
37
40
|
oldest:
|
|
@@ -7,6 +7,20 @@ are written by [release-please](https://github.com/googleapis/release-please)
|
|
|
7
7
|
from the conventional commit subjects on `main`; see
|
|
8
8
|
[CONTRIBUTING.md](CONTRIBUTING.md) for how a release is cut.
|
|
9
9
|
|
|
10
|
+
## [0.13.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.12.0...v0.13.0) (2026-09-26)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
* run generated modules on python 3.9 and keep forwarded numbers exact ([#34](https://github.com/IgnaceMaes/redis-lua-py/issues/34)) ([7178211](https://github.com/IgnaceMaes/redis-lua-py/commit/7178211f73e2503188240d62ebe8ab18278bdba2))
|
|
16
|
+
|
|
17
|
+
## [0.12.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.11.0...v0.12.0) (2026-09-25)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
* str.isdigit, partition and annotated locals ([#32](https://github.com/IgnaceMaes/redis-lua-py/issues/32)) ([38e863c](https://github.com/IgnaceMaes/redis-lua-py/commit/38e863c96a89cd0c2a82d0bc59d73771f295d164))
|
|
23
|
+
|
|
10
24
|
## [0.11.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.10.0...v0.11.0) (2026-09-25)
|
|
11
25
|
|
|
12
26
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: redis-lua-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: Write Redis Lua scripts as real Python functions, not strings.
|
|
5
5
|
Project-URL: Homepage, https://ignacemaes.com/redis-lua-py/
|
|
6
6
|
Project-URL: Documentation, https://ignacemaes.com/redis-lua-py/
|
|
@@ -103,7 +103,7 @@ Nothing is hidden. Every script exposes the Lua it produced:
|
|
|
103
103
|
-- Generated by redis-lua-py from src/limits.py:6. Do not edit.
|
|
104
104
|
local key = KEYS[1]
|
|
105
105
|
local limit = tonumber(ARGV[1])
|
|
106
|
-
local ttl =
|
|
106
|
+
local ttl = ARGV[2]
|
|
107
107
|
local current = redis.call('INCR', key)
|
|
108
108
|
if current == 1 then
|
|
109
109
|
redis.call('EXPIRE', key, ttl)
|
|
@@ -78,7 +78,7 @@ Nothing is hidden. Every script exposes the Lua it produced:
|
|
|
78
78
|
-- Generated by redis-lua-py from src/limits.py:6. Do not edit.
|
|
79
79
|
local key = KEYS[1]
|
|
80
80
|
local limit = tonumber(ARGV[1])
|
|
81
|
-
local ttl =
|
|
81
|
+
local ttl = ARGV[2]
|
|
82
82
|
local current = redis.call('INCR', key)
|
|
83
83
|
if current == 1 then
|
|
84
84
|
redis.call('EXPIRE', key, ttl)
|
|
@@ -42,8 +42,8 @@ def claim_jobs(queue: Key, processing: Key, now: int, limit: int) -> list[bytes]
|
|
|
42
42
|
```lua
|
|
43
43
|
local queue = KEYS[1]
|
|
44
44
|
local processing = KEYS[2]
|
|
45
|
-
local now =
|
|
46
|
-
local limit =
|
|
45
|
+
local now = ARGV[1]
|
|
46
|
+
local limit = ARGV[2]
|
|
47
47
|
local ids = redis.call('ZRANGEBYSCORE', queue, 0, now, 'LIMIT', 0, limit)
|
|
48
48
|
local claimed = {}
|
|
49
49
|
for __i1 = 1, #ids do
|
|
@@ -12,6 +12,12 @@ develop. What you ship is that module: plain Python that needs only the
|
|
|
12
12
|
standard library, with a typed function per script, called exactly the way
|
|
13
13
|
the `@script` would be.
|
|
14
14
|
|
|
15
|
+
redis-lua-py itself needs Python 3.10 to generate the module, but what it
|
|
16
|
+
generates runs on Python 3.9 as well, so a library that still supports 3.9 can
|
|
17
|
+
ship it. Its annotations may use `X | Y`, since `from __future__ import
|
|
18
|
+
annotations` keeps them from being evaluated; everything that runs is 3.9
|
|
19
|
+
code.
|
|
20
|
+
|
|
15
21
|
## Generate a module
|
|
16
22
|
|
|
17
23
|
Keep the scripts outside the package you ship, and redis-lua-py in your
|
|
@@ -68,7 +74,7 @@ RATE_LIMIT = """\
|
|
|
68
74
|
-- Generated by redis-lua-py from redis_scripts/limits.py:6. Do not edit.
|
|
69
75
|
local key = KEYS[1]
|
|
70
76
|
local limit = tonumber(ARGV[1])
|
|
71
|
-
local ttl =
|
|
77
|
+
local ttl = ARGV[2]
|
|
72
78
|
local current = redis.call('INCR', key)
|
|
73
79
|
if current == 1 then
|
|
74
80
|
redis.call('EXPIRE', key, ttl)
|
|
@@ -11,7 +11,7 @@ Nothing is hidden. Every script exposes the Lua it produced:
|
|
|
11
11
|
-- Generated by redis-lua-py from src/limits.py:6. Do not edit.
|
|
12
12
|
local key = KEYS[1]
|
|
13
13
|
local limit = tonumber(ARGV[1])
|
|
14
|
-
local ttl =
|
|
14
|
+
local ttl = ARGV[2]
|
|
15
15
|
local current = redis.call('INCR', key)
|
|
16
16
|
if current == 1 then
|
|
17
17
|
redis.call('EXPIRE', key, ttl)
|
|
@@ -11,7 +11,7 @@ def rate_limit(key: Key, limit: int, ttl: int) -> int: ...
|
|
|
11
11
|
```lua
|
|
12
12
|
local key = KEYS[1]
|
|
13
13
|
local limit = tonumber(ARGV[1])
|
|
14
|
-
local ttl =
|
|
14
|
+
local ttl = ARGV[2]
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
!!! warning "Annotate every key"
|
|
@@ -28,18 +28,27 @@ to it on the way in.
|
|
|
28
28
|
| Annotation | In the script |
|
|
29
29
|
| --- | --- |
|
|
30
30
|
| `Key` | `KEYS[n]`, and what the cluster routes on |
|
|
31
|
-
| `int`, `float` | wrapped in `tonumber`, so it is a number by the time your comparison runs |
|
|
31
|
+
| `int`, `float` | wrapped in `tonumber`, so it is a number by the time your comparison runs; left as text when the body only hands it to Redis |
|
|
32
32
|
| `str` | passed through as the string it already is |
|
|
33
33
|
| `bool` | encoded as `"1"` or `"0"`, and a Lua boolean in the body |
|
|
34
34
|
| `bytes`, `memoryview` | passed through untouched — see [Binary values](binary-values.md) |
|
|
35
35
|
|
|
36
|
+
### Numbers only handed to Redis stay text
|
|
37
|
+
|
|
38
|
+
In `rate_limit`, `ttl` is never compared or added to, only passed to `EXPIRE`,
|
|
39
|
+
so it is left as the text it arrived as: `local ttl = ARGV[2]`. A Lua number is
|
|
40
|
+
a double, which rounds an integer past 2^53, and which older servers write back
|
|
41
|
+
with 17 digits, `0.1` as `0.10000000000000001`. Left as text, the value reaches
|
|
42
|
+
Redis exactly as the caller passed it. The parameter is still an `int` to the
|
|
43
|
+
caller, and it is converted as soon as the body does anything else with it.
|
|
44
|
+
|
|
36
45
|
### `float` carries a caveat
|
|
37
46
|
|
|
38
|
-
Annotate `float` for arithmetic, not for a value you mean to
|
|
39
|
-
|
|
40
|
-
text with `%.14g`, so a value with more significant digits than that
|
|
41
|
-
come back as it went in. Annotate `str` and call `str()` at the call
|
|
42
|
-
the value is only being carried.
|
|
47
|
+
Annotate `float` for arithmetic, not for a value you mean to return unchanged.
|
|
48
|
+
Unless the body only hands it to Redis, it is a Lua number, and Lua renders a
|
|
49
|
+
number back to text with `%.14g`, so a value with more significant digits than that
|
|
50
|
+
does not come back as it went in. Annotate `str` and call `str()` at the call
|
|
51
|
+
site when the value is only being carried.
|
|
43
52
|
|
|
44
53
|
### `bool` is a boolean in the body
|
|
45
54
|
|
|
@@ -70,7 +79,7 @@ delete_tagged(client, tag="purged", keys=["a", "b", "c"], stamp=1700000000)
|
|
|
70
79
|
|
|
71
80
|
```lua
|
|
72
81
|
local tag = KEYS[1]
|
|
73
|
-
local stamp =
|
|
82
|
+
local stamp = ARGV[1]
|
|
74
83
|
local keys = {}
|
|
75
84
|
for __i1 = 2, #KEYS do
|
|
76
85
|
keys[#keys + 1] = KEYS[__i1]
|
|
@@ -82,7 +82,7 @@ redis.register_function{
|
|
|
82
82
|
function_name = 'hit',
|
|
83
83
|
callback = function(KEYS, ARGV)
|
|
84
84
|
local key = KEYS[1]
|
|
85
|
-
local ttl =
|
|
85
|
+
local ttl = ARGV[1]
|
|
86
86
|
local current = redis.call('INCR', key)
|
|
87
87
|
if current == 1 then
|
|
88
88
|
redis.call('EXPIRE', key, ttl)
|
|
@@ -12,7 +12,8 @@ pip install redis-lua-py
|
|
|
12
12
|
|
|
13
13
|
## Requirements
|
|
14
14
|
|
|
15
|
-
- Python 3.10 or newer
|
|
15
|
+
- Python 3.10 or newer. Modules [generated ahead of time](guide/build-time.md)
|
|
16
|
+
run on Python 3.9 too, without this package installed.
|
|
16
17
|
- A Redis client: [redis-py](https://github.com/redis/redis-py) 4.2 or newer,
|
|
17
18
|
or [coredis](https://github.com/alisaifee/coredis)
|
|
18
19
|
|
|
@@ -28,6 +29,31 @@ cluster, Sentinel, or an in-process [fakeredis](guide/testing.md).
|
|
|
28
29
|
[coredis](https://github.com/alisaifee/coredis) clients work as well, and are
|
|
29
30
|
tested against. See [Async](guide/async.md#coredis).
|
|
30
31
|
|
|
32
|
+
### A client wrapper
|
|
33
|
+
|
|
34
|
+
An application that wraps its client, to prefix keys per tenant for example,
|
|
35
|
+
needs one method: `register_script(source)`. It returns a callable with the
|
|
36
|
+
shape of redis-py's `Script`, `(keys=..., args=..., client=...)`, and a script
|
|
37
|
+
runs through it:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
class PrefixedScript:
|
|
41
|
+
def __init__(self, owner: "PrefixedClient", script: Script) -> None:
|
|
42
|
+
self._owner, self._script = owner, script
|
|
43
|
+
|
|
44
|
+
def __call__(self, keys=(), args=(), client=None):
|
|
45
|
+
keys = [f"{self._owner.prefix}:{key}" for key in keys]
|
|
46
|
+
return self._script(keys=keys, args=args)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PrefixedClient:
|
|
50
|
+
def register_script(self, source: str) -> PrefixedScript:
|
|
51
|
+
return PrefixedScript(self, self._redis.register_script(source))
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The wrapper's other methods never come into it, so an `eval` with its own
|
|
55
|
+
signature is fine.
|
|
56
|
+
|
|
31
57
|
## For testing
|
|
32
58
|
|
|
33
59
|
[fakeredis](https://github.com/cunla/fakeredis-py) embeds a real Lua
|
|
@@ -56,7 +56,7 @@ print(rate_limit.lua)
|
|
|
56
56
|
-- Generated by redis-lua-py from limits.py:5. Do not edit.
|
|
57
57
|
local key = KEYS[1]
|
|
58
58
|
local limit = tonumber(ARGV[1])
|
|
59
|
-
local ttl =
|
|
59
|
+
local ttl = ARGV[2]
|
|
60
60
|
local current = redis.call('INCR', key)
|
|
61
61
|
if current == 1 then
|
|
62
62
|
redis.call('EXPIRE', key, ttl)
|
|
@@ -55,9 +55,17 @@ that is not known, it looks at the subscript:
|
|
|
55
55
|
A value's type is known from its annotation, a literal, the builtin, method or
|
|
56
56
|
operator that produced it, a `range()` or `enumerate()` loop variable, or
|
|
57
57
|
every assignment to the name agreeing. So after `counts = {}`, `counts[0]` is
|
|
58
|
-
the key `0`. A
|
|
58
|
+
the key `0`. A value that comes from elsewhere -- a Redis reply,
|
|
59
59
|
`cjson.decode`, a helper's parameter -- is not known, and an integer subscript
|
|
60
|
-
on it is taken to be a position.
|
|
60
|
+
on it is taken to be a table position. For a string, that reads `nil`, so
|
|
61
|
+
annotate the local to say what it holds:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
text: str | None = redis.get(key)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
An annotation on a local is a declaration, not a conversion: it decides how
|
|
68
|
+
the value is indexed and added, and changes nothing at runtime.
|
|
61
69
|
|
|
62
70
|
`items[-1]` compiles to `items[#items]`, which needs a name to count back from.
|
|
63
71
|
Indexing a string gives a one-character string, as it does in Python, through
|
|
@@ -5,8 +5,9 @@ language with a faithful Lua meaning is accepted.
|
|
|
5
5
|
|
|
6
6
|
**Supported:**
|
|
7
7
|
|
|
8
|
-
- **Statements:** assignment, including unpacking (`a, b = b, a`)
|
|
9
|
-
|
|
8
|
+
- **Statements:** assignment, including unpacking (`a, b = b, a`) and
|
|
9
|
+
annotated assignment (`text: str = redis.get(key)`), whose annotation tells
|
|
10
|
+
the compiler the value's type; augmented assignment; `if`/`elif`/`else`; `while`; `break`; `continue`;
|
|
10
11
|
`return`.
|
|
11
12
|
- **Errors:** `try` with one `except` (bare or `Exception`), `else` and
|
|
12
13
|
`finally`; `raise SomeError("message")`, and a bare `raise` inside `except`;
|
|
@@ -23,8 +24,10 @@ language with a faithful Lua meaning is accepted.
|
|
|
23
24
|
a name, and slices of lists and strings, with a step or without.
|
|
24
25
|
- **Strings:** f-strings with format specs; `%` formatting; `+` and `*` on a
|
|
25
26
|
string; `str.join`, `upper`, `lower`, `strip`, `lstrip`, `rstrip`,
|
|
26
|
-
`startswith`, `endswith`, `find`, `split` and `replace`;
|
|
27
|
-
`
|
|
27
|
+
`startswith`, `endswith`, `find`, `split`, `partition` and `replace`;
|
|
28
|
+
`isdigit`, `isalpha`, `isalnum` and `isspace`, which test ASCII as they do
|
|
29
|
+
on bytes; `encode()` and `decode()` in UTF-8, which leave the bytes as they
|
|
30
|
+
are.
|
|
28
31
|
- **Builtins and methods:** `len()`, `int()`, which truncates toward zero,
|
|
29
32
|
`float()`, `str()`, `min()`, `max()`, `abs()`, `ord()`, `chr()`, `.append()`,
|
|
30
33
|
`.insert()`, `.pop()` and `dict.get()`; `isinstance()` against `str`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "redis-lua-py"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.13.0"
|
|
4
4
|
description = "Write Redis Lua scripts as real Python functions, not strings."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -65,6 +65,10 @@ addopts = "-q"
|
|
|
65
65
|
src = ["src", "tests"]
|
|
66
66
|
line-length = 100
|
|
67
67
|
target-version = "py310"
|
|
68
|
+
# Generated modules must import on Python 3.9, which libraries that vendor them
|
|
69
|
+
# may still support. Linting what they are built from as 3.9 code keeps a
|
|
70
|
+
# 3.10-only construct out of them, and keeps ruff from asking for one.
|
|
71
|
+
per-file-target-version = { "src/redis_lua_py/_portable.py" = "py39", "tests/generated_scripts.py" = "py39" }
|
|
68
72
|
|
|
69
73
|
[tool.ruff.lint]
|
|
70
74
|
select = ["E", "F", "I", "UP", "B", "SIM", "RUF", "N", "C4", "PT"]
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import ast
|
|
6
|
-
from collections.abc import Iterator
|
|
6
|
+
from collections.abc import Callable, Iterator
|
|
7
7
|
|
|
8
8
|
from .. import _lua as lua
|
|
9
9
|
|
|
@@ -134,6 +134,32 @@ def walk_scope(root: ast.AST) -> Iterator[ast.AST]:
|
|
|
134
134
|
pending.extend(ast.iter_child_nodes(node))
|
|
135
135
|
|
|
136
136
|
|
|
137
|
+
def only_forwarded(
|
|
138
|
+
root: ast.FunctionDef, name: str, is_command: Callable[[ast.Call], bool]
|
|
139
|
+
) -> bool:
|
|
140
|
+
"""True if every use of a name is a whole argument to a Redis command.
|
|
141
|
+
|
|
142
|
+
Such a value is text going back to Redis, whatever it is annotated as.
|
|
143
|
+
Anything else -- arithmetic, a comparison, an assignment to it, a use in
|
|
144
|
+
a nested function that may shadow it -- makes the answer False, as does
|
|
145
|
+
no use at all.
|
|
146
|
+
"""
|
|
147
|
+
forwarded = {
|
|
148
|
+
id(arg)
|
|
149
|
+
for statement in root.body
|
|
150
|
+
for node in ast.walk(statement)
|
|
151
|
+
if isinstance(node, ast.Call) and is_command(node)
|
|
152
|
+
for arg in node.args
|
|
153
|
+
}
|
|
154
|
+
uses = [
|
|
155
|
+
node
|
|
156
|
+
for statement in root.body
|
|
157
|
+
for node in ast.walk(statement)
|
|
158
|
+
if isinstance(node, ast.Name) and node.id == name
|
|
159
|
+
]
|
|
160
|
+
return bool(uses) and all(id(use) in forwarded for use in uses)
|
|
161
|
+
|
|
162
|
+
|
|
137
163
|
def target_names(target: ast.expr) -> list[str]:
|
|
138
164
|
"""The names an assignment or loop target binds."""
|
|
139
165
|
if isinstance(target, ast.Name):
|
|
@@ -26,7 +26,7 @@ from .. import _lua as lua
|
|
|
26
26
|
from .._runtime import _Namespace
|
|
27
27
|
from ..errors import UnsupportedSyntax
|
|
28
28
|
from .analysis import UNBOUND, as_literal, binop_kind, dotted_name, value_kind
|
|
29
|
-
from .tables import MATH_BY_OBJECT, RECEIVER_FALLBACK, REDIS_CONSTANTS
|
|
29
|
+
from .tables import MATH_BY_OBJECT, RECEIVER_FALLBACK, REDIS_CONSTANTS, STRING_TESTS
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def is_redis_py(value: object) -> bool:
|
|
@@ -82,6 +82,9 @@ class CompilerBase(ABC):
|
|
|
82
82
|
self.helpers: set[str] = set()
|
|
83
83
|
# What a parameter or loop variable is known to be; see kind().
|
|
84
84
|
self.kinds: dict[str, str] = {}
|
|
85
|
+
# Kinds that an annotation on a local declares. They are the author's
|
|
86
|
+
# word, so they win over what the assigned values suggest.
|
|
87
|
+
self.declared: dict[str, str] = {}
|
|
85
88
|
# Every value assigned to each name, to work out what a local holds.
|
|
86
89
|
self.values: dict[str, list[ast.expr]] = {}
|
|
87
90
|
# Names bound in ways that say nothing about their type.
|
|
@@ -293,6 +296,13 @@ class CompilerBase(ABC):
|
|
|
293
296
|
return "str"
|
|
294
297
|
if name in {"int", "float", "tonumber", "len", "abs", "ord"}:
|
|
295
298
|
return "num"
|
|
299
|
+
if name in {"min", "max"} and node.args:
|
|
300
|
+
# Python also takes the least of strings, or of one list,
|
|
301
|
+
# so only numbers in make a number out.
|
|
302
|
+
sides = {self.kind(arg) for arg in node.args}
|
|
303
|
+
if sides <= {"num", "?"}:
|
|
304
|
+
return "?" if sides == {"?"} else "num"
|
|
305
|
+
return None
|
|
296
306
|
return "num" if self.math_attr(node.func) is not None else None
|
|
297
307
|
case ast.Call(func=ast.Attribute(value=receiver, attr=attr)):
|
|
298
308
|
if self.math_attr(node.func) is not None:
|
|
@@ -303,9 +313,9 @@ class CompilerBase(ABC):
|
|
|
303
313
|
return "str"
|
|
304
314
|
if attr in {"encode", "decode"}:
|
|
305
315
|
return "str"
|
|
306
|
-
if attr in {"startswith", "endswith"}:
|
|
316
|
+
if attr in {"startswith", "endswith", *STRING_TESTS}:
|
|
307
317
|
return "bool"
|
|
308
|
-
if attr
|
|
318
|
+
if attr in {"split", "partition"}:
|
|
309
319
|
return "list"
|
|
310
320
|
return "num" if attr == "find" else None
|
|
311
321
|
case ast.Attribute():
|
|
@@ -321,6 +331,8 @@ class CompilerBase(ABC):
|
|
|
321
331
|
def name_kind(self, name: str) -> str | None:
|
|
322
332
|
if name not in self.known:
|
|
323
333
|
return value_kind(self.globalns.get(name, UNBOUND))
|
|
334
|
+
if name in self.declared:
|
|
335
|
+
return self.declared[name]
|
|
324
336
|
if name in self._resolving:
|
|
325
337
|
# A name whose value depends on itself, as in `n += 1`, is whatever
|
|
326
338
|
# its other assignments make it.
|
|
@@ -18,6 +18,7 @@ from .tables import (
|
|
|
18
18
|
METHOD_HINT,
|
|
19
19
|
REDIS_DIRECT,
|
|
20
20
|
STRING_METHODS,
|
|
21
|
+
STRING_TESTS,
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
|
|
@@ -26,7 +27,12 @@ class CallCompiler(ExpressionCompiler):
|
|
|
26
27
|
|
|
27
28
|
def call(self, node: ast.Call) -> lua.Expr:
|
|
28
29
|
if node.keywords:
|
|
29
|
-
self.fail(
|
|
30
|
+
self.fail(
|
|
31
|
+
node,
|
|
32
|
+
"keyword arguments are not supported in a script body",
|
|
33
|
+
hint="Pass a Redis command's options positionally, as its syntax "
|
|
34
|
+
"writes them: redis.set(key, value, 'EX', ttl), not ex=ttl.",
|
|
35
|
+
)
|
|
30
36
|
if (
|
|
31
37
|
isinstance(node.func, ast.Name)
|
|
32
38
|
and node.func.id == "isinstance"
|
|
@@ -205,7 +211,8 @@ class CallCompiler(ExpressionCompiler):
|
|
|
205
211
|
return target
|
|
206
212
|
allowed = {"get": (1, 2), "split": (0, 1), "replace": (2,)}.get(attr)
|
|
207
213
|
if allowed is None:
|
|
208
|
-
|
|
214
|
+
no_args = {"upper", "lower", "strip", "lstrip", "rstrip", *STRING_TESTS}
|
|
215
|
+
allowed = (0,) if attr in no_args else (1,)
|
|
209
216
|
if len(args) not in allowed:
|
|
210
217
|
if attr in {"strip", "lstrip", "rstrip"}:
|
|
211
218
|
self.fail(
|
|
@@ -218,6 +225,9 @@ class CallCompiler(ExpressionCompiler):
|
|
|
218
225
|
|
|
219
226
|
if attr in {"upper", "lower"}:
|
|
220
227
|
return lua.Call(lua.Global(f"string.{attr}"), (target,))
|
|
228
|
+
if attr in STRING_TESTS:
|
|
229
|
+
found = lua.Call(lua.Global("string.find"), (target, lua.Str(STRING_TESTS[attr])))
|
|
230
|
+
return lua.BinOp("~=", found, lua.Nil())
|
|
221
231
|
if attr in {"strip", "lstrip", "rstrip"}:
|
|
222
232
|
pattern = {"strip": "^%s*(.-)%s*$", "lstrip": "^%s*(.*)$", "rstrip": "^(.-)%s*$"}
|
|
223
233
|
return lua.Call(lua.Global("string.match"), (target, lua.Str(pattern[attr])))
|
|
@@ -149,6 +149,15 @@ local function __find(s, sub)
|
|
|
149
149
|
local i = string.find(s, sub, 1, true)
|
|
150
150
|
if i == nil then return -1 end
|
|
151
151
|
return i - 1
|
|
152
|
+
end""",
|
|
153
|
+
"__partition": """\
|
|
154
|
+
-- str.partition: before, separator and after the first plain match, or the
|
|
155
|
+
-- whole string and two empty strings when there is none.
|
|
156
|
+
local function __partition(s, sep)
|
|
157
|
+
if sep == '' then error('empty separator', 0) end
|
|
158
|
+
local i, j = string.find(s, sep, 1, true)
|
|
159
|
+
if i == nil then return {s, '', ''} end
|
|
160
|
+
return {string.sub(s, 1, i - 1), sep, string.sub(s, j + 1)}
|
|
152
161
|
end""",
|
|
153
162
|
"__split": """\
|
|
154
163
|
-- str.split: on a plain separator, or on runs of whitespace without one.
|
|
@@ -5,8 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
import ast
|
|
6
6
|
|
|
7
7
|
from .. import _lua as lua
|
|
8
|
-
from .analysis import declares_exactly, target_names, walk_scope
|
|
8
|
+
from .analysis import declares_exactly, only_forwarded, target_names, walk_scope
|
|
9
9
|
from .base import CompilerBase
|
|
10
|
+
from .tables import REDIS_DIRECT
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class ScopeCompiler(CompilerBase):
|
|
@@ -44,6 +45,10 @@ class ScopeCompiler(CompilerBase):
|
|
|
44
45
|
node.target, ast.Name
|
|
45
46
|
):
|
|
46
47
|
record(node.target.id, node)
|
|
48
|
+
if isinstance(node, ast.AnnAssign):
|
|
49
|
+
declared = self._annotation_kind(self._without_none(node.annotation))
|
|
50
|
+
if declared is not None:
|
|
51
|
+
self.declared.setdefault(node.target.id, declared)
|
|
47
52
|
if isinstance(node, ast.AugAssign):
|
|
48
53
|
value: ast.expr = ast.BinOp(left=node.target, op=node.op, right=node.value)
|
|
49
54
|
self.values.setdefault(node.target.id, []).append(value)
|
|
@@ -118,7 +123,12 @@ class ScopeCompiler(CompilerBase):
|
|
|
118
123
|
self.variadic_key = name
|
|
119
124
|
else:
|
|
120
125
|
if self.variadic_arg is not None:
|
|
121
|
-
self.fail(
|
|
126
|
+
self.fail(
|
|
127
|
+
arg,
|
|
128
|
+
"only one list parameter of arguments is supported",
|
|
129
|
+
hint="Interleave the values in one list and index it "
|
|
130
|
+
"with a stride, as in values[3 * i + 1].",
|
|
131
|
+
)
|
|
122
132
|
self.variadic_arg = name
|
|
123
133
|
if self._is_numeric(item):
|
|
124
134
|
self.numeric_args.add(name)
|
|
@@ -132,9 +142,12 @@ class ScopeCompiler(CompilerBase):
|
|
|
132
142
|
source = lua.Index(lua.Global("ARGV"), lua.Num(len(self.args)))
|
|
133
143
|
if self._is_numeric(arg.annotation):
|
|
134
144
|
# ARGV always arrives as strings; an int/float annotation
|
|
135
|
-
# is the author asking for the conversion.
|
|
145
|
+
# is the author asking for the conversion. Unless the value
|
|
146
|
+
# only goes back to Redis: a Lua number is a double, so
|
|
147
|
+
# converting would round an integer past 2^53 on the way.
|
|
136
148
|
self.numeric_args.add(name)
|
|
137
|
-
|
|
149
|
+
if not only_forwarded(self.func, name, self._is_command):
|
|
150
|
+
source = lua.Call(lua.Global("tonumber"), (source,))
|
|
138
151
|
elif self._annotation_name(arg.annotation) == "bool":
|
|
139
152
|
# A bool arrives as "1" or "0", and "0" is a string Lua and
|
|
140
153
|
# Python both count as true. Comparing makes it a boolean.
|
|
@@ -176,6 +189,16 @@ class ScopeCompiler(CompilerBase):
|
|
|
176
189
|
return node.value.rsplit(".", 1)[-1].split("[", 1)[0]
|
|
177
190
|
return None
|
|
178
191
|
|
|
192
|
+
def _is_command(self, node: ast.Call) -> bool:
|
|
193
|
+
"""A call that sends its arguments to Redis as they are."""
|
|
194
|
+
func = node.func
|
|
195
|
+
return (
|
|
196
|
+
isinstance(func, ast.Attribute)
|
|
197
|
+
and isinstance(func.value, ast.Name)
|
|
198
|
+
and self.namespace_kind(func.value.id) == "redis"
|
|
199
|
+
and (func.attr not in REDIS_DIRECT or func.attr in {"call", "pcall"})
|
|
200
|
+
)
|
|
201
|
+
|
|
179
202
|
def _is_key(self, node: ast.expr | None) -> bool:
|
|
180
203
|
return self._annotation_name(node) == "Key"
|
|
181
204
|
|
|
@@ -192,6 +215,26 @@ class ScopeCompiler(CompilerBase):
|
|
|
192
215
|
return "str"
|
|
193
216
|
return None
|
|
194
217
|
|
|
218
|
+
@staticmethod
|
|
219
|
+
def _without_none(node: ast.expr | None) -> ast.expr | None:
|
|
220
|
+
"""``X`` from ``X | None`` or ``Optional[X]``: None is no kind of its own."""
|
|
221
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
222
|
+
try:
|
|
223
|
+
node = ast.parse(node.value, mode="eval").body
|
|
224
|
+
except SyntaxError:
|
|
225
|
+
return None
|
|
226
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
|
|
227
|
+
sides = [
|
|
228
|
+
side
|
|
229
|
+
for side in (node.left, node.right)
|
|
230
|
+
if not (isinstance(side, ast.Constant) and side.value is None)
|
|
231
|
+
]
|
|
232
|
+
return sides[0] if len(sides) == 1 else None
|
|
233
|
+
name = ScopeCompiler._annotation_name(node)
|
|
234
|
+
if isinstance(node, ast.Subscript) and name == "Optional":
|
|
235
|
+
return node.slice
|
|
236
|
+
return node
|
|
237
|
+
|
|
195
238
|
@staticmethod
|
|
196
239
|
def _list_item(node: ast.expr | None) -> ast.expr | None:
|
|
197
240
|
"""The element annotation of a ``list[...]`` parameter, if it is one."""
|
|
@@ -272,6 +272,7 @@ class Compiler(ControlFlowCompiler):
|
|
|
272
272
|
# What the script's names hold is known inside the function too, except
|
|
273
273
|
# for its parameters, which say nothing about their type.
|
|
274
274
|
child.kinds = {k: v for k, v in self.kinds.items() if k not in params}
|
|
275
|
+
child.declared = {k: v for k, v in self.declared.items() if k not in params}
|
|
275
276
|
child.values = {k: list(v) for k, v in self.values.items() if k not in params}
|
|
276
277
|
child.opaque = self.opaque - set(params)
|
|
277
278
|
child._temp = self._temp
|
|
@@ -133,8 +133,8 @@ MATH_BY_OBJECT: dict[object, str] = {getattr(math, name): name for name in [*MAT
|
|
|
133
133
|
|
|
134
134
|
METHOD_HINT = (
|
|
135
135
|
"Available: the redis and cjson namespaces; list append, insert and pop; str join, "
|
|
136
|
-
"upper, lower, strip, lstrip, rstrip, startswith, endswith, find, split,
|
|
137
|
-
"encode and decode; and dict get."
|
|
136
|
+
"upper, lower, strip, lstrip, rstrip, startswith, endswith, find, split, partition, "
|
|
137
|
+
"replace, isdigit, isalpha, isalnum, isspace, encode and decode; and dict get."
|
|
138
138
|
)
|
|
139
139
|
|
|
140
140
|
|
|
@@ -152,9 +152,24 @@ STRING_METHODS = frozenset(
|
|
|
152
152
|
"endswith",
|
|
153
153
|
"find",
|
|
154
154
|
"split",
|
|
155
|
+
"partition",
|
|
155
156
|
"replace",
|
|
157
|
+
"isdigit",
|
|
158
|
+
"isalpha",
|
|
159
|
+
"isalnum",
|
|
160
|
+
"isspace",
|
|
156
161
|
"encode",
|
|
157
162
|
"decode",
|
|
158
163
|
"get",
|
|
159
164
|
}
|
|
160
165
|
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# The str.is* tests, as Lua patterns over the whole string. Lua's classes are
|
|
169
|
+
# ASCII, which is exactly what the same methods on bytes test.
|
|
170
|
+
STRING_TESTS: dict[str, str] = {
|
|
171
|
+
"isdigit": "^%d+$",
|
|
172
|
+
"isalpha": "^%a+$",
|
|
173
|
+
"isalnum": "^%w+$",
|
|
174
|
+
"isspace": "^%s+$",
|
|
175
|
+
}
|
|
@@ -8,19 +8,24 @@ stay in step: there is only one copy of this code to get right.
|
|
|
8
8
|
So this module imports only the standard library, and nothing from the
|
|
9
9
|
package, and every name it defines starts with an underscore, to stay out of
|
|
10
10
|
the way of the scripts in a generated module.
|
|
11
|
+
|
|
12
|
+
It is also the one module here that runs on Python 3.9, because a library
|
|
13
|
+
that vendors generated code may still support it. Annotations are never
|
|
14
|
+
evaluated, so they can use ``X | Y``; anything evaluated cannot, which is why
|
|
15
|
+
the aliases below spell ``Union`` and ``isinstance`` takes tuples.
|
|
11
16
|
"""
|
|
12
17
|
|
|
13
18
|
from __future__ import annotations
|
|
14
19
|
|
|
15
20
|
from collections.abc import Iterable
|
|
16
|
-
from typing import Any, Protocol
|
|
21
|
+
from typing import Any, Protocol, Union
|
|
17
22
|
from weakref import WeakKeyDictionary
|
|
18
23
|
|
|
19
24
|
#: What a generated signature accepts for a key, the same as redis-py does.
|
|
20
|
-
_Key = str
|
|
25
|
+
_Key = Union[str, bytes, memoryview]
|
|
21
26
|
|
|
22
27
|
#: What a generated signature accepts for an argument it has no better type for.
|
|
23
|
-
_Arg = str
|
|
28
|
+
_Arg = Union[str, bytes, memoryview, int, float]
|
|
24
29
|
|
|
25
30
|
|
|
26
31
|
class _SyncClient(Protocol):
|
|
@@ -61,9 +66,9 @@ def _encode(
|
|
|
61
66
|
"""
|
|
62
67
|
if isinstance(value, bool):
|
|
63
68
|
return "1" if value else "0"
|
|
64
|
-
if isinstance(value, str
|
|
69
|
+
if isinstance(value, (str, bytes, memoryview)):
|
|
65
70
|
return value
|
|
66
|
-
if isinstance(value, int
|
|
71
|
+
if isinstance(value, (int, float)):
|
|
67
72
|
return repr(value) if isinstance(value, float) else str(value)
|
|
68
73
|
raise error(
|
|
69
74
|
f"argument {name!r} is a {type(value).__name__}, which has no Redis representation. "
|
|
@@ -77,7 +82,7 @@ def _items(name: str, value: object, error: type[Exception] = TypeError) -> list
|
|
|
77
82
|
A string is iterable too, and splitting a key into characters is never
|
|
78
83
|
what was meant, so it is refused rather than spread.
|
|
79
84
|
"""
|
|
80
|
-
if isinstance(value, str
|
|
85
|
+
if isinstance(value, (str, bytes, memoryview)) or not isinstance(value, Iterable):
|
|
81
86
|
raise error(
|
|
82
87
|
f"argument {name!r} takes a list, got a {type(value).__name__}. "
|
|
83
88
|
"Wrap a single value in a list."
|