redis-lua-py 0.4.0__tar.gz → 0.5.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 (81) hide show
  1. redis_lua_py-0.5.0/.release-please-manifest.json +3 -0
  2. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/CHANGELOG.md +10 -0
  3. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/PKG-INFO +9 -1
  4. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/README.md +8 -0
  5. redis_lua_py-0.5.0/docs/guide/build-time.md +152 -0
  6. redis_lua_py-0.5.0/docs/guide/redis-functions.md +139 -0
  7. redis_lua_py-0.5.0/docs/reference/api.md +284 -0
  8. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/reference/errors.md +16 -5
  9. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/reference/lua-vs-python.md +81 -6
  10. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/reference/supported-subset.md +18 -8
  11. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/pyproject.toml +4 -1
  12. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/src/redis_lua_py/__init__.py +15 -6
  13. redis_lua_py-0.5.0/src/redis_lua_py/__main__.py +73 -0
  14. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/src/redis_lua_py/_compile.py +875 -81
  15. redis_lua_py-0.5.0/src/redis_lua_py/_library.py +256 -0
  16. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/src/redis_lua_py/_lua.py +24 -0
  17. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/src/redis_lua_py/_script.py +82 -36
  18. redis_lua_py-0.5.0/src/redis_lua_py/codegen.py +221 -0
  19. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/src/redis_lua_py/errors.py +9 -0
  20. redis_lua_py-0.5.0/tests/codegen_scripts.py +41 -0
  21. redis_lua_py-0.5.0/tests/test_codegen.py +229 -0
  22. redis_lua_py-0.5.0/tests/test_control_flow.py +322 -0
  23. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_errors.py +29 -34
  24. redis_lua_py-0.5.0/tests/test_functions.py +223 -0
  25. redis_lua_py-0.5.0/tests/test_strings_and_indexing.py +284 -0
  26. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_typing.py +19 -1
  27. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/uv.lock +1 -1
  28. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/zensical.toml +2 -0
  29. redis_lua_py-0.4.0/.release-please-manifest.json +0 -3
  30. redis_lua_py-0.4.0/docs/reference/api.md +0 -146
  31. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/assets/README.md +0 -0
  32. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/assets/banner-dark.svg +0 -0
  33. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/assets/banner-light.svg +0 -0
  34. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/assets/generate.py +0 -0
  35. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/assets/logo.svg +0 -0
  36. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/assets/logomark.svg +0 -0
  37. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/assets/social-preview.png +0 -0
  38. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/assets/social-preview.svg +0 -0
  39. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/workflows/ci.yml +0 -0
  40. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/workflows/docs.yml +0 -0
  41. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/workflows/pr-title.yml +0 -0
  42. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.github/workflows/release.yml +0 -0
  43. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.gitignore +0 -0
  44. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/.python-version +0 -0
  45. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/CONTRIBUTING.md +0 -0
  46. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/LICENSE +0 -0
  47. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/assets/logo.svg +0 -0
  48. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/contributing.md +0 -0
  49. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/development.md +0 -0
  50. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/examples.md +0 -0
  51. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/guide/async.md +0 -0
  52. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/guide/binary-values.md +0 -0
  53. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/guide/binding-a-client.md +0 -0
  54. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/guide/calling-redis-commands.md +0 -0
  55. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/guide/constants.md +0 -0
  56. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/guide/generated-lua.md +0 -0
  57. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/guide/keys-and-arguments.md +0 -0
  58. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/guide/return-values.md +0 -0
  59. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/guide/testing.md +0 -0
  60. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/index.md +0 -0
  61. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/installation.md +0 -0
  62. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/quickstart.md +0 -0
  63. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/docs/stylesheets/extra.css +0 -0
  64. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/release-please-config.json +0 -0
  65. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/scripts/generate_commands.py +0 -0
  66. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/src/redis_lua_py/_commands.py +0 -0
  67. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/src/redis_lua_py/_runtime.py +0 -0
  68. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/src/redis_lua_py/py.typed +0 -0
  69. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/conftest.py +0 -0
  70. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/module_with_client.py +0 -0
  71. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/module_without_import.py +0 -0
  72. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_binary.py +0 -0
  73. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_binding.py +0 -0
  74. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_commands.py +0 -0
  75. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_compile.py +0 -0
  76. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_constants.py +0 -0
  77. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_execute.py +0 -0
  78. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_mistranslations.py +0 -0
  79. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_namespace.py +0 -0
  80. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_semantics.py +0 -0
  81. {redis_lua_py-0.4.0 → redis_lua_py-0.5.0}/tests/test_table_stakes.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.5.0"
3
+ }
@@ -7,6 +7,16 @@ 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.5.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.4.0...v0.5.0) (2026-09-12)
11
+
12
+
13
+ ### Added
14
+
15
+ * compile continue, try/except/else/finally, raise and assert ([#11](https://github.com/IgnaceMaes/redis-lua-py/issues/11)) ([aeca575](https://github.com/IgnaceMaes/redis-lua-py/commit/aeca5757fffe42e88b49b18fa4d3fe91897fad2e))
16
+ * compile membership, slicing, string methods and formatting, and key variables ([#13](https://github.com/IgnaceMaes/redis-lua-py/issues/13)) ([4880c81](https://github.com/IgnaceMaes/redis-lua-py/commit/4880c8156d849755259dea9a8c29cf69392ecd7e))
17
+ * compile redis functions libraries, and script flags ([#15](https://github.com/IgnaceMaes/redis-lua-py/issues/15)) ([5980f03](https://github.com/IgnaceMaes/redis-lua-py/commit/5980f0317acbe9a7a96ffaff029f2d76143b7e32))
18
+ * generate lua ahead of time for code that ships without redis-lua-py ([#14](https://github.com/IgnaceMaes/redis-lua-py/issues/14)) ([83d6b9d](https://github.com/IgnaceMaes/redis-lua-py/commit/83d6b9da0b540cefc0be9ec51fbead54c4d0fade))
19
+
10
20
  ## [0.4.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.3.0...v0.4.0) (2026-09-12)
11
21
 
12
22
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: redis-lua-py
3
- Version: 0.4.0
3
+ Version: 0.5.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/
@@ -144,6 +144,14 @@ script cache is cold once per script rather than once per environment.
144
144
  the same script object, 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
+ - **[Build-time generation](https://ignacemaes.com/redis-lua-py/guide/build-time/)**
148
+ for libraries — `python -m redis_lua_py generate` writes the Lua to a module
149
+ of plain strings, so your users never depend on this package, and `--check`
150
+ keeps it current in CI.
151
+ - **[Redis Functions](https://ignacemaes.com/redis-lua-py/guide/redis-functions/)** —
152
+ the same Python compiles into a function library, loaded with `FUNCTION LOAD`
153
+ on first use and called with `FCALL`, and scripts take Redis 7 flags such as
154
+ `no-writes`.
147
155
  - **[The gaps between Lua and Python](https://ignacemaes.com/redis-lua-py/reference/lua-vs-python/)**
148
156
  are closed or refused — truthiness, 1-based indexing, `false` versus `nil`,
149
157
  block scope, and the nil that truncates a returned table.
@@ -118,6 +118,14 @@ script cache is cold once per script rather than once per environment.
118
118
  the same script object, 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
+ - **[Build-time generation](https://ignacemaes.com/redis-lua-py/guide/build-time/)**
122
+ for libraries — `python -m redis_lua_py generate` writes the Lua to a module
123
+ of plain strings, so your users never depend on this package, and `--check`
124
+ keeps it current in CI.
125
+ - **[Redis Functions](https://ignacemaes.com/redis-lua-py/guide/redis-functions/)** —
126
+ the same Python compiles into a function library, loaded with `FUNCTION LOAD`
127
+ on first use and called with `FCALL`, and scripts take Redis 7 flags such as
128
+ `no-writes`.
121
129
  - **[The gaps between Lua and Python](https://ignacemaes.com/redis-lua-py/reference/lua-vs-python/)**
122
130
  are closed or refused — truthiness, 1-based indexing, `false` versus `nil`,
123
131
  block scope, and the nil that truncates a returned table.
@@ -0,0 +1,152 @@
1
+ # Shipping without the dependency
2
+
3
+ Calling a script needs redis-lua-py at runtime: it compiles the body when the
4
+ module is imported, and resolves keys and arguments on every call. In an
5
+ application that is a dependency you chose. In a library it is a dependency
6
+ every one of your users inherits, along with the import-time compile, for Lua
7
+ that never changes between your releases.
8
+
9
+ So a library can compile ahead of time instead. You write the scripts with
10
+ `@script`, as anywhere else, and generate plain Lua from them while you
11
+ develop. What you ship is that Lua: no import of this package, no compile
12
+ step, nothing added to your dependencies.
13
+
14
+ ## Generate a module of strings
15
+
16
+ Keep the scripts outside the package you ship, and redis-lua-py in your
17
+ development dependencies:
18
+
19
+ ```bash
20
+ uv add --dev redis-lua-py
21
+ ```
22
+
23
+ ```
24
+ myproj/
25
+ ├── pyproject.toml
26
+ ├── redis_scripts/
27
+ │ └── limits.py # the @script functions; never shipped
28
+ └── src/myproj/
29
+ ├── _lua.py # generated, checked in, shipped
30
+ └── limits.py # the code that runs them
31
+ ```
32
+
33
+ Then, from the project root:
34
+
35
+ ```bash
36
+ python -m redis_lua_py generate redis_scripts.limits --out src/myproj/_lua.py
37
+ ```
38
+
39
+ The output imports nothing. Each script is a string constant, named after the
40
+ function in capitals, with a comment recording the order its keys and
41
+ arguments go in:
42
+
43
+ ```python
44
+ """Redis Lua scripts compiled by redis-lua-py from redis_scripts.limits. Do not edit.
45
+
46
+ Regenerate with:
47
+
48
+ python -m redis_lua_py generate redis_scripts.limits --out src/myproj/_lua.py
49
+ """
50
+
51
+ __all__ = [
52
+ "RATE_LIMIT",
53
+ ]
54
+
55
+ # rate_limit -- KEYS: key; ARGV: limit, ttl
56
+ RATE_LIMIT = """\
57
+ -- rate_limit
58
+ -- Generated by redis-lua-py from redis_scripts/limits.py:6. Do not edit.
59
+ local key = KEYS[1]
60
+ local limit = tonumber(ARGV[1])
61
+ local ttl = tonumber(ARGV[2])
62
+ local current = redis.call('INCR', key)
63
+ if current == 1 then
64
+ redis.call('EXPIRE', key, ttl)
65
+ end
66
+ if current > limit then
67
+ return -1
68
+ end
69
+ return limit - current
70
+ """
71
+ ```
72
+
73
+ Your library runs it the way it would run any Lua, through redis-py:
74
+
75
+ ```python
76
+ from redis import Redis
77
+
78
+ from ._lua import RATE_LIMIT
79
+
80
+
81
+ class Limiter:
82
+ def __init__(self, client: Redis) -> None:
83
+ self._rate_limit = client.register_script(RATE_LIMIT)
84
+
85
+ def hit(self, key: str, limit: int, ttl: int) -> int:
86
+ return self._rate_limit(keys=[key], args=[limit, ttl])
87
+ ```
88
+
89
+ Every path in the file is relative to the project root, so it comes out
90
+ byte-for-byte the same on every machine, and regenerating it without a change
91
+ to the scripts leaves it untouched.
92
+
93
+ ## Or one `.lua` file per script
94
+
95
+ Pass a directory instead of a `.py` path:
96
+
97
+ ```bash
98
+ python -m redis_lua_py generate redis_scripts.limits --out src/myproj/lua/
99
+ ```
100
+
101
+ That writes `rate_limit.lua` and a file for every other script, for a library
102
+ that already loads its Lua from files. When a script is renamed or removed, its
103
+ old file is deleted. Only files carrying the generated header are ever
104
+ deleted, so hand-written Lua in the same directory is safe. The flip side is
105
+ that a script compiled with `header=False` leaves its old file behind when it
106
+ is renamed.
107
+
108
+ ## Keep it current
109
+
110
+ Checking generated code in only works if something notices when it goes stale.
111
+ `--check` writes nothing, and exits 1 with a diff if the output no longer
112
+ matches the scripts. Run it in CI:
113
+
114
+ ```yaml
115
+ - run: uv run python -m redis_lua_py generate redis_scripts.limits --out src/myproj/_lua.py --check
116
+ ```
117
+
118
+ Or as a test, which fails with the same diff:
119
+
120
+ ```python
121
+ from redis_lua_py import codegen
122
+
123
+
124
+ def test_generated_lua_is_current():
125
+ codegen.check("redis_scripts.limits", "src/myproj/_lua.py")
126
+ ```
127
+
128
+ A test imports `redis_scripts.limits` like any other module, so the project
129
+ root has to be on the path. With pytest, set `pythonpath = ["."]` under
130
+ `[tool.pytest.ini_options]`.
131
+
132
+ ## What you give up
133
+
134
+ The generated Lua is identical to what a `@script` call would send, so the
135
+ behaviour inside Redis is the same. What stays behind is the caller's side:
136
+
137
+ - **Argument checking.** A call through `CompiledScript` refuses a missing,
138
+ misspelled or duplicated argument, and a value such as `None` that has no
139
+ Redis representation. With a plain string, you pass `keys` and `args` in the
140
+ order the comment records, and nothing checks them.
141
+ - **Booleans.** A `CompiledScript` sends `True` as `1`. redis-py refuses a
142
+ `bool` outright, so convert it yourself.
143
+ - **Cluster pipelines.** A `CompiledScript` switches to `EVAL` on a redis-py
144
+ cluster pipeline, which refuses `EVALSHA`. A registered string does not.
145
+
146
+ Keep writing tests that call the scripts through `@script` against
147
+ [fakeredis](testing.md#run-the-behaviour-without-a-server) as well: those run
148
+ the same Lua, with the checks still in place.
149
+
150
+ The script module itself is ordinary Python, so point your type checker and
151
+ linter at it along with the rest of the project. Lines in the generated file
152
+ are as long as the Lua's, so leave that one file out of any line-length rule.
@@ -0,0 +1,139 @@
1
+ # Redis Functions
2
+
3
+ A script is sent with `EVALSHA`, and lives in a cache the server is free to
4
+ drop. Since Redis 7 there is an alternative: a **function library**. It is
5
+ loaded once with `FUNCTION LOAD`, persists and replicates with the data, and
6
+ each function in it is called by name with `FCALL`.
7
+
8
+ The same Python compiles to either. A body follows exactly the rules of a
9
+ script's.
10
+
11
+ ```python
12
+ from redis_lua_py import Key, Library, redis
13
+
14
+ limits = Library("limits")
15
+
16
+
17
+ @limits.function
18
+ def hit(key: Key, ttl: int) -> int:
19
+ current = redis.incr(key)
20
+ if current == 1:
21
+ redis.expire(key, ttl)
22
+ return current
23
+
24
+
25
+ @limits.function(flags=["no-writes"])
26
+ def peek(key: Key) -> int:
27
+ return int(redis.get(key) or 0)
28
+ ```
29
+
30
+ ```python
31
+ hit(client, key="user:42", ttl=60) # FCALL hit 1 user:42 60
32
+ peek(client, key="user:42") # FCALL_RO peek 1 user:42
33
+ ```
34
+
35
+ A function is called like a script: pass a sync client and you get a value,
36
+ an async one and you get an awaitable, and [`bind`](binding-a-client.md)
37
+ works the same way.
38
+
39
+ ## Loading
40
+
41
+ There is nothing to do up front. When a call finds that the server does not
42
+ have the library, it loads it with `FUNCTION LOAD REPLACE` and tries again.
43
+
44
+ To load it explicitly, at deploy time for instance:
45
+
46
+ ```python
47
+ limits.load(client)
48
+ ```
49
+
50
+ !!! warning "Load before a pipeline"
51
+
52
+ A call queued in a pipeline cannot load the library halfway through
53
+ `execute()`. Call `load()` before queueing calls to a library the server
54
+ may not have yet.
55
+
56
+ On Redis Cluster, redis-py sends `FUNCTION LOAD` to every primary, and routes
57
+ `FCALL` on the function's keys, as it does a script.
58
+
59
+ ## What the library looks like
60
+
61
+ `limits.lua` is the whole library, exactly as `FUNCTION LOAD` receives it:
62
+
63
+ ```lua
64
+ #!lua name=limits
65
+ -- Generated by redis-lua-py. Do not edit.
66
+ -- Python truthiness: 0, '', empty tables and nil are all false.
67
+ local function __truthy(v)
68
+ if v == nil or v == false then return false end
69
+ if v == 0 or v == '' then return false end
70
+ if type(v) == 'table' and next(v) == nil then return false end
71
+ return true
72
+ end
73
+ -- Python's `a or b`: a when it is truthy by Python's rules, otherwise b.
74
+ local function __or(a, b)
75
+ if __truthy(a) then return a end
76
+ return b
77
+ end
78
+ -- hit, from limits.py:6
79
+ redis.register_function{
80
+ function_name = 'hit',
81
+ callback = function(KEYS, ARGV)
82
+ local key = KEYS[1]
83
+ local ttl = tonumber(ARGV[1])
84
+ local current = redis.call('INCR', key)
85
+ if current == 1 then
86
+ redis.call('EXPIRE', key, ttl)
87
+ end
88
+ return current
89
+ end,
90
+ }
91
+ -- peek, from limits.py:14
92
+ redis.register_function{
93
+ function_name = 'peek',
94
+ callback = function(KEYS, ARGV)
95
+ local key = KEYS[1]
96
+ return tonumber(__or(redis.call('GET', key), 0))
97
+ end,
98
+ flags = {'no-writes'},
99
+ }
100
+ ```
101
+
102
+ A helper any function needs is emitted once, at the top, where every callback
103
+ can see it. The callback names its parameters `KEYS` and `ARGV`, so a function
104
+ body compiles to exactly what the same body would as a script.
105
+
106
+ Library and function names take letters, digits and underscores, which is all
107
+ Redis accepts. Function names are global on the server, so two libraries
108
+ cannot both define a `hit`.
109
+
110
+ ## Flags
111
+
112
+ `flags` takes the flags Redis defines: `no-writes`, `allow-oom`,
113
+ `allow-stale`, `no-cluster` and `allow-cross-slot-keys`. A `no-writes`
114
+ function is called with `FCALL_RO`, which a replica accepts.
115
+
116
+ Scripts take the same flags. Redis 7 reads them from a `#!lua` line at the very
117
+ top of the script, which is where they go:
118
+
119
+ ```python
120
+ @script(flags=["no-writes"])
121
+ def read(key: Key) -> bytes:
122
+ return redis.get(key)
123
+ ```
124
+
125
+ ```lua
126
+ #!lua flags=no-writes
127
+ -- read
128
+ -- Generated by redis-lua-py from app/cache.py:12. Do not edit.
129
+ local key = KEYS[1]
130
+ return redis.call('GET', key)
131
+ ```
132
+
133
+ A misspelled flag is refused at import, like everything else.
134
+
135
+ ## Testing
136
+
137
+ fakeredis has neither `FUNCTION` nor script flags. Test a library against a
138
+ real server, or check its source as a golden file: `limits.lua` is the whole
139
+ of it, as [a script's `.lua` is](testing.md#snapshot-the-lua).
@@ -0,0 +1,284 @@
1
+ # API reference
2
+
3
+ Everything in `redis_lua_py.__all__`. The package is small on purpose: one
4
+ decorator, a library for Redis Functions, one annotation, two namespaces, and
5
+ the errors.
6
+
7
+ ```python
8
+ from redis_lua_py import Key, Library, call, cjson, redis, script
9
+ from redis_lua_py import BoundScript, CompiledScript, LibraryFunction
10
+ ```
11
+
12
+ ## `script`
13
+
14
+ ```python
15
+ def script(
16
+ func: Callable[..., R] | None = None,
17
+ /,
18
+ *,
19
+ name: str | None = None,
20
+ header: bool = True,
21
+ flags: Iterable[str] = (),
22
+ ) -> CompiledScript[R] | Callable[[Callable[..., R]], CompiledScript[R]]
23
+ ```
24
+
25
+ Compile a function into a Redis Lua script. Usable bare or called:
26
+
27
+ ```python
28
+ @script
29
+ def touch(key: Key) -> int: ...
30
+
31
+
32
+ @script(name="touch_v2", header=False)
33
+ def touch(key: Key) -> int: ...
34
+ ```
35
+
36
+ | Parameter | Meaning |
37
+ | --- | --- |
38
+ | `name` | overrides the name in the generated header and in errors |
39
+ | `header` | `False` drops the provenance comment entirely, for anyone who wants the script body and nothing else |
40
+ | `flags` | script flags for Redis 7, such as `no-writes`, written on a `#!lua` first line; see [Redis Functions](../guide/redis-functions.md#flags) |
41
+
42
+ Parameters annotated [`Key`](#key) become `KEYS`, in declaration order; every
43
+ other parameter becomes `ARGV`. See
44
+ [Keys and arguments](../guide/keys-and-arguments.md).
45
+
46
+ The return annotation describes what the *caller* gets back, and is carried
47
+ through to the call: `-> int` makes the script a `CompiledScript[int]`. The
48
+ compiler itself does not read it. See
49
+ [What the caller gets](../guide/return-values.md).
50
+
51
+ Define scripts at module level, where they compile once at import.
52
+
53
+ Raises [`UnsupportedSyntax`](errors.md#unsupportedsyntax) at decoration time,
54
+ pointing at the line at fault, if the body strays outside
55
+ [the supported subset](supported-subset.md).
56
+
57
+ ## `Key`
58
+
59
+ ```python
60
+ class Key(str)
61
+ ```
62
+
63
+ Marks a parameter as a Redis key. A `str` subclass, so it is inert at runtime
64
+ and the annotation is the whole of it.
65
+
66
+ Getting this right matters: Redis Cluster routes a script by its declared keys,
67
+ so a key passed as an argument will be invisible to the router.
68
+
69
+ ## `redis`
70
+
71
+ The script namespace. `redis.incr(key)` becomes `redis.call('INCR', key)`;
72
+ underscores split into subcommand tokens, so `redis.script_load(x)` becomes
73
+ `redis.call('SCRIPT', 'LOAD', x)`.
74
+
75
+ Names are checked at compile time against Redis' own command table;
76
+ `redis.call(...)` itself is never checked and is the escape hatch. See
77
+ [Calling Redis commands](../guide/calling-redis-commands.md).
78
+
79
+ The compiler identifies the namespace by value rather than by the name it is
80
+ imported under, so every alias works and nothing is reserved.
81
+
82
+ ## `call`
83
+
84
+ An alias of [`redis`](#redis), for modules that would rather not rename
85
+ anything.
86
+
87
+ ## `cjson`
88
+
89
+ The JSON library Redis exposes to scripts: `cjson.encode` and `cjson.decode`,
90
+ which pass through under their own names.
91
+
92
+ ## `CompiledScript`
93
+
94
+ ```python
95
+ class CompiledScript(Generic[R])
96
+ ```
97
+
98
+ What `@script` returns. Calling it runs `EVALSHA` and falls back to `EVAL` the
99
+ first time, or whenever the server has dropped the script from its cache.
100
+
101
+ ```python
102
+ script(client, /, *positional, **keyword) -> R # sync client
103
+ script(client, /, *positional, **keyword) -> Awaitable[R] # async client
104
+ ```
105
+
106
+ | Attribute | Type | What it is |
107
+ | --- | --- | --- |
108
+ | `name` | `str` | the script's name, in the header and in errors |
109
+ | `lua` | `str` | the full Lua source, exactly as sent to Redis |
110
+ | `params` | `tuple[str, ...]` | every parameter, in declaration order |
111
+ | `keys` | `tuple[str, ...]` | the parameters annotated `Key` or `list[Key]`, in `KEYS` order |
112
+ | `args` | `tuple[str, ...]` | everything else, in `ARGV` order |
113
+ | `variadic_key` | `str \| None` | the `list[Key]` parameter, which fills the rest of `KEYS` |
114
+ | `variadic_arg` | `str \| None` | the list parameter that fills the rest of `ARGV` |
115
+ | `doc` | `str \| None` | the function's docstring |
116
+ | `source` | `str` | where it was defined, repo-relative |
117
+
118
+ ### `bind`
119
+
120
+ ```python
121
+ def bind(self, client) -> BoundScript[R] # sync client
122
+ def bind(self, client) -> BoundScript[Awaitable[R]] # async client
123
+ ```
124
+
125
+ Attach a client, so calls do not have to pass one. See
126
+ [Binding a client](../guide/binding-a-client.md).
127
+
128
+ ## `BoundScript`
129
+
130
+ ```python
131
+ class BoundScript(Generic[T])
132
+ ```
133
+
134
+ A script with its client already attached, produced by
135
+ [`bind`](#bind). The type parameter is what a call returns: the script's own
136
+ return type for a sync client, an awaitable of it for an async one.
137
+
138
+ ```python
139
+ bound(*positional, **keyword) -> T
140
+ ```
141
+
142
+ Exposes `name`, `lua`, `params`, `keys`, `args` and `doc` from the script it
143
+ wraps, and leaves that script usable against any other client. A
144
+ [`LibraryFunction`](#libraryfunction) binds the same way.
145
+
146
+ ## `Library`
147
+
148
+ ```python
149
+ class Library(name: str)
150
+ ```
151
+
152
+ A Redis Functions library. `name` is the library name Redis registers, and
153
+ takes letters, digits and underscores. See
154
+ [Redis Functions](../guide/redis-functions.md).
155
+
156
+ ### `function`
157
+
158
+ ```python
159
+ def function(
160
+ self,
161
+ func: Callable[..., R] | None = None,
162
+ /,
163
+ *,
164
+ name: str | None = None,
165
+ flags: Iterable[str] = (),
166
+ ) -> LibraryFunction[R] | Callable[[Callable[..., R]], LibraryFunction[R]]
167
+ ```
168
+
169
+ Compile a function into the library, under the same rules as
170
+ [`script`](#script). Usable bare or called.
171
+
172
+ | Parameter | Meaning |
173
+ | --- | --- |
174
+ | `name` | the function name Redis registers, instead of the Python name |
175
+ | `flags` | function flags, such as `no-writes`; a `no-writes` function is called with `FCALL_RO` |
176
+
177
+ ### `load`
178
+
179
+ ```python
180
+ def load(self, client) -> Any
181
+ ```
182
+
183
+ Load the library with `FUNCTION LOAD REPLACE`. Calling a function loads the
184
+ library when the server lacks it, so this is only needed before queueing calls
185
+ in a pipeline, or to load at deploy time.
186
+
187
+ | Attribute | Type | What it is |
188
+ | --- | --- | --- |
189
+ | `name` | `str` | the library name |
190
+ | `lua` | `str` | the library source, exactly as `FUNCTION LOAD` receives it |
191
+ | `functions` | `tuple[LibraryFunction, ...]` | the functions, in the order they were added |
192
+
193
+ ## `LibraryFunction`
194
+
195
+ ```python
196
+ class LibraryFunction(Generic[R])
197
+ ```
198
+
199
+ What `Library.function` returns. Calling it sends `FCALL`, or `FCALL_RO` for a
200
+ `no-writes` function, and loads the library first if the server does not have
201
+ it.
202
+
203
+ ```python
204
+ function(client, /, *positional, **keyword) -> R # sync client
205
+ function(client, /, *positional, **keyword) -> Awaitable[R] # async client
206
+ ```
207
+
208
+ It has the same `name`, `params`, `keys`, `args`, `variadic_key`,
209
+ `variadic_arg`, `doc` and `source` as a [`CompiledScript`](#compiledscript),
210
+ and a `bind` that works the same way, plus:
211
+
212
+ | Attribute | Type | What it is |
213
+ | --- | --- | --- |
214
+ | `library` | `Library` | the library the function belongs to |
215
+ | `lua` | `str` | the source of that whole library |
216
+ | `flags` | `tuple[str, ...]` | the function's flags |
217
+ | `read_only` | `bool` | whether it is flagged `no-writes`, and so called with `FCALL_RO` |
218
+
219
+ ## `codegen`
220
+
221
+ ```python
222
+ from redis_lua_py import codegen
223
+ ```
224
+
225
+ Compile scripts to Lua ahead of time, for code that should not depend on this
226
+ package at runtime. See
227
+ [Shipping without the dependency](../guide/build-time.md). Every function takes
228
+ the module as a module object or a dotted name, and `out` as a path:
229
+
230
+ - ending in `.py`, for one module holding every script as a string constant
231
+ and importing nothing
232
+ - anything else, for a directory with one `.lua` file per script
233
+
234
+ ### `codegen.generate`
235
+
236
+ ```python
237
+ def generate(module: ModuleType | str, out: str | Path) -> list[Path]
238
+ ```
239
+
240
+ Write the scripts under `out`, and return the paths that changed. A file that
241
+ already holds the right content is left alone. In a directory, a `.lua` file
242
+ generated earlier for a script that no longer exists is removed.
243
+
244
+ ### `codegen.check`
245
+
246
+ ```python
247
+ def check(module: ModuleType | str, out: str | Path) -> None
248
+ ```
249
+
250
+ Raise [`StaleLuaError`](errors.md#staleluaerror), with a diff, unless `out`
251
+ holds exactly what `generate` would write. Writes nothing.
252
+
253
+ ### `codegen.render`
254
+
255
+ ```python
256
+ def render(module: ModuleType | str, out: str | Path) -> dict[Path, str]
257
+ ```
258
+
259
+ The files `generate` would write, and what each would hold.
260
+
261
+ ### `codegen.collect`
262
+
263
+ ```python
264
+ def collect(module: ModuleType | str) -> dict[str, CompiledScript[Any]]
265
+ ```
266
+
267
+ Every script a module holds, by the name it is bound to, in definition order.
268
+ A script bound to two names is collected once, under the first.
269
+
270
+ ### The command line
271
+
272
+ ```bash
273
+ python -m redis_lua_py generate MODULE --out PATH [--check]
274
+ ```
275
+
276
+ `generate` or, with `--check`, `check`, exiting 1 on an out-of-date output, a
277
+ module that cannot be imported, or a script that does not compile. Installing
278
+ the package also provides the same command as `redis-lua-py`.
279
+
280
+ ## Errors and warnings
281
+
282
+ `CompileError`, `UnsupportedSyntax`, `ScriptArgumentError`, `StaleLuaError`,
283
+ `RedisLuaError`, `RedisLuaWarning` and `NilTruncationWarning` have
284
+ [their own page](errors.md).
@@ -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
- Lua 5.1 has no 'continue' statement
7
+ chained comparisons are not supported
8
8
  File "/srv/app/limits.py", line 12
9
- continue
10
- ^
11
- hint: Invert the condition and put the rest of the loop body inside the if.
9
+ if 0 < n < limit:
10
+ ^
11
+ hint: Split 'a < b < c' into 'a < b and b < c'.
12
12
  ```
13
13
 
14
14
  ## The hierarchy
@@ -17,7 +17,8 @@ Lua 5.1 has no 'continue' statement
17
17
  RedisLuaError
18
18
  ├── CompileError
19
19
  │ └── UnsupportedSyntax
20
- └── ScriptArgumentError
20
+ ├── ScriptArgumentError
21
+ └── StaleLuaError
21
22
 
22
23
  RedisLuaWarning (a UserWarning)
23
24
  └── NilTruncationWarning
@@ -53,6 +54,16 @@ is the only error in the list raised at call time rather than at import.
53
54
  rate_limit() has no parameter 'tll'; did you mean 'ttl'? (parameters: key, limit, ttl)
54
55
  ```
55
56
 
57
+ ## `StaleLuaError`
58
+
59
+ Lua generated ahead of time no longer matches the scripts it came from. Raised
60
+ by [`codegen.check`](api.md#codegencheck), with a diff and the command that
61
+ regenerates the output. See
62
+ [Shipping without the dependency](../guide/build-time.md#keep-it-current).
63
+
64
+ It is also an `AssertionError`, so a test that calls `check` reports a failed
65
+ assertion rather than an error in the test itself.
66
+
56
67
  ## `RedisLuaWarning`
57
68
 
58
69
  Base class for every warning this package raises. A `UserWarning`, so it is