redis-lua-py 0.8.0__tar.gz → 0.10.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/.github/workflows/ci.yml +1 -1
  2. redis_lua_py-0.10.0/.release-please-manifest.json +3 -0
  3. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/CHANGELOG.md +18 -0
  4. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/CONTRIBUTING.md +3 -2
  5. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/PKG-INFO +2 -3
  6. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/README.md +1 -1
  7. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/development.md +3 -2
  8. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/guide/calling-redis-commands.md +16 -0
  9. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/installation.md +7 -4
  10. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/reference/api.md +4 -0
  11. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/pyproject.toml +5 -2
  12. redis_lua_py-0.10.0/scripts/generate_commands.py +329 -0
  13. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/__init__.py +1 -1
  14. redis_lua_py-0.10.0/src/redis_lua_py/_command_stubs.py +7466 -0
  15. redis_lua_py-0.10.0/src/redis_lua_py/_runtime.py +208 -0
  16. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_commands.py +72 -0
  17. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_typing.py +26 -1
  18. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/uv.lock +3 -5
  19. redis_lua_py-0.8.0/.release-please-manifest.json +0 -3
  20. redis_lua_py-0.8.0/scripts/generate_commands.py +0 -105
  21. redis_lua_py-0.8.0/src/redis_lua_py/_runtime.py +0 -72
  22. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/.github/assets/README.md +0 -0
  23. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/.github/assets/banner-dark.svg +0 -0
  24. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/.github/assets/banner-light.svg +0 -0
  25. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/.github/assets/generate.py +0 -0
  26. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/.github/assets/logo.svg +0 -0
  27. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/.github/assets/logomark.svg +0 -0
  28. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/.github/assets/social-preview.png +0 -0
  29. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/.github/assets/social-preview.svg +0 -0
  30. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/.github/workflows/docs.yml +0 -0
  31. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/.github/workflows/pr-title.yml +0 -0
  32. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/.github/workflows/release.yml +0 -0
  33. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/.gitignore +0 -0
  34. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/.python-version +0 -0
  35. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/LICENSE +0 -0
  36. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/assets/logo.svg +0 -0
  37. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/contributing.md +0 -0
  38. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/examples.md +0 -0
  39. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/guide/async.md +0 -0
  40. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/guide/binary-values.md +0 -0
  41. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/guide/binding-a-client.md +0 -0
  42. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/guide/build-time.md +0 -0
  43. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/guide/constants.md +0 -0
  44. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/guide/generated-lua.md +0 -0
  45. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/guide/keys-and-arguments.md +0 -0
  46. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/guide/redis-functions.md +0 -0
  47. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/guide/return-values.md +0 -0
  48. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/guide/testing.md +0 -0
  49. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/index.md +0 -0
  50. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/quickstart.md +0 -0
  51. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/reference/errors.md +0 -0
  52. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/reference/lua-vs-python.md +0 -0
  53. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/reference/supported-subset.md +0 -0
  54. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/docs/stylesheets/extra.css +0 -0
  55. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/release-please-config.json +0 -0
  56. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/__main__.py +0 -0
  57. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/_commands.py +0 -0
  58. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/_compile/__init__.py +0 -0
  59. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/_compile/analysis.py +0 -0
  60. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/_compile/base.py +0 -0
  61. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/_compile/calls.py +0 -0
  62. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/_compile/control.py +0 -0
  63. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/_compile/expressions.py +0 -0
  64. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/_compile/helpers.py +0 -0
  65. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/_compile/scope.py +0 -0
  66. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/_compile/source.py +0 -0
  67. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/_compile/statements.py +0 -0
  68. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/_compile/tables.py +0 -0
  69. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/_library.py +0 -0
  70. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/_lua.py +0 -0
  71. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/_portable.py +0 -0
  72. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/_script.py +0 -0
  73. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/codegen.py +0 -0
  74. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/errors.py +0 -0
  75. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/src/redis_lua_py/py.typed +0 -0
  76. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/codegen_scripts.py +0 -0
  77. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/conftest.py +0 -0
  78. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/generated_scripts.py +0 -0
  79. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/module_with_client.py +0 -0
  80. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/module_without_import.py +0 -0
  81. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_binary.py +0 -0
  82. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_binding.py +0 -0
  83. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_booleans_and_conversions.py +0 -0
  84. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_codegen.py +0 -0
  85. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_compile.py +0 -0
  86. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_constants.py +0 -0
  87. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_control_flow.py +0 -0
  88. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_coredis.py +0 -0
  89. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_errors.py +0 -0
  90. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_execute.py +0 -0
  91. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_expressions.py +0 -0
  92. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_functions.py +0 -0
  93. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_mistranslations.py +0 -0
  94. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_namespace.py +0 -0
  95. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_semantics.py +0 -0
  96. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_strings_and_indexing.py +0 -0
  97. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/tests/test_table_stakes.py +0 -0
  98. {redis_lua_py-0.8.0 → redis_lua_py-0.10.0}/zensical.toml +0 -0
@@ -54,7 +54,7 @@ jobs:
54
54
  with:
55
55
  enable-cache: true
56
56
  - run: uv sync --dev --python 3.10
57
- # The floor in pyproject.toml is only a claim until something runs on it.
57
+ # The redis-py floor the docs name is only a claim until something runs on it.
58
58
  # fakeredis declares redis>=4.3 but works on 4.2, the first release with
59
59
  # redis.asyncio, so the pin goes in after the resolver is done.
60
60
  - run: uv pip install --no-deps "redis==4.2.0" async-timeout deprecated packaging
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.10.0"
3
+ }
@@ -7,6 +7,24 @@ 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.10.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.9.0...v0.10.0) (2026-09-14)
11
+
12
+
13
+ ### Added
14
+
15
+ * document every redis command on hover in editors ([#28](https://github.com/IgnaceMaes/redis-lua-py/issues/28)) ([5d86e35](https://github.com/IgnaceMaes/redis-lua-py/commit/5d86e355c97d9a597cfc59985feaa88bc3a6fc17))
16
+
17
+ ## [0.9.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.8.0...v0.9.0) (2026-09-13)
18
+
19
+
20
+ ### ⚠ BREAKING CHANGES
21
+
22
+ * installing redis-lua-py no longer installs redis-py. Add redis-py or coredis to your own dependencies.
23
+
24
+ ### Added
25
+
26
+ * drop the redis-py dependency ([#26](https://github.com/IgnaceMaes/redis-lua-py/issues/26)) ([e6e40e9](https://github.com/IgnaceMaes/redis-lua-py/commit/e6e40e914afb4043468d9b5b15f970940effcd63))
27
+
10
28
  ## [0.8.0](https://github.com/IgnaceMaes/redis-lua-py/compare/v0.7.0...v0.8.0) (2026-09-13)
11
29
 
12
30
 
@@ -12,8 +12,9 @@ Redis.
12
12
 
13
13
  `src/redis_lua_py/_commands.py` is generated, not written. It is the table
14
14
  command names are checked against, and it comes from the command definitions
15
- in the Redis source. Refresh it when a Redis release adds commands, and commit
16
- the result:
15
+ in the Redis source. The same run writes `src/redis_lua_py/_command_stubs.py`,
16
+ the typed method per command that editors show on hover. Refresh both when a
17
+ Redis release adds commands, and commit the result:
17
18
 
18
19
  ```sh
19
20
  uv run python scripts/generate_commands.py 8.10.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: redis-lua-py
3
- Version: 0.8.0
3
+ Version: 0.10.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/
@@ -21,7 +21,6 @@ Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Topic :: Database
22
22
  Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.10
24
- Requires-Dist: redis>=4.2
25
24
  Description-Content-Type: text/markdown
26
25
 
27
26
  <p align="center">
@@ -89,7 +88,7 @@ for the script namespace, so the two never collide.
89
88
  uv add redis-lua-py
90
89
  ```
91
90
 
92
- Python 3.10+, and redis-py 4.2+ as the only dependency.
91
+ Python 3.10+, and no dependencies: bring your own client, redis-py 4.2+ or coredis.
93
92
 
94
93
  ## What it compiles to
95
94
 
@@ -63,7 +63,7 @@ for the script namespace, so the two never collide.
63
63
  uv add redis-lua-py
64
64
  ```
65
65
 
66
- Python 3.10+, and redis-py 4.2+ as the only dependency.
66
+ Python 3.10+, and no dependencies: bring your own client, redis-py 4.2+ or coredis.
67
67
 
68
68
  ## What it compiles to
69
69
 
@@ -24,8 +24,9 @@ part you would most want a real server for.
24
24
 
25
25
  `src/redis_lua_py/_commands.py` is generated, not written. It is the table
26
26
  command names are checked against, and it comes from the command definitions
27
- in the Redis source. Refresh it when a Redis release adds commands, and commit
28
- the result:
27
+ in the Redis source. The same run writes `src/redis_lua_py/_command_stubs.py`,
28
+ the typed method per command that editors show on hover. Refresh both when a
29
+ Redis release adds commands, and commit the result:
29
30
 
30
31
  ```bash
31
32
  uv run python scripts/generate_commands.py 8.10.1
@@ -58,6 +58,22 @@ commands are checked down to the subcommand, and a hyphenated one is reached
58
58
  through its underscores — `redis.client_no_evict("on")` compiles to
59
59
  `redis.call('CLIENT', 'NO-EVICT', 'on')`.
60
60
 
61
+ ## In your editor
62
+
63
+ Type checkers see every command as a method, generated from the same command
64
+ definitions as the table, so hovering `redis.hset` shows what the command does,
65
+ its syntax, its reply and its complexity, with a link to its page on redis.io.
66
+ The Lua API's own functions, such as `redis.pcall` and `redis.log`, and
67
+ `cjson.encode` / `cjson.decode`, are documented the same way.
68
+
69
+ The leading plain arguments are named, so a call with too few or too many is
70
+ flagged before the compiler sees it. Past the first option the order depends on
71
+ which options are given, so the rest is `*args` and the syntax in the hover says
72
+ what goes there. Every return type is `Any`: what a reply looks like inside Lua
73
+ is not something Python can know. A spelling that is not one method per
74
+ command, such as `redis.debug_object(k)`, still type-checks, and is left to the
75
+ compiler.
76
+
61
77
  ## The escape hatch
62
78
 
63
79
  `redis.call(...)` is deliberately never checked. It is the escape hatch for
@@ -13,8 +13,12 @@ pip install redis-lua-py
13
13
  ## Requirements
14
14
 
15
15
  - Python 3.10 or newer
16
- - [redis-py](https://github.com/redis/redis-py) 4.2 or newer — the only
17
- dependency, and installed with the package
16
+ - A Redis client: [redis-py](https://github.com/redis/redis-py) 4.2 or newer,
17
+ or [coredis](https://github.com/alisaifee/coredis)
18
+
19
+ The package has no dependencies of its own. It only calls the client you pass
20
+ it, so it never installs one: add redis-py or coredis yourself, as you would
21
+ to talk to Redis at all.
18
22
 
19
23
  Script caching, `EVALSHA` and the `NOSCRIPT` reload are handled by redis-py's
20
24
  own script machinery, which this library defers to rather than reimplementing.
@@ -22,8 +26,7 @@ Anything redis-py can talk to, this can run against: a standalone server, a
22
26
  cluster, Sentinel, or an in-process [fakeredis](guide/testing.md).
23
27
 
24
28
  [coredis](https://github.com/alisaifee/coredis) clients work as well, and are
25
- tested against, but coredis is not a dependency: install it yourself. See
26
- [Async](guide/async.md#coredis).
29
+ tested against. See [Async](guide/async.md#coredis).
27
30
 
28
31
  ## For testing
29
32
 
@@ -79,6 +79,10 @@ Names are checked at compile time against Redis' own command table;
79
79
  The compiler identifies the namespace by value rather than by the name it is
80
80
  imported under, so every alias works and nothing is reserved.
81
81
 
82
+ Type checkers see a method per command, documented from Redis' own command
83
+ definitions, so an editor shows each command's syntax on hover. See
84
+ [In your editor](../guide/calling-redis-commands.md#in-your-editor).
85
+
82
86
  ## `call`
83
87
 
84
88
  An alias of [`redis`](#redis), for modules that would rather not rename
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "redis-lua-py"
3
- version = "0.8.0"
3
+ version = "0.10.0"
4
4
  description = "Write Redis Lua scripts as real Python functions, not strings."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -19,7 +19,7 @@ classifiers = [
19
19
  "Topic :: Database",
20
20
  "Typing :: Typed",
21
21
  ]
22
- dependencies = ["redis>=4.2"]
22
+ dependencies = []
23
23
 
24
24
  [project.urls]
25
25
  Homepage = "https://ignacemaes.com/redis-lua-py/"
@@ -33,6 +33,9 @@ redis-lua-py = "redis_lua_py.__main__:main"
33
33
 
34
34
  [dependency-groups]
35
35
  dev = [
36
+ # The client every test runs through. Not a dependency of the package, which
37
+ # only calls the client it is handed: that client's library is already installed.
38
+ "redis>=4.2",
36
39
  "pytest>=8.3",
37
40
  "pytest-asyncio>=1.0",
38
41
  "fakeredis[lua]>=2.30",
@@ -0,0 +1,329 @@
1
+ """Regenerate the command table and its typed stubs from the Redis source tree.
2
+
3
+ The compiler refuses a command Redis does not have, which means it needs to
4
+ know what Redis has. The authority for that is ``src/commands/*.json`` in the
5
+ Redis repository — the same files the server itself is built from — so this
6
+ downloads a tagged release and reads them rather than transcribing a list.
7
+
8
+ uv run python scripts/generate_commands.py [version]
9
+
10
+ Two files come out of it. ``src/redis_lua_py/_commands.py`` is the table the
11
+ compiler checks names against. ``src/redis_lua_py/_command_stubs.py`` is a class
12
+ with one method per command, carrying the summary, syntax and complexity from
13
+ the same JSON, which type checkers see in place of the namespace so that an
14
+ editor can show them on hover.
15
+
16
+ Run it when a Redis release adds commands, and commit the result. The command
17
+ table only gates the ``redis.<name>()`` spelling: ``redis.call('NEW.CMD', ...)``
18
+ is deliberately never checked, so a stale table can never block a caller.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import io
24
+ import json
25
+ import keyword
26
+ import re
27
+ import subprocess
28
+ import sys
29
+ import tarfile
30
+ import textwrap
31
+ import urllib.request
32
+ from pathlib import Path
33
+ from typing import Any
34
+
35
+ from redis_lua_py._compile.tables import COMMAND_ALIASES, REDIS_DIRECT
36
+
37
+ DEFAULT_VERSION = "8.10.1"
38
+ SOURCE = "https://github.com/redis/redis/archive/refs/tags/{version}.tar.gz"
39
+ PACKAGE = Path(__file__).resolve().parent.parent / "src" / "redis_lua_py"
40
+ TABLE = PACKAGE / "_commands.py"
41
+ STUBS = PACKAGE / "_command_stubs.py"
42
+
43
+ Spec = dict[str, Any]
44
+
45
+ #: Argument types that are a single value, and so can be a named parameter.
46
+ VALUE_TYPES = frozenset({"key", "string", "integer", "double", "unix-time", "pattern"})
47
+
48
+ #: Docstring text is wrapped to this, inside a method eight spaces deep.
49
+ WIDTH = 88
50
+
51
+
52
+ def fetch(version: str) -> tuple[dict[str, Spec], dict[str, dict[str, Spec]]]:
53
+ """Read every command definition out of a released Redis tarball.
54
+
55
+ A subcommand's file names it bare -- ``hotkeys-get.json`` defines ``GET``
56
+ with ``"container": "HOTKEYS"`` -- so entries are separated as they are
57
+ read. Collecting them into one dict first would let ``HOTKEYS GET`` quietly
58
+ displace plain ``GET``.
59
+ """
60
+ url = SOURCE.format(version=version)
61
+ print(f"fetching {url}", file=sys.stderr)
62
+ with urllib.request.urlopen(url) as response:
63
+ payload = response.read()
64
+
65
+ commands: dict[str, Spec] = {}
66
+ subcommands: dict[str, dict[str, Spec]] = {}
67
+ with tarfile.open(fileobj=io.BytesIO(payload), mode="r:gz") as archive:
68
+ for member in archive.getmembers():
69
+ parts = Path(member.name).parts
70
+ if parts[1:3] != ("src", "commands") or not member.name.endswith(".json"):
71
+ continue
72
+ handle = archive.extractfile(member)
73
+ if handle is None: # pragma: no cover - directories were filtered out
74
+ continue
75
+ for name, spec in json.loads(handle.read()).items():
76
+ container = spec.get("container")
77
+ if isinstance(container, str):
78
+ subcommands.setdefault(container.upper(), {})[name.upper()] = spec
79
+ else:
80
+ commands[name.upper()] = spec
81
+ if not commands:
82
+ raise SystemExit(f"no command definitions found in {url}")
83
+ return commands, subcommands
84
+
85
+
86
+ def render_table(version: str, commands: set[str], subcommands: dict[str, set[str]]) -> str:
87
+ lines = [
88
+ '"""The commands Redis accepts, by name.',
89
+ "",
90
+ f"Generated by scripts/generate_commands.py from Redis {version}. Do not edit.",
91
+ "",
92
+ "``COMMANDS`` holds every command that is not a subcommand; ``SUBCOMMANDS``",
93
+ "maps each container command to the subcommands it takes. Together they are",
94
+ "what ``redis.<name>()`` is checked against, so that a name Redis does not",
95
+ "have is refused at import rather than at the first execution of a branch.",
96
+ '"""',
97
+ "",
98
+ "from __future__ import annotations",
99
+ "",
100
+ f'REDIS_VERSION = "{version}"',
101
+ "",
102
+ "COMMANDS: frozenset[str] = frozenset(",
103
+ " {",
104
+ ]
105
+ lines += [f' "{name}",' for name in sorted(commands)]
106
+ lines += [" }", ")", "", "SUBCOMMANDS: dict[str, frozenset[str]] = {"]
107
+ for container in sorted(subcommands):
108
+ lines.append(f' "{container}": frozenset(')
109
+ lines.append(" {")
110
+ lines += [f' "{name}",' for name in sorted(subcommands[container])]
111
+ lines += [" }", " ),"]
112
+ lines += ["}", ""]
113
+ return "\n".join(lines)
114
+
115
+
116
+ def syntax(arg: Spec) -> str:
117
+ """Render one argument the way the Redis documentation writes it."""
118
+ kind = arg["type"]
119
+ token = arg.get("token")
120
+ if kind == "pure-token":
121
+ text = token or arg.get("display_text") or arg["name"].upper()
122
+ else:
123
+ if kind == "oneof":
124
+ text = " | ".join(syntax(child) for child in arg["arguments"])
125
+ if not arg.get("optional") or token or arg.get("multiple"):
126
+ text = f"<{text}>"
127
+ elif kind == "block":
128
+ text = " ".join(syntax(child) for child in arg["arguments"])
129
+ else:
130
+ text = arg.get("display", arg["name"])
131
+ if arg.get("multiple"):
132
+ if token and arg.get("multiple_token"):
133
+ text = f"{token} {text} [{token} {text} ...]"
134
+ else:
135
+ text = f"{text} [{text} ...]"
136
+ if token:
137
+ text = f"{token} {text}"
138
+ elif token:
139
+ text = f"{token} {text}"
140
+ return f"[{text}]" if arg.get("optional") else text
141
+
142
+
143
+ def identifier(name: str, taken: set[str]) -> str:
144
+ base = re.sub(r"\W", "_", name.lower())
145
+ if keyword.iskeyword(base) or base in {"self", "args"} or base[0].isdigit():
146
+ base += "_"
147
+ candidate, n = base, 2
148
+ while candidate in taken:
149
+ candidate, n = f"{base}{n}", n + 1
150
+ taken.add(candidate)
151
+ return candidate
152
+
153
+
154
+ def parameters(tokens: tuple[str, ...], spec: Spec) -> tuple[list[str], bool]:
155
+ """The named parameters a command takes, and whether more may follow.
156
+
157
+ Only the leading run of plain required values is named. Past the first
158
+ token, option or group, the order and count depend on which options are
159
+ given, so the rest is left to ``*args`` and the syntax in the docstring.
160
+ Redis' own arity settles the count: a signature that disagrees with it is
161
+ widened to ``*args`` rather than trusted.
162
+ """
163
+ arguments = spec.get("arguments", [])
164
+ names: list[str] = []
165
+ taken: set[str] = set()
166
+ rest = False
167
+ for arg in arguments:
168
+ if arg["type"] not in VALUE_TYPES or arg.get("token") or arg.get("optional"):
169
+ rest = True
170
+ break
171
+ names.append(identifier(arg["name"], taken))
172
+ if arg.get("multiple"):
173
+ rest = True
174
+ break
175
+
176
+ arity = spec["arity"]
177
+ fixed = len(tokens) + len(names)
178
+ if arity < 0:
179
+ if fixed > -arity:
180
+ names = names[: -arity - len(tokens)]
181
+ return names, True
182
+ if rest or fixed != arity:
183
+ return names[: max(arity - len(tokens), 0)], True
184
+ return names, False
185
+
186
+
187
+ def plain(text: str) -> str:
188
+ """Turn the JSON's Markdown code spans into reST ones: `!GET` -> ``GET``."""
189
+ return re.sub(r"`!?([^`]+)`", r"``\1``", " ".join(text.split()))
190
+
191
+
192
+ def replies(schema: Spec) -> list[str]:
193
+ if "description" in schema:
194
+ return [schema["description"]]
195
+ for key in ("oneOf", "anyOf"):
196
+ if key in schema:
197
+ return [option["description"] for option in schema[key] if "description" in option]
198
+ return []
199
+
200
+
201
+ def docstring(tokens: tuple[str, ...], spec: Spec, has_args: bool) -> list[str]:
202
+ paragraphs: list[list[str]] = [textwrap.wrap(plain(spec["summary"]), WIDTH)]
203
+
204
+ if "deprecated_since" in spec:
205
+ note = f"Deprecated since Redis {spec['deprecated_since']}"
206
+ if "replaced_by" in spec:
207
+ note += f", replaced by {plain(spec['replaced_by'])}"
208
+ paragraphs.append(textwrap.wrap(note + ".", WIDTH))
209
+
210
+ usage = " ".join([*tokens, *(syntax(arg) for arg in spec.get("arguments", []))])
211
+ wrapped = textwrap.wrap(
212
+ usage,
213
+ WIDTH - 4,
214
+ subsequent_indent=" ",
215
+ break_long_words=False,
216
+ break_on_hyphens=False,
217
+ )
218
+ paragraphs.append(["Syntax::"])
219
+ paragraphs.append([" " + line for line in wrapped])
220
+
221
+ reply = [plain(text) for text in replies(spec.get("reply_schema", {}))]
222
+ if len(reply) == 1:
223
+ paragraphs.append(textwrap.wrap(f"Reply: {reply[0]}", WIDTH))
224
+ elif reply:
225
+ bullets = ["Reply, one of:", ""]
226
+ for text in reply:
227
+ bullets += textwrap.wrap(text, WIDTH, initial_indent="- ", subsequent_indent=" ")
228
+ paragraphs.append(bullets)
229
+
230
+ if "complexity" in spec:
231
+ paragraphs.append(textwrap.wrap(f"Time complexity: {plain(spec['complexity'])}", WIDTH))
232
+
233
+ call = ", ".join([*(f"'{token}'" for token in tokens), *(["..."] if has_args else [])])
234
+ paragraphs.append(
235
+ textwrap.wrap(
236
+ f"Available since Redis {spec['since']}. Compiles to ``redis.call({call})``.", WIDTH
237
+ )
238
+ )
239
+ if spec["group"] != "sentinel": # Sentinel commands have no page of their own.
240
+ page = "-".join(tokens).lower()
241
+ paragraphs.append([f"https://redis.io/docs/latest/commands/{page}/"])
242
+
243
+ lines: list[str] = []
244
+ for paragraph in paragraphs:
245
+ if lines:
246
+ lines.append("")
247
+ lines += paragraph
248
+ return lines
249
+
250
+
251
+ def method(name: str, tokens: tuple[str, ...], spec: Spec) -> list[str]:
252
+ names, rest = parameters(tokens, spec)
253
+ params = ["self", *(f"{param}: Any" for param in names)]
254
+ if names:
255
+ params.append("/")
256
+ if rest:
257
+ params.append("*args: Any")
258
+
259
+ doc = [line.replace("\\", "\\\\") for line in docstring(tokens, spec, bool(names) or rest)]
260
+ lines = [f" def {name}({', '.join(params)}) -> Any:"]
261
+ lines.append(f' """{doc[0]}')
262
+ lines += [f" {line}".rstrip() for line in doc[1:]]
263
+ lines.append(' """')
264
+ return lines
265
+
266
+
267
+ def render_stubs(
268
+ version: str, commands: dict[str, Spec], subcommands: dict[str, dict[str, Spec]]
269
+ ) -> str:
270
+ entries: dict[str, tuple[tuple[str, ...], Spec]] = {}
271
+ for command, spec in commands.items():
272
+ if command in subcommands:
273
+ continue # A container needs a subcommand; it is not callable bare.
274
+ if "-" in command and command.split("-")[0] in commands:
275
+ # redis.restore_asking reaches RESTORE with an ASKING argument, not
276
+ # RESTORE-ASKING, so a stub under that name would describe the wrong command.
277
+ continue
278
+ entries[command.lower().replace("-", "_")] = ((command,), spec)
279
+ for container, members in subcommands.items():
280
+ for sub, spec in members.items():
281
+ entries[f"{container}_{sub}".lower().replace("-", "_")] = ((container, sub), spec)
282
+ for alias, tokens in COMMAND_ALIASES.items():
283
+ entries[alias] = (tokens, commands[tokens[0]])
284
+ for name in [*entries]:
285
+ if keyword.iskeyword(name) or name in REDIS_DIRECT:
286
+ del entries[name]
287
+
288
+ lines = [
289
+ '"""A typed method for every Redis command a script body can call.',
290
+ "",
291
+ f"Generated by scripts/generate_commands.py from Redis {version}. Do not edit.",
292
+ "",
293
+ "Nothing imports this at runtime. Type checkers see the ``redis`` namespace as",
294
+ "a subclass of ``RedisCommands``, so an editor can show each command's syntax",
295
+ "and documentation on hover, while the compiler goes on resolving the",
296
+ "namespace by value.",
297
+ '"""',
298
+ "",
299
+ "from __future__ import annotations",
300
+ "",
301
+ "from typing import Any",
302
+ "",
303
+ "",
304
+ "class RedisCommands:",
305
+ ' """The Redis commands, as ``redis.<name>(...)`` spells them."""',
306
+ ]
307
+ for name in sorted(entries):
308
+ lines.append("")
309
+ lines += method(name, *entries[name])
310
+ lines.append("")
311
+ return "\n".join(lines)
312
+
313
+
314
+ def main() -> None:
315
+ version = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_VERSION
316
+ commands, subcommands = fetch(version)
317
+ names = {container: set(members) for container, members in subcommands.items()}
318
+ TABLE.write_text(render_table(version, set(commands), names))
319
+ STUBS.write_text(render_stubs(version, commands, subcommands))
320
+ subprocess.run([sys.executable, "-m", "ruff", "format", str(STUBS)], check=True)
321
+ print(
322
+ f"wrote {TABLE.relative_to(Path.cwd())} and {STUBS.relative_to(Path.cwd())}: "
323
+ f"{len(commands)} commands, {len(subcommands)} containers",
324
+ file=sys.stderr,
325
+ )
326
+
327
+
328
+ if __name__ == "__main__":
329
+ main()
@@ -67,7 +67,7 @@ __all__ = [
67
67
  "script",
68
68
  ]
69
69
 
70
- __version__ = "0.8.0" # x-release-please-version
70
+ __version__ = "0.10.0" # x-release-please-version
71
71
 
72
72
 
73
73
  @overload