redis-lua-py 0.10.0__tar.gz → 0.12.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.12.0/.release-please-manifest.json +3 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/CHANGELOG.md +20 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/PKG-INFO +1 -1
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/guide/async.md +5 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/guide/build-time.md +2 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/installation.md +25 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/reference/api.md +2 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/reference/lua-vs-python.md +37 -2
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/reference/supported-subset.md +11 -6
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/pyproject.toml +1 -1
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/__init__.py +1 -1
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/base.py +13 -4
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/calls.py +77 -15
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/control.py +6 -4
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/expressions.py +7 -7
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/helpers.py +9 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/scope.py +36 -7
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/statements.py +1 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/tables.py +31 -2
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_library.py +7 -1
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_lua.py +50 -5
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_portable.py +14 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_runtime.py +7 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_script.py +11 -1
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/codegen.py +2 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/generated_scripts.py +24 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_booleans_and_conversions.py +164 -1
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_errors.py +34 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_strings_and_indexing.py +63 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_typing.py +25 -1
- redis_lua_py-0.10.0/.release-please-manifest.json +0 -3
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/.github/assets/README.md +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/.github/assets/banner-dark.svg +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/.github/assets/banner-light.svg +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/.github/assets/generate.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/.github/assets/logo.svg +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/.github/assets/logomark.svg +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/.github/assets/social-preview.png +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/.github/assets/social-preview.svg +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/.github/workflows/ci.yml +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/.github/workflows/docs.yml +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/.github/workflows/pr-title.yml +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/.github/workflows/release.yml +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/.gitignore +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/.python-version +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/CONTRIBUTING.md +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/LICENSE +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/README.md +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/assets/logo.svg +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/contributing.md +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/development.md +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/examples.md +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/guide/binary-values.md +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/guide/binding-a-client.md +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/guide/calling-redis-commands.md +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/guide/constants.md +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/guide/generated-lua.md +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/guide/keys-and-arguments.md +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/guide/redis-functions.md +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/guide/return-values.md +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/guide/testing.md +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/index.md +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/quickstart.md +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/reference/errors.md +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/docs/stylesheets/extra.css +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/release-please-config.json +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/scripts/generate_commands.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/__main__.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_command_stubs.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_commands.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/__init__.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/analysis.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/source.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/errors.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/src/redis_lua_py/py.typed +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/codegen_scripts.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/conftest.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/module_with_client.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/module_without_import.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_binary.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_binding.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_codegen.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_commands.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_compile.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_constants.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_control_flow.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_coredis.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_execute.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_expressions.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_functions.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_mistranslations.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_namespace.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_semantics.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/tests/test_table_stakes.py +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/uv.lock +0 -0
- {redis_lua_py-0.10.0 → redis_lua_py-0.12.0}/zensical.toml +0 -0
|
@@ -7,6 +7,26 @@ 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.12.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.11.0...v0.12.0) (2026-09-25)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
* str.isdigit, partition and annotated locals ([#32](https://github.com/IgnaceMaes/redis-lua-py/issues/32)) ([38e863c](https://github.com/IgnaceMaes/redis-lua-py/commit/38e863c96a89cd0c2a82d0bc59d73771f295d164))
|
|
16
|
+
|
|
17
|
+
## [0.11.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.10.0...v0.11.0) (2026-09-25)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
* compile isinstance and cjson.null in script bodies ([373c4b7](https://github.com/IgnaceMaes/redis-lua-py/commit/373c4b79d58994e7a67ada0db9572496839af8da))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
* rename locals that would shadow the Lua globals scripts rely on ([373c4b7](https://github.com/IgnaceMaes/redis-lua-py/commit/373c4b79d58994e7a67ada0db9572496839af8da))
|
|
28
|
+
* type a client wrapper with getattr forwarding as sync, not async ([373c4b7](https://github.com/IgnaceMaes/redis-lua-py/commit/373c4b79d58994e7a67ada0db9572496839af8da))
|
|
29
|
+
|
|
10
30
|
## [0.10.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.9.0...v0.10.0) (2026-09-14)
|
|
11
31
|
|
|
12
32
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: redis-lua-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.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/
|
|
@@ -22,6 +22,11 @@ a = rate_limit(sync_client, key="u:1", limit=10, ttl=60) # int
|
|
|
22
22
|
b = rate_limit(async_client, key="u:1", limit=10, ttl=60) # Awaitable[int]
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
A client is typed as sync when it has `__enter__`, as every redis-py sync
|
|
26
|
+
client does, and as async when it has `__aenter__`. The sync overload comes
|
|
27
|
+
first, so a wrapper of your own that forwards attributes through `__getattr__`,
|
|
28
|
+
which mypy takes to provide both, is typed as the sync client it wraps.
|
|
29
|
+
|
|
25
30
|
Script caching, `EVALSHA`, and the `NOSCRIPT` reload are handled by redis-py's
|
|
26
31
|
own script machinery, which this defers to rather than reimplementing.
|
|
27
32
|
|
|
@@ -28,6 +28,31 @@ cluster, Sentinel, or an in-process [fakeredis](guide/testing.md).
|
|
|
28
28
|
[coredis](https://github.com/alisaifee/coredis) clients work as well, and are
|
|
29
29
|
tested against. See [Async](guide/async.md#coredis).
|
|
30
30
|
|
|
31
|
+
### A client wrapper
|
|
32
|
+
|
|
33
|
+
An application that wraps its client, to prefix keys per tenant for example,
|
|
34
|
+
needs one method: `register_script(source)`. It returns a callable with the
|
|
35
|
+
shape of redis-py's `Script`, `(keys=..., args=..., client=...)`, and a script
|
|
36
|
+
runs through it:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
class PrefixedScript:
|
|
40
|
+
def __init__(self, owner: "PrefixedClient", script: Script) -> None:
|
|
41
|
+
self._owner, self._script = owner, script
|
|
42
|
+
|
|
43
|
+
def __call__(self, keys=(), args=(), client=None):
|
|
44
|
+
keys = [f"{self._owner.prefix}:{key}" for key in keys]
|
|
45
|
+
return self._script(keys=keys, args=args)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PrefixedClient:
|
|
49
|
+
def register_script(self, source: str) -> PrefixedScript:
|
|
50
|
+
return PrefixedScript(self, self._redis.register_script(source))
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The wrapper's other methods never come into it, so an `eval` with its own
|
|
54
|
+
signature is fine.
|
|
55
|
+
|
|
31
56
|
## For testing
|
|
32
57
|
|
|
33
58
|
[fakeredis](https://github.com/cunla/fakeredis-py) embeds a real Lua
|
|
@@ -92,6 +92,8 @@ anything.
|
|
|
92
92
|
|
|
93
93
|
The JSON library Redis exposes to scripts: `cjson.encode` and `cjson.decode`,
|
|
94
94
|
which pass through under their own names.
|
|
95
|
+
`cjson.null` is JSON `null`, which `None` is not: a field set to `None` is
|
|
96
|
+
dropped from an encoded object.
|
|
95
97
|
|
|
96
98
|
## `CompiledScript`
|
|
97
99
|
|
|
@@ -19,6 +19,24 @@ never runs. `x is None` compiles to a helper accepting both, which also takes
|
|
|
19
19
|
twice. `x == None` and `x != None` compile to the same helper, since that is
|
|
20
20
|
plainly what they mean.
|
|
21
21
|
|
|
22
|
+
`cjson.decode` gives JSON `null` as `cjson.null`, which is neither: compare
|
|
23
|
+
with `== cjson.null`. Setting a field to `None` removes it, so write
|
|
24
|
+
`cjson.null` where the encoded object should keep the field as `null`.
|
|
25
|
+
|
|
26
|
+
## `isinstance` has Lua's types
|
|
27
|
+
|
|
28
|
+
`isinstance(x, dict)` compiles to `type(x) == 'table'`, the usual guard on a
|
|
29
|
+
decoded JSON value before reading its fields. Lua has fewer types than Python,
|
|
30
|
+
so some tests answer for more than their name says:
|
|
31
|
+
|
|
32
|
+
- `int` and `float` both test for a number, which may have a fraction;
|
|
33
|
+
- `dict` and `list` both test for a table;
|
|
34
|
+
- `str` and `bytes` both test for a string;
|
|
35
|
+
- `bool` is not a number, so `isinstance(True, int)` is false.
|
|
36
|
+
|
|
37
|
+
A tuple or a `|` union of types tests for any of them. A class of your own has
|
|
38
|
+
no Lua counterpart, and is refused.
|
|
39
|
+
|
|
22
40
|
## Indexing is closed
|
|
23
41
|
|
|
24
42
|
Lua tables are 1-based. `items[0]` compiles to `items[1]`. Write Python indices
|
|
@@ -37,9 +55,17 @@ that is not known, it looks at the subscript:
|
|
|
37
55
|
A value's type is known from its annotation, a literal, the builtin, method or
|
|
38
56
|
operator that produced it, a `range()` or `enumerate()` loop variable, or
|
|
39
57
|
every assignment to the name agreeing. So after `counts = {}`, `counts[0]` is
|
|
40
|
-
the key `0`. A
|
|
58
|
+
the key `0`. A value that comes from elsewhere -- a Redis reply,
|
|
41
59
|
`cjson.decode`, a helper's parameter -- is not known, and an integer subscript
|
|
42
|
-
on it is taken to be a position.
|
|
60
|
+
on it is taken to be a table position. For a string, that reads `nil`, so
|
|
61
|
+
annotate the local to say what it holds:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
text: str | None = redis.get(key)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
An annotation on a local is a declaration, not a conversion: it decides how
|
|
68
|
+
the value is indexed and added, and changes nothing at runtime.
|
|
43
69
|
|
|
44
70
|
`items[-1]` compiles to `items[#items]`, which needs a name to count back from.
|
|
45
71
|
Indexing a string gives a one-character string, as it does in Python, through
|
|
@@ -53,6 +79,15 @@ Python scopes a name to the whole function; Lua's `local` scopes it to the
|
|
|
53
79
|
enclosing block. A name assigned inside an `if` and read after it is hoisted to
|
|
54
80
|
the top of the script, so it does not silently read back `nil`.
|
|
55
81
|
|
|
82
|
+
## Names Lua already uses are closed
|
|
83
|
+
|
|
84
|
+
The generated Lua calls globals such as `type`, `error`, `tostring` and
|
|
85
|
+
`pairs`, and reads `KEYS` and `ARGV`. A local of the same name would shadow
|
|
86
|
+
them for the rest of the script, so a parameter named `error` would break
|
|
87
|
+
every `raise` after it. Such a name is renamed with a trailing underscore
|
|
88
|
+
instead: `local error_ = ARGV[1]`. The keyword argument on the Python side
|
|
89
|
+
keeps its name.
|
|
90
|
+
|
|
56
91
|
## Strings are closed
|
|
57
92
|
|
|
58
93
|
Lua's `+` is only arithmetic. It will add `"1" + "2"` to `3`. So `+`
|
|
@@ -5,8 +5,9 @@ language with a faithful Lua meaning is accepted.
|
|
|
5
5
|
|
|
6
6
|
**Supported:**
|
|
7
7
|
|
|
8
|
-
- **Statements:** assignment, including unpacking (`a, b = b, a`)
|
|
9
|
-
|
|
8
|
+
- **Statements:** assignment, including unpacking (`a, b = b, a`) and
|
|
9
|
+
annotated assignment (`text: str = redis.get(key)`), whose annotation tells
|
|
10
|
+
the compiler the value's type; augmented assignment; `if`/`elif`/`else`; `while`; `break`; `continue`;
|
|
10
11
|
`return`.
|
|
11
12
|
- **Errors:** `try` with one `except` (bare or `Exception`), `else` and
|
|
12
13
|
`finally`; `raise SomeError("message")`, and a bare `raise` inside `except`;
|
|
@@ -23,14 +24,18 @@ language with a faithful Lua meaning is accepted.
|
|
|
23
24
|
a name, and slices of lists and strings, with a step or without.
|
|
24
25
|
- **Strings:** f-strings with format specs; `%` formatting; `+` and `*` on a
|
|
25
26
|
string; `str.join`, `upper`, `lower`, `strip`, `lstrip`, `rstrip`,
|
|
26
|
-
`startswith`, `endswith`, `find`, `split` and `replace`;
|
|
27
|
-
`
|
|
27
|
+
`startswith`, `endswith`, `find`, `split`, `partition` and `replace`;
|
|
28
|
+
`isdigit`, `isalpha`, `isalnum` and `isspace`, which test ASCII as they do
|
|
29
|
+
on bytes; `encode()` and `decode()` in UTF-8, which leave the bytes as they
|
|
30
|
+
are.
|
|
28
31
|
- **Builtins and methods:** `len()`, `int()`, which truncates toward zero,
|
|
29
32
|
`float()`, `str()`, `min()`, `max()`, `abs()`, `ord()`, `chr()`, `.append()`,
|
|
30
|
-
`.insert()`, `.pop()` and `dict.get()
|
|
33
|
+
`.insert()`, `.pop()` and `dict.get()`; `isinstance()` against `str`,
|
|
34
|
+
`bytes`, `int`, `float`, `bool`, `dict` and `list`.
|
|
31
35
|
- **The `math` module:** `floor`, `ceil`, `sqrt`, `fabs`, `fmod`, `exp`,
|
|
32
36
|
`log`, `log10` and `pow`, imported either way.
|
|
33
|
-
- **Calls:** into `redis` and `cjson`, with `*xs` allowed as the last argument
|
|
37
|
+
- **Calls:** into `redis` and `cjson`, with `*xs` allowed as the last argument,
|
|
38
|
+
and `cjson.null` for a JSON null.
|
|
34
39
|
- **Parameters:** `list[Key]` and `list[...]`, for a variable number of keys
|
|
35
40
|
and arguments.
|
|
36
41
|
- **Module-level constants.**
|
|
@@ -26,7 +26,7 @@ from .. import _lua as lua
|
|
|
26
26
|
from .._runtime import _Namespace
|
|
27
27
|
from ..errors import UnsupportedSyntax
|
|
28
28
|
from .analysis import UNBOUND, as_literal, binop_kind, dotted_name, value_kind
|
|
29
|
-
from .tables import MATH_BY_OBJECT, RECEIVER_FALLBACK, REDIS_CONSTANTS
|
|
29
|
+
from .tables import MATH_BY_OBJECT, RECEIVER_FALLBACK, REDIS_CONSTANTS, STRING_TESTS
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def is_redis_py(value: object) -> bool:
|
|
@@ -82,6 +82,9 @@ class CompilerBase(ABC):
|
|
|
82
82
|
self.helpers: set[str] = set()
|
|
83
83
|
# What a parameter or loop variable is known to be; see kind().
|
|
84
84
|
self.kinds: dict[str, str] = {}
|
|
85
|
+
# Kinds that an annotation on a local declares. They are the author's
|
|
86
|
+
# word, so they win over what the assigned values suggest.
|
|
87
|
+
self.declared: dict[str, str] = {}
|
|
85
88
|
# Every value assigned to each name, to work out what a local holds.
|
|
86
89
|
self.values: dict[str, list[ast.expr]] = {}
|
|
87
90
|
# Names bound in ways that say nothing about their type.
|
|
@@ -150,7 +153,9 @@ class CompilerBase(ABC):
|
|
|
150
153
|
and parts[1] in REDIS_CONSTANTS
|
|
151
154
|
and self.namespace_kind(parts[0]) == "redis"
|
|
152
155
|
):
|
|
153
|
-
return lua.Index(lua.
|
|
156
|
+
return lua.Index(lua.Global("redis"), lua.Str(parts[1]))
|
|
157
|
+
if parts[1:] == ("null",) and self.namespace_kind(parts[0]) == "cjson":
|
|
158
|
+
return lua.Index(lua.Global("cjson"), lua.Str("null"))
|
|
154
159
|
root = self.globalns.get(parts[0], UNBOUND)
|
|
155
160
|
# A namespace answers to every attribute with a call stub, and
|
|
156
161
|
# redis-py has a clearer error of its own; neither is a constant.
|
|
@@ -285,6 +290,8 @@ class CompilerBase(ABC):
|
|
|
285
290
|
case ast.Subscript(value=value):
|
|
286
291
|
return "str" if self.kind(value) == "str" else None
|
|
287
292
|
case ast.Call(func=ast.Name(id=name)) if not self.is_callable(name):
|
|
293
|
+
if name == "isinstance":
|
|
294
|
+
return "bool"
|
|
288
295
|
if name in {"str", "tostring", "chr"}:
|
|
289
296
|
return "str"
|
|
290
297
|
if name in {"int", "float", "tonumber", "len", "abs", "ord"}:
|
|
@@ -299,9 +306,9 @@ class CompilerBase(ABC):
|
|
|
299
306
|
return "str"
|
|
300
307
|
if attr in {"encode", "decode"}:
|
|
301
308
|
return "str"
|
|
302
|
-
if attr in {"startswith", "endswith"}:
|
|
309
|
+
if attr in {"startswith", "endswith", *STRING_TESTS}:
|
|
303
310
|
return "bool"
|
|
304
|
-
if attr
|
|
311
|
+
if attr in {"split", "partition"}:
|
|
305
312
|
return "list"
|
|
306
313
|
return "num" if attr == "find" else None
|
|
307
314
|
case ast.Attribute():
|
|
@@ -317,6 +324,8 @@ class CompilerBase(ABC):
|
|
|
317
324
|
def name_kind(self, name: str) -> str | None:
|
|
318
325
|
if name not in self.known:
|
|
319
326
|
return value_kind(self.globalns.get(name, UNBOUND))
|
|
327
|
+
if name in self.declared:
|
|
328
|
+
return self.declared[name]
|
|
320
329
|
if name in self._resolving:
|
|
321
330
|
# A name whose value depends on itself, as in `n += 1`, is whatever
|
|
322
331
|
# its other assignments make it.
|
|
@@ -7,15 +7,18 @@ import difflib
|
|
|
7
7
|
|
|
8
8
|
from .. import _lua as lua
|
|
9
9
|
from .._commands import COMMANDS, SUBCOMMANDS
|
|
10
|
+
from .analysis import is_cheap
|
|
10
11
|
from .expressions import ExpressionCompiler
|
|
11
12
|
from .tables import (
|
|
12
13
|
BUILTIN_FUNCS,
|
|
13
14
|
COMMAND_ALIASES,
|
|
14
15
|
COMMAND_SPELLINGS,
|
|
16
|
+
ISINSTANCE_TYPES,
|
|
15
17
|
MATH_FUNCS,
|
|
16
18
|
METHOD_HINT,
|
|
17
19
|
REDIS_DIRECT,
|
|
18
20
|
STRING_METHODS,
|
|
21
|
+
STRING_TESTS,
|
|
19
22
|
)
|
|
20
23
|
|
|
21
24
|
|
|
@@ -24,7 +27,19 @@ class CallCompiler(ExpressionCompiler):
|
|
|
24
27
|
|
|
25
28
|
def call(self, node: ast.Call) -> lua.Expr:
|
|
26
29
|
if node.keywords:
|
|
27
|
-
self.fail(
|
|
30
|
+
self.fail(
|
|
31
|
+
node,
|
|
32
|
+
"keyword arguments are not supported in a script body",
|
|
33
|
+
hint="Pass a Redis command's options positionally, as its syntax "
|
|
34
|
+
"writes them: redis.set(key, value, 'EX', ttl), not ex=ttl.",
|
|
35
|
+
)
|
|
36
|
+
if (
|
|
37
|
+
isinstance(node.func, ast.Name)
|
|
38
|
+
and node.func.id == "isinstance"
|
|
39
|
+
and not self.is_callable("isinstance")
|
|
40
|
+
):
|
|
41
|
+
# Its second argument names types, which have no value in Lua.
|
|
42
|
+
return self.isinstance_call(node)
|
|
28
43
|
args = self.call_args(node)
|
|
29
44
|
|
|
30
45
|
math_attr = self.math_attr(node.func)
|
|
@@ -45,11 +60,11 @@ class CallCompiler(ExpressionCompiler):
|
|
|
45
60
|
case ast.Name(id="int"):
|
|
46
61
|
return self.int_call(node, args)
|
|
47
62
|
case ast.Name(id=name) if name in BUILTIN_FUNCS:
|
|
48
|
-
return lua.Call(lua.
|
|
63
|
+
return lua.Call(lua.Global(BUILTIN_FUNCS[name]), args)
|
|
49
64
|
case ast.Attribute(value=receiver, attr="join") if not self.is_namespace(receiver):
|
|
50
65
|
if len(args) != 1:
|
|
51
66
|
self.fail(node, "join() takes exactly one argument")
|
|
52
|
-
return lua.Call(lua.
|
|
67
|
+
return lua.Call(lua.Global("table.concat"), (args[0], self.expr(receiver)))
|
|
53
68
|
case ast.Attribute(value=ast.Name(id=recv) as receiver, attr="pop" | "insert") if (
|
|
54
69
|
recv in self.known
|
|
55
70
|
):
|
|
@@ -84,7 +99,7 @@ class CallCompiler(ExpressionCompiler):
|
|
|
84
99
|
hint="Lua's unpack() only expands in the last position. Append the "
|
|
85
100
|
"trailing values to the list first, then splat it.",
|
|
86
101
|
)
|
|
87
|
-
args.append(lua.Call(lua.
|
|
102
|
+
args.append(lua.Call(lua.Global("unpack"), (self.expr(arg.value),)))
|
|
88
103
|
else:
|
|
89
104
|
args.append(self.expr(arg))
|
|
90
105
|
return tuple(args)
|
|
@@ -100,6 +115,42 @@ class CallCompiler(ExpressionCompiler):
|
|
|
100
115
|
self.helpers.add("__int")
|
|
101
116
|
return lua.Call(lua.Name("__int"), args)
|
|
102
117
|
|
|
118
|
+
def isinstance_call(self, node: ast.Call) -> lua.Expr:
|
|
119
|
+
"""``isinstance(x, T)``: a test of Lua's ``type()``, which has fewer types."""
|
|
120
|
+
if len(node.args) != 2 or isinstance(node.args[0], ast.Starred):
|
|
121
|
+
self.fail(node, "isinstance() takes exactly two arguments")
|
|
122
|
+
match node.args[1]:
|
|
123
|
+
case ast.Tuple(elts=elts):
|
|
124
|
+
classes = elts
|
|
125
|
+
case ast.BinOp(op=ast.BitOr()) as union:
|
|
126
|
+
classes = union_members(union)
|
|
127
|
+
case single:
|
|
128
|
+
classes = [single]
|
|
129
|
+
lua_types: list[str] = []
|
|
130
|
+
for cls in classes:
|
|
131
|
+
if not isinstance(cls, ast.Name) or cls.id not in ISINSTANCE_TYPES:
|
|
132
|
+
self.fail(
|
|
133
|
+
cls,
|
|
134
|
+
"isinstance() only tests for a builtin type",
|
|
135
|
+
hint=f"Available: {', '.join(ISINSTANCE_TYPES)}. Lua knows no classes.",
|
|
136
|
+
)
|
|
137
|
+
if ISINSTANCE_TYPES[cls.id] not in lua_types:
|
|
138
|
+
lua_types.append(ISINSTANCE_TYPES[cls.id])
|
|
139
|
+
subject = self.expr(node.args[0])
|
|
140
|
+
if len(lua_types) > 1 and not is_cheap(node.args[0]):
|
|
141
|
+
self.fail(
|
|
142
|
+
node.args[0],
|
|
143
|
+
"isinstance() against several types needs a name to test",
|
|
144
|
+
hint="Assign the value to a name first, so that it is evaluated once.",
|
|
145
|
+
)
|
|
146
|
+
tests = [
|
|
147
|
+
lua.BinOp("==", lua.Call(lua.Global("type"), (subject,)), lua.Str(t)) for t in lua_types
|
|
148
|
+
]
|
|
149
|
+
test = tests[0]
|
|
150
|
+
for other in tests[1:]:
|
|
151
|
+
test = lua.BinOp("or", test, other)
|
|
152
|
+
return test
|
|
153
|
+
|
|
103
154
|
def math_call(self, node: ast.Call, attr: str, args: tuple[lua.Expr, ...]) -> lua.Expr:
|
|
104
155
|
if attr == "log":
|
|
105
156
|
if len(args) != 1:
|
|
@@ -108,7 +159,7 @@ class CallCompiler(ExpressionCompiler):
|
|
|
108
159
|
"math.log() with a base is not supported",
|
|
109
160
|
hint="Lua 5.1's math.log takes no base; divide by math.log(base) instead.",
|
|
110
161
|
)
|
|
111
|
-
return lua.Call(lua.
|
|
162
|
+
return lua.Call(lua.Global("math.log"), args)
|
|
112
163
|
target = MATH_FUNCS.get(attr)
|
|
113
164
|
if target is None:
|
|
114
165
|
self.fail(
|
|
@@ -116,7 +167,7 @@ class CallCompiler(ExpressionCompiler):
|
|
|
116
167
|
f"math.{attr}() has no Lua counterpart",
|
|
117
168
|
hint=f"Available: {listing(sorted([*MATH_FUNCS, 'log']), limit=10)}.",
|
|
118
169
|
)
|
|
119
|
-
return lua.Call(lua.
|
|
170
|
+
return lua.Call(lua.Global(target), args)
|
|
120
171
|
|
|
121
172
|
def list_method(self, node: ast.Call, receiver: ast.Name) -> lua.Expr:
|
|
122
173
|
"""``xs.pop()`` and ``xs.insert(i, x)``, with the index shifted to 1-based."""
|
|
@@ -130,16 +181,16 @@ class CallCompiler(ExpressionCompiler):
|
|
|
130
181
|
target = self.expr(receiver)
|
|
131
182
|
if node.func.attr == "pop":
|
|
132
183
|
if not node.args:
|
|
133
|
-
return lua.Call(lua.
|
|
184
|
+
return lua.Call(lua.Global("table.remove"), (target,))
|
|
134
185
|
if len(node.args) == 1:
|
|
135
186
|
return lua.Call(
|
|
136
|
-
lua.
|
|
187
|
+
lua.Global("table.remove"), (target, self.index(node.args[0], "list"))
|
|
137
188
|
)
|
|
138
189
|
self.fail(node, "pop() takes at most one argument")
|
|
139
190
|
if len(node.args) != 2:
|
|
140
191
|
self.fail(node, "insert() takes exactly two arguments")
|
|
141
192
|
return lua.Call(
|
|
142
|
-
lua.
|
|
193
|
+
lua.Global("table.insert"),
|
|
143
194
|
(target, self.index(node.args[0], "list"), self.expr(node.args[1])),
|
|
144
195
|
)
|
|
145
196
|
|
|
@@ -160,7 +211,8 @@ class CallCompiler(ExpressionCompiler):
|
|
|
160
211
|
return target
|
|
161
212
|
allowed = {"get": (1, 2), "split": (0, 1), "replace": (2,)}.get(attr)
|
|
162
213
|
if allowed is None:
|
|
163
|
-
|
|
214
|
+
no_args = {"upper", "lower", "strip", "lstrip", "rstrip", *STRING_TESTS}
|
|
215
|
+
allowed = (0,) if attr in no_args else (1,)
|
|
164
216
|
if len(args) not in allowed:
|
|
165
217
|
if attr in {"strip", "lstrip", "rstrip"}:
|
|
166
218
|
self.fail(
|
|
@@ -172,10 +224,13 @@ class CallCompiler(ExpressionCompiler):
|
|
|
172
224
|
self.fail(node, f"{attr}() takes {expected} argument(s) inside a script")
|
|
173
225
|
|
|
174
226
|
if attr in {"upper", "lower"}:
|
|
175
|
-
return lua.Call(lua.
|
|
227
|
+
return lua.Call(lua.Global(f"string.{attr}"), (target,))
|
|
228
|
+
if attr in STRING_TESTS:
|
|
229
|
+
found = lua.Call(lua.Global("string.find"), (target, lua.Str(STRING_TESTS[attr])))
|
|
230
|
+
return lua.BinOp("~=", found, lua.Nil())
|
|
176
231
|
if attr in {"strip", "lstrip", "rstrip"}:
|
|
177
232
|
pattern = {"strip": "^%s*(.-)%s*$", "lstrip": "^%s*(.*)$", "rstrip": "^(.-)%s*$"}
|
|
178
|
-
return lua.Call(lua.
|
|
233
|
+
return lua.Call(lua.Global("string.match"), (target, lua.Str(pattern[attr])))
|
|
179
234
|
if attr == "get":
|
|
180
235
|
self.helpers.add("__get")
|
|
181
236
|
default = args[1] if len(args) == 2 else lua.Nil()
|
|
@@ -191,7 +246,7 @@ class CallCompiler(ExpressionCompiler):
|
|
|
191
246
|
if kind == "cjson":
|
|
192
247
|
if attr not in {"encode", "decode"}:
|
|
193
248
|
self.fail(node, f"cjson has no {attr!r} function")
|
|
194
|
-
return lua.Call(lua.Index(lua.
|
|
249
|
+
return lua.Call(lua.Index(lua.Global("cjson"), lua.Str(attr)), args)
|
|
195
250
|
if kind == "redis":
|
|
196
251
|
return self.redis_call(node, attr, args)
|
|
197
252
|
self.fail(
|
|
@@ -202,11 +257,11 @@ class CallCompiler(ExpressionCompiler):
|
|
|
202
257
|
|
|
203
258
|
def redis_call(self, node: ast.Call, attr: str, args: tuple[lua.Expr, ...]) -> lua.Expr:
|
|
204
259
|
if attr in REDIS_DIRECT:
|
|
205
|
-
return lua.Call(lua.Index(lua.
|
|
260
|
+
return lua.Call(lua.Index(lua.Global("redis"), lua.Str(attr)), args)
|
|
206
261
|
if attr.startswith("_"):
|
|
207
262
|
self.fail(node, f"redis.{attr} is not a Redis command")
|
|
208
263
|
tokens = tuple(lua.Str(token) for token in self.command_tokens(node, attr))
|
|
209
|
-
return lua.Call(lua.Index(lua.
|
|
264
|
+
return lua.Call(lua.Index(lua.Global("redis"), lua.Str("call")), tokens + args)
|
|
210
265
|
|
|
211
266
|
def command_tokens(self, node: ast.Call, attr: str) -> tuple[str, ...]:
|
|
212
267
|
"""Turn an attribute name into the command tokens Redis expects.
|
|
@@ -296,3 +351,10 @@ def did_you_mean(attr: str, options: set[str], prefix: str = "") -> str | None:
|
|
|
296
351
|
if not matches:
|
|
297
352
|
return None
|
|
298
353
|
return f"Did you mean redis.{prefix}{matches[0]}()?"
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def union_members(node: ast.expr) -> list[ast.expr]:
|
|
357
|
+
"""The classes of an ``A | B | C`` union, left to right."""
|
|
358
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
|
|
359
|
+
return [*union_members(node.left), *union_members(node.right)]
|
|
360
|
+
return [node]
|
|
@@ -75,8 +75,8 @@ class ControlFlowCompiler(CallCompiler):
|
|
|
75
75
|
the caller, or an except block, sees exactly what was raised.
|
|
76
76
|
"""
|
|
77
77
|
if not isinstance(message, lua.Str):
|
|
78
|
-
message = lua.Call(lua.
|
|
79
|
-
return lua.ExprStat(lua.Call(lua.
|
|
78
|
+
message = lua.Call(lua.Global("tostring"), (message,))
|
|
79
|
+
return lua.ExprStat(lua.Call(lua.Global("error"), (message, lua.Num(0))))
|
|
80
80
|
|
|
81
81
|
def raise_stmt(self, node: ast.Raise) -> lua.Stat:
|
|
82
82
|
"""``raise SomeError('message')``, which reaches the caller as an error reply."""
|
|
@@ -151,6 +151,8 @@ class ControlFlowCompiler(CallCompiler):
|
|
|
151
151
|
|
|
152
152
|
failure: list[lua.Stat] = []
|
|
153
153
|
if handler.name is not None:
|
|
154
|
+
if not lua.is_identifier(handler.name):
|
|
155
|
+
self.fail(handler, f"{handler.name!r} is a reserved word in Lua")
|
|
154
156
|
self.helpers.add("__errmsg")
|
|
155
157
|
message = lua.Call(lua.Name("__errmsg"), (lua.Name(result),))
|
|
156
158
|
failure.append(lua.Local([handler.name], [message]))
|
|
@@ -182,7 +184,7 @@ class ControlFlowCompiler(CallCompiler):
|
|
|
182
184
|
self.flow.pop()
|
|
183
185
|
returns = contains_return(body)
|
|
184
186
|
names = [ok, result, value] if returns else [ok, result]
|
|
185
|
-
call = lua.Call(lua.
|
|
187
|
+
call = lua.Call(lua.Global("pcall"), (lua.Name(function),))
|
|
186
188
|
return (
|
|
187
189
|
[lua.LocalFunction(function, [], inner), lua.Local(names, [call])],
|
|
188
190
|
ok,
|
|
@@ -287,7 +289,7 @@ class ControlFlowCompiler(CallCompiler):
|
|
|
287
289
|
if method == "values":
|
|
288
290
|
self._temp += 1
|
|
289
291
|
names = [f"__k{self._temp}", names[0]]
|
|
290
|
-
iterator = lua.Call(lua.
|
|
292
|
+
iterator = lua.Call(lua.Global("pairs"), (iterable,))
|
|
291
293
|
return lua.GenericFor(names, iterator, self.loop_body(node.body))
|
|
292
294
|
|
|
293
295
|
def enumerate_loop(self, node: ast.For, names: list[str], call: ast.Call) -> lua.Stat:
|
|
@@ -80,11 +80,11 @@ class ExpressionCompiler(ScopeCompiler):
|
|
|
80
80
|
return self.percent_format(node)
|
|
81
81
|
if isinstance(node.op, ast.Mult) and "str" in (left_kind, right_kind):
|
|
82
82
|
text, count = (node.left, node.right) if left_kind == "str" else (node.right, node.left)
|
|
83
|
-
return lua.Call(lua.
|
|
83
|
+
return lua.Call(lua.Global("string.rep"), (self.expr(text), self.expr(count)))
|
|
84
84
|
|
|
85
85
|
left, right = self.expr(node.left), self.expr(node.right)
|
|
86
86
|
if isinstance(node.op, ast.FloorDiv):
|
|
87
|
-
return lua.Call(lua.
|
|
87
|
+
return lua.Call(lua.Global("math.floor"), (lua.BinOp("/", left, right),))
|
|
88
88
|
op = BIN_OPS.get(type(node.op))
|
|
89
89
|
if op is None:
|
|
90
90
|
self.fail(node, f"the {type(node.op).__name__} operator is not supported")
|
|
@@ -104,7 +104,7 @@ class ExpressionCompiler(ScopeCompiler):
|
|
|
104
104
|
self.fail(node.right, "%-formatting from a mapping is not supported")
|
|
105
105
|
values = node.right.elts if isinstance(node.right, ast.Tuple) else [node.right]
|
|
106
106
|
args = (self.expr(node.left), *(self.expr(v) for v in values))
|
|
107
|
-
return lua.Call(lua.
|
|
107
|
+
return lua.Call(lua.Global("string.format"), args)
|
|
108
108
|
|
|
109
109
|
def compare(self, node: ast.Compare) -> lua.Expr:
|
|
110
110
|
if len(node.ops) != 1:
|
|
@@ -200,7 +200,7 @@ class ExpressionCompiler(ScopeCompiler):
|
|
|
200
200
|
helper = "__inlist"
|
|
201
201
|
elif kind == "str":
|
|
202
202
|
find = lua.Call(
|
|
203
|
-
lua.
|
|
203
|
+
lua.Global("string.find"),
|
|
204
204
|
(self.expr(container), self.expr(item), lua.Num(1), lua.Bool(True)),
|
|
205
205
|
)
|
|
206
206
|
return lua.BinOp("==" if negate else "~=", find, lua.Nil())
|
|
@@ -236,10 +236,10 @@ class ExpressionCompiler(ScopeCompiler):
|
|
|
236
236
|
)
|
|
237
237
|
inner = self.expr(value.value)
|
|
238
238
|
if value.format_spec is None:
|
|
239
|
-
parts.append(lua.Call(lua.
|
|
239
|
+
parts.append(lua.Call(lua.Global("tostring"), (inner,)))
|
|
240
240
|
else:
|
|
241
241
|
spec = lua.Str(self.printf_spec(value))
|
|
242
|
-
parts.append(lua.Call(lua.
|
|
242
|
+
parts.append(lua.Call(lua.Global("string.format"), (spec, inner)))
|
|
243
243
|
else: # pragma: no cover - JoinedStr only holds these two kinds
|
|
244
244
|
self.fail(value, "unsupported f-string component")
|
|
245
245
|
if not parts:
|
|
@@ -309,7 +309,7 @@ class ExpressionCompiler(ScopeCompiler):
|
|
|
309
309
|
if position is not None and position < 0
|
|
310
310
|
else self.index(node.slice, container)
|
|
311
311
|
)
|
|
312
|
-
return lua.Call(lua.
|
|
312
|
+
return lua.Call(lua.Global("string.sub"), (self.expr(node.value), at, at))
|
|
313
313
|
|
|
314
314
|
if position is not None and position < 0:
|
|
315
315
|
if not isinstance(node.value, ast.Name):
|
|
@@ -149,6 +149,15 @@ local function __find(s, sub)
|
|
|
149
149
|
local i = string.find(s, sub, 1, true)
|
|
150
150
|
if i == nil then return -1 end
|
|
151
151
|
return i - 1
|
|
152
|
+
end""",
|
|
153
|
+
"__partition": """\
|
|
154
|
+
-- str.partition: before, separator and after the first plain match, or the
|
|
155
|
+
-- whole string and two empty strings when there is none.
|
|
156
|
+
local function __partition(s, sep)
|
|
157
|
+
if sep == '' then error('empty separator', 0) end
|
|
158
|
+
local i, j = string.find(s, sep, 1, true)
|
|
159
|
+
if i == nil then return {s, '', ''} end
|
|
160
|
+
return {string.sub(s, 1, i - 1), sep, string.sub(s, j + 1)}
|
|
152
161
|
end""",
|
|
153
162
|
"__split": """\
|
|
154
163
|
-- str.split: on a plain separator, or on runs of whitespace without one.
|