snail-lang 0.7.2__cp38-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/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from ._native import __build_info__, compile, compile_ast, exec, parse, parse_ast
4
+
5
+
6
+ def _resolve_version() -> str:
7
+ try:
8
+ from importlib.metadata import version
9
+
10
+ return version("snail-lang")
11
+ except Exception: # pragma: no cover - during development
12
+ return "0.0.0"
13
+
14
+
15
+ def __getattr__(name: str):
16
+ if name == "__version__":
17
+ value = _resolve_version()
18
+ globals()["__version__"] = value
19
+ return value
20
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
21
+
22
+
23
+ def __dir__() -> list[str]:
24
+ return sorted(list(globals().keys()) + ["__version__"])
25
+
26
+
27
+ __all__ = [
28
+ "compile",
29
+ "compile_ast",
30
+ "exec",
31
+ "parse",
32
+ "parse_ast",
33
+ "__version__",
34
+ "__build_info__",
35
+ ]
snail/_native.pyd ADDED
Binary file
snail/cli.py ADDED
@@ -0,0 +1,423 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from typing import Optional
6
+
7
+ from . import __build_info__, compile_ast, exec
8
+
9
+ _USAGE = (
10
+ "snail [options] -f <file> [args]...\n"
11
+ " snail [options] <code> [args]..."
12
+ )
13
+ _DESCRIPTION = "Snail programming language interpreter"
14
+ _BOOLEAN_FLAGS = frozenset("amPIvh")
15
+ _VALUE_FLAGS = frozenset("fbe")
16
+
17
+
18
+ def _display_filename(filename: str) -> str:
19
+ if filename.startswith("snail:"):
20
+ return filename
21
+ return f"snail:{filename}"
22
+
23
+
24
+ def _trim_internal_prefix(stack, internal_files: set[str]) -> None:
25
+ if not stack:
26
+ return
27
+ trim_count = 0
28
+ for frame in stack:
29
+ filename = frame.filename
30
+ if filename.startswith("snail:"):
31
+ break
32
+ if filename in internal_files:
33
+ trim_count += 1
34
+ continue
35
+ if os.path.isabs(filename) and os.path.abspath(filename) in internal_files:
36
+ trim_count += 1
37
+ continue
38
+ break
39
+ if 0 < trim_count < len(stack):
40
+ del stack[:trim_count]
41
+
42
+
43
+ def _trim_traceback_exception(tb_exc, internal_files: set[str]) -> None:
44
+ _trim_internal_prefix(tb_exc.stack, internal_files)
45
+ cause = getattr(tb_exc, "__cause__", None)
46
+ if cause is not None:
47
+ _trim_traceback_exception(cause, internal_files)
48
+ context = getattr(tb_exc, "__context__", None)
49
+ if context is not None:
50
+ _trim_traceback_exception(context, internal_files)
51
+ for group_exc in getattr(tb_exc, "exceptions", ()) or ():
52
+ _trim_traceback_exception(group_exc, internal_files)
53
+
54
+
55
+ def _install_trimmed_excepthook() -> None:
56
+ entrypoint = os.path.abspath(sys.argv[0])
57
+ cli_path = os.path.abspath(__file__)
58
+ internal_files = {entrypoint, cli_path}
59
+ original_excepthook = sys.excepthook
60
+
61
+ def _snail_excepthook(
62
+ exc_type: type[BaseException],
63
+ exc: BaseException,
64
+ tb: object,
65
+ ) -> None:
66
+ if exc_type is KeyboardInterrupt:
67
+ return original_excepthook(exc_type, exc, tb)
68
+ import traceback
69
+
70
+ tb_exc = traceback.TracebackException(
71
+ exc_type,
72
+ exc,
73
+ tb,
74
+ capture_locals=False,
75
+ )
76
+ _trim_traceback_exception(tb_exc, internal_files)
77
+ try:
78
+ import _colorize
79
+
80
+ colorize = _colorize.can_colorize(file=sys.stderr)
81
+ except Exception:
82
+ colorize = hasattr(sys.stderr, "isatty") and sys.stderr.isatty()
83
+ try:
84
+ formatted = tb_exc.format(colorize=colorize)
85
+ except TypeError:
86
+ formatted = tb_exc.format()
87
+ for line in formatted:
88
+ sys.stderr.write(line)
89
+
90
+ sys.excepthook = _snail_excepthook
91
+
92
+
93
+ class _Args:
94
+ def __init__(self) -> None:
95
+ self.file: Optional[str] = None
96
+ self.awk = False
97
+ self.map = False
98
+ self.no_print = False
99
+ self.no_auto_import = False
100
+ self.debug = False
101
+ self.debug_snail_ast = False
102
+ self.debug_python_ast = False
103
+ self.version = False
104
+ self.help = False
105
+ self.begin_code: list[str] = []
106
+ self.end_code: list[str] = []
107
+ self.args: list[str] = []
108
+
109
+
110
+ def _print_help(file=None) -> None:
111
+ if file is None:
112
+ file = sys.stdout
113
+ print(f"usage: {_USAGE}", file=file)
114
+ print("", file=file)
115
+ print(_DESCRIPTION, file=file)
116
+ print("", file=file)
117
+ print("options:", file=file)
118
+ print(" -f <file> read Snail source from file", file=file)
119
+ print(" -a, --awk awk mode", file=file)
120
+ print(" -m, --map map mode (process files one at a time)", file=file)
121
+ print(
122
+ " -b, --begin <code> begin block code (awk/map mode, repeatable)",
123
+ file=file,
124
+ )
125
+ print(
126
+ " -e, --end <code> end block code (awk/map mode, repeatable)",
127
+ file=file,
128
+ )
129
+ print(
130
+ " -P, --no-print disable auto-print of implicit return value",
131
+ file=file,
132
+ )
133
+ print(" -I, --no-auto-import disable auto-imports", file=file)
134
+ print(" --debug parse and compile, then print, do not run", file=file)
135
+ print(" --debug-snail-ast parse and print Snail AST, do not run", file=file)
136
+ print(" --debug-python-ast parse and print Python AST, do not run", file=file)
137
+ print(" -v, --version show version and exit", file=file)
138
+ print(" -h, --help show this help message and exit", file=file)
139
+
140
+
141
+ def _expand_short_options(argv: list[str]) -> list[str]:
142
+ expanded: list[str] = []
143
+ idx = 0
144
+ while idx < len(argv):
145
+ token = argv[idx]
146
+ if token == "--":
147
+ expanded.append(token)
148
+ expanded.extend(argv[idx + 1 :])
149
+ return expanded
150
+ if token == "-" or not token.startswith("-") or token.startswith("--"):
151
+ expanded.append(token)
152
+ idx += 1
153
+ continue
154
+ if len(token) == 2:
155
+ expanded.append(token)
156
+ idx += 1
157
+ continue
158
+
159
+ flags = token[1:]
160
+ pos = 0
161
+ while pos < len(flags):
162
+ flag = flags[pos]
163
+ if flag in _BOOLEAN_FLAGS:
164
+ expanded.append(f"-{flag}")
165
+ pos += 1
166
+ continue
167
+ if flag in _VALUE_FLAGS:
168
+ remainder = flags[pos + 1 :]
169
+ if not remainder:
170
+ expanded.append(f"-{flag}")
171
+ pos += 1
172
+ continue
173
+ if all(
174
+ ch in _BOOLEAN_FLAGS or ch in _VALUE_FLAGS for ch in remainder
175
+ ):
176
+ raise ValueError(
177
+ f"option -{flag} requires an argument and must be last in a "
178
+ "combined flag group"
179
+ )
180
+ expanded.append(f"-{flag}")
181
+ expanded.append(remainder)
182
+ pos = len(flags)
183
+ break
184
+ raise ValueError(f"unknown option: -{flag}")
185
+ idx += 1
186
+ return expanded
187
+
188
+
189
+ def _parse_args(argv: list[str]) -> _Args:
190
+ argv = _expand_short_options(argv)
191
+ args = _Args()
192
+ idx = 0
193
+ code_found = False
194
+ while idx < len(argv):
195
+ token = argv[idx]
196
+ if token == "--":
197
+ args.args = argv[idx + 1 :]
198
+ return args
199
+ if token == "-" or not token.startswith("-"):
200
+ if not code_found:
201
+ # This is the code (or the first arg when -f is used)
202
+ args.args = [token]
203
+ code_found = True
204
+ else:
205
+ args.args.append(token)
206
+ idx += 1
207
+ continue
208
+ if token in ("-h", "--help"):
209
+ args.help = True
210
+ return args
211
+ if token in ("-v", "--version"):
212
+ args.version = True
213
+ idx += 1
214
+ continue
215
+ if token in ("-a", "--awk"):
216
+ args.awk = True
217
+ idx += 1
218
+ continue
219
+ if token in ("-m", "--map"):
220
+ args.map = True
221
+ idx += 1
222
+ continue
223
+ if token in ("-P", "--no-print"):
224
+ args.no_print = True
225
+ idx += 1
226
+ continue
227
+ if token in ("-I", "--no-auto-import"):
228
+ args.no_auto_import = True
229
+ idx += 1
230
+ continue
231
+ if token == "--debug":
232
+ args.debug = True
233
+ idx += 1
234
+ continue
235
+ if token == "--debug-snail-ast":
236
+ args.debug_snail_ast = True
237
+ idx += 1
238
+ continue
239
+ if token == "--debug-python-ast":
240
+ args.debug_python_ast = True
241
+ idx += 1
242
+ continue
243
+ if token == "-f":
244
+ if idx + 1 >= len(argv):
245
+ raise ValueError("option -f requires an argument")
246
+ args.file = argv[idx + 1]
247
+ code_found = True
248
+ idx += 2
249
+ continue
250
+ if token in ("-b", "--begin"):
251
+ if idx + 1 >= len(argv):
252
+ raise ValueError(f"option {token} requires an argument")
253
+ args.begin_code.append(argv[idx + 1])
254
+ idx += 2
255
+ continue
256
+ if token in ("-e", "--end"):
257
+ if idx + 1 >= len(argv):
258
+ raise ValueError(f"option {token} requires an argument")
259
+ args.end_code.append(argv[idx + 1])
260
+ idx += 2
261
+ continue
262
+ raise ValueError(f"unknown option: {token}")
263
+ return args
264
+
265
+
266
+ def _format_version(version: str, build_info: Optional[dict[str, object]]) -> str:
267
+ display_version = version if version.startswith("v") else f"v{version}"
268
+ if not build_info:
269
+ return display_version
270
+ git_rev = build_info.get("git_rev")
271
+ if not git_rev:
272
+ return display_version
273
+
274
+ suffixes: list[str] = []
275
+ if build_info.get("dirty"):
276
+ suffixes.append("!dirty")
277
+ if build_info.get("untagged"):
278
+ suffixes.append("!untagged")
279
+
280
+ if suffixes:
281
+ return f"{display_version} ({git_rev}) {' '.join(suffixes)}"
282
+ return f"{display_version} ({git_rev})"
283
+
284
+
285
+ def _get_version() -> str:
286
+ from . import __version__ as version
287
+
288
+ return version
289
+
290
+
291
+ def _format_python_runtime() -> str:
292
+ version = (
293
+ f"{sys.version_info.major}."
294
+ f"{sys.version_info.minor}."
295
+ f"{sys.version_info.micro}"
296
+ )
297
+ executable = sys.executable or "<unknown>"
298
+ if executable != "<unknown>":
299
+ executable = os.path.abspath(executable)
300
+ return f"Python {version} ({executable})"
301
+
302
+
303
+ def main(argv: Optional[list[str]] = None) -> int:
304
+ if argv is None:
305
+ _install_trimmed_excepthook()
306
+ argv = sys.argv[1:]
307
+
308
+ try:
309
+ namespace = _parse_args(argv)
310
+ except ValueError as exc:
311
+ _print_help(file=sys.stderr)
312
+ print(f"error: {exc}", file=sys.stderr)
313
+ return 2
314
+
315
+ if namespace.help:
316
+ _print_help()
317
+ return 0
318
+ if namespace.version:
319
+ print(_format_version(_get_version(), __build_info__))
320
+ print(_format_python_runtime())
321
+ return 0
322
+
323
+ # Validate --awk and --map are mutually exclusive
324
+ if namespace.awk and namespace.map:
325
+ print("error: --awk and --map cannot be used together", file=sys.stderr)
326
+ return 2
327
+
328
+ # Validate -b/--begin and -e/--end only with --awk or --map mode
329
+ if (namespace.begin_code or namespace.end_code) and not (namespace.awk or namespace.map):
330
+ print(
331
+ "error: -b/--begin and -e/--end options require --awk or --map mode",
332
+ file=sys.stderr,
333
+ )
334
+ return 2
335
+
336
+ mode = "map" if namespace.map else ("awk" if namespace.awk else "snail")
337
+
338
+ if namespace.file:
339
+ from pathlib import Path
340
+
341
+ path = Path(namespace.file)
342
+ try:
343
+ source = path.read_text()
344
+ except OSError as exc:
345
+ print(f"failed to read {path}: {exc}", file=sys.stderr)
346
+ return 1
347
+ filename = str(path)
348
+ args = [filename, *namespace.args]
349
+ else:
350
+ if not namespace.args:
351
+ print("no input provided", file=sys.stderr)
352
+ return 1
353
+ source = namespace.args[0]
354
+ filename = "<cmd>"
355
+ args = ["--", *namespace.args[1:]]
356
+
357
+ if namespace.debug_snail_ast:
358
+ from . import parse_ast
359
+
360
+ snail_ast = parse_ast(
361
+ source,
362
+ mode=mode,
363
+ filename=filename,
364
+ begin_code=namespace.begin_code,
365
+ end_code=namespace.end_code,
366
+ )
367
+ print(snail_ast)
368
+ return 0
369
+
370
+ if namespace.debug_python_ast:
371
+ import ast
372
+
373
+ python_ast = compile_ast(
374
+ source,
375
+ mode=mode,
376
+ auto_print=not namespace.no_print,
377
+ filename=filename,
378
+ begin_code=namespace.begin_code,
379
+ end_code=namespace.end_code,
380
+ )
381
+ try:
382
+ output = ast.dump(python_ast, indent=2)
383
+ except TypeError:
384
+ output = ast.dump(python_ast)
385
+ print(output)
386
+ return 0
387
+
388
+ if namespace.debug:
389
+ import ast
390
+ import builtins
391
+
392
+ python_ast = compile_ast(
393
+ source,
394
+ mode=mode,
395
+ auto_print=not namespace.no_print,
396
+ filename=filename,
397
+ begin_code=namespace.begin_code,
398
+ end_code=namespace.end_code,
399
+ )
400
+ builtins.compile(python_ast, _display_filename(filename), "exec")
401
+ try:
402
+ output = ast.unparse(python_ast)
403
+ except AttributeError:
404
+ import astunparse
405
+
406
+ output = astunparse.unparse(python_ast).rstrip("\n")
407
+ print(output)
408
+ return 0
409
+
410
+ return exec(
411
+ source,
412
+ argv=args,
413
+ mode=mode,
414
+ auto_print=not namespace.no_print,
415
+ auto_import=not namespace.no_auto_import,
416
+ filename=filename,
417
+ begin_code=namespace.begin_code,
418
+ end_code=namespace.end_code,
419
+ )
420
+
421
+
422
+ if __name__ == "__main__":
423
+ raise SystemExit(main())
@@ -0,0 +1,261 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ from typing import Optional
5
+
6
+ __all__ = ["install_helpers", "AutoImportDict", "AUTO_IMPORT_NAMES"]
7
+
8
+ # Names that can be auto-imported when first referenced.
9
+ # Maps name -> (module, attribute) where attribute is None for whole-module imports.
10
+ AUTO_IMPORT_NAMES: dict[str, tuple[str, Optional[str]]] = {
11
+ # Whole module imports: import X
12
+ "sys": ("sys", None),
13
+ "os": ("os", None),
14
+ # Attribute imports: from X import Y
15
+ "Path": ("pathlib", "Path"),
16
+ }
17
+
18
+
19
+ class AutoImportDict(dict):
20
+ """A dict subclass that lazily imports allowed names on first access.
21
+
22
+ When a key lookup fails, if the key is in AUTO_IMPORT_NAMES,
23
+ the corresponding module/attribute is imported and stored in the dict.
24
+ Supports both whole-module imports (import sys) and attribute imports
25
+ (from pathlib import Path).
26
+ """
27
+
28
+ def __missing__(self, key: str) -> object:
29
+ if key in AUTO_IMPORT_NAMES:
30
+ module_name, attr_name = AUTO_IMPORT_NAMES[key]
31
+ module = importlib.import_module(module_name)
32
+ value = getattr(module, attr_name) if attr_name else module
33
+ self[key] = value
34
+ return value
35
+ raise KeyError(key)
36
+
37
+
38
+ _compact_try = None
39
+ _regex_search = None
40
+ _regex_compile = None
41
+ _subprocess_capture = None
42
+ _subprocess_status = None
43
+ _jmespath_query = None
44
+ _js = None
45
+ _lazy_text_class = None
46
+ _lazy_file_class = None
47
+ _incr_attr = None
48
+ _incr_index = None
49
+ _aug_attr = None
50
+ _aug_index = None
51
+ _env_map = None
52
+
53
+
54
+ def _get_compact_try():
55
+ global _compact_try
56
+ if _compact_try is None:
57
+ from .compact_try import compact_try
58
+
59
+ _compact_try = compact_try
60
+ return _compact_try
61
+
62
+
63
+ def _get_regex_search():
64
+ global _regex_search
65
+ if _regex_search is None:
66
+ from .regex import regex_search
67
+
68
+ _regex_search = regex_search
69
+ return _regex_search
70
+
71
+
72
+ def _get_regex_compile():
73
+ global _regex_compile
74
+ if _regex_compile is None:
75
+ from .regex import regex_compile
76
+
77
+ _regex_compile = regex_compile
78
+ return _regex_compile
79
+
80
+
81
+ def _get_subprocess_capture():
82
+ global _subprocess_capture
83
+ if _subprocess_capture is None:
84
+ from .subprocess import SubprocessCapture
85
+
86
+ _subprocess_capture = SubprocessCapture
87
+ return _subprocess_capture
88
+
89
+
90
+ def _get_subprocess_status():
91
+ global _subprocess_status
92
+ if _subprocess_status is None:
93
+ from .subprocess import SubprocessStatus
94
+
95
+ _subprocess_status = SubprocessStatus
96
+ return _subprocess_status
97
+
98
+
99
+ def _get_jmespath_query():
100
+ global _jmespath_query
101
+ if _jmespath_query is None:
102
+ from .structured_accessor import __snail_jmespath_query
103
+
104
+ _jmespath_query = __snail_jmespath_query
105
+ return _jmespath_query
106
+
107
+
108
+ def _get_js():
109
+ global _js
110
+ if _js is None:
111
+ from .structured_accessor import js
112
+
113
+ _js = js
114
+ return _js
115
+
116
+
117
+ def _get_lazy_text_class():
118
+ global _lazy_text_class
119
+ if _lazy_text_class is None:
120
+ from .lazy_text import LazyText
121
+
122
+ _lazy_text_class = LazyText
123
+ return _lazy_text_class
124
+
125
+
126
+ def _get_lazy_file_class():
127
+ global _lazy_file_class
128
+ if _lazy_file_class is None:
129
+ from .lazy_file import LazyFile
130
+
131
+ _lazy_file_class = LazyFile
132
+ return _lazy_file_class
133
+
134
+
135
+ def _get_env_map():
136
+ global _env_map
137
+ if _env_map is None:
138
+ from .env import EnvMap
139
+
140
+ _env_map = EnvMap()
141
+ return _env_map
142
+
143
+
144
+ def _get_incr_attr():
145
+ global _incr_attr
146
+ if _incr_attr is None:
147
+ from .augmented import __snail_incr_attr
148
+
149
+ _incr_attr = __snail_incr_attr
150
+ return _incr_attr
151
+
152
+
153
+ def _get_incr_index():
154
+ global _incr_index
155
+ if _incr_index is None:
156
+ from .augmented import __snail_incr_index
157
+
158
+ _incr_index = __snail_incr_index
159
+ return _incr_index
160
+
161
+
162
+ def _get_aug_attr():
163
+ global _aug_attr
164
+ if _aug_attr is None:
165
+ from .augmented import __snail_aug_attr
166
+
167
+ _aug_attr = __snail_aug_attr
168
+ return _aug_attr
169
+
170
+
171
+ def _get_aug_index():
172
+ global _aug_index
173
+ if _aug_index is None:
174
+ from .augmented import __snail_aug_index
175
+
176
+ _aug_index = __snail_aug_index
177
+ return _aug_index
178
+
179
+
180
+ def _lazy_compact_try(expr_fn, fallback_fn=None):
181
+ return _get_compact_try()(expr_fn, fallback_fn)
182
+
183
+
184
+ def _lazy_regex_search(value, pattern):
185
+ return _get_regex_search()(value, pattern)
186
+
187
+
188
+ def _lazy_regex_compile(pattern):
189
+ return _get_regex_compile()(pattern)
190
+
191
+
192
+ def _lazy_subprocess_capture(cmd: str):
193
+ return _get_subprocess_capture()(cmd)
194
+
195
+
196
+ def _lazy_subprocess_status(cmd: str):
197
+ return _get_subprocess_status()(cmd)
198
+
199
+
200
+ def _lazy_jmespath_query(query: str):
201
+ return _get_jmespath_query()(query)
202
+
203
+
204
+ def _lazy_js(input_data=None):
205
+ return _get_js()(input_data)
206
+
207
+
208
+ def _lazy_incr_attr(obj, attr: str, delta: int, pre: bool):
209
+ return _get_incr_attr()(obj, attr, delta, pre)
210
+
211
+
212
+ def _lazy_incr_index(obj, index, delta: int, pre: bool):
213
+ return _get_incr_index()(obj, index, delta, pre)
214
+
215
+
216
+ def _lazy_aug_attr(obj, attr: str, value, op: str):
217
+ return _get_aug_attr()(obj, attr, value, op)
218
+
219
+
220
+ def _lazy_aug_index(obj, index, value, op: str):
221
+ return _get_aug_index()(obj, index, value, op)
222
+
223
+
224
+ def __snail_partial(func, /, *args, **kwargs):
225
+ import functools
226
+
227
+ return functools.partial(func, *args, **kwargs)
228
+
229
+
230
+ def __snail_contains__(left, right):
231
+ method = getattr(right, "__snail_contains__", None)
232
+ if method is not None:
233
+ return method(left)
234
+ return left in right
235
+
236
+
237
+ def __snail_contains_not__(left, right):
238
+ method = getattr(right, "__snail_contains__", None)
239
+ if method is not None:
240
+ return not bool(method(left))
241
+ return left not in right
242
+
243
+
244
+ def install_helpers(globals_dict: dict) -> None:
245
+ globals_dict["__snail_compact_try"] = _lazy_compact_try
246
+ globals_dict["__snail_regex_search"] = _lazy_regex_search
247
+ globals_dict["__snail_regex_compile"] = _lazy_regex_compile
248
+ globals_dict["__SnailSubprocessCapture"] = _lazy_subprocess_capture
249
+ globals_dict["__SnailSubprocessStatus"] = _lazy_subprocess_status
250
+ globals_dict["__snail_jmespath_query"] = _lazy_jmespath_query
251
+ globals_dict["__snail_partial"] = __snail_partial
252
+ globals_dict["__snail_contains__"] = __snail_contains__
253
+ globals_dict["__snail_contains_not__"] = __snail_contains_not__
254
+ globals_dict["__snail_incr_attr"] = _lazy_incr_attr
255
+ globals_dict["__snail_incr_index"] = _lazy_incr_index
256
+ globals_dict["__snail_aug_attr"] = _lazy_aug_attr
257
+ globals_dict["__snail_aug_index"] = _lazy_aug_index
258
+ globals_dict["__snail_env"] = _get_env_map()
259
+ globals_dict["js"] = _lazy_js
260
+ globals_dict["__SnailLazyText"] = _get_lazy_text_class()
261
+ globals_dict["__SnailLazyFile"] = _get_lazy_file_class()