redis-lua-py 0.7.0__tar.gz → 0.8.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.8.0/.release-please-manifest.json +3 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/CHANGELOG.md +11 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/PKG-INFO +1 -1
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/guide/keys-and-arguments.md +9 -4
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/guide/return-values.md +3 -1
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/reference/lua-vs-python.md +5 -3
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/reference/supported-subset.md +5 -4
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/pyproject.toml +1 -1
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/__init__.py +1 -1
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/_compile/analysis.py +1 -1
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/_compile/base.py +16 -7
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/_compile/calls.py +30 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/_compile/expressions.py +7 -2
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/_compile/helpers.py +9 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/_compile/scope.py +7 -2
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/_compile/tables.py +4 -2
- redis_lua_py-0.8.0/tests/test_booleans_and_conversions.py +162 -0
- redis_lua_py-0.7.0/.release-please-manifest.json +0 -3
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/.github/assets/README.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/.github/assets/banner-dark.svg +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/.github/assets/banner-light.svg +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/.github/assets/generate.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/.github/assets/logo.svg +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/.github/assets/logomark.svg +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/.github/assets/social-preview.png +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/.github/assets/social-preview.svg +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/.github/workflows/ci.yml +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/.github/workflows/docs.yml +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/.github/workflows/pr-title.yml +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/.github/workflows/release.yml +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/.gitignore +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/.python-version +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/CONTRIBUTING.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/LICENSE +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/README.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/assets/logo.svg +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/contributing.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/development.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/examples.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/guide/async.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/guide/binary-values.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/guide/binding-a-client.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/guide/build-time.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/guide/calling-redis-commands.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/guide/constants.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/guide/generated-lua.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/guide/redis-functions.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/guide/testing.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/index.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/installation.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/quickstart.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/reference/api.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/reference/errors.md +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/docs/stylesheets/extra.css +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/release-please-config.json +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/scripts/generate_commands.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/__main__.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/_commands.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/_compile/__init__.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/_compile/control.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/_compile/source.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/_compile/statements.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/_library.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/_lua.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/_portable.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/_runtime.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/_script.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/codegen.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/errors.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/src/redis_lua_py/py.typed +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/codegen_scripts.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/conftest.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/generated_scripts.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/module_with_client.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/module_without_import.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/test_binary.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/test_binding.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/test_codegen.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/test_commands.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/test_compile.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/test_constants.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/test_control_flow.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/test_coredis.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/test_errors.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/test_execute.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/test_expressions.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/test_functions.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/test_mistranslations.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/test_namespace.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/test_semantics.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/test_strings_and_indexing.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/test_table_stakes.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/tests/test_typing.py +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/uv.lock +0 -0
- {redis_lua_py-0.7.0 → redis_lua_py-0.8.0}/zensical.toml +0 -0
|
@@ -7,6 +7,17 @@ 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.8.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.7.0...v0.8.0) (2026-09-13)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### ⚠ BREAKING CHANGES
|
|
14
|
+
|
|
15
|
+
* a bool parameter is a boolean in the body rather than "1" or "0", and integer conversion of a non-number raises instead of giving nil.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
* give bool arguments, int() and known booleans their python meaning ([#24](https://github.com/IgnaceMaes/redis-lua-py/issues/24)) ([27b3de8](https://github.com/IgnaceMaes/redis-lua-py/commit/27b3de8e7c12aea9076b940e6a7f369b054876f3))
|
|
20
|
+
|
|
10
21
|
## [0.7.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.6.0...v0.7.0) (2026-09-12)
|
|
11
22
|
|
|
12
23
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: redis-lua-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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/
|
|
@@ -30,7 +30,7 @@ to it on the way in.
|
|
|
30
30
|
| `Key` | `KEYS[n]`, and what the cluster routes on |
|
|
31
31
|
| `int`, `float` | wrapped in `tonumber`, so it is a number by the time your comparison runs |
|
|
32
32
|
| `str` | passed through as the string it already is |
|
|
33
|
-
| `bool` | encoded as `"1"` or `"0"
|
|
33
|
+
| `bool` | encoded as `"1"` or `"0"`, and a Lua boolean in the body |
|
|
34
34
|
| `bytes`, `memoryview` | passed through untouched — see [Binary values](binary-values.md) |
|
|
35
35
|
|
|
36
36
|
### `float` carries a caveat
|
|
@@ -41,10 +41,15 @@ text with `%.14g`, so a value with more significant digits than that does not
|
|
|
41
41
|
come back as it went in. Annotate `str` and call `str()` at the call site when
|
|
42
42
|
the value is only being carried.
|
|
43
43
|
|
|
44
|
-
### `bool`
|
|
44
|
+
### `bool` is a boolean in the body
|
|
45
45
|
|
|
46
|
-
`bool`
|
|
47
|
-
|
|
46
|
+
`bool` travels as `"1"` or `"0"`, and arrives as `ARGV[n] == '1'`, so
|
|
47
|
+
`if flag:` means what it says. `"0"` is a non-empty string, which both Lua and
|
|
48
|
+
Python count as true, so passing the string through would make every flag set.
|
|
49
|
+
|
|
50
|
+
A Lua boolean cannot be a command argument, as in redis-py. Write `int(flag)`
|
|
51
|
+
where the body hands it to Redis, or annotate the parameter `int` instead:
|
|
52
|
+
pass `True`, and the body gets `1`, with no `1 if flag else 0` at the call site.
|
|
48
53
|
|
|
49
54
|
## A variable number of keys or arguments
|
|
50
55
|
|
|
@@ -30,7 +30,9 @@ def preview(doc: Key) -> list[int | bytes]:
|
|
|
30
30
|
|
|
31
31
|
`b""` and `""` compile to the same Lua string; only one of them also describes
|
|
32
32
|
what the caller receives, which keeps the body and the signature agreeing
|
|
33
|
-
about the same thing.
|
|
33
|
+
about the same thing. For a string built in the body, `str(n).encode()` does
|
|
34
|
+
the same: Lua strings are already bytes, so `encode()` compiles to nothing, and
|
|
35
|
+
the body type-checks against `-> bytes`.
|
|
34
36
|
|
|
35
37
|
Return values follow Redis' own conversion rules: `True` becomes `1`, `False`
|
|
36
38
|
and `None` become nil, floats are truncated to integers. Return a string, or
|
|
@@ -4,9 +4,11 @@ These are the gaps that matter. Most are closed for you; the rest are refused.
|
|
|
4
4
|
|
|
5
5
|
## Truthiness is closed
|
|
6
6
|
|
|
7
|
-
Lua counts `0` and `''` as true. Any condition that is not
|
|
8
|
-
routed through a generated `__truthy` helper, so `if count:` means
|
|
9
|
-
means in Python.
|
|
7
|
+
Lua counts `0` and `''` as true. Any condition that is not known to be a
|
|
8
|
+
boolean is routed through a generated `__truthy` helper, so `if count:` means
|
|
9
|
+
what it means in Python. A comparison, `not`, `startswith()`, `endswith()`, a
|
|
10
|
+
`bool` parameter, and a name only ever assigned one of those are used as they
|
|
11
|
+
are, and `and`/`or` between them compile to Lua's own operators.
|
|
10
12
|
|
|
11
13
|
## Missing values are closed
|
|
12
14
|
|
|
@@ -23,10 +23,11 @@ language with a faithful Lua meaning is accepted.
|
|
|
23
23
|
a name, and slices of lists and strings, with a step or without.
|
|
24
24
|
- **Strings:** f-strings with format specs; `%` formatting; `+` and `*` on a
|
|
25
25
|
string; `str.join`, `upper`, `lower`, `strip`, `lstrip`, `rstrip`,
|
|
26
|
-
`startswith`, `endswith`, `find`, `split` and `replace
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
`
|
|
26
|
+
`startswith`, `endswith`, `find`, `split` and `replace`; `encode()` and
|
|
27
|
+
`decode()` in UTF-8, which leave the bytes as they are.
|
|
28
|
+
- **Builtins and methods:** `len()`, `int()`, which truncates toward zero,
|
|
29
|
+
`float()`, `str()`, `min()`, `max()`, `abs()`, `ord()`, `chr()`, `.append()`,
|
|
30
|
+
`.insert()`, `.pop()` and `dict.get()`.
|
|
30
31
|
- **The `math` module:** `floor`, `ceil`, `sqrt`, `fabs`, `fmod`, `exp`,
|
|
31
32
|
`log`, `log10` and `pow`, imported either way.
|
|
32
33
|
- **Calls:** into `redis` and `cjson`, with `*xs` allowed as the last argument.
|
|
@@ -217,7 +217,7 @@ def without_finally(node: ast.Try) -> ast.Try:
|
|
|
217
217
|
def value_kind(value: object) -> str | None:
|
|
218
218
|
"""The kind of a module-level constant, as folded into the script."""
|
|
219
219
|
if isinstance(value, bool):
|
|
220
|
-
return
|
|
220
|
+
return "bool"
|
|
221
221
|
if isinstance(value, str | bytes):
|
|
222
222
|
return "str"
|
|
223
223
|
if isinstance(value, int | float):
|
|
@@ -242,16 +242,21 @@ class CompilerBase(ABC):
|
|
|
242
242
|
def kind(self, node: ast.expr) -> str | None:
|
|
243
243
|
"""What an expression is statically known to be, or None.
|
|
244
244
|
|
|
245
|
-
One of "str", "num", "list", "dict" or "func". Lua does not
|
|
246
|
-
several translations do: a subscript is shifted to 1-based
|
|
247
|
-
and not for a dict, `+` joins strings and adds numbers, `in`
|
|
248
|
-
list and looks up a dict's keys,
|
|
249
|
-
a number differently
|
|
250
|
-
|
|
245
|
+
One of "str", "num", "bool", "list", "dict" or "func". Lua does not
|
|
246
|
+
care, but several translations do: a subscript is shifted to 1-based
|
|
247
|
+
for a list and not for a dict, `+` joins strings and adds numbers, `in`
|
|
248
|
+
searches a list and looks up a dict's keys, a format spec aligns a
|
|
249
|
+
string and a number differently, and a boolean needs no truthiness
|
|
250
|
+
helper. None means "only known at runtime", and the translation then
|
|
251
|
+
decides there, or refuses.
|
|
251
252
|
"""
|
|
252
253
|
match node:
|
|
253
254
|
case ast.Constant(value=bool()):
|
|
254
|
-
return
|
|
255
|
+
return "bool"
|
|
256
|
+
case ast.Compare() | ast.UnaryOp(op=ast.Not()):
|
|
257
|
+
return "bool"
|
|
258
|
+
case ast.BoolOp(values=values):
|
|
259
|
+
return "bool" if all(self.kind(value) == "bool" for value in values) else None
|
|
255
260
|
case ast.Constant(value=str() | bytes()):
|
|
256
261
|
return "str"
|
|
257
262
|
case ast.Constant(value=int() | float()):
|
|
@@ -292,6 +297,10 @@ class CompilerBase(ABC):
|
|
|
292
297
|
return None
|
|
293
298
|
if attr in {"upper", "lower", "strip", "lstrip", "rstrip", "replace", "join"}:
|
|
294
299
|
return "str"
|
|
300
|
+
if attr in {"encode", "decode"}:
|
|
301
|
+
return "str"
|
|
302
|
+
if attr in {"startswith", "endswith"}:
|
|
303
|
+
return "bool"
|
|
295
304
|
if attr == "split":
|
|
296
305
|
return "list"
|
|
297
306
|
return "num" if attr == "find" else None
|
|
@@ -42,6 +42,8 @@ class CallCompiler(ExpressionCompiler):
|
|
|
42
42
|
self.helpers.add("__dictlen")
|
|
43
43
|
return lua.Call(lua.Name("__dictlen"), args)
|
|
44
44
|
return lua.UnOp("#", args[0])
|
|
45
|
+
case ast.Name(id="int"):
|
|
46
|
+
return self.int_call(node, args)
|
|
45
47
|
case ast.Name(id=name) if name in BUILTIN_FUNCS:
|
|
46
48
|
return lua.Call(lua.Name(BUILTIN_FUNCS[name]), args)
|
|
47
49
|
case ast.Attribute(value=receiver, attr="join") if not self.is_namespace(receiver):
|
|
@@ -87,6 +89,17 @@ class CallCompiler(ExpressionCompiler):
|
|
|
87
89
|
args.append(self.expr(arg))
|
|
88
90
|
return tuple(args)
|
|
89
91
|
|
|
92
|
+
def int_call(self, node: ast.Call, args: tuple[lua.Expr, ...]) -> lua.Expr:
|
|
93
|
+
"""``int(x)``: truncated toward zero, which tonumber alone does not do."""
|
|
94
|
+
if len(args) != 1:
|
|
95
|
+
self.fail(
|
|
96
|
+
node, "int() takes exactly one argument", hint="int() with a base is not supported."
|
|
97
|
+
)
|
|
98
|
+
if self.kind(node.args[0]) == "bool":
|
|
99
|
+
return lua.BinOp("or", lua.BinOp("and", args[0], lua.Num(1)), lua.Num(0))
|
|
100
|
+
self.helpers.add("__int")
|
|
101
|
+
return lua.Call(lua.Name("__int"), args)
|
|
102
|
+
|
|
90
103
|
def math_call(self, node: ast.Call, attr: str, args: tuple[lua.Expr, ...]) -> lua.Expr:
|
|
91
104
|
if attr == "log":
|
|
92
105
|
if len(args) != 1:
|
|
@@ -135,6 +148,16 @@ class CallCompiler(ExpressionCompiler):
|
|
|
135
148
|
) -> lua.Expr:
|
|
136
149
|
"""The str methods, and dict.get, over Lua's string library and helpers."""
|
|
137
150
|
target = self.expr(receiver)
|
|
151
|
+
if attr in {"encode", "decode"}:
|
|
152
|
+
# Every Lua string is already bytes, so there is nothing to convert.
|
|
153
|
+
if len(node.args) > 1 or (node.args and not is_utf8(node.args[0])):
|
|
154
|
+
self.fail(
|
|
155
|
+
node,
|
|
156
|
+
f"{attr}() only supports UTF-8 inside a script",
|
|
157
|
+
hint="Lua strings are bytes, so encode() and decode() leave them as they are; "
|
|
158
|
+
"another encoding has no counterpart.",
|
|
159
|
+
)
|
|
160
|
+
return target
|
|
138
161
|
allowed = {"get": (1, 2), "split": (0, 1), "replace": (2,)}.get(attr)
|
|
139
162
|
if allowed is None:
|
|
140
163
|
allowed = (0,) if attr in {"upper", "lower", "strip", "lstrip", "rstrip"} else (1,)
|
|
@@ -254,6 +277,13 @@ class CallCompiler(ExpressionCompiler):
|
|
|
254
277
|
)
|
|
255
278
|
|
|
256
279
|
|
|
280
|
+
def is_utf8(node: ast.expr) -> bool:
|
|
281
|
+
"""Whether an encode()/decode() argument names UTF-8."""
|
|
282
|
+
if not isinstance(node, ast.Constant) or not isinstance(node.value, str):
|
|
283
|
+
return False
|
|
284
|
+
return node.value.lower().replace("_", "-") in {"utf-8", "utf8"}
|
|
285
|
+
|
|
286
|
+
|
|
257
287
|
def listing(names: list[str], limit: int = 6) -> str:
|
|
258
288
|
"""A comma-separated sample, only trailing off when there is more."""
|
|
259
289
|
shown = ", ".join(names[:limit])
|
|
@@ -393,6 +393,9 @@ class ExpressionCompiler(ScopeCompiler):
|
|
|
393
393
|
that is a name or a literal goes through a helper; anything else is
|
|
394
394
|
wrapped in a function, so it only runs when Python would run it.
|
|
395
395
|
"""
|
|
396
|
+
if self.kind(node) == "bool":
|
|
397
|
+
# Between booleans, Lua's and/or mean exactly what Python's do.
|
|
398
|
+
return self.condition(node)
|
|
396
399
|
is_or = isinstance(node.op, ast.Or)
|
|
397
400
|
self.helpers.add("__truthy")
|
|
398
401
|
result = self.expr(node.values[0])
|
|
@@ -432,9 +435,11 @@ class ExpressionCompiler(ScopeCompiler):
|
|
|
432
435
|
def condition(self, node: ast.expr) -> lua.Expr:
|
|
433
436
|
"""Compile an expression used for its truth value.
|
|
434
437
|
|
|
435
|
-
Lua treats 0 and '' as true, so anything that is not
|
|
436
|
-
gets routed through the __truthy helper.
|
|
438
|
+
Lua treats 0 and '' as true, so anything that is not known to be a
|
|
439
|
+
boolean gets routed through the __truthy helper.
|
|
437
440
|
"""
|
|
441
|
+
if self.kind(node) == "bool" and not isinstance(node, ast.BoolOp | ast.UnaryOp):
|
|
442
|
+
return self.expr(node)
|
|
438
443
|
match node:
|
|
439
444
|
case ast.Compare():
|
|
440
445
|
return self.compare(node)
|
|
@@ -125,6 +125,15 @@ local function __add(a, b)
|
|
|
125
125
|
return out
|
|
126
126
|
end
|
|
127
127
|
return a + b
|
|
128
|
+
end""",
|
|
129
|
+
"__int": """\
|
|
130
|
+
-- Python's int(): a number truncated toward zero, and an error rather than
|
|
131
|
+
-- nil for anything that is not a number.
|
|
132
|
+
local function __int(v)
|
|
133
|
+
local n = tonumber(v)
|
|
134
|
+
if n == nil then error('invalid literal for int(): ' .. tostring(v), 0) end
|
|
135
|
+
if n < 0 then return math.ceil(n) end
|
|
136
|
+
return math.floor(n)
|
|
128
137
|
end""",
|
|
129
138
|
"__startswith": """\
|
|
130
139
|
local function __startswith(s, prefix)
|
|
@@ -135,6 +135,10 @@ class ScopeCompiler(CompilerBase):
|
|
|
135
135
|
# is the author asking for the conversion.
|
|
136
136
|
self.numeric_args.add(name)
|
|
137
137
|
source = lua.Call(lua.Name("tonumber"), (source,))
|
|
138
|
+
elif self._annotation_name(arg.annotation) == "bool":
|
|
139
|
+
# A bool arrives as "1" or "0", and "0" is a string Lua and
|
|
140
|
+
# Python both count as true. Comparing makes it a boolean.
|
|
141
|
+
source = lua.BinOp("==", source, lua.Str("1"))
|
|
138
142
|
kind = self._annotation_kind(arg.annotation)
|
|
139
143
|
if kind is not None:
|
|
140
144
|
self.kinds[name] = kind
|
|
@@ -182,8 +186,9 @@ class ScopeCompiler(CompilerBase):
|
|
|
182
186
|
name = self._annotation_name(node)
|
|
183
187
|
if name in {"int", "float"}:
|
|
184
188
|
return "num"
|
|
185
|
-
|
|
186
|
-
|
|
189
|
+
if name == "bool":
|
|
190
|
+
return "bool"
|
|
191
|
+
if name in {"Key", "str", "bytes", "memoryview"}:
|
|
187
192
|
return "str"
|
|
188
193
|
return None
|
|
189
194
|
|
|
@@ -119,8 +119,8 @@ MATH_BY_OBJECT: dict[object, str] = {getattr(math, name): name for name in [*MAT
|
|
|
119
119
|
|
|
120
120
|
METHOD_HINT = (
|
|
121
121
|
"Available: the redis and cjson namespaces; list append, insert and pop; str join, "
|
|
122
|
-
"upper, lower, strip, lstrip, rstrip, startswith, endswith, find, split
|
|
123
|
-
"and dict get."
|
|
122
|
+
"upper, lower, strip, lstrip, rstrip, startswith, endswith, find, split, replace, "
|
|
123
|
+
"encode and decode; and dict get."
|
|
124
124
|
)
|
|
125
125
|
|
|
126
126
|
|
|
@@ -139,6 +139,8 @@ STRING_METHODS = frozenset(
|
|
|
139
139
|
"find",
|
|
140
140
|
"split",
|
|
141
141
|
"replace",
|
|
142
|
+
"encode",
|
|
143
|
+
"decode",
|
|
142
144
|
"get",
|
|
143
145
|
}
|
|
144
146
|
)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Booleans, int() and str/bytes conversions, where Lua's meaning differs from Python's.
|
|
2
|
+
|
|
3
|
+
Each test runs the script as well as inspecting it: a translation that reads
|
|
4
|
+
well but answers differently is the failure being guarded against.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from redis_lua_py import CompiledScript, Key, UnsupportedSyntax, redis, script
|
|
15
|
+
|
|
16
|
+
Body = Callable[[CompiledScript[object]], str]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestBoolArguments:
|
|
20
|
+
"""A bool travels as "1" or "0", and "0" is a string Lua counts as true."""
|
|
21
|
+
|
|
22
|
+
def test_false_is_false_in_a_condition(self, client: Any) -> None:
|
|
23
|
+
@script
|
|
24
|
+
def s(flag: bool) -> str:
|
|
25
|
+
if flag:
|
|
26
|
+
return "yes"
|
|
27
|
+
return "no"
|
|
28
|
+
|
|
29
|
+
assert s(client, flag=True) == b"yes"
|
|
30
|
+
assert s(client, flag=False) == b"no"
|
|
31
|
+
|
|
32
|
+
def test_arrives_as_a_lua_boolean(self, body: Body) -> None:
|
|
33
|
+
@script
|
|
34
|
+
def s(flag: bool) -> int:
|
|
35
|
+
if not flag:
|
|
36
|
+
return 0
|
|
37
|
+
return 1
|
|
38
|
+
|
|
39
|
+
emitted = body(s)
|
|
40
|
+
assert "local flag = ARGV[1] == '1'" in emitted
|
|
41
|
+
assert "__truthy" not in emitted
|
|
42
|
+
|
|
43
|
+
def test_int_of_a_bool(self, client: Any, body: Body) -> None:
|
|
44
|
+
@script
|
|
45
|
+
def s(flag: bool) -> int:
|
|
46
|
+
return int(flag) + 1
|
|
47
|
+
|
|
48
|
+
assert "__int" not in body(s)
|
|
49
|
+
assert s(client, flag=True) == 2
|
|
50
|
+
assert s(client, flag=False) == 1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestKnownBooleans:
|
|
54
|
+
"""A value known to be a boolean needs no truthiness helper."""
|
|
55
|
+
|
|
56
|
+
def test_a_comparison_kept_in_a_name(self, client: Any, body: Body) -> None:
|
|
57
|
+
@script
|
|
58
|
+
def s(k: Key) -> int:
|
|
59
|
+
empty = redis.llen(k) == 0
|
|
60
|
+
if empty:
|
|
61
|
+
return 0
|
|
62
|
+
return 1
|
|
63
|
+
|
|
64
|
+
assert "__truthy" not in body(s)
|
|
65
|
+
assert s(client, k="list") == 0
|
|
66
|
+
client.rpush("list", "x")
|
|
67
|
+
assert s(client, k="list") == 1
|
|
68
|
+
|
|
69
|
+
def test_startswith_and_endswith(self, client: Any, body: Body) -> None:
|
|
70
|
+
@script
|
|
71
|
+
def s(k: Key, prefix: str, suffix: str) -> int:
|
|
72
|
+
value = redis.get(k)
|
|
73
|
+
if value is not None and value.startswith(prefix) and not value.endswith(suffix):
|
|
74
|
+
return 1
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
assert "__truthy" not in body(s)
|
|
78
|
+
client.set("k", "pointer:abc")
|
|
79
|
+
assert s(client, k="k", prefix="pointer:", suffix="xyz") == 1
|
|
80
|
+
assert s(client, k="k", prefix="inline:", suffix="xyz") == 0
|
|
81
|
+
assert s(client, k="k", prefix="pointer:", suffix="abc") == 0
|
|
82
|
+
assert s(client, k="missing", prefix="pointer:", suffix="xyz") == 0
|
|
83
|
+
|
|
84
|
+
def test_and_or_between_booleans_use_luas_operators(self, client: Any, body: Body) -> None:
|
|
85
|
+
@script
|
|
86
|
+
def s(a: int, b: int) -> list[int]:
|
|
87
|
+
both = a > 0 and b > 0
|
|
88
|
+
either = a > 0 or b > 0
|
|
89
|
+
return [int(both), int(either)]
|
|
90
|
+
|
|
91
|
+
emitted = body(s)
|
|
92
|
+
for helper in ("__truthy", "__and", "__or", "__int"):
|
|
93
|
+
assert helper not in emitted
|
|
94
|
+
assert s(client, a=1, b=0) == [0, 1]
|
|
95
|
+
assert s(client, a=1, b=1) == [1, 1]
|
|
96
|
+
assert s(client, a=0, b=0) == [0, 0]
|
|
97
|
+
|
|
98
|
+
def test_a_bool_constant(self, client: Any, body: Body) -> None:
|
|
99
|
+
@script
|
|
100
|
+
def s(n: int) -> int:
|
|
101
|
+
found = False
|
|
102
|
+
if n > 3:
|
|
103
|
+
found = True
|
|
104
|
+
if found:
|
|
105
|
+
return 1
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
assert "__truthy" not in body(s)
|
|
109
|
+
assert s(client, n=5) == 1
|
|
110
|
+
assert s(client, n=1) == 0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TestInt:
|
|
114
|
+
"""int() truncates toward zero, where tonumber keeps the fraction."""
|
|
115
|
+
|
|
116
|
+
@pytest.mark.parametrize(
|
|
117
|
+
("value", "expected"), [("7", 7), ("3.7", 3), ("-3.7", -3), ("-0.5", 0), ("12", 12)]
|
|
118
|
+
)
|
|
119
|
+
def test_truncates_toward_zero(self, client: Any, value: str, expected: int) -> None:
|
|
120
|
+
@script
|
|
121
|
+
def s(v: str) -> list[int]:
|
|
122
|
+
# Scaled, so Redis' own truncation of a numeric reply cannot hide ours.
|
|
123
|
+
return [int(v) * 10]
|
|
124
|
+
|
|
125
|
+
assert s(client, v=value) == [expected * 10]
|
|
126
|
+
|
|
127
|
+
def test_a_missing_value_raises(self, client: Any) -> None:
|
|
128
|
+
@script
|
|
129
|
+
def s(k: Key) -> int:
|
|
130
|
+
return int(redis.get(k))
|
|
131
|
+
|
|
132
|
+
with pytest.raises(Exception, match="invalid literal for int"):
|
|
133
|
+
s(client, k="missing")
|
|
134
|
+
|
|
135
|
+
def test_a_base_is_refused(self) -> None:
|
|
136
|
+
with pytest.raises(UnsupportedSyntax, match="int\\(\\) takes exactly one argument"):
|
|
137
|
+
|
|
138
|
+
@script
|
|
139
|
+
def s(v: str) -> int:
|
|
140
|
+
return int(v, 16) # type: ignore[call-overload]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class TestEncodeDecode:
|
|
144
|
+
"""Lua strings are bytes, so encode() and decode() have nothing to convert."""
|
|
145
|
+
|
|
146
|
+
def test_pass_through(self, client: Any, body: Body) -> None:
|
|
147
|
+
@script
|
|
148
|
+
def s(k: Key, text: str) -> bytes:
|
|
149
|
+
redis.set(k, text.encode())
|
|
150
|
+
return str(redis.get(k).decode("utf-8")).encode("utf-8")
|
|
151
|
+
|
|
152
|
+
emitted = body(s)
|
|
153
|
+
assert "encode" not in emitted
|
|
154
|
+
assert "decode" not in emitted
|
|
155
|
+
assert s(client, k="k", text="héllo") == "héllo".encode()
|
|
156
|
+
|
|
157
|
+
def test_another_encoding_is_refused(self) -> None:
|
|
158
|
+
with pytest.raises(UnsupportedSyntax, match="only supports UTF-8"):
|
|
159
|
+
|
|
160
|
+
@script
|
|
161
|
+
def s(text: str) -> bytes:
|
|
162
|
+
return text.encode("latin-1")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|