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.
Files changed (71) hide show
  1. redis_lua_py-0.4.0/.release-please-manifest.json +3 -0
  2. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/CHANGELOG.md +12 -0
  3. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/PKG-INFO +1 -1
  4. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/guide/calling-redis-commands.md +27 -2
  5. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/guide/keys-and-arguments.md +36 -0
  6. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/reference/api.md +4 -2
  7. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/reference/errors.md +4 -5
  8. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/reference/lua-vs-python.md +35 -4
  9. redis_lua_py-0.4.0/docs/reference/supported-subset.md +43 -0
  10. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/pyproject.toml +1 -1
  11. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/src/redis_lua_py/__init__.py +1 -1
  12. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/src/redis_lua_py/_compile.py +603 -65
  13. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/src/redis_lua_py/_lua.py +52 -2
  14. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/src/redis_lua_py/_script.py +42 -5
  15. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_errors.py +22 -18
  16. redis_lua_py-0.4.0/tests/test_mistranslations.py +113 -0
  17. redis_lua_py-0.4.0/tests/test_table_stakes.py +377 -0
  18. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/uv.lock +1 -1
  19. redis_lua_py-0.3.0/.release-please-manifest.json +0 -3
  20. redis_lua_py-0.3.0/docs/reference/supported-subset.md +0 -31
  21. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/assets/README.md +0 -0
  22. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/assets/banner-dark.svg +0 -0
  23. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/assets/banner-light.svg +0 -0
  24. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/assets/generate.py +0 -0
  25. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/assets/logo.svg +0 -0
  26. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/assets/logomark.svg +0 -0
  27. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/assets/social-preview.png +0 -0
  28. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/assets/social-preview.svg +0 -0
  29. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/workflows/ci.yml +0 -0
  30. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/workflows/docs.yml +0 -0
  31. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/workflows/pr-title.yml +0 -0
  32. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.github/workflows/release.yml +0 -0
  33. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.gitignore +0 -0
  34. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/.python-version +0 -0
  35. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/CONTRIBUTING.md +0 -0
  36. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/LICENSE +0 -0
  37. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/README.md +0 -0
  38. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/assets/logo.svg +0 -0
  39. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/contributing.md +0 -0
  40. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/development.md +0 -0
  41. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/examples.md +0 -0
  42. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/guide/async.md +0 -0
  43. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/guide/binary-values.md +0 -0
  44. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/guide/binding-a-client.md +0 -0
  45. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/guide/constants.md +0 -0
  46. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/guide/generated-lua.md +0 -0
  47. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/guide/return-values.md +0 -0
  48. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/guide/testing.md +0 -0
  49. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/index.md +0 -0
  50. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/installation.md +0 -0
  51. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/quickstart.md +0 -0
  52. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/docs/stylesheets/extra.css +0 -0
  53. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/release-please-config.json +0 -0
  54. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/scripts/generate_commands.py +0 -0
  55. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/src/redis_lua_py/_commands.py +0 -0
  56. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/src/redis_lua_py/_runtime.py +0 -0
  57. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/src/redis_lua_py/errors.py +0 -0
  58. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/src/redis_lua_py/py.typed +0 -0
  59. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/conftest.py +0 -0
  60. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/module_with_client.py +0 -0
  61. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/module_without_import.py +0 -0
  62. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_binary.py +0 -0
  63. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_binding.py +0 -0
  64. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_commands.py +0 -0
  65. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_compile.py +0 -0
  66. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_constants.py +0 -0
  67. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_execute.py +0 -0
  68. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_namespace.py +0 -0
  69. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_semantics.py +0 -0
  70. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/tests/test_typing.py +0 -0
  71. {redis_lua_py-0.3.0 → redis_lua_py-0.4.0}/zensical.toml +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.4.0"
3
+ }
@@ -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.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` and `cjson.encode` / `cjson.decode` pass through under their own
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
- 'and'/'or' are only supported in an if or while condition
7
+ Lua 5.1 has no 'continue' statement
8
8
  File "/srv/app/limits.py", line 12
9
- flag = a and b
10
- ^
11
- hint: In Python these return an operand, which does not survive the
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` / `or` work only in conditions
36
+ ## `and`, `or` and conditional expressions are closed
36
37
 
37
- In Python they return an operand, not a boolean, and that does not survive the
38
- truthiness difference. Use an `if`.
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).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "redis-lua-py"
3
- version = "0.3.0"
3
+ version = "0.4.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"
@@ -62,7 +62,7 @@ __all__ = [
62
62
  "script",
63
63
  ]
64
64
 
65
- __version__ = "0.3.0" # x-release-please-version
65
+ __version__ = "0.4.0" # x-release-please-version
66
66
 
67
67
 
68
68
  @overload