redis-lua-py 0.3.0__tar.gz → 0.4.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.4.0/.release-please-manifest.json +3 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/CHANGELOG.md +12 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/PKG-INFO +1 -1
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/guide/calling-redis-commands.md +27 -2
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/guide/keys-and-arguments.md +36 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/reference/api.md +4 -2
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/reference/errors.md +4 -5
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/reference/lua-vs-python.md +35 -4
- redis_lua_py-0.4.0/docs/reference/supported-subset.md +43 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/pyproject.toml +1 -1
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/src/redis_lua_py/__init__.py +1 -1
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/src/redis_lua_py/_compile.py +603 -65
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/src/redis_lua_py/_lua.py +52 -2
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/src/redis_lua_py/_script.py +42 -5
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_errors.py +22 -18
- redis_lua_py-0.4.0/tests/test_mistranslations.py +113 -0
- redis_lua_py-0.4.0/tests/test_table_stakes.py +377 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/uv.lock +1 -1
- redis_lua_py-0.3.0/.release-please-manifest.json +0 -3
- redis_lua_py-0.3.0/docs/reference/supported-subset.md +0 -31
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/assets/README.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/assets/banner-dark.svg +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/assets/banner-light.svg +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/assets/generate.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/assets/logo.svg +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/assets/logomark.svg +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/assets/social-preview.png +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/assets/social-preview.svg +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/workflows/ci.yml +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/workflows/docs.yml +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/workflows/pr-title.yml +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/workflows/release.yml +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.gitignore +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.python-version +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/CONTRIBUTING.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/LICENSE +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/README.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/assets/logo.svg +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/contributing.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/development.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/examples.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/guide/async.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/guide/binary-values.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/guide/binding-a-client.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/guide/constants.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/guide/generated-lua.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/guide/return-values.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/guide/testing.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/index.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/installation.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/quickstart.md +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/stylesheets/extra.css +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/release-please-config.json +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/scripts/generate_commands.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/src/redis_lua_py/_commands.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/src/redis_lua_py/_runtime.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/src/redis_lua_py/errors.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/src/redis_lua_py/py.typed +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/conftest.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/module_with_client.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/module_without_import.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_binary.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_binding.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_commands.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_compile.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_constants.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_execute.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_namespace.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_semantics.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_typing.py +0 -0
- {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/zensical.toml +0 -0
|
@@ -7,6 +7,18 @@ 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.4.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.3.0...v0.4.0) (2026-09-12)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
* compile variable keys, splat calls, and/or values, helper functions and dict loops ([#10](https://github.com/IgnaceMaes/redis-lua-py/issues/10)) ([271f3f6](https://github.com/IgnaceMaes/redis-lua-py/commit/271f3f6be23d78246d91cde5de4fec05cee28832))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
* stop silently mistranslating reassigned parameters, infinity, == None and set_repl ([#8](https://github.com/IgnaceMaes/redis-lua-py/issues/8)) ([930c382](https://github.com/IgnaceMaes/redis-lua-py/commit/930c38283f366fd9042c4cc20cdf7eeb34e2d8a5))
|
|
21
|
+
|
|
10
22
|
## [0.3.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.2.1...v0.3.0) (2026-09-12)
|
|
11
23
|
|
|
12
24
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: redis-lua-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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/
|
|
@@ -5,8 +5,33 @@ split into subcommand tokens, so `redis.script_load(x)` compiles to
|
|
|
5
5
|
`redis.call('SCRIPT', 'LOAD', x)`.
|
|
6
6
|
|
|
7
7
|
`redis.pcall`, `redis.error_reply`, `redis.status_reply`, `redis.sha1hex`,
|
|
8
|
-
`redis.log`
|
|
9
|
-
names.
|
|
8
|
+
`redis.log`, `redis.set_repl`, `redis.acl_check_cmd` and `cjson.encode` /
|
|
9
|
+
`cjson.decode` pass through under their own names. So do the constants they
|
|
10
|
+
take: `redis.LOG_WARNING` and the other log levels, `redis.REPL_ALL` and the
|
|
11
|
+
other replication modes, and `redis.REDIS_VERSION`.
|
|
12
|
+
|
|
13
|
+
## Splatting a list into a command
|
|
14
|
+
|
|
15
|
+
A starred argument compiles to `unpack`, which is how a list becomes the
|
|
16
|
+
arguments of a command:
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
@script
|
|
20
|
+
def push_all(queue: Key, jobs: list[str]) -> int:
|
|
21
|
+
return redis.rpush(queue, *jobs)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```lua
|
|
25
|
+
return redis.call('RPUSH', queue, unpack(jobs))
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
It has to be the last argument, because Lua's `unpack` only expands there.
|
|
29
|
+
|
|
30
|
+
!!! warning "unpack has a limit"
|
|
31
|
+
|
|
32
|
+
`unpack` puts every element on Lua's stack at once, and Redis' Lua refuses
|
|
33
|
+
past roughly eight thousand values with "too many results to unpack".
|
|
34
|
+
Split a list that can grow that large into chunks, one command per chunk.
|
|
10
35
|
|
|
11
36
|
## Names are checked, not just uppercased
|
|
12
37
|
|
|
@@ -46,6 +46,42 @@ the value is only being carried.
|
|
|
46
46
|
`bool` encodes to `"1"` or `"0"`. Paired with an `int` annotation that deletes
|
|
47
47
|
the `1 if flag else 0` from the call site: pass `True`, and the body gets `1`.
|
|
48
48
|
|
|
49
|
+
## A variable number of keys or arguments
|
|
50
|
+
|
|
51
|
+
Annotate a parameter `list[Key]` to take any number of keys, or `list[str]`,
|
|
52
|
+
`list[int]` and so on to take any number of arguments. Its elements fill
|
|
53
|
+
whatever is left of `KEYS` or `ARGV` after the fixed parameters, wherever the
|
|
54
|
+
list is declared, and arrive in the body as a table:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
@script
|
|
58
|
+
def delete_tagged(tag: Key, keys: list[Key], stamp: int) -> int:
|
|
59
|
+
redis.set(tag, stamp)
|
|
60
|
+
return redis.delete(*keys)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
delete_tagged(client, tag="purged", keys=["a", "b", "c"], stamp=1700000000)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```lua
|
|
67
|
+
local tag = KEYS[1]
|
|
68
|
+
local stamp = tonumber(ARGV[1])
|
|
69
|
+
local keys = {}
|
|
70
|
+
for __i1 = 2, #KEYS do
|
|
71
|
+
keys[#keys + 1] = KEYS[__i1]
|
|
72
|
+
end
|
|
73
|
+
redis.call('SET', tag, stamp)
|
|
74
|
+
return redis.call('DEL', unpack(keys))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A script can take one list of keys and one list of arguments, since `KEYS`
|
|
78
|
+
and `ARGV` only have positions to tell them apart. Every element of a
|
|
79
|
+
`list[Key]` is a declared key, so Redis Cluster routes on all of them. An
|
|
80
|
+
element annotation of `int` or `float` converts each element, as it would a
|
|
81
|
+
single argument. Passing a string where a list is expected raises
|
|
82
|
+
[`ScriptArgumentError`](../reference/errors.md#scriptargumenterror) rather
|
|
83
|
+
than spreading it into characters.
|
|
84
|
+
|
|
49
85
|
## Positional or keyword
|
|
50
86
|
|
|
51
87
|
Scripts accept either; keyword is clearer at the call site and is what the
|
|
@@ -105,8 +105,10 @@ script(client, /, *positional, **keyword) -> Awaitable[R] # async client
|
|
|
105
105
|
| `name` | `str` | the script's name, in the header and in errors |
|
|
106
106
|
| `lua` | `str` | the full Lua source, exactly as sent to Redis |
|
|
107
107
|
| `params` | `tuple[str, ...]` | every parameter, in declaration order |
|
|
108
|
-
| `keys` | `tuple[str, ...]` | the parameters annotated `Key` |
|
|
109
|
-
| `args` | `tuple[str, ...]` | everything else |
|
|
108
|
+
| `keys` | `tuple[str, ...]` | the parameters annotated `Key` or `list[Key]`, in `KEYS` order |
|
|
109
|
+
| `args` | `tuple[str, ...]` | everything else, in `ARGV` order |
|
|
110
|
+
| `variadic_key` | `str \| None` | the `list[Key]` parameter, which fills the rest of `KEYS` |
|
|
111
|
+
| `variadic_arg` | `str \| None` | the list parameter that fills the rest of `ARGV` |
|
|
110
112
|
| `doc` | `str \| None` | the function's docstring |
|
|
111
113
|
| `source` | `str` | where it was defined, repo-relative |
|
|
112
114
|
|
|
@@ -4,12 +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
|
+
Lua 5.1 has no 'continue' statement
|
|
8
8
|
File "/srv/app/limits.py", line 12
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
hint:
|
|
12
|
-
difference in truthiness. Use an if statement instead.
|
|
9
|
+
continue
|
|
10
|
+
^
|
|
11
|
+
hint: Invert the condition and put the rest of the loop body inside the if.
|
|
13
12
|
```
|
|
14
13
|
|
|
15
14
|
## The hierarchy
|
|
@@ -14,7 +14,8 @@ A Redis command with nothing to return hands Lua `false`, not `nil`. This is
|
|
|
14
14
|
the classic trap: a hand-written `== nil` never matches, so the branch silently
|
|
15
15
|
never runs. `x is None` compiles to a helper accepting both, which also takes
|
|
16
16
|
`x` as an argument — so `if redis.hget(k, f) is None:` does not run the command
|
|
17
|
-
twice.
|
|
17
|
+
twice. `x == None` and `x != None` compile to the same helper, since that is
|
|
18
|
+
plainly what they mean.
|
|
18
19
|
|
|
19
20
|
## Indexing is closed
|
|
20
21
|
|
|
@@ -32,10 +33,40 @@ the top of the script, so it does not silently read back `nil`.
|
|
|
32
33
|
|
|
33
34
|
Use an f-string, which compiles to Lua's `..`.
|
|
34
35
|
|
|
35
|
-
## `and
|
|
36
|
+
## `and`, `or` and conditional expressions are closed
|
|
36
37
|
|
|
37
|
-
In Python
|
|
38
|
-
truthiness
|
|
38
|
+
In Python, `a or b` returns one of its operands, chosen by Python's
|
|
39
|
+
truthiness. Lua's own `or` uses Lua's truthiness, where `0` and `''` are true,
|
|
40
|
+
so the Lua idiom `tonumber(x) or 0` means something different there.
|
|
41
|
+
|
|
42
|
+
A right side that is a name or a literal compiles to a small `__or` / `__and`
|
|
43
|
+
helper. Anything else is wrapped in a function called on the spot, so that it
|
|
44
|
+
only runs when Python would run it. `flag or redis.incr(k)` does not
|
|
45
|
+
increment when `flag` is set.
|
|
46
|
+
|
|
47
|
+
`a if c else b` compiles to `c and a or b` when `a` is a literal. Otherwise it
|
|
48
|
+
becomes the same kind of function, because `c and a or b` is wrong whenever
|
|
49
|
+
`a` can be false or nil.
|
|
50
|
+
|
|
51
|
+
## Dicts iterate in no particular order
|
|
52
|
+
|
|
53
|
+
`.items()`, `.keys()` and `.values()` compile to Lua's `pairs()`, which visits
|
|
54
|
+
entries in no fixed order. Sort the result if the order reaches the caller.
|
|
55
|
+
|
|
56
|
+
Iterating a dict directly with `for k in d` walks its array part, which a dict
|
|
57
|
+
does not have, so the loop never runs. Say `.keys()`.
|
|
58
|
+
|
|
59
|
+
## Redis replies are flat lists, not dicts
|
|
60
|
+
|
|
61
|
+
To Lua, `HGETALL` returns `[field, value, field, value, ...]`, not a table
|
|
62
|
+
keyed by field, so `.items()` on it does not mean what it would in redis-py.
|
|
63
|
+
Walk it in pairs instead:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
fields = redis.hgetall(k)
|
|
67
|
+
for i in range(0, len(fields), 2):
|
|
68
|
+
redis.hset(copy, fields[i], fields[i + 1])
|
|
69
|
+
```
|
|
39
70
|
|
|
40
71
|
## There is no `continue`
|
|
41
72
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# The supported subset
|
|
2
|
+
|
|
3
|
+
A script body is Python that Python never runs, so only the part of the
|
|
4
|
+
language with a faithful Lua meaning is accepted.
|
|
5
|
+
|
|
6
|
+
**Supported:**
|
|
7
|
+
|
|
8
|
+
- **Statements:** assignment, including unpacking (`a, b = b, a`);
|
|
9
|
+
augmented assignment; `if`/`elif`/`else`; `while`; `break`; `return`.
|
|
10
|
+
- **Loops:** `for ... in` over a table, `range()`, `enumerate()`, or a dict's
|
|
11
|
+
`.items()`, `.keys()` and `.values()`, binding a name or a tuple of names.
|
|
12
|
+
- **Helper functions** defined with `def` at the top level of the body. They
|
|
13
|
+
can call each other and themselves.
|
|
14
|
+
- **Expressions:** comparisons; arithmetic; `and`/`or`, and `a if c else b`,
|
|
15
|
+
both as values; f-strings; list and dict literals.
|
|
16
|
+
- **Builtins and methods:** `len()`, `int()`, `float()`, `str()`, `min()`,
|
|
17
|
+
`max()`, `abs()`, `.append()`, `.insert()`, `.pop()` and `str.join()`.
|
|
18
|
+
- **The `math` module:** `floor`, `ceil`, `sqrt`, `fabs`, `fmod`, `exp`,
|
|
19
|
+
`log`, `log10` and `pow`, imported either way.
|
|
20
|
+
- **Calls:** into `redis` and `cjson`, with `*xs` allowed as the last argument.
|
|
21
|
+
- **Parameters:** `list[Key]` and `list[...]`, for a variable number of keys
|
|
22
|
+
and arguments.
|
|
23
|
+
- **Module-level constants.**
|
|
24
|
+
|
|
25
|
+
Everything else raises
|
|
26
|
+
[`UnsupportedSyntax`](errors.md#unsupportedsyntax) when the module is imported,
|
|
27
|
+
with a caret under the line at fault:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
Lua 5.1 has no 'continue' statement
|
|
31
|
+
File "/srv/app/limits.py", line 12
|
|
32
|
+
continue
|
|
33
|
+
^
|
|
34
|
+
hint: Invert the condition and put the rest of the loop body inside the if.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Failing at import, loudly, is deliberate. A body that looks like Python but is
|
|
38
|
+
never run by Python is exactly where a quiet mistranslation would cost the
|
|
39
|
+
most.
|
|
40
|
+
|
|
41
|
+
The gaps that a supported construct still carries — truthiness, 1-based
|
|
42
|
+
indexing, block scope — are covered in
|
|
43
|
+
[Where Lua differs from Python](lua-vs-python.md).
|