snail-lang 0.7.1__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,386 @@
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.version = False
103
+ self.help = False
104
+ self.begin_code: list[str] = []
105
+ self.end_code: list[str] = []
106
+ self.args: list[str] = []
107
+
108
+
109
+ def _print_help(file=None) -> None:
110
+ if file is None:
111
+ file = sys.stdout
112
+ print(f"usage: {_USAGE}", file=file)
113
+ print("", file=file)
114
+ print(_DESCRIPTION, file=file)
115
+ print("", file=file)
116
+ print("options:", file=file)
117
+ print(" -f <file> read Snail source from file", file=file)
118
+ print(" -a, --awk awk mode", file=file)
119
+ print(" -m, --map map mode (process files one at a time)", file=file)
120
+ print(
121
+ " -b, --begin <code> begin block code (awk/map mode, repeatable)",
122
+ file=file,
123
+ )
124
+ print(
125
+ " -e, --end <code> end block code (awk/map mode, repeatable)",
126
+ file=file,
127
+ )
128
+ print(
129
+ " -P, --no-print disable auto-print of implicit return value",
130
+ file=file,
131
+ )
132
+ print(" -I, --no-auto-import disable auto-imports", file=file)
133
+ print(" --debug parse and compile, then print, do not run", file=file)
134
+ print(" --debug-snail-ast parse and print Snail AST, do not run", file=file)
135
+ print(" -v, --version show version and exit", file=file)
136
+ print(" -h, --help show this help message and exit", file=file)
137
+
138
+
139
+ def _expand_short_options(argv: list[str]) -> list[str]:
140
+ expanded: list[str] = []
141
+ idx = 0
142
+ while idx < len(argv):
143
+ token = argv[idx]
144
+ if token == "--":
145
+ expanded.append(token)
146
+ expanded.extend(argv[idx + 1 :])
147
+ return expanded
148
+ if token == "-" or not token.startswith("-") or token.startswith("--"):
149
+ expanded.append(token)
150
+ idx += 1
151
+ continue
152
+ if len(token) == 2:
153
+ expanded.append(token)
154
+ idx += 1
155
+ continue
156
+
157
+ flags = token[1:]
158
+ pos = 0
159
+ while pos < len(flags):
160
+ flag = flags[pos]
161
+ if flag in _BOOLEAN_FLAGS:
162
+ expanded.append(f"-{flag}")
163
+ pos += 1
164
+ continue
165
+ if flag in _VALUE_FLAGS:
166
+ remainder = flags[pos + 1 :]
167
+ if not remainder:
168
+ expanded.append(f"-{flag}")
169
+ pos += 1
170
+ continue
171
+ if all(
172
+ ch in _BOOLEAN_FLAGS or ch in _VALUE_FLAGS for ch in remainder
173
+ ):
174
+ raise ValueError(
175
+ f"option -{flag} requires an argument and must be last in a "
176
+ "combined flag group"
177
+ )
178
+ expanded.append(f"-{flag}")
179
+ expanded.append(remainder)
180
+ pos = len(flags)
181
+ break
182
+ raise ValueError(f"unknown option: -{flag}")
183
+ idx += 1
184
+ return expanded
185
+
186
+
187
+ def _parse_args(argv: list[str]) -> _Args:
188
+ argv = _expand_short_options(argv)
189
+ args = _Args()
190
+ idx = 0
191
+ code_found = False
192
+ while idx < len(argv):
193
+ token = argv[idx]
194
+ if token == "--":
195
+ args.args = argv[idx + 1 :]
196
+ return args
197
+ if token == "-" or not token.startswith("-"):
198
+ if not code_found:
199
+ # This is the code (or the first arg when -f is used)
200
+ args.args = [token]
201
+ code_found = True
202
+ else:
203
+ args.args.append(token)
204
+ idx += 1
205
+ continue
206
+ if token in ("-h", "--help"):
207
+ args.help = True
208
+ return args
209
+ if token in ("-v", "--version"):
210
+ args.version = True
211
+ idx += 1
212
+ continue
213
+ if token in ("-a", "--awk"):
214
+ args.awk = True
215
+ idx += 1
216
+ continue
217
+ if token in ("-m", "--map"):
218
+ args.map = True
219
+ idx += 1
220
+ continue
221
+ if token in ("-P", "--no-print"):
222
+ args.no_print = True
223
+ idx += 1
224
+ continue
225
+ if token in ("-I", "--no-auto-import"):
226
+ args.no_auto_import = True
227
+ idx += 1
228
+ continue
229
+ if token == "--debug":
230
+ args.debug = True
231
+ idx += 1
232
+ continue
233
+ if token == "--debug-snail-ast":
234
+ args.debug_snail_ast = True
235
+ idx += 1
236
+ continue
237
+ if token == "-f":
238
+ if idx + 1 >= len(argv):
239
+ raise ValueError("option -f requires an argument")
240
+ args.file = argv[idx + 1]
241
+ code_found = True
242
+ idx += 2
243
+ continue
244
+ if token in ("-b", "--begin"):
245
+ if idx + 1 >= len(argv):
246
+ raise ValueError(f"option {token} requires an argument")
247
+ args.begin_code.append(argv[idx + 1])
248
+ idx += 2
249
+ continue
250
+ if token in ("-e", "--end"):
251
+ if idx + 1 >= len(argv):
252
+ raise ValueError(f"option {token} requires an argument")
253
+ args.end_code.append(argv[idx + 1])
254
+ idx += 2
255
+ continue
256
+ raise ValueError(f"unknown option: {token}")
257
+ return args
258
+
259
+
260
+ def _format_version(version: str, build_info: Optional[dict[str, object]]) -> str:
261
+ display_version = version if version.startswith("v") else f"v{version}"
262
+ if not build_info:
263
+ return display_version
264
+ git_rev = build_info.get("git_rev")
265
+ if not git_rev:
266
+ return display_version
267
+
268
+ suffixes: list[str] = []
269
+ if build_info.get("dirty"):
270
+ suffixes.append("!dirty")
271
+ if build_info.get("untagged"):
272
+ suffixes.append("!untagged")
273
+
274
+ if suffixes:
275
+ return f"{display_version} ({git_rev}) {' '.join(suffixes)}"
276
+ return f"{display_version} ({git_rev})"
277
+
278
+
279
+ def _get_version() -> str:
280
+ from . import __version__ as version
281
+
282
+ return version
283
+
284
+
285
+ def main(argv: Optional[list[str]] = None) -> int:
286
+ if argv is None:
287
+ _install_trimmed_excepthook()
288
+ argv = sys.argv[1:]
289
+
290
+ try:
291
+ namespace = _parse_args(argv)
292
+ except ValueError as exc:
293
+ _print_help(file=sys.stderr)
294
+ print(f"error: {exc}", file=sys.stderr)
295
+ return 2
296
+
297
+ if namespace.help:
298
+ _print_help()
299
+ return 0
300
+ if namespace.version:
301
+ print(_format_version(_get_version(), __build_info__))
302
+ return 0
303
+
304
+ # Validate --awk and --map are mutually exclusive
305
+ if namespace.awk and namespace.map:
306
+ print("error: --awk and --map cannot be used together", file=sys.stderr)
307
+ return 2
308
+
309
+ # Validate -b/--begin and -e/--end only with --awk or --map mode
310
+ if (namespace.begin_code or namespace.end_code) and not (namespace.awk or namespace.map):
311
+ print(
312
+ "error: -b/--begin and -e/--end options require --awk or --map mode",
313
+ file=sys.stderr,
314
+ )
315
+ return 2
316
+
317
+ mode = "map" if namespace.map else ("awk" if namespace.awk else "snail")
318
+
319
+ if namespace.file:
320
+ from pathlib import Path
321
+
322
+ path = Path(namespace.file)
323
+ try:
324
+ source = path.read_text()
325
+ except OSError as exc:
326
+ print(f"failed to read {path}: {exc}", file=sys.stderr)
327
+ return 1
328
+ filename = str(path)
329
+ args = [filename, *namespace.args]
330
+ else:
331
+ if not namespace.args:
332
+ print("no input provided", file=sys.stderr)
333
+ return 1
334
+ source = namespace.args[0]
335
+ filename = "<cmd>"
336
+ args = ["--", *namespace.args[1:]]
337
+
338
+ if namespace.debug_snail_ast:
339
+ from . import parse_ast
340
+
341
+ snail_ast = parse_ast(
342
+ source,
343
+ mode=mode,
344
+ filename=filename,
345
+ begin_code=namespace.begin_code,
346
+ end_code=namespace.end_code,
347
+ )
348
+ print(snail_ast)
349
+ return 0
350
+
351
+ if namespace.debug:
352
+ import ast
353
+ import builtins
354
+
355
+ python_ast = compile_ast(
356
+ source,
357
+ mode=mode,
358
+ auto_print=not namespace.no_print,
359
+ filename=filename,
360
+ begin_code=namespace.begin_code,
361
+ end_code=namespace.end_code,
362
+ )
363
+ builtins.compile(python_ast, _display_filename(filename), "exec")
364
+ try:
365
+ output = ast.unparse(python_ast)
366
+ except AttributeError:
367
+ import astunparse
368
+
369
+ output = astunparse.unparse(python_ast).rstrip("\n")
370
+ print(output)
371
+ return 0
372
+
373
+ return exec(
374
+ source,
375
+ argv=args,
376
+ mode=mode,
377
+ auto_print=not namespace.no_print,
378
+ auto_import=not namespace.no_auto_import,
379
+ filename=filename,
380
+ begin_code=namespace.begin_code,
381
+ end_code=namespace.end_code,
382
+ )
383
+
384
+
385
+ if __name__ == "__main__":
386
+ raise SystemExit(main())
@@ -0,0 +1,250 @@
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
+
52
+
53
+ def _get_compact_try():
54
+ global _compact_try
55
+ if _compact_try is None:
56
+ from .compact_try import compact_try
57
+
58
+ _compact_try = compact_try
59
+ return _compact_try
60
+
61
+
62
+ def _get_regex_search():
63
+ global _regex_search
64
+ if _regex_search is None:
65
+ from .regex import regex_search
66
+
67
+ _regex_search = regex_search
68
+ return _regex_search
69
+
70
+
71
+ def _get_regex_compile():
72
+ global _regex_compile
73
+ if _regex_compile is None:
74
+ from .regex import regex_compile
75
+
76
+ _regex_compile = regex_compile
77
+ return _regex_compile
78
+
79
+
80
+ def _get_subprocess_capture():
81
+ global _subprocess_capture
82
+ if _subprocess_capture is None:
83
+ from .subprocess import SubprocessCapture
84
+
85
+ _subprocess_capture = SubprocessCapture
86
+ return _subprocess_capture
87
+
88
+
89
+ def _get_subprocess_status():
90
+ global _subprocess_status
91
+ if _subprocess_status is None:
92
+ from .subprocess import SubprocessStatus
93
+
94
+ _subprocess_status = SubprocessStatus
95
+ return _subprocess_status
96
+
97
+
98
+ def _get_jmespath_query():
99
+ global _jmespath_query
100
+ if _jmespath_query is None:
101
+ from .structured_accessor import __snail_jmespath_query
102
+
103
+ _jmespath_query = __snail_jmespath_query
104
+ return _jmespath_query
105
+
106
+
107
+ def _get_js():
108
+ global _js
109
+ if _js is None:
110
+ from .structured_accessor import js
111
+
112
+ _js = js
113
+ return _js
114
+
115
+
116
+ def _get_lazy_text_class():
117
+ global _lazy_text_class
118
+ if _lazy_text_class is None:
119
+ from .lazy_text import LazyText
120
+
121
+ _lazy_text_class = LazyText
122
+ return _lazy_text_class
123
+
124
+
125
+ def _get_lazy_file_class():
126
+ global _lazy_file_class
127
+ if _lazy_file_class is None:
128
+ from .lazy_file import LazyFile
129
+
130
+ _lazy_file_class = LazyFile
131
+ return _lazy_file_class
132
+
133
+
134
+ def _get_incr_attr():
135
+ global _incr_attr
136
+ if _incr_attr is None:
137
+ from .augmented import __snail_incr_attr
138
+
139
+ _incr_attr = __snail_incr_attr
140
+ return _incr_attr
141
+
142
+
143
+ def _get_incr_index():
144
+ global _incr_index
145
+ if _incr_index is None:
146
+ from .augmented import __snail_incr_index
147
+
148
+ _incr_index = __snail_incr_index
149
+ return _incr_index
150
+
151
+
152
+ def _get_aug_attr():
153
+ global _aug_attr
154
+ if _aug_attr is None:
155
+ from .augmented import __snail_aug_attr
156
+
157
+ _aug_attr = __snail_aug_attr
158
+ return _aug_attr
159
+
160
+
161
+ def _get_aug_index():
162
+ global _aug_index
163
+ if _aug_index is None:
164
+ from .augmented import __snail_aug_index
165
+
166
+ _aug_index = __snail_aug_index
167
+ return _aug_index
168
+
169
+
170
+ def _lazy_compact_try(expr_fn, fallback_fn=None):
171
+ return _get_compact_try()(expr_fn, fallback_fn)
172
+
173
+
174
+ def _lazy_regex_search(value, pattern):
175
+ return _get_regex_search()(value, pattern)
176
+
177
+
178
+ def _lazy_regex_compile(pattern):
179
+ return _get_regex_compile()(pattern)
180
+
181
+
182
+ def _lazy_subprocess_capture(cmd: str):
183
+ return _get_subprocess_capture()(cmd)
184
+
185
+
186
+ def _lazy_subprocess_status(cmd: str):
187
+ return _get_subprocess_status()(cmd)
188
+
189
+
190
+ def _lazy_jmespath_query(query: str):
191
+ return _get_jmespath_query()(query)
192
+
193
+
194
+ def _lazy_js(input_data=None):
195
+ return _get_js()(input_data)
196
+
197
+
198
+ def _lazy_incr_attr(obj, attr: str, delta: int, pre: bool):
199
+ return _get_incr_attr()(obj, attr, delta, pre)
200
+
201
+
202
+ def _lazy_incr_index(obj, index, delta: int, pre: bool):
203
+ return _get_incr_index()(obj, index, delta, pre)
204
+
205
+
206
+ def _lazy_aug_attr(obj, attr: str, value, op: str):
207
+ return _get_aug_attr()(obj, attr, value, op)
208
+
209
+
210
+ def _lazy_aug_index(obj, index, value, op: str):
211
+ return _get_aug_index()(obj, index, value, op)
212
+
213
+
214
+ def __snail_partial(func, /, *args, **kwargs):
215
+ import functools
216
+
217
+ return functools.partial(func, *args, **kwargs)
218
+
219
+
220
+ def __snail_contains__(left, right):
221
+ method = getattr(right, "__snail_contains__", None)
222
+ if method is not None:
223
+ return method(left)
224
+ return left in right
225
+
226
+
227
+ def __snail_contains_not__(left, right):
228
+ method = getattr(right, "__snail_contains__", None)
229
+ if method is not None:
230
+ return not bool(method(left))
231
+ return left not in right
232
+
233
+
234
+ def install_helpers(globals_dict: dict) -> None:
235
+ globals_dict["__snail_compact_try"] = _lazy_compact_try
236
+ globals_dict["__snail_regex_search"] = _lazy_regex_search
237
+ globals_dict["__snail_regex_compile"] = _lazy_regex_compile
238
+ globals_dict["__SnailSubprocessCapture"] = _lazy_subprocess_capture
239
+ globals_dict["__SnailSubprocessStatus"] = _lazy_subprocess_status
240
+ globals_dict["__snail_jmespath_query"] = _lazy_jmespath_query
241
+ globals_dict["__snail_partial"] = __snail_partial
242
+ globals_dict["__snail_contains__"] = __snail_contains__
243
+ globals_dict["__snail_contains_not__"] = __snail_contains_not__
244
+ globals_dict["__snail_incr_attr"] = _lazy_incr_attr
245
+ globals_dict["__snail_incr_index"] = _lazy_incr_index
246
+ globals_dict["__snail_aug_attr"] = _lazy_aug_attr
247
+ globals_dict["__snail_aug_index"] = _lazy_aug_index
248
+ globals_dict["js"] = _lazy_js
249
+ globals_dict["__SnailLazyText"] = _get_lazy_text_class()
250
+ globals_dict["__SnailLazyFile"] = _get_lazy_file_class()
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ import operator as _op
4
+
5
+ _OPS = {
6
+ "+": _op.add,
7
+ "-": _op.sub,
8
+ "*": _op.mul,
9
+ "/": _op.truediv,
10
+ "//": _op.floordiv,
11
+ "%": _op.mod,
12
+ "**": _op.pow,
13
+ }
14
+
15
+
16
+ def _apply_op(left, right, op: str):
17
+ try:
18
+ func = _OPS[op]
19
+ except KeyError as exc:
20
+ raise ValueError(f"unknown augmented op: {op}") from exc
21
+ return func(left, right)
22
+
23
+
24
+ def __snail_incr_attr(obj, attr: str, delta: int, pre: bool):
25
+ old = getattr(obj, attr)
26
+ new = old + delta
27
+ setattr(obj, attr, new)
28
+ return new if pre else old
29
+
30
+
31
+ def __snail_incr_index(obj, index, delta: int, pre: bool):
32
+ old = obj[index]
33
+ new = old + delta
34
+ obj[index] = new
35
+ return new if pre else old
36
+
37
+
38
+ def __snail_aug_attr(obj, attr: str, value, op: str):
39
+ old = getattr(obj, attr)
40
+ new = _apply_op(old, value, op)
41
+ setattr(obj, attr, new)
42
+ return new
43
+
44
+
45
+ def __snail_aug_index(obj, index, value, op: str):
46
+ old = obj[index]
47
+ new = _apply_op(old, value, op)
48
+ obj[index] = new
49
+ return new
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def compact_try(expr_fn, fallback_fn=None):
5
+ try:
6
+ return expr_fn()
7
+ except Exception as exc:
8
+ if fallback_fn is None:
9
+ fallback_member = getattr(exc, "__fallback__", None)
10
+ if callable(fallback_member):
11
+ return fallback_member()
12
+ return None
13
+ return fallback_fn(exc)
@@ -0,0 +1,41 @@
1
+ """Lazy file opener for map mode."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class LazyFile:
7
+ """Context manager that opens the file on first access."""
8
+
9
+ __slots__ = ("_path", "_mode", "_kwargs", "_fd", "_closed")
10
+
11
+ def __init__(self, path, mode="r", **kwargs):
12
+ self._path = path
13
+ self._mode = mode
14
+ self._kwargs = kwargs
15
+ self._fd = None
16
+ self._closed = False
17
+
18
+ def _ensure_open(self):
19
+ if self._closed:
20
+ raise ValueError("I/O operation on closed file.")
21
+ if self._fd is None:
22
+ self._fd = open(self._path, self._mode, **self._kwargs)
23
+ return self._fd
24
+
25
+ def __enter__(self):
26
+ return self
27
+
28
+ def __exit__(self, exc_type, exc, tb):
29
+ self._closed = True
30
+ if self._fd is not None:
31
+ self._fd.close()
32
+ return False
33
+
34
+ def __getattr__(self, name):
35
+ return getattr(self._ensure_open(), name)
36
+
37
+ def __iter__(self):
38
+ return iter(self._ensure_open())
39
+
40
+ def __next__(self):
41
+ return next(self._ensure_open())
@@ -0,0 +1,50 @@
1
+ """Lazy text content reader for map mode."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class LazyText:
7
+ """Lazily reads file content on first access."""
8
+
9
+ __slots__ = ("_fd", "_text")
10
+
11
+ def __init__(self, fd):
12
+ self._fd = fd
13
+ self._text = None
14
+
15
+ def _ensure_loaded(self):
16
+ if self._text is None:
17
+ self._text = self._fd.read()
18
+ return self._text
19
+
20
+ def __str__(self):
21
+ return self._ensure_loaded()
22
+
23
+ def __repr__(self):
24
+ return repr(str(self))
25
+
26
+ def __eq__(self, other):
27
+ if isinstance(other, LazyText):
28
+ return str(self) == str(other)
29
+ return str(self) == other
30
+
31
+ def __hash__(self):
32
+ return hash(str(self))
33
+
34
+ def __len__(self):
35
+ return len(str(self))
36
+
37
+ def __iter__(self):
38
+ return iter(str(self))
39
+
40
+ def __contains__(self, item):
41
+ return item in str(self)
42
+
43
+ def __add__(self, other):
44
+ return str(self) + other
45
+
46
+ def __radd__(self, other):
47
+ return other + str(self)
48
+
49
+ def __getattr__(self, name):
50
+ return getattr(str(self), name)
snail/runtime/regex.py ADDED
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+
6
+ class SnailRegex:
7
+ def __init__(self, pattern: str) -> None:
8
+ self.pattern = pattern
9
+ self._regex = re.compile(pattern)
10
+
11
+ def search(self, value):
12
+ return regex_search(value, self._regex)
13
+
14
+ def __contains__(self, value):
15
+ return bool(self.__snail_contains__(value))
16
+
17
+ def __snail_contains__(self, value):
18
+ return self.search(value)
19
+
20
+ def __repr__(self) -> str:
21
+ return f"/{self.pattern}/"
22
+
23
+
24
+ def regex_search(value, pattern):
25
+ if isinstance(pattern, SnailRegex):
26
+ return pattern.search(value)
27
+ if hasattr(pattern, "search"):
28
+ match = pattern.search(value)
29
+ else:
30
+ match = re.search(pattern, value)
31
+ if match is None:
32
+ return ()
33
+ return (match.group(0),) + match.groups()
34
+
35
+
36
+ def regex_compile(pattern):
37
+ return SnailRegex(pattern)
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ import json as _json
4
+ import os as _os
5
+ import sys as _sys
6
+
7
+ def __snail_jmespath_query(query: str):
8
+ """Create a callable that applies JMESPath query.
9
+
10
+ Used by the $[query] syntax which lowers to __snail_jmespath_query(query).
11
+ """
12
+
13
+ import jmespath as _jmespath
14
+
15
+ def apply(data):
16
+ return _jmespath.search(query, data)
17
+
18
+ return apply
19
+
20
+
21
+ def _parse_jsonl(content: str):
22
+ lines = [line for line in content.splitlines() if line.strip()]
23
+ if not lines:
24
+ return []
25
+
26
+ items = []
27
+ for line in lines:
28
+ try:
29
+ items.append(_json.loads(line))
30
+ except _json.JSONDecodeError as exc:
31
+ raise _json.JSONDecodeError(
32
+ f"Invalid JSONL line: {exc.msg}",
33
+ line,
34
+ exc.pos,
35
+ ) from exc
36
+ return items
37
+
38
+
39
+ def js(input_data=None):
40
+ """Parse JSON from various input sources.
41
+
42
+ Returns the parsed Python object (dict, list, etc.) directly.
43
+ If called with no arguments, reads from stdin.
44
+ """
45
+ if input_data is None:
46
+ input_data = _sys.stdin
47
+
48
+ if isinstance(input_data, str):
49
+ try:
50
+ return _json.loads(input_data)
51
+ except _json.JSONDecodeError:
52
+ if _os.path.exists(input_data):
53
+ with open(input_data, "r", encoding="utf-8") as handle:
54
+ content = handle.read()
55
+ try:
56
+ return _json.loads(content)
57
+ except _json.JSONDecodeError:
58
+ return _parse_jsonl(content)
59
+ else:
60
+ return _parse_jsonl(input_data)
61
+ elif hasattr(input_data, "read"):
62
+ content = input_data.read()
63
+ if isinstance(content, bytes):
64
+ content = content.decode("utf-8")
65
+ try:
66
+ return _json.loads(content)
67
+ except _json.JSONDecodeError:
68
+ return _parse_jsonl(content)
69
+ elif isinstance(input_data, (dict, list, int, float, bool)) or input_data is None:
70
+ return input_data
71
+ else:
72
+ raise TypeError(
73
+ f"js() input must be JSON-compatible, got {type(input_data).__name__}"
74
+ )
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+
5
+
6
+ class SubprocessCapture:
7
+ def __init__(self, cmd: str) -> None:
8
+ self.cmd = cmd
9
+
10
+ def __call__(self, input_data=None):
11
+ try:
12
+ if input_data is None:
13
+ completed = subprocess.run(
14
+ self.cmd,
15
+ shell=True,
16
+ check=True,
17
+ text=True,
18
+ stdout=subprocess.PIPE,
19
+ )
20
+ else:
21
+ if not isinstance(input_data, (str, bytes)):
22
+ input_data = str(input_data)
23
+ completed = subprocess.run(
24
+ self.cmd,
25
+ shell=True,
26
+ check=True,
27
+ text=True,
28
+ input=input_data,
29
+ stdout=subprocess.PIPE,
30
+ )
31
+ return completed.stdout.rstrip("\n")
32
+ except subprocess.CalledProcessError as exc:
33
+
34
+ def __fallback(exc=exc):
35
+ raise exc
36
+
37
+ exc.__fallback__ = __fallback
38
+ raise
39
+
40
+
41
+ class SubprocessStatus:
42
+ def __init__(self, cmd: str) -> None:
43
+ self.cmd = cmd
44
+
45
+ def __call__(self, input_data=None):
46
+ try:
47
+ if input_data is None:
48
+ subprocess.run(self.cmd, shell=True, check=True)
49
+ else:
50
+ if not isinstance(input_data, (str, bytes)):
51
+ input_data = str(input_data)
52
+ subprocess.run(
53
+ self.cmd,
54
+ shell=True,
55
+ check=True,
56
+ text=True,
57
+ input=input_data,
58
+ )
59
+ return 0
60
+ except subprocess.CalledProcessError as exc:
61
+
62
+ def __fallback(exc=exc):
63
+ return exc.returncode
64
+
65
+ exc.__fallback__ = __fallback
66
+ raise
@@ -0,0 +1,318 @@
1
+ Metadata-Version: 2.4
2
+ Name: snail-lang
3
+ Version: 0.7.1
4
+ Requires-Dist: astunparse>=1.6.3 ; python_full_version < '3.9'
5
+ Requires-Dist: jmespath>=1.0.1
6
+ Requires-Dist: maturin>=1.5 ; extra == 'dev'
7
+ Requires-Dist: pytest ; extra == 'dev'
8
+ Provides-Extra: dev
9
+ License-File: LICENSE
10
+ Summary: Snail programming language interpreter
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
13
+
14
+ <p align="center">
15
+ <img src="logo.png" alt="Snail logo" width="200">
16
+ </p>
17
+
18
+ <h1 align="center">Snail</h1>
19
+
20
+ **Snail** is a programming language that compiles to Python, combining Python's familiarity and extensive libraries with Perl/awk-inspired syntax for quick scripts and one-liners. Its what you get when you shove a snake in a shell.
21
+
22
+ ## AI Slop!
23
+
24
+ Snail is me learning how to devlop code using LLMs. I think its neat, and
25
+ maybe useful. I don't think this is high quality. I am going to try and LLM my
26
+ way into something good, but its certainly not there yet.
27
+
28
+ ## Installing Snail
29
+
30
+ ```bash
31
+ pip install snail-lang
32
+ -or-
33
+ uv tool install snail-lang
34
+ ```
35
+
36
+ That installs the `snail` CLI for your user; try it with `snail "print('hello')"` once the install completes.
37
+
38
+ ## ✨ What Makes Snail Unique
39
+
40
+ ### Curly Braces, Not Indentation
41
+
42
+ Write Python logic without worrying about whitespace:
43
+
44
+ ```snail
45
+ def process(items) {
46
+ for item in items {
47
+ if item > 0 { print(item) }
48
+ else { continue }
49
+ }
50
+ }
51
+ ```
52
+
53
+ Note, since it is jarring to write python with semicolons everywhere,
54
+ semicolons are optional. You can separate statements with newlines.
55
+
56
+ ### Awk Mode
57
+
58
+ Process files line-by-line with familiar awk semantics:
59
+
60
+ ```snail-awk("hello world\nfoo bar\n")
61
+ BEGIN { print("start") }
62
+ /hello/ { print("matched:", $0) }
63
+ { print($1, "->", $2) }
64
+ END { print("done") }
65
+ ```
66
+
67
+ **Built-in variables:**
68
+
69
+ | Variable | Description |
70
+ |----------|-------------|
71
+ | `$0` | Current line (with newline stripped) |
72
+ | `$1`, `$2`, ... | Individual fields (whitespace-split) |
73
+ | `$f` | All fields as a list |
74
+ | `$n` | Global line number (across all files) |
75
+ | `$fn` | Per-file line number |
76
+ | `$p` | Current file path |
77
+ | `$m` | Last regex match object |
78
+
79
+ Begin/end blocks can live in the source file (`BEGIN { ... }` / `END { ... }`) or be supplied
80
+ via CLI flags (`-b`/`--begin`, `-e`/`--end`) for setup and teardown. CLI BEGIN blocks run
81
+ before in-file BEGIN blocks; CLI END blocks run after in-file END blocks.
82
+ `BEGIN` and `END` are reserved keywords in all modes.
83
+ BEGIN/END blocks are regular Snail blocks, so awk/map-only `$` variables are not available inside them.
84
+ ```bash
85
+ echo -e "5\n4\n3\n2\n1" | snail --awk --begin 'total = 0' --end 'print("Sum:", total)' '/^[0-9]+/ { total = total + int($1) }'
86
+ ```
87
+
88
+ ### Map Mode
89
+
90
+ Process files one at a time instead of line-by-line:
91
+
92
+ ```snail-map
93
+ BEGIN { print("start") }
94
+ print("File:", $src)
95
+ print("Size:", len($text), "bytes")
96
+ END { print("done") }
97
+ ```
98
+
99
+ **Built-in variables:**
100
+
101
+ | Variable | Description |
102
+ |----------|-------------|
103
+ | `$src` | Current file path |
104
+ | `$fd` | Open file handle for the current file |
105
+ | `$text` | Lazy text view of the current file contents |
106
+
107
+ Begin/end blocks can live in the source file (`BEGIN { ... }` / `END { ... }`) or be supplied
108
+ via CLI flags (`-b`/`--begin`, `-e`/`--end`) for setup and teardown. CLI BEGIN blocks run
109
+ before in-file BEGIN blocks; CLI END blocks run after in-file END blocks.
110
+ BEGIN/END blocks are regular Snail blocks, so awk/map-only `$` variables are not available inside them.
111
+ `BEGIN` and `END` are reserved keywords in all modes.
112
+ ```bash
113
+ snail --map --begin "print('start')" --end "print('done')" "print($src)" *.txt
114
+ ```
115
+
116
+ ### Compact Error Handling
117
+
118
+ The `?` operator makes error handling terse yet expressive:
119
+
120
+ ```snail
121
+ # Swallow exception, return None
122
+ err = risky()?
123
+
124
+ # Swallow exception, return exception object
125
+ err = risky():$e?
126
+
127
+ # Provide a fallback value (exception available as $e)
128
+ value = js("malformed json"):%{"error": "invalid json"}?
129
+ details = fetch_url("foo.com"):"default html"?
130
+ exception_info = fetch_url("example.com"):$e.http_response_code?
131
+
132
+ # Access attributes directly
133
+ name = risky("")?.__class__.__name__
134
+ args = risky("becomes a list"):[1,2,3]?[0]
135
+ ```
136
+
137
+ ### Destructuring + `if let` / `while let`
138
+
139
+ Unpack tuples and lists directly, including Python-style rest bindings:
140
+
141
+ ```snail
142
+ x, *xs = [1, 2, 3]
143
+
144
+ if let [head, *tail] = [1, 2, 3]; head > 0 {
145
+ print(head, tail)
146
+ }
147
+ ```
148
+
149
+ `if let`/`while let` only enter the block when the destructuring succeeds. A guard
150
+ after `;` lets you add a boolean check that runs after the bindings are created.
151
+
152
+ Note that this syntax is more powerful than the walrus operator as that does
153
+ not allow for destructuring.
154
+
155
+
156
+ ### Pipeline Operator
157
+
158
+ The `|` operator enables data pipelining as syntactic sugar for nested
159
+ function calls. `x | y | z` becomes `z(y(x))`. This lets you stay in a
160
+ shell mindset.
161
+
162
+ ```snail
163
+ # Pipe data to subprocess stdin
164
+ result = "hello\nworld" | $(grep hello)
165
+
166
+ # Chain multiple transformations
167
+ output = "foo\nbar" | $(grep foo) | $(wc -l)
168
+
169
+ # Custom pipeline handlers
170
+ class Doubler {
171
+ def __call__(self, x) { return x * 2 }
172
+ }
173
+ doubled = 21 | Doubler() # yields 42
174
+ ```
175
+
176
+ Arbitrary callables make up pipelines, even if they have multiple parameters.
177
+ Snail supports this via placeholders.
178
+ ```snail
179
+ greeting = "World" | greet("Hello ", _) # greet("Hello ", "World")
180
+ excited = "World" | greet(_, "!") # greet("World", "!")
181
+ formal = "World" | greet("Hello ", suffix=_) # greet("Hello ", "World")
182
+ ```
183
+
184
+ When a pipeline targets a call expression, the left-hand value is passed to the
185
+ resulting callable. If the call includes a single `_` placeholder, Snail substitutes
186
+ the piped value at that position (including keyword arguments). Only one
187
+ placeholder is allowed in a piped call. Outside of pipeline calls, `_` remains a
188
+ normal identifier.
189
+
190
+ ### Built-in Subprocess
191
+
192
+ Shell commands are first-class citizens with capturing and non-capturing
193
+ forms.
194
+
195
+ ```snail
196
+ # Capture command output with interpolation
197
+ greeting = $(echo hello {name})
198
+
199
+ # Pipe data through commands
200
+ result = "foo\nbar\nbaz" | $(grep bar) | $(cat -n)
201
+
202
+ # Check command status
203
+ @(make build)? # returns exit code on failure instead of raising
204
+ ```
205
+
206
+
207
+ ### Regex Literals
208
+
209
+ Snail supports first class patterns. Think of them as an infinte set.
210
+
211
+ ```snail
212
+ if bad_email in /^[\w.]+@[\w.]+$/ {
213
+ print("Valid email")
214
+ }
215
+
216
+ # Compiled regex for reuse
217
+ pattern = /\d{3}-\d{4}/
218
+ match = pattern.search(phone)
219
+ match2 = "555-1212" in pattern
220
+ ```
221
+
222
+ Snail regexes don't return a match object, rather they return a tuple
223
+ containing all of the match groups, including group 0. Both `search` and `in`
224
+ return the same tuple (or `()` when there is no match).
225
+
226
+ ### JSON Queries with JMESPath
227
+
228
+ Parse and query JSON data with the `js()` function and structured pipeline accessor:
229
+
230
+ ```snail
231
+ # Parse JSON and query with $[jmespath]
232
+
233
+ # JSON query with JMESPath
234
+ data = js($(curl -s https://api.github.com/repos/sudonym1/snail))
235
+ counts = data | $[stargazers_count]
236
+
237
+ # Inline parsing and querying
238
+ result = js('{{"foo": 12}}') | $[foo]
239
+
240
+ # JSONL parsing returns a list
241
+ names = js('{{"name": "Ada"}}\n{{"name": "Lin"}}') | $[[*].name]
242
+ ```
243
+
244
+ ### Full Python Interoperability
245
+
246
+ Snail compiles to Python AST—import any Python module, use any library, in any
247
+ environment. Assuming that you are using Python 3.8 or later.
248
+
249
+ ## 🚀 Quick Start
250
+
251
+ ```bash
252
+ # One-liner: arithmetic + interpolation
253
+ snail 'name="Snail"; print("{name} says: {6 * 7}")'
254
+
255
+ # JSON query with JMESPath
256
+ snail 'js($(curl -s https://api.github.com/repos/sudonym1/snail)) | $[stargazers_count]'
257
+
258
+ # Compact error handling with fallback
259
+ snail 'result = int("oops"):"bad int {$e}"?; print(result)'
260
+
261
+ # Regex match and capture
262
+ snail 'if let [_, user, domain] = "user@example.com" in /^[\w.]+@([\w.]+)$/ { print(domain) }'
263
+
264
+ # Awk mode: print line numbers for matches
265
+ rg -n "TODO" README.md | snail --awk '/TODO/ { print("{$n}: {$0}") }'
266
+ ```
267
+
268
+ ## 📚 Documentation
269
+
270
+ Documentation is WIP
271
+
272
+ - **[Language Reference](docs/REFERENCE.md)** — Complete syntax and semantics
273
+ - **[examples/all_syntax.snail](examples/all_syntax.snail)** — Every feature in one file
274
+ - **[examples/awk.snail](examples/awk.snail)** — Awk mode examples
275
+ - **[examples/map.snail](examples/map.snail)** — Map mode examples
276
+
277
+ ## 🔌 Editor Support
278
+
279
+ Vim/Neovim plugin with syntax highlighting, formatting, and run commands:
280
+
281
+ ```vim
282
+ Plug 'sudonym1/snail', { 'rtp': 'extras/vim' }
283
+ ```
284
+
285
+ See [extras/vim/README.md](extras/vim/README.md) for details. Tree-sitter grammar available in `extras/tree-sitter-snail/`.
286
+
287
+ ## Performance
288
+
289
+ Section is WIP
290
+
291
+ Startup performance is benchmarked with `./benchmarks/startup.py`. On my
292
+ machine snail adds 5 ms of overhead above the regular python3 interpreter.
293
+
294
+ ## 🛠️ Building from Source
295
+
296
+ ### Prerequisites
297
+
298
+ **Python 3.8+** (required at runtime)
299
+
300
+ Snail runs in-process via a Pyo3 extension module, so it uses the active Python environment.
301
+
302
+ Installation per platform:
303
+ - **Ubuntu/Debian**: `sudo apt install python3 python3-dev`
304
+ - **Fedora/RHEL**: `sudo dnf install python3 python3-devel`
305
+ - **macOS**: `brew install python@3.12` (or use the system Python 3)
306
+ - **Windows**: Download from [python.org](https://www.python.org/downloads/)
307
+
308
+ ### Build, Test, and Install
309
+
310
+ ```bash
311
+ # Clone the repository
312
+ git clone https://github.com/sudonym1/snail.git
313
+ cd snail
314
+
315
+ make test
316
+ make install
317
+ ```
318
+
@@ -0,0 +1,16 @@
1
+ snail\__init__.py,sha256=ycv9SAHU4p6ndYoMuBL022C3Nv4O9gPPH16cGN_mMqM,821
2
+ snail\_native.pyd,sha256=MaXUH30xxyYSiSt0w6qqZjuCRS72zABnqES-Dvtkt3s,2135552
3
+ snail\cli.py,sha256=k9u3iVwZCH1KLYYBqF9geq3QNsG7CBkzelBaDr2xqBQ,12184
4
+ snail\runtime\__init__.py,sha256=PvekYIXdfs8xT3fi_ZEunarstfgc4AF6b9SqbJfwsDE,6766
5
+ snail\runtime\augmented.py,sha256=ZBlgJL1v-uRxPOiB2QToshx6vreousSTgT3rbUiPyQ0,1091
6
+ snail\runtime\compact_try.py,sha256=0nxfcAOsISMo3RsVb3_9kYemXtFLfW3kHrQSjAdkJxE,393
7
+ snail\runtime\lazy_file.py,sha256=mYMQdTv9c3_g_0OzjqczBZcmiX7DgTd1yY7YS9cmPsY,1100
8
+ snail\runtime\lazy_text.py,sha256=xNXY0r5VjK-BLRQQOXj3EmGzzjBBnPPJZZfMUkhlqx4,1143
9
+ snail\runtime\regex.py,sha256=Vc1LJe6OfCh0G8z_n83rGz8tqrjyESg4nbbwFrR2ARw,916
10
+ snail\runtime\structured_accessor.py,sha256=qSXauwYFZ9Ig_ACsxKarU3O49OGxbyhvm8LhEPRyT70,2237
11
+ snail\runtime\subprocess.py,sha256=ILDdfpqRXMFjWKYdpxQvEpie_31kItCuDO2WlsNpnks,1955
12
+ snail_lang-0.7.1.dist-info\METADATA,sha256=tdjZgk_uEjI6gbQvdsA3Ls570bRyOFzsvmuboCkfn60,9645
13
+ snail_lang-0.7.1.dist-info\WHEEL,sha256=gPqN4EsdiAyGvmfrYy_ONrF276O8o0hPitI2CKZrEFA,95
14
+ snail_lang-0.7.1.dist-info\entry_points.txt,sha256=8u6pjHZtKGdaZQlr6yKbftplObC07caDZ44j5PIPU6M,39
15
+ snail_lang-0.7.1.dist-info\licenses\LICENSE,sha256=iT_e39WExFFehp3Zh4faVe3dEv3BrhhX_v3obKdBM9M,1082
16
+ snail_lang-0.7.1.dist-info\RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.11.5)
3
+ Root-Is-Purelib: false
4
+ Tag: cp38-abi3-win_amd64
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ snail=snail.cli:main
@@ -0,0 +1,20 @@
1
+ Copyright 2026 Seamus Connor
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the “Software”), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
20
+