redis-lua-py 0.6.0__tar.gz → 0.7.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.7.0/.release-please-manifest.json +3 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/CHANGELOG.md +17 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/PKG-INFO +3 -3
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/README.md +2 -2
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/guide/async.md +28 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/guide/redis-functions.md +6 -4
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/index.md +3 -2
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/installation.md +4 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/reference/errors.md +4 -4
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/reference/lua-vs-python.md +27 -13
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/reference/supported-subset.md +12 -10
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/pyproject.toml +4 -1
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/src/redis_lua_py/__init__.py +1 -1
- redis_lua_py-0.7.0/src/redis_lua_py/_compile/__init__.py +181 -0
- redis_lua_py-0.7.0/src/redis_lua_py/_compile/analysis.py +271 -0
- redis_lua_py-0.7.0/src/redis_lua_py/_compile/base.py +342 -0
- redis_lua_py-0.7.0/src/redis_lua_py/_compile/calls.py +268 -0
- redis_lua_py-0.7.0/src/redis_lua_py/_compile/control.py +343 -0
- redis_lua_py-0.7.0/src/redis_lua_py/_compile/expressions.py +453 -0
- redis_lua_py-0.7.0/src/redis_lua_py/_compile/helpers.py +204 -0
- redis_lua_py-0.7.0/src/redis_lua_py/_compile/scope.py +228 -0
- redis_lua_py-0.7.0/src/redis_lua_py/_compile/source.py +62 -0
- redis_lua_py-0.7.0/src/redis_lua_py/_compile/statements.py +317 -0
- redis_lua_py-0.7.0/src/redis_lua_py/_compile/tables.py +144 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/src/redis_lua_py/_library.py +38 -12
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/src/redis_lua_py/_lua.py +9 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/src/redis_lua_py/_script.py +3 -2
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/test_commands.py +4 -3
- redis_lua_py-0.7.0/tests/test_coredis.py +163 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/test_errors.py +6 -37
- redis_lua_py-0.7.0/tests/test_expressions.py +174 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/test_namespace.py +8 -7
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/test_strings_and_indexing.py +113 -5
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/test_typing.py +19 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/uv.lock +186 -2
- redis_lua_py-0.6.0/.release-please-manifest.json +0 -3
- redis_lua_py-0.6.0/src/redis_lua_py/_compile.py +0 -2472
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/.github/assets/README.md +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/.github/assets/banner-dark.svg +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/.github/assets/banner-light.svg +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/.github/assets/generate.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/.github/assets/logo.svg +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/.github/assets/logomark.svg +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/.github/assets/social-preview.png +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/.github/assets/social-preview.svg +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/.github/workflows/ci.yml +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/.github/workflows/docs.yml +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/.github/workflows/pr-title.yml +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/.github/workflows/release.yml +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/.gitignore +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/.python-version +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/CONTRIBUTING.md +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/LICENSE +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/assets/logo.svg +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/contributing.md +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/development.md +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/examples.md +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/guide/binary-values.md +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/guide/binding-a-client.md +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/guide/build-time.md +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/guide/calling-redis-commands.md +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/guide/constants.md +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/guide/generated-lua.md +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/guide/keys-and-arguments.md +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/guide/return-values.md +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/guide/testing.md +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/quickstart.md +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/reference/api.md +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/docs/stylesheets/extra.css +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/release-please-config.json +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/scripts/generate_commands.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/src/redis_lua_py/__main__.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/src/redis_lua_py/_commands.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/src/redis_lua_py/_portable.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/src/redis_lua_py/_runtime.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/src/redis_lua_py/codegen.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/src/redis_lua_py/errors.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/src/redis_lua_py/py.typed +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/codegen_scripts.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/conftest.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/generated_scripts.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/module_with_client.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/module_without_import.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/test_binary.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/test_binding.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/test_codegen.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/test_compile.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/test_constants.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/test_control_flow.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/test_execute.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/test_functions.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/test_mistranslations.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/test_semantics.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/tests/test_table_stakes.py +0 -0
- {redis_lua_py-0.6.0 → redis_lua_py-0.7.0}/zensical.toml +0 -0
|
@@ -7,6 +7,23 @@ 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.7.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.6.0...v0.7.0) (2026-09-12)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### ⚠ BREAKING CHANGES
|
|
14
|
+
|
|
15
|
+
* a + b where neither side is known to be a number or a string now joins two strings instead of adding them as numbers, as Python does, so wrap a side in int to add. Integer keys of a dict the compiler knows to be one are no longer shifted by one, and a lambda or helper function that reads a loop variable is refused.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
* compile lambdas, chained comparisons and slice steps, and tell dicts from lists ([6024009](https://github.com/IgnaceMaes/redis-lua-py/commit/6024009eaefeb20ccb743225c609799aa332f7c2))
|
|
20
|
+
* support coredis clients ([#22](https://github.com/IgnaceMaes/redis-lua-py/issues/22)) ([3d07a89](https://github.com/IgnaceMaes/redis-lua-py/commit/3d07a89f5b4c55eddca3a1f3bae5f45b41fd4156))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
* split the compiler into a package of modules ([#20](https://github.com/IgnaceMaes/redis-lua-py/issues/20)) ([5f67766](https://github.com/IgnaceMaes/redis-lua-py/commit/5f67766f015c038086894f3b61fa76d6f9deef12))
|
|
26
|
+
|
|
10
27
|
## [0.6.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.5.1...v0.6.0) (2026-09-12)
|
|
11
28
|
|
|
12
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: redis-lua-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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/
|
|
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
|
|
|
40
40
|
|
|
41
41
|
<p align="center">
|
|
42
42
|
Write Redis Lua scripts as real Python functions, not as strings.<br>
|
|
43
|
-
Compiled at import, checked by <code>mypy</code>, sent with <code>EVALSHA</code>. Sync and async redis-py.
|
|
43
|
+
Compiled at import, checked by <code>mypy</code>, sent with <code>EVALSHA</code>. Sync and async redis-py, and coredis.
|
|
44
44
|
</p>
|
|
45
45
|
|
|
46
46
|
<p align="center">
|
|
@@ -141,7 +141,7 @@ script cache is cold once per script rather than once per environment.
|
|
|
141
141
|
a script is a `CompiledScript[R]`, and an async client gives you
|
|
142
142
|
`Awaitable[R]`.
|
|
143
143
|
- **[Sync and async](https://ignacemaes.com/redis-lua-py/guide/async/)** from
|
|
144
|
-
the same script object, and
|
|
144
|
+
the same script object, with redis-py or coredis, and
|
|
145
145
|
**[`bind`](https://ignacemaes.com/redis-lua-py/guide/binding-a-client/)** when
|
|
146
146
|
passing the client every time gets repetitive.
|
|
147
147
|
- **[Build-time generation](https://ignacemaes.com/redis-lua-py/guide/build-time/)**
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
16
16
|
Write Redis Lua scripts as real Python functions, not as strings.<br>
|
|
17
|
-
Compiled at import, checked by <code>mypy</code>, sent with <code>EVALSHA</code>. Sync and async redis-py.
|
|
17
|
+
Compiled at import, checked by <code>mypy</code>, sent with <code>EVALSHA</code>. Sync and async redis-py, and coredis.
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
20
|
<p align="center">
|
|
@@ -115,7 +115,7 @@ script cache is cold once per script rather than once per environment.
|
|
|
115
115
|
a script is a `CompiledScript[R]`, and an async client gives you
|
|
116
116
|
`Awaitable[R]`.
|
|
117
117
|
- **[Sync and async](https://ignacemaes.com/redis-lua-py/guide/async/)** from
|
|
118
|
-
the same script object, and
|
|
118
|
+
the same script object, with redis-py or coredis, and
|
|
119
119
|
**[`bind`](https://ignacemaes.com/redis-lua-py/guide/binding-a-client/)** when
|
|
120
120
|
passing the client every time gets repetitive.
|
|
121
121
|
- **[Build-time generation](https://ignacemaes.com/redis-lua-py/guide/build-time/)**
|
|
@@ -27,3 +27,31 @@ own script machinery, which this defers to rather than reimplementing.
|
|
|
27
27
|
|
|
28
28
|
[`bind`](binding-a-client.md) works on async clients just as well, and carries
|
|
29
29
|
the awaitable through.
|
|
30
|
+
|
|
31
|
+
## coredis
|
|
32
|
+
|
|
33
|
+
[coredis](https://github.com/alisaifee/coredis), which is async only, works
|
|
34
|
+
the same way. A script, a bound script and a
|
|
35
|
+
[library function](redis-functions.md) each return an awaitable, and the
|
|
36
|
+
overloads type it as `Awaitable[R]`:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import coredis
|
|
40
|
+
|
|
41
|
+
async with coredis.Redis() as client:
|
|
42
|
+
remaining = await rate_limit(client, key="user:42", limit=10, ttl=60)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Script caching is coredis's own `register_script`, which reloads a script the
|
|
46
|
+
server has dropped. In a coredis pipeline a call is queued like any other
|
|
47
|
+
command, and coredis loads the pipeline's scripts before running it; await
|
|
48
|
+
each call once the `async with` block has run the pipeline:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
async with client.pipeline() as pipe:
|
|
52
|
+
queued = rate_limit(pipe, key="user:42", limit=10, ttl=60)
|
|
53
|
+
remaining = await queued
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
As with redis-py, a client created with `decode_responses=True` decodes
|
|
57
|
+
replies, so a script annotated `bytes` returns `str` from it.
|
|
@@ -34,7 +34,8 @@ peek(client, key="user:42") # FCALL_RO peek 1 user:42
|
|
|
34
34
|
|
|
35
35
|
A function is called like a script: pass a sync client and you get a value,
|
|
36
36
|
an async one and you get an awaitable, and [`bind`](binding-a-client.md)
|
|
37
|
-
works the same way.
|
|
37
|
+
works the same way. With a [coredis](async.md#coredis) client, the call goes
|
|
38
|
+
through its `fcall` or `fcall_ro`, and loading through its `function_load`.
|
|
38
39
|
|
|
39
40
|
## Loading
|
|
40
41
|
|
|
@@ -51,10 +52,11 @@ limits.load(client)
|
|
|
51
52
|
|
|
52
53
|
A call queued in a pipeline cannot load the library halfway through
|
|
53
54
|
`execute()`. Call `load()` before queueing calls to a library the server
|
|
54
|
-
may not have yet.
|
|
55
|
+
may not have yet. The same goes for a coredis pipeline, which runs when its
|
|
56
|
+
`async with` block ends: `await limits.load(client)` before entering it.
|
|
55
57
|
|
|
56
|
-
On Redis Cluster, redis-py
|
|
57
|
-
`FCALL` on the function's keys, as
|
|
58
|
+
On Redis Cluster, redis-py and coredis send `FUNCTION LOAD` to every primary,
|
|
59
|
+
and route `FCALL` on the function's keys, as they do a script.
|
|
58
60
|
|
|
59
61
|
## What the library looks like
|
|
60
62
|
|
|
@@ -6,7 +6,7 @@ hide:
|
|
|
6
6
|
# redis-lua-py
|
|
7
7
|
|
|
8
8
|
Write Redis Lua scripts as real Python functions, not as strings. Compiled at
|
|
9
|
-
import, checked by `mypy`, sent with `EVALSHA`. Sync and async redis-py.
|
|
9
|
+
import, checked by `mypy`, sent with `EVALSHA`. Sync and async redis-py, and coredis.
|
|
10
10
|
|
|
11
11
|
```python
|
|
12
12
|
from redis_lua_py import Key, redis, script
|
|
@@ -68,7 +68,8 @@ requirements, or go straight to the [Quickstart](quickstart.md).
|
|
|
68
68
|
- **Typed on the caller's side.** A script is a `CompiledScript[R]`, so
|
|
69
69
|
`rate_limit(...)` returns an `int` rather than `Any`, and an async client
|
|
70
70
|
gives you `Awaitable[R]`. See [What the caller gets](guide/return-values.md).
|
|
71
|
-
- **Sync and async from the same script object
|
|
71
|
+
- **Sync and async from the same script object**, with redis-py or coredis.
|
|
72
|
+
See [Async](guide/async.md).
|
|
72
73
|
- **Binary-safe throughout.** Nothing here decodes. See
|
|
73
74
|
[Binary values](guide/binary-values.md).
|
|
74
75
|
|
|
@@ -21,6 +21,10 @@ own script machinery, which this library defers to rather than reimplementing.
|
|
|
21
21
|
Anything redis-py can talk to, this can run against: a standalone server, a
|
|
22
22
|
cluster, Sentinel, or an in-process [fakeredis](guide/testing.md).
|
|
23
23
|
|
|
24
|
+
[coredis](https://github.com/alisaifee/coredis) clients work as well, and are
|
|
25
|
+
tested against, but coredis is not a dependency: install it yourself. See
|
|
26
|
+
[Async](guide/async.md#coredis).
|
|
27
|
+
|
|
24
28
|
## For testing
|
|
25
29
|
|
|
26
30
|
[fakeredis](https://github.com/cunla/fakeredis-py) embeds a real Lua
|
|
@@ -4,11 +4,11 @@ Every error this package raises carries a caret under the line at fault, and a
|
|
|
4
4
|
hint saying what to write instead.
|
|
5
5
|
|
|
6
6
|
```
|
|
7
|
-
|
|
7
|
+
math.log() with a base is not supported
|
|
8
8
|
File "/srv/app/limits.py", line 12
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
hint:
|
|
9
|
+
buckets = math.log(n, 2)
|
|
10
|
+
^
|
|
11
|
+
hint: Lua 5.1's math.log takes no base; divide by math.log(base) instead.
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## The hierarchy
|
|
@@ -23,22 +23,27 @@ Lua tables are 1-based. `items[0]` compiles to `items[1]`. Write Python indices
|
|
|
23
23
|
and let the compiler shift them.
|
|
24
24
|
|
|
25
25
|
A dict key must not be shifted, and Lua cannot tell a list from a dict. So the
|
|
26
|
-
compiler looks at
|
|
26
|
+
compiler looks first at what is subscripted. A dict is indexed by its keys as
|
|
27
|
+
they are, integer keys included, and a list's index is shifted by one. When
|
|
28
|
+
that is not known, it looks at the subscript:
|
|
27
29
|
|
|
28
30
|
- a string literal, or a value known to be a string, is used as it is;
|
|
29
31
|
- an integer literal, or a value known to be a number, is shifted by one;
|
|
30
32
|
- anything else goes through a small `__key` helper, which shifts numbers and
|
|
31
33
|
leaves everything else alone, at runtime.
|
|
32
34
|
|
|
33
|
-
A value is known
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
A value's type is known from its annotation, a literal, the builtin, method or
|
|
36
|
+
operator that produced it, a `range()` or `enumerate()` loop variable, or
|
|
37
|
+
every assignment to the name agreeing. So after `counts = {}`, `counts[0]` is
|
|
38
|
+
the key `0`. A table that comes from elsewhere -- a Redis reply,
|
|
39
|
+
`cjson.decode`, a helper's parameter -- is not known, and an integer subscript
|
|
40
|
+
on it is taken to be a position.
|
|
37
41
|
|
|
38
42
|
`items[-1]` compiles to `items[#items]`, which needs a name to count back from.
|
|
39
43
|
Indexing a string gives a one-character string, as it does in Python, through
|
|
40
|
-
`string.sub`. Slices, `v[i:j]`, work on lists and strings, with
|
|
41
|
-
missing bounds
|
|
44
|
+
`string.sub`. Slices, `v[i:j]` and `v[i:j:k]`, work on lists and strings, with
|
|
45
|
+
negative and missing bounds, and bounds are clamped exactly as Python clamps
|
|
46
|
+
them. A negative step walks backwards, so `word[::-1]` reverses.
|
|
42
47
|
|
|
43
48
|
## Assignment scope is closed
|
|
44
49
|
|
|
@@ -50,8 +55,10 @@ the top of the script, so it does not silently read back `nil`.
|
|
|
50
55
|
|
|
51
56
|
Lua's `+` is only arithmetic. It will add `"1" + "2"` to `3`. So `+`
|
|
52
57
|
compiles to Lua's `..` wherever either side is known to be a string, as
|
|
53
|
-
above,
|
|
54
|
-
|
|
58
|
+
above, to Lua's `+` wherever either side is known to be a number, and
|
|
59
|
+
`"=" * n` to `string.rep`. Where neither side is known, as with two Redis
|
|
60
|
+
replies, a small `__add` helper decides at runtime: two strings are joined,
|
|
61
|
+
two lists are joined into a new list, and anything else is added.
|
|
55
62
|
|
|
56
63
|
The string methods compile to Lua's string library, and to small helpers where
|
|
57
64
|
Python means something Lua's own functions do not:
|
|
@@ -60,7 +67,9 @@ Python means something Lua's own functions do not:
|
|
|
60
67
|
substring. `string.find` would read `.` as a pattern.
|
|
61
68
|
- **`strip`, `lstrip` and `rstrip`** strip whitespace, and take no argument.
|
|
62
69
|
- **`x in s`** is a substring test on a string. On a list it is an element test;
|
|
63
|
-
on a dict it is a key test.
|
|
70
|
+
on a dict it is a key test. A table whose type is not known, such as a
|
|
71
|
+
decoded JSON value, is taken to be a dict when any of its keys is not a list
|
|
72
|
+
position, and a list otherwise.
|
|
64
73
|
|
|
65
74
|
`"%s: %d" % (name, n)` and f-string format specs such as `{price:8.2f}` compile
|
|
66
75
|
to `string.format`. Width, precision, sign and zero padding are supported. A
|
|
@@ -88,8 +97,10 @@ becomes the same kind of function, because `c and a or b` is wrong whenever
|
|
|
88
97
|
`.items()`, `.keys()` and `.values()` compile to Lua's `pairs()`, which visits
|
|
89
98
|
entries in no fixed order. Sort the result if the order reaches the caller.
|
|
90
99
|
|
|
91
|
-
|
|
92
|
-
does
|
|
100
|
+
`for k in d` walks the keys of a dict the compiler knows to be one, as
|
|
101
|
+
`.keys()` does, and `len(d)` counts them. On a table whose type is not known,
|
|
102
|
+
say `.keys()`: there, `for k in d` and `len(d)` see only list positions, which
|
|
103
|
+
a dict does not have.
|
|
93
104
|
|
|
94
105
|
## Redis replies are flat lists, not dicts
|
|
95
106
|
|
|
@@ -149,7 +160,10 @@ kept, whether or not something catches it.
|
|
|
149
160
|
|
|
150
161
|
## A loop variable does not outlive its loop
|
|
151
162
|
|
|
152
|
-
Unlike in Python.
|
|
163
|
+
Unlike in Python. Lua also gives each step of a loop a variable of its own,
|
|
164
|
+
which a function defined inside the loop would keep, where in Python every
|
|
165
|
+
step shares one. So a `lambda` or a helper function that reads a loop variable
|
|
166
|
+
is refused; pass the value in as a parameter instead.
|
|
153
167
|
|
|
154
168
|
## A nil inside a returned table truncates the reply
|
|
155
169
|
|
|
@@ -13,12 +13,14 @@ language with a faithful Lua meaning is accepted.
|
|
|
13
13
|
`assert`.
|
|
14
14
|
- **Loops:** `for ... in` over a table, `range()`, `enumerate()`, or a dict's
|
|
15
15
|
`.items()`, `.keys()` and `.values()`, binding a name or a tuple of names.
|
|
16
|
-
- **Helper functions** defined with `def` at the top level of the body
|
|
17
|
-
can call each other and
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
- **Helper functions** defined with `def` at the top level of the body, and
|
|
17
|
+
`lambda` wherever an expression can go. They can call each other and
|
|
18
|
+
themselves, and be passed to a helper that calls them.
|
|
19
|
+
- **Expressions:** comparisons, chained ones such as `0 < n <= limit`
|
|
20
|
+
included; arithmetic; `in` and `not in`; `and`/`or`, and `a if c else b`,
|
|
21
|
+
both as values; list and dict literals.
|
|
22
|
+
- **Subscripts:** indices, dict keys of any type, negative literal indices on
|
|
23
|
+
a name, and slices of lists and strings, with a step or without.
|
|
22
24
|
- **Strings:** f-strings with format specs; `%` formatting; `+` and `*` on a
|
|
23
25
|
string; `str.join`, `upper`, `lower`, `strip`, `lstrip`, `rstrip`,
|
|
24
26
|
`startswith`, `endswith`, `find`, `split` and `replace`.
|
|
@@ -37,11 +39,11 @@ Everything else raises
|
|
|
37
39
|
with a caret under the line at fault:
|
|
38
40
|
|
|
39
41
|
```
|
|
40
|
-
|
|
42
|
+
math.log() with a base is not supported
|
|
41
43
|
File "/srv/app/limits.py", line 12
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
hint:
|
|
44
|
+
buckets = math.log(n, 2)
|
|
45
|
+
^
|
|
46
|
+
hint: Lua 5.1's math.log takes no base; divide by math.log(base) instead.
|
|
45
47
|
```
|
|
46
48
|
|
|
47
49
|
Failing at import, loudly, is deliberate. A body that looks like Python but is
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "redis-lua-py"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.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"
|
|
@@ -41,6 +41,9 @@ dev = [
|
|
|
41
41
|
"zensical>=0.0.61",
|
|
42
42
|
# typing.assert_type, for tests/test_typing.py, arrived in Python 3.11.
|
|
43
43
|
"typing_extensions>=4.2; python_version < '3.11'",
|
|
44
|
+
# Tested against, never required: coredis shapes a command differently
|
|
45
|
+
# from redis-py, and tests/test_coredis.py runs the same scripts through it.
|
|
46
|
+
"coredis>=6.9",
|
|
44
47
|
]
|
|
45
48
|
|
|
46
49
|
[build-system]
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Turn a Python function into Lua.
|
|
2
|
+
|
|
3
|
+
The supported subset is deliberately small. Anything outside it raises
|
|
4
|
+
:class:`UnsupportedSyntax` pointing at the offending line, because a script
|
|
5
|
+
body that *looks* like Python but is never executed by Python is exactly the
|
|
6
|
+
place where a silent mistranslation would be most expensive.
|
|
7
|
+
|
|
8
|
+
The compiler itself is in layers, described in ``base``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import ast
|
|
14
|
+
import warnings
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any, TypeVar
|
|
18
|
+
|
|
19
|
+
from .. import _lua as lua
|
|
20
|
+
from .._script import CompiledScript
|
|
21
|
+
from ..errors import CompileError, NilTruncationWarning, render_location
|
|
22
|
+
from .analysis import unassigned_in_returns
|
|
23
|
+
from .helpers import helper_order, helper_source
|
|
24
|
+
from .source import parse_function, provenance
|
|
25
|
+
from .statements import Compiler
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"SCRIPT_FLAGS",
|
|
29
|
+
"CompiledBody",
|
|
30
|
+
"check_flags",
|
|
31
|
+
"compile_body",
|
|
32
|
+
"compile_function",
|
|
33
|
+
"helper_order",
|
|
34
|
+
"helper_source",
|
|
35
|
+
"provenance",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
R = TypeVar("R")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
#: The flags Redis 7 defines, for a script's `#!lua` line and for a function.
|
|
42
|
+
SCRIPT_FLAGS = frozenset(
|
|
43
|
+
{"no-writes", "allow-oom", "allow-stale", "no-cluster", "allow-cross-slot-keys"}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def check_flags(flags: tuple[str, ...], *, owner: str) -> tuple[str, ...]:
|
|
48
|
+
"""Refuse a flag Redis does not define, where the mistake is made."""
|
|
49
|
+
for flag in flags:
|
|
50
|
+
if flag not in SCRIPT_FLAGS:
|
|
51
|
+
raise CompileError(
|
|
52
|
+
f"{owner}: unknown flag {flag!r}; Redis defines {', '.join(sorted(SCRIPT_FLAGS))}"
|
|
53
|
+
)
|
|
54
|
+
return flags
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class CompiledBody:
|
|
59
|
+
"""A function compiled to Lua, before it is framed as a script or a library function."""
|
|
60
|
+
|
|
61
|
+
name: str
|
|
62
|
+
#: The prelude binding KEYS and ARGV, and the statements; no helpers.
|
|
63
|
+
body: str
|
|
64
|
+
#: The helpers the body calls, in the order they must be emitted.
|
|
65
|
+
helpers: tuple[str, ...]
|
|
66
|
+
params: tuple[str, ...]
|
|
67
|
+
keys: tuple[str, ...]
|
|
68
|
+
args: tuple[str, ...]
|
|
69
|
+
variadic_key: str | None
|
|
70
|
+
variadic_arg: str | None
|
|
71
|
+
doc: str | None
|
|
72
|
+
filename: str
|
|
73
|
+
first_lineno: int
|
|
74
|
+
#: Each parameter's annotation as written, in the order of ``params``.
|
|
75
|
+
param_annotations: tuple[str | None, ...] = ()
|
|
76
|
+
return_annotation: str | None = None
|
|
77
|
+
keyword_only: tuple[str, ...] = ()
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def provenance(self) -> str:
|
|
81
|
+
return f"{provenance(self.filename)}:{self.first_lineno}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def compile_function(
|
|
85
|
+
func: Callable[..., R],
|
|
86
|
+
*,
|
|
87
|
+
name: str | None = None,
|
|
88
|
+
header: bool = True,
|
|
89
|
+
flags: tuple[str, ...] = (),
|
|
90
|
+
) -> CompiledScript[R]:
|
|
91
|
+
compiled = compile_body(func, name=name)
|
|
92
|
+
parts: list[str] = []
|
|
93
|
+
if flags:
|
|
94
|
+
# Redis only reads a script's flags from a shebang on its first line.
|
|
95
|
+
parts.append(f"#!lua flags={','.join(flags)}")
|
|
96
|
+
if header:
|
|
97
|
+
parts.append(
|
|
98
|
+
f"-- {compiled.name}\n"
|
|
99
|
+
f"-- Generated by redis-lua-py from {compiled.provenance}. Do not edit."
|
|
100
|
+
)
|
|
101
|
+
parts.extend(helper_source(helper) for helper in compiled.helpers)
|
|
102
|
+
parts.append(compiled.body)
|
|
103
|
+
|
|
104
|
+
return CompiledScript(
|
|
105
|
+
name=compiled.name,
|
|
106
|
+
lua="\n".join(parts) + "\n",
|
|
107
|
+
params=compiled.params,
|
|
108
|
+
keys=compiled.keys,
|
|
109
|
+
args=compiled.args,
|
|
110
|
+
variadic_key=compiled.variadic_key,
|
|
111
|
+
variadic_arg=compiled.variadic_arg,
|
|
112
|
+
doc=compiled.doc,
|
|
113
|
+
source=f"{compiled.filename}:{compiled.first_lineno}",
|
|
114
|
+
param_annotations=compiled.param_annotations,
|
|
115
|
+
return_annotation=compiled.return_annotation,
|
|
116
|
+
keyword_only=compiled.keyword_only,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def compile_body(func: Callable[..., Any], *, name: str | None = None) -> CompiledBody:
|
|
121
|
+
node, filename, first_lineno, lines = parse_function(func)
|
|
122
|
+
compiler = Compiler(
|
|
123
|
+
node,
|
|
124
|
+
filename=filename,
|
|
125
|
+
first_lineno=first_lineno,
|
|
126
|
+
lines=lines,
|
|
127
|
+
# Receivers are resolved against the defining module, so the namespace
|
|
128
|
+
# is recognised under whatever name it was imported as.
|
|
129
|
+
globalns=getattr(func, "__globals__", {}),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
prelude = compiler.compile_signature()
|
|
133
|
+
compiler.collect_assigned()
|
|
134
|
+
|
|
135
|
+
body = node.body
|
|
136
|
+
doc = ast.get_docstring(node)
|
|
137
|
+
if doc is not None:
|
|
138
|
+
body = body[1:]
|
|
139
|
+
|
|
140
|
+
statements = compiler.block(body)
|
|
141
|
+
if compiler.hoisted:
|
|
142
|
+
prelude.append(lua.Local(sorted(set(compiler.hoisted)), []))
|
|
143
|
+
|
|
144
|
+
for element, unassigned in unassigned_in_returns(body, set(compiler.params), compiler.known):
|
|
145
|
+
warnings.warn_explicit(
|
|
146
|
+
render_location(
|
|
147
|
+
f"{unassigned!r} is not assigned on every path to this return, and a nil "
|
|
148
|
+
"in a returned table truncates the reply there",
|
|
149
|
+
filename=filename,
|
|
150
|
+
lineno=first_lineno + element.lineno - 1,
|
|
151
|
+
col=element.col_offset,
|
|
152
|
+
source_line=lines[element.lineno - 1] if element.lineno <= len(lines) else None,
|
|
153
|
+
hint="Give it a value before the branch, so that every branch returns a "
|
|
154
|
+
"table of the same shape.",
|
|
155
|
+
),
|
|
156
|
+
NilTruncationWarning,
|
|
157
|
+
filename,
|
|
158
|
+
first_lineno + element.lineno - 1,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return CompiledBody(
|
|
162
|
+
name=name or func.__name__,
|
|
163
|
+
body=lua.emit(prelude + statements).rstrip(),
|
|
164
|
+
helpers=tuple(helper_order(compiler.helpers)),
|
|
165
|
+
params=tuple(compiler.params),
|
|
166
|
+
keys=tuple(compiler.keys),
|
|
167
|
+
args=tuple(compiler.args),
|
|
168
|
+
variadic_key=compiler.variadic_key,
|
|
169
|
+
variadic_arg=compiler.variadic_arg,
|
|
170
|
+
doc=doc,
|
|
171
|
+
filename=filename,
|
|
172
|
+
first_lineno=first_lineno,
|
|
173
|
+
# Kept as source text: the compiler does not need them, but a generated
|
|
174
|
+
# function repeats them in its signature.
|
|
175
|
+
param_annotations=tuple(
|
|
176
|
+
None if arg.annotation is None else ast.unparse(arg.annotation)
|
|
177
|
+
for arg in [*node.args.args, *node.args.kwonlyargs]
|
|
178
|
+
),
|
|
179
|
+
return_annotation=None if node.returns is None else ast.unparse(node.returns),
|
|
180
|
+
keyword_only=tuple(arg.arg for arg in node.args.kwonlyargs),
|
|
181
|
+
)
|