snail-lang 0.3.4__cp310-abi3-win_amd64.whl → 0.3.7__cp310-abi3-win_amd64.whl
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.
- snail/_native.pyd +0 -0
- snail/cli.py +2 -0
- snail/runtime/__init__.py +43 -9
- snail/runtime/structured_accessor.py +48 -44
- snail/runtime/subprocess.py +2 -2
- {snail_lang-0.3.4.dist-info → snail_lang-0.3.7.dist-info}/METADATA +27 -15
- {snail_lang-0.3.4.dist-info → snail_lang-0.3.7.dist-info}/RECORD +10 -10
- {snail_lang-0.3.4.dist-info → snail_lang-0.3.7.dist-info}/WHEEL +0 -0
- {snail_lang-0.3.4.dist-info → snail_lang-0.3.7.dist-info}/entry_points.txt +0 -0
- {snail_lang-0.3.4.dist-info → snail_lang-0.3.7.dist-info}/licenses/LICENSE +0 -0
snail/_native.pyd
CHANGED
|
Binary file
|
snail/cli.py
CHANGED
|
@@ -21,6 +21,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
21
21
|
parser.add_argument("-f", dest="file", metavar="file")
|
|
22
22
|
parser.add_argument("-a", "--awk", action="store_true")
|
|
23
23
|
parser.add_argument("-P", "--no-print", action="store_true")
|
|
24
|
+
parser.add_argument("-I", "--no-auto-import", action="store_true")
|
|
24
25
|
parser.add_argument("--parse-only", action="store_true")
|
|
25
26
|
parser.add_argument("-v", "--version", action="store_true")
|
|
26
27
|
parser.add_argument("args", nargs=argparse.REMAINDER)
|
|
@@ -59,6 +60,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
59
60
|
argv=args,
|
|
60
61
|
mode=mode,
|
|
61
62
|
auto_print=not namespace.no_print,
|
|
63
|
+
auto_import=not namespace.no_auto_import,
|
|
62
64
|
filename=filename,
|
|
63
65
|
)
|
|
64
66
|
|
snail/runtime/__init__.py
CHANGED
|
@@ -1,16 +1,51 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import functools
|
|
4
|
+
import importlib
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
3
7
|
from .compact_try import compact_try
|
|
4
8
|
from .regex import regex_compile, regex_search
|
|
5
9
|
from .structured_accessor import (
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
StructuredAccessor,
|
|
9
|
-
json,
|
|
10
|
+
__snail_jmespath_query,
|
|
11
|
+
js,
|
|
10
12
|
)
|
|
11
13
|
from .subprocess import SubprocessCapture, SubprocessStatus
|
|
12
14
|
|
|
13
|
-
__all__ = ["install_helpers"]
|
|
15
|
+
__all__ = ["install_helpers", "AutoImportDict", "AUTO_IMPORT_NAMES"]
|
|
16
|
+
|
|
17
|
+
# Names that can be auto-imported when first referenced.
|
|
18
|
+
# Maps name -> (module, attribute) where attribute is None for whole-module imports.
|
|
19
|
+
AUTO_IMPORT_NAMES: dict[str, tuple[str, str | None]] = {
|
|
20
|
+
# Whole module imports: import X
|
|
21
|
+
"sys": ("sys", None),
|
|
22
|
+
"os": ("os", None),
|
|
23
|
+
# Attribute imports: from X import Y
|
|
24
|
+
"Path": ("pathlib", "Path"),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AutoImportDict(dict):
|
|
29
|
+
"""A dict subclass that lazily imports allowed names on first access.
|
|
30
|
+
|
|
31
|
+
When a key lookup fails, if the key is in AUTO_IMPORT_NAMES,
|
|
32
|
+
the corresponding module/attribute is imported and stored in the dict.
|
|
33
|
+
Supports both whole-module imports (import sys) and attribute imports
|
|
34
|
+
(from pathlib import Path).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __missing__(self, key: str) -> Any:
|
|
38
|
+
if key in AUTO_IMPORT_NAMES:
|
|
39
|
+
module_name, attr_name = AUTO_IMPORT_NAMES[key]
|
|
40
|
+
module = importlib.import_module(module_name)
|
|
41
|
+
value = getattr(module, attr_name) if attr_name else module
|
|
42
|
+
self[key] = value
|
|
43
|
+
return value
|
|
44
|
+
raise KeyError(key)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def __snail_partial(func, /, *args, **kwargs):
|
|
48
|
+
return functools.partial(func, *args, **kwargs)
|
|
14
49
|
|
|
15
50
|
|
|
16
51
|
def install_helpers(globals_dict: dict) -> None:
|
|
@@ -19,7 +54,6 @@ def install_helpers(globals_dict: dict) -> None:
|
|
|
19
54
|
globals_dict["__snail_regex_compile"] = regex_compile
|
|
20
55
|
globals_dict["__SnailSubprocessCapture"] = SubprocessCapture
|
|
21
56
|
globals_dict["__SnailSubprocessStatus"] = SubprocessStatus
|
|
22
|
-
globals_dict["
|
|
23
|
-
globals_dict["
|
|
24
|
-
globals_dict["
|
|
25
|
-
globals_dict["json"] = json
|
|
57
|
+
globals_dict["__snail_jmespath_query"] = __snail_jmespath_query
|
|
58
|
+
globals_dict["__snail_partial"] = __snail_partial
|
|
59
|
+
globals_dict["js"] = js
|
|
@@ -1,71 +1,75 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json as _json
|
|
4
|
+
import os as _os
|
|
4
5
|
import sys as _sys
|
|
5
6
|
|
|
6
7
|
from ..vendor import jmespath
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
self.query = query
|
|
10
|
+
def __snail_jmespath_query(query: str):
|
|
11
|
+
"""Create a callable that applies JMESPath query.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
raise TypeError(
|
|
16
|
-
"Pipeline target must implement __structured__, "
|
|
17
|
-
f"got {type(obj).__name__}"
|
|
18
|
-
)
|
|
19
|
-
return obj.__structured__(self.query)
|
|
13
|
+
Used by the $[query] syntax which lowers to __snail_jmespath_query(query).
|
|
14
|
+
"""
|
|
20
15
|
|
|
16
|
+
def apply(data):
|
|
17
|
+
return jmespath.search(query, data)
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
def __init__(self, data) -> None:
|
|
24
|
-
self.data = data
|
|
19
|
+
return apply
|
|
25
20
|
|
|
26
|
-
def __structured__(self, query: str):
|
|
27
|
-
return jmespath.search(query, self.data)
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
def _parse_jsonl(content: str):
|
|
23
|
+
lines = [line for line in content.splitlines() if line.strip()]
|
|
24
|
+
if not lines:
|
|
25
|
+
return []
|
|
31
26
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
27
|
+
items = []
|
|
28
|
+
for line in lines:
|
|
29
|
+
try:
|
|
30
|
+
items.append(_json.loads(line))
|
|
31
|
+
except _json.JSONDecodeError as exc:
|
|
32
|
+
raise _json.JSONDecodeError(
|
|
33
|
+
f"Invalid JSONL line: {exc.msg}",
|
|
34
|
+
line,
|
|
35
|
+
exc.pos,
|
|
36
|
+
) from exc
|
|
37
|
+
return items
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def js(input_data=None):
|
|
41
|
+
"""Parse JSON from various input sources.
|
|
42
|
+
|
|
43
|
+
Returns the parsed Python object (dict, list, etc.) directly.
|
|
44
|
+
If called with no arguments, reads from stdin.
|
|
45
|
+
"""
|
|
50
46
|
if input_data is None:
|
|
51
|
-
|
|
47
|
+
input_data = _sys.stdin
|
|
52
48
|
|
|
53
49
|
if isinstance(input_data, str):
|
|
54
50
|
try:
|
|
55
|
-
|
|
51
|
+
return _json.loads(input_data)
|
|
56
52
|
except _json.JSONDecodeError:
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
if _os.path.exists(input_data):
|
|
54
|
+
with open(input_data, "r", encoding="utf-8") as handle:
|
|
55
|
+
content = handle.read()
|
|
56
|
+
try:
|
|
57
|
+
return _json.loads(content)
|
|
58
|
+
except _json.JSONDecodeError:
|
|
59
|
+
return _parse_jsonl(content)
|
|
60
|
+
else:
|
|
61
|
+
return _parse_jsonl(input_data)
|
|
59
62
|
elif hasattr(input_data, "read"):
|
|
60
63
|
content = input_data.read()
|
|
61
64
|
if isinstance(content, bytes):
|
|
62
65
|
content = content.decode("utf-8")
|
|
63
|
-
|
|
66
|
+
try:
|
|
67
|
+
return _json.loads(content)
|
|
68
|
+
except _json.JSONDecodeError:
|
|
69
|
+
return _parse_jsonl(content)
|
|
64
70
|
elif isinstance(input_data, (dict, list, int, float, bool)) or input_data is None:
|
|
65
|
-
|
|
71
|
+
return input_data
|
|
66
72
|
else:
|
|
67
73
|
raise TypeError(
|
|
68
|
-
f"
|
|
74
|
+
f"js() input must be JSON-compatible, got {type(input_data).__name__}"
|
|
69
75
|
)
|
|
70
|
-
|
|
71
|
-
return JsonObject(data)
|
snail/runtime/subprocess.py
CHANGED
|
@@ -7,7 +7,7 @@ class SubprocessCapture:
|
|
|
7
7
|
def __init__(self, cmd: str) -> None:
|
|
8
8
|
self.cmd = cmd
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def __call__(self, input_data=None):
|
|
11
11
|
try:
|
|
12
12
|
if input_data is None:
|
|
13
13
|
completed = subprocess.run(
|
|
@@ -42,7 +42,7 @@ class SubprocessStatus:
|
|
|
42
42
|
def __init__(self, cmd: str) -> None:
|
|
43
43
|
self.cmd = cmd
|
|
44
44
|
|
|
45
|
-
def
|
|
45
|
+
def __call__(self, input_data=None):
|
|
46
46
|
try:
|
|
47
47
|
if input_data is None:
|
|
48
48
|
subprocess.run(self.cmd, shell=True, check=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: snail-lang
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
4
4
|
Requires-Dist: maturin>=1.5 ; extra == 'dev'
|
|
5
5
|
Requires-Dist: pytest ; extra == 'dev'
|
|
6
6
|
Provides-Extra: dev
|
|
@@ -64,7 +64,7 @@ The `?` operator makes error handling terse yet expressive:
|
|
|
64
64
|
err = risky_operation()?
|
|
65
65
|
|
|
66
66
|
# Provide a fallback value (exception available as $e)
|
|
67
|
-
value =
|
|
67
|
+
value = js(data):{}?
|
|
68
68
|
details = fetch_url(url):"Error: {$e}"?
|
|
69
69
|
|
|
70
70
|
# Access attributes directly
|
|
@@ -101,7 +101,7 @@ Built-in variables: `$l` (line), `$f` (fields), `$n` (line number), `$fn` (per-f
|
|
|
101
101
|
|
|
102
102
|
### Pipeline Operator
|
|
103
103
|
|
|
104
|
-
The `|` operator enables data pipelining through
|
|
104
|
+
The `|` operator enables data pipelining through pipeline-aware callables:
|
|
105
105
|
|
|
106
106
|
```snail
|
|
107
107
|
# Pipe data to subprocess stdin
|
|
@@ -112,23 +112,37 @@ output = "foo\nbar" | $(grep foo) | $(wc -l)
|
|
|
112
112
|
|
|
113
113
|
# Custom pipeline handlers
|
|
114
114
|
class Doubler {
|
|
115
|
-
def
|
|
115
|
+
def __call__(self, x) { return x * 2 }
|
|
116
116
|
}
|
|
117
117
|
doubled = 21 | Doubler() # yields 42
|
|
118
|
+
|
|
119
|
+
# Use placeholders to control where piped values land in calls
|
|
120
|
+
greeting = "World" | greet("Hello ", _) # greet("Hello ", "World")
|
|
121
|
+
excited = "World" | greet(_, "!") # greet("World", "!")
|
|
122
|
+
formal = "World" | greet("Hello ", suffix=_) # greet("Hello ", "World")
|
|
118
123
|
```
|
|
119
124
|
|
|
125
|
+
When a pipeline targets a call expression, the left-hand value is passed to the
|
|
126
|
+
resulting callable. If the call includes a single `_` placeholder, Snail substitutes
|
|
127
|
+
the piped value at that position (including keyword arguments). Only one
|
|
128
|
+
placeholder is allowed in a piped call. Outside of pipeline calls, `_` remains a
|
|
129
|
+
normal identifier.
|
|
130
|
+
|
|
120
131
|
### JSON Queries with JMESPath
|
|
121
132
|
|
|
122
|
-
Parse and query JSON data with the `
|
|
133
|
+
Parse and query JSON data with the `js()` function and structured pipeline accessor:
|
|
123
134
|
|
|
124
135
|
```snail
|
|
125
136
|
# Parse JSON and query with $[jmespath]
|
|
126
|
-
data =
|
|
137
|
+
data = js($(curl -s api.example.com/users))
|
|
127
138
|
names = data | $[users[*].name]
|
|
128
139
|
first_email = data | $[users[0].email]
|
|
129
140
|
|
|
130
141
|
# Inline parsing and querying
|
|
131
|
-
result =
|
|
142
|
+
result = js('{"foo": 12}') | $[foo]
|
|
143
|
+
|
|
144
|
+
# JSONL parsing returns a list
|
|
145
|
+
names = js('{"name": "Ada"}\n{"name": "Lin"}') | $[[*].name]
|
|
132
146
|
```
|
|
133
147
|
|
|
134
148
|
### Full Python Interoperability
|
|
@@ -147,7 +161,7 @@ filtered = df[df["value"] > 100]
|
|
|
147
161
|
|
|
148
162
|
```bash
|
|
149
163
|
# Install from PyPI
|
|
150
|
-
pip install snail
|
|
164
|
+
pip install snail-lang
|
|
151
165
|
|
|
152
166
|
# Run a one-liner
|
|
153
167
|
snail "print('Hello, Snail!')"
|
|
@@ -179,10 +193,9 @@ flowchart TB
|
|
|
179
193
|
C2[crates/snail-ast/src/awk.rs<br/>AwkProgram AST]
|
|
180
194
|
end
|
|
181
195
|
|
|
182
|
-
subgraph Lowering["Lowering
|
|
196
|
+
subgraph Lowering["Lowering"]
|
|
183
197
|
D1[crates/snail-lower/<br/>AST → Python AST Transform]
|
|
184
198
|
D2[python/snail/runtime/<br/>Runtime Helpers]
|
|
185
|
-
D3[crates/snail-codegen/<br/>Python AST → Source Code]
|
|
186
199
|
end
|
|
187
200
|
|
|
188
201
|
subgraph Execution
|
|
@@ -198,9 +211,8 @@ flowchart TB
|
|
|
198
211
|
C1 --> D1
|
|
199
212
|
C2 --> D1
|
|
200
213
|
D1 --> D2
|
|
201
|
-
D1 -->
|
|
202
|
-
D2 -->
|
|
203
|
-
D3 --> E1
|
|
214
|
+
D1 --> E1
|
|
215
|
+
D2 --> E1
|
|
204
216
|
E1 --> E2
|
|
205
217
|
E2 --> F[Python Execution]
|
|
206
218
|
|
|
@@ -218,7 +230,7 @@ flowchart TB
|
|
|
218
230
|
- `$(cmd)` subprocess capture → `__SnailSubprocessCapture`
|
|
219
231
|
- `@(cmd)` subprocess status → `__SnailSubprocessStatus`
|
|
220
232
|
- Regex literals → `__snail_regex_search` and `__snail_regex_compile`
|
|
221
|
-
- **
|
|
233
|
+
- **Execution**: Compiles Python AST directly for in-process execution
|
|
222
234
|
- **CLI**: Python wrapper (`python/snail/cli.py`) that executes via the extension module
|
|
223
235
|
|
|
224
236
|
## 📚 Documentation
|
|
@@ -333,7 +345,7 @@ python3 -m venv myenv
|
|
|
333
345
|
source myenv/bin/activate # On Windows: myenv\Scripts\activate
|
|
334
346
|
|
|
335
347
|
# Install and run
|
|
336
|
-
pip install snail
|
|
348
|
+
pip install snail-lang
|
|
337
349
|
snail "import sys; print(sys.prefix)"
|
|
338
350
|
```
|
|
339
351
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
snail\__init__.py,sha256=QQdNHRMalbxBvCW3RVDkMh-F4ScPOtikNbmkpC4NznE,282
|
|
2
|
-
snail\_native.pyd,sha256=
|
|
3
|
-
snail\cli.py,sha256=
|
|
4
|
-
snail\runtime\__init__.py,sha256=
|
|
2
|
+
snail\_native.pyd,sha256=bkJymYDAuavT8dk1cd-jII59pC74GdZrj2R8ERCpcA0,1511936
|
|
3
|
+
snail\cli.py,sha256=m1b8qS7SgyiEnPeC6l5gqq_VTm3AO01s4yt5R-ywP44,2048
|
|
4
|
+
snail\runtime\__init__.py,sha256=CZAaF3SHKUCuaEog6XnjcorchR19HKVWH4D92TYqOgU,2115
|
|
5
5
|
snail\runtime\compact_try.py,sha256=GsGN8zJ_6kmv26_Wn1s9dfylcJjE0fZGQxplCR2Mx8A,392
|
|
6
6
|
snail\runtime\regex.py,sha256=MH30z5yLiQrfk4uRWwVxneOvDbI8GCUP33Qhl8xvsHg,191
|
|
7
|
-
snail\runtime\structured_accessor.py,sha256=
|
|
8
|
-
snail\runtime\subprocess.py,sha256=
|
|
7
|
+
snail\runtime\structured_accessor.py,sha256=mX5kANTV-Y04TDOjhJdcFrl9zPcu4NgvLGWfzuyuNWM,2235
|
|
8
|
+
snail\runtime\subprocess.py,sha256=ILDdfpqRXMFjWKYdpxQvEpie_31kItCuDO2WlsNpnks,1955
|
|
9
9
|
snail\vendor\__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
snail\vendor\jmespath\LICENSE,sha256=2KWiDylgnlq10P3XDxl8OYugQB6n-HRR_M9FUsOlplI,1134
|
|
11
11
|
snail\vendor\jmespath\__init__.py,sha256=uGroTH0i_1gETCVguY0L1EJiALD4b2Gr01KPySp2LJU,278
|
|
@@ -16,8 +16,8 @@ snail\vendor\jmespath\functions.py,sha256=YZ9H_JOmpn2-hHGX6Dif-9I2xb7w30fm-al8gA
|
|
|
16
16
|
snail\vendor\jmespath\lexer.py,sha256=xM9tayCW9o9_wc7a8aAu563QCVJ2SlotHj5BDc78-80,9668
|
|
17
17
|
snail\vendor\jmespath\parser.py,sha256=8FEJChdCFtdC0cq8TQdu2YxN5Y2ahZtFUkBkkUuTuzU,19423
|
|
18
18
|
snail\vendor\jmespath\visitor.py,sha256=VN1KjK_Shmv4qTAVb8WaKOGC2LgCjo2xNg9k5CSFBv0,11129
|
|
19
|
-
snail_lang-0.3.
|
|
20
|
-
snail_lang-0.3.
|
|
21
|
-
snail_lang-0.3.
|
|
22
|
-
snail_lang-0.3.
|
|
23
|
-
snail_lang-0.3.
|
|
19
|
+
snail_lang-0.3.7.dist-info\METADATA,sha256=LLOmdEiFolJ9OqebnsL7D9EIF7YA5o-iUk4WkBszVpU,9856
|
|
20
|
+
snail_lang-0.3.7.dist-info\WHEEL,sha256=ZMDDxh9OPoaLQ4P2dJmgI1XsENYSzjzq8fErKKVw5iE,96
|
|
21
|
+
snail_lang-0.3.7.dist-info\entry_points.txt,sha256=8u6pjHZtKGdaZQlr6yKbftplObC07caDZ44j5PIPU6M,39
|
|
22
|
+
snail_lang-0.3.7.dist-info\licenses\LICENSE,sha256=iT_e39WExFFehp3Zh4faVe3dEv3BrhhX_v3obKdBM9M,1082
|
|
23
|
+
snail_lang-0.3.7.dist-info\RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|