redis-lua-py 0.11.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.11.0 → redis_lua_py-0.12.0}/CHANGELOG.md +7 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/PKG-INFO +1 -1
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/installation.md +25 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/reference/lua-vs-python.md +10 -2
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/reference/supported-subset.md +7 -4
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/pyproject.toml +1 -1
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/__init__.py +1 -1
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/base.py +8 -3
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/calls.py +12 -2
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/helpers.py +9 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/scope.py +30 -1
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/statements.py +1 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/tables.py +17 -2
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_errors.py +22 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_strings_and_indexing.py +63 -0
- redis_lua_py-0.11.0/.release-please-manifest.json +0 -3
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/.github/assets/README.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/.github/assets/banner-dark.svg +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/.github/assets/banner-light.svg +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/.github/assets/generate.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/.github/assets/logo.svg +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/.github/assets/logomark.svg +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/.github/assets/social-preview.png +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/.github/assets/social-preview.svg +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/.github/workflows/ci.yml +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/.github/workflows/docs.yml +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/.github/workflows/pr-title.yml +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/.github/workflows/release.yml +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/.gitignore +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/.python-version +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/CONTRIBUTING.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/LICENSE +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/README.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/assets/logo.svg +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/contributing.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/development.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/examples.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/guide/async.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/guide/binary-values.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/guide/binding-a-client.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/guide/build-time.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/guide/calling-redis-commands.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/guide/constants.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/guide/generated-lua.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/guide/keys-and-arguments.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/guide/redis-functions.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/guide/return-values.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/guide/testing.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/index.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/quickstart.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/reference/api.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/reference/errors.md +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/docs/stylesheets/extra.css +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/release-please-config.json +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/scripts/generate_commands.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/__main__.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_command_stubs.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_commands.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/__init__.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/analysis.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/control.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/expressions.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_compile/source.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_library.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_lua.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_portable.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_runtime.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/_script.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/codegen.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/errors.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/src/redis_lua_py/py.typed +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/codegen_scripts.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/conftest.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/generated_scripts.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/module_with_client.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/module_without_import.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_binary.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_binding.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_booleans_and_conversions.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_codegen.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_commands.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_compile.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_constants.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_control_flow.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_coredis.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_execute.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_expressions.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_functions.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_mistranslations.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_namespace.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_semantics.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_table_stakes.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/tests/test_typing.py +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/uv.lock +0 -0
- {redis_lua_py-0.11.0 → redis_lua_py-0.12.0}/zensical.toml +0 -0
|
@@ -7,6 +7,13 @@ 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
|
+
|
|
10
17
|
## [0.11.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.10.0...v0.11.0) (2026-09-25)
|
|
11
18
|
|
|
12
19
|
|
|
@@ -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/
|
|
@@ -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
|
|
@@ -55,9 +55,17 @@ that is not known, it looks at the subscript:
|
|
|
55
55
|
A value's type is known from its annotation, a literal, the builtin, method or
|
|
56
56
|
operator that produced it, a `range()` or `enumerate()` loop variable, or
|
|
57
57
|
every assignment to the name agreeing. So after `counts = {}`, `counts[0]` is
|
|
58
|
-
the key `0`. A
|
|
58
|
+
the key `0`. A value that comes from elsewhere -- a Redis reply,
|
|
59
59
|
`cjson.decode`, a helper's parameter -- is not known, and an integer subscript
|
|
60
|
-
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.
|
|
61
69
|
|
|
62
70
|
`items[-1]` compiles to `items[#items]`, which needs a name to count back from.
|
|
63
71
|
Indexing a string gives a one-character string, as it does in Python, through
|
|
@@ -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,8 +24,10 @@ 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
33
|
`.insert()`, `.pop()` and `dict.get()`; `isinstance()` against `str`,
|
|
@@ -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.
|
|
@@ -303,9 +306,9 @@ class CompilerBase(ABC):
|
|
|
303
306
|
return "str"
|
|
304
307
|
if attr in {"encode", "decode"}:
|
|
305
308
|
return "str"
|
|
306
|
-
if attr in {"startswith", "endswith"}:
|
|
309
|
+
if attr in {"startswith", "endswith", *STRING_TESTS}:
|
|
307
310
|
return "bool"
|
|
308
|
-
if attr
|
|
311
|
+
if attr in {"split", "partition"}:
|
|
309
312
|
return "list"
|
|
310
313
|
return "num" if attr == "find" else None
|
|
311
314
|
case ast.Attribute():
|
|
@@ -321,6 +324,8 @@ class CompilerBase(ABC):
|
|
|
321
324
|
def name_kind(self, name: str) -> str | None:
|
|
322
325
|
if name not in self.known:
|
|
323
326
|
return value_kind(self.globalns.get(name, UNBOUND))
|
|
327
|
+
if name in self.declared:
|
|
328
|
+
return self.declared[name]
|
|
324
329
|
if name in self._resolving:
|
|
325
330
|
# A name whose value depends on itself, as in `n += 1`, is whatever
|
|
326
331
|
# its other assignments make it.
|
|
@@ -18,6 +18,7 @@ from .tables import (
|
|
|
18
18
|
METHOD_HINT,
|
|
19
19
|
REDIS_DIRECT,
|
|
20
20
|
STRING_METHODS,
|
|
21
|
+
STRING_TESTS,
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
|
|
@@ -26,7 +27,12 @@ class CallCompiler(ExpressionCompiler):
|
|
|
26
27
|
|
|
27
28
|
def call(self, node: ast.Call) -> lua.Expr:
|
|
28
29
|
if node.keywords:
|
|
29
|
-
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
|
+
)
|
|
30
36
|
if (
|
|
31
37
|
isinstance(node.func, ast.Name)
|
|
32
38
|
and node.func.id == "isinstance"
|
|
@@ -205,7 +211,8 @@ class CallCompiler(ExpressionCompiler):
|
|
|
205
211
|
return target
|
|
206
212
|
allowed = {"get": (1, 2), "split": (0, 1), "replace": (2,)}.get(attr)
|
|
207
213
|
if allowed is None:
|
|
208
|
-
|
|
214
|
+
no_args = {"upper", "lower", "strip", "lstrip", "rstrip", *STRING_TESTS}
|
|
215
|
+
allowed = (0,) if attr in no_args else (1,)
|
|
209
216
|
if len(args) not in allowed:
|
|
210
217
|
if attr in {"strip", "lstrip", "rstrip"}:
|
|
211
218
|
self.fail(
|
|
@@ -218,6 +225,9 @@ class CallCompiler(ExpressionCompiler):
|
|
|
218
225
|
|
|
219
226
|
if attr in {"upper", "lower"}:
|
|
220
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())
|
|
221
231
|
if attr in {"strip", "lstrip", "rstrip"}:
|
|
222
232
|
pattern = {"strip": "^%s*(.-)%s*$", "lstrip": "^%s*(.*)$", "rstrip": "^(.-)%s*$"}
|
|
223
233
|
return lua.Call(lua.Global("string.match"), (target, lua.Str(pattern[attr])))
|
|
@@ -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.
|
|
@@ -44,6 +44,10 @@ class ScopeCompiler(CompilerBase):
|
|
|
44
44
|
node.target, ast.Name
|
|
45
45
|
):
|
|
46
46
|
record(node.target.id, node)
|
|
47
|
+
if isinstance(node, ast.AnnAssign):
|
|
48
|
+
declared = self._annotation_kind(self._without_none(node.annotation))
|
|
49
|
+
if declared is not None:
|
|
50
|
+
self.declared.setdefault(node.target.id, declared)
|
|
47
51
|
if isinstance(node, ast.AugAssign):
|
|
48
52
|
value: ast.expr = ast.BinOp(left=node.target, op=node.op, right=node.value)
|
|
49
53
|
self.values.setdefault(node.target.id, []).append(value)
|
|
@@ -118,7 +122,12 @@ class ScopeCompiler(CompilerBase):
|
|
|
118
122
|
self.variadic_key = name
|
|
119
123
|
else:
|
|
120
124
|
if self.variadic_arg is not None:
|
|
121
|
-
self.fail(
|
|
125
|
+
self.fail(
|
|
126
|
+
arg,
|
|
127
|
+
"only one list parameter of arguments is supported",
|
|
128
|
+
hint="Interleave the values in one list and index it "
|
|
129
|
+
"with a stride, as in values[3 * i + 1].",
|
|
130
|
+
)
|
|
122
131
|
self.variadic_arg = name
|
|
123
132
|
if self._is_numeric(item):
|
|
124
133
|
self.numeric_args.add(name)
|
|
@@ -192,6 +201,26 @@ class ScopeCompiler(CompilerBase):
|
|
|
192
201
|
return "str"
|
|
193
202
|
return None
|
|
194
203
|
|
|
204
|
+
@staticmethod
|
|
205
|
+
def _without_none(node: ast.expr | None) -> ast.expr | None:
|
|
206
|
+
"""``X`` from ``X | None`` or ``Optional[X]``: None is no kind of its own."""
|
|
207
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
208
|
+
try:
|
|
209
|
+
node = ast.parse(node.value, mode="eval").body
|
|
210
|
+
except SyntaxError:
|
|
211
|
+
return None
|
|
212
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
|
|
213
|
+
sides = [
|
|
214
|
+
side
|
|
215
|
+
for side in (node.left, node.right)
|
|
216
|
+
if not (isinstance(side, ast.Constant) and side.value is None)
|
|
217
|
+
]
|
|
218
|
+
return sides[0] if len(sides) == 1 else None
|
|
219
|
+
name = ScopeCompiler._annotation_name(node)
|
|
220
|
+
if isinstance(node, ast.Subscript) and name == "Optional":
|
|
221
|
+
return node.slice
|
|
222
|
+
return node
|
|
223
|
+
|
|
195
224
|
@staticmethod
|
|
196
225
|
def _list_item(node: ast.expr | None) -> ast.expr | None:
|
|
197
226
|
"""The element annotation of a ``list[...]`` parameter, if it is one."""
|
|
@@ -272,6 +272,7 @@ class Compiler(ControlFlowCompiler):
|
|
|
272
272
|
# What the script's names hold is known inside the function too, except
|
|
273
273
|
# for its parameters, which say nothing about their type.
|
|
274
274
|
child.kinds = {k: v for k, v in self.kinds.items() if k not in params}
|
|
275
|
+
child.declared = {k: v for k, v in self.declared.items() if k not in params}
|
|
275
276
|
child.values = {k: list(v) for k, v in self.values.items() if k not in params}
|
|
276
277
|
child.opaque = self.opaque - set(params)
|
|
277
278
|
child._temp = self._temp
|
|
@@ -133,8 +133,8 @@ MATH_BY_OBJECT: dict[object, str] = {getattr(math, name): name for name in [*MAT
|
|
|
133
133
|
|
|
134
134
|
METHOD_HINT = (
|
|
135
135
|
"Available: the redis and cjson namespaces; list append, insert and pop; str join, "
|
|
136
|
-
"upper, lower, strip, lstrip, rstrip, startswith, endswith, find, split,
|
|
137
|
-
"encode and decode; and dict get."
|
|
136
|
+
"upper, lower, strip, lstrip, rstrip, startswith, endswith, find, split, partition, "
|
|
137
|
+
"replace, isdigit, isalpha, isalnum, isspace, encode and decode; and dict get."
|
|
138
138
|
)
|
|
139
139
|
|
|
140
140
|
|
|
@@ -152,9 +152,24 @@ STRING_METHODS = frozenset(
|
|
|
152
152
|
"endswith",
|
|
153
153
|
"find",
|
|
154
154
|
"split",
|
|
155
|
+
"partition",
|
|
155
156
|
"replace",
|
|
157
|
+
"isdigit",
|
|
158
|
+
"isalpha",
|
|
159
|
+
"isalnum",
|
|
160
|
+
"isspace",
|
|
156
161
|
"encode",
|
|
157
162
|
"decode",
|
|
158
163
|
"get",
|
|
159
164
|
}
|
|
160
165
|
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# The str.is* tests, as Lua patterns over the whole string. Lua's classes are
|
|
169
|
+
# ASCII, which is exactly what the same methods on bytes test.
|
|
170
|
+
STRING_TESTS: dict[str, str] = {
|
|
171
|
+
"isdigit": "^%d+$",
|
|
172
|
+
"isalpha": "^%a+$",
|
|
173
|
+
"isalnum": "^%w+$",
|
|
174
|
+
"isspace": "^%s+$",
|
|
175
|
+
}
|
|
@@ -172,3 +172,25 @@ def test_an_except_name_that_is_a_lua_keyword_is_rejected() -> None:
|
|
|
172
172
|
except Exception as end:
|
|
173
173
|
return len(end)
|
|
174
174
|
return 0
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_keyword_arguments_hint_at_positional_options() -> None:
|
|
178
|
+
with pytest.raises(UnsupportedSyntax) as info:
|
|
179
|
+
|
|
180
|
+
@script
|
|
181
|
+
def put(key: Key, ttl: int) -> None:
|
|
182
|
+
redis.set(key, "v", ex=ttl)
|
|
183
|
+
|
|
184
|
+
assert info.value.hint is not None
|
|
185
|
+
assert "'EX', ttl" in info.value.hint
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_a_second_list_of_arguments_hints_at_a_stride() -> None:
|
|
189
|
+
with pytest.raises(UnsupportedSyntax) as info:
|
|
190
|
+
|
|
191
|
+
@script
|
|
192
|
+
def check(keys: list[Key], limits: list[int], ttls: list[int]) -> None:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
assert info.value.hint is not None
|
|
196
|
+
assert "3 * i" in info.value.hint
|
|
@@ -303,6 +303,69 @@ class TestStringMethods:
|
|
|
303
303
|
|
|
304
304
|
assert shifted(client, c="a") == b"b"
|
|
305
305
|
|
|
306
|
+
def test_character_class_tests(self, client: Any) -> None:
|
|
307
|
+
@script
|
|
308
|
+
def classes(text: str) -> list[bool]:
|
|
309
|
+
return [text.isdigit(), text.isalpha(), text.isalnum(), text.isspace()]
|
|
310
|
+
|
|
311
|
+
assert "__" not in classes.lua # a pattern match inline, no helper
|
|
312
|
+
assert classes(client, text="0042") == [1, None, 1, None]
|
|
313
|
+
assert classes(client, text="abC") == [None, 1, 1, None]
|
|
314
|
+
assert classes(client, text=" \t\n") == [None, None, None, 1]
|
|
315
|
+
assert classes(client, text="") == [None, None, None, None]
|
|
316
|
+
assert classes(client, text="-1") == [None, None, None, None]
|
|
317
|
+
|
|
318
|
+
def test_partition(self, client: Any) -> None:
|
|
319
|
+
@script
|
|
320
|
+
def parts(text: str) -> list[list[bytes]]:
|
|
321
|
+
return [text.partition(":"), text.partition("?")]
|
|
322
|
+
|
|
323
|
+
assert parts(client, text="5:a:b") == [[b"5", b":", b"a:b"], [b"5:a:b", b"", b""]]
|
|
324
|
+
|
|
325
|
+
def test_partition_and_isdigit_parse_a_version_prefix(self, client: Any) -> None:
|
|
326
|
+
@script
|
|
327
|
+
def version_of(key: Key) -> int:
|
|
328
|
+
raw: str | None = redis.get(key)
|
|
329
|
+
if raw is None:
|
|
330
|
+
return 0
|
|
331
|
+
prefix, sep, _body = raw.partition(":")
|
|
332
|
+
if sep and prefix.isdigit() and len(prefix) <= 15:
|
|
333
|
+
return int(prefix)
|
|
334
|
+
return 0
|
|
335
|
+
|
|
336
|
+
cases = {b"12:[]": 12, b"x:[]": 0, b"12": 0, b":[]": 0, b"1e3:[]": 0, b"-1:[]": 0}
|
|
337
|
+
for stored, expected in cases.items():
|
|
338
|
+
client.set("v", stored)
|
|
339
|
+
assert version_of(client, key="v") == expected, stored
|
|
340
|
+
client.delete("v")
|
|
341
|
+
assert version_of(client, key="v") == 0
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class TestAnnotatedLocals:
|
|
345
|
+
def test_an_annotation_makes_a_reply_a_string(self, client: Any) -> None:
|
|
346
|
+
@script
|
|
347
|
+
def first_char(key: Key) -> bytes:
|
|
348
|
+
text: str = redis.get(key)
|
|
349
|
+
return text[0]
|
|
350
|
+
|
|
351
|
+
assert "string.sub" in first_char.lua
|
|
352
|
+
client.set("t", "hello")
|
|
353
|
+
assert first_char(client, key="t") == b"h"
|
|
354
|
+
|
|
355
|
+
def test_an_optional_annotation_declares_the_inner_kind(self, client: Any) -> None:
|
|
356
|
+
@script
|
|
357
|
+
def digits_of(key: Key) -> list[bytes]:
|
|
358
|
+
text: str | None = redis.get(key)
|
|
359
|
+
out = []
|
|
360
|
+
if text is not None:
|
|
361
|
+
for i in range(len(text)):
|
|
362
|
+
out.append(text[i])
|
|
363
|
+
return out
|
|
364
|
+
|
|
365
|
+
assert "string.sub" in digits_of.lua
|
|
366
|
+
client.set("t", "42")
|
|
367
|
+
assert digits_of(client, key="t") == [b"4", b"2"]
|
|
368
|
+
|
|
306
369
|
|
|
307
370
|
class TestFormatting:
|
|
308
371
|
def test_percent_formatting(self, client: Any) -> None:
|
|
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
|
|
File without changes
|
|
File without changes
|