snail-lang 0.2.0__cp310-abi3-manylinux_2_34_x86_64.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,10 @@
1
+ from ._native import compile, exec, parse
2
+
3
+ try:
4
+ from importlib.metadata import version
5
+
6
+ __version__ = version("snail")
7
+ except Exception: # pragma: no cover - during development
8
+ __version__ = "0.0.0"
9
+
10
+ __all__ = ["compile", "exec", "parse", "__version__"]
snail/_native.abi3.so ADDED
Binary file
snail/cli.py ADDED
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from . import __version__, exec, parse
8
+
9
+
10
+ def _build_parser() -> argparse.ArgumentParser:
11
+ return argparse.ArgumentParser(
12
+ prog="snail",
13
+ description="Snail programming language interpreter",
14
+ usage="snail [options] -f <file> [args]...\n snail [options] <code> [args]...",
15
+ add_help=True,
16
+ )
17
+
18
+
19
+ def main(argv: list[str] | None = None) -> int:
20
+ parser = _build_parser()
21
+ parser.add_argument("-f", dest="file", metavar="file")
22
+ parser.add_argument("-a", "--awk", action="store_true")
23
+ parser.add_argument("-P", "--no-print", action="store_true")
24
+ parser.add_argument("--parse-only", action="store_true")
25
+ parser.add_argument("-v", "--version", action="store_true")
26
+ parser.add_argument("args", nargs=argparse.REMAINDER)
27
+
28
+ namespace = parser.parse_args(argv)
29
+
30
+ if namespace.version:
31
+ print(__version__)
32
+ return 0
33
+
34
+ mode = "awk" if namespace.awk else "snail"
35
+
36
+ if namespace.file:
37
+ path = Path(namespace.file)
38
+ try:
39
+ source = path.read_text()
40
+ except OSError as exc:
41
+ print(f"failed to read {path}: {exc}", file=sys.stderr)
42
+ return 1
43
+ filename = str(path)
44
+ args = [filename, *namespace.args]
45
+ else:
46
+ if not namespace.args:
47
+ print("no input provided", file=sys.stderr)
48
+ return 1
49
+ source = namespace.args[0]
50
+ filename = "<cmd>"
51
+ args = ["--", *namespace.args[1:]]
52
+
53
+ if namespace.parse_only:
54
+ parse(source, mode=mode, filename=filename)
55
+ return 0
56
+
57
+ return exec(
58
+ source,
59
+ argv=args,
60
+ mode=mode,
61
+ auto_print=not namespace.no_print,
62
+ filename=filename,
63
+ )
64
+
65
+
66
+ if __name__ == "__main__":
67
+ raise SystemExit(main())
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from .compact_try import compact_try
4
+ from .regex import regex_compile, regex_search
5
+ from .structured_accessor import (
6
+ JsonObject,
7
+ JsonPipelineWrapper,
8
+ StructuredAccessor,
9
+ json,
10
+ )
11
+ from .subprocess import SubprocessCapture, SubprocessStatus
12
+
13
+ __all__ = ["install_helpers"]
14
+
15
+
16
+ def install_helpers(globals_dict: dict) -> None:
17
+ globals_dict["__snail_compact_try"] = compact_try
18
+ globals_dict["__snail_regex_search"] = regex_search
19
+ globals_dict["__snail_regex_compile"] = regex_compile
20
+ globals_dict["__SnailSubprocessCapture"] = SubprocessCapture
21
+ globals_dict["__SnailSubprocessStatus"] = SubprocessStatus
22
+ globals_dict["__SnailStructuredAccessor"] = StructuredAccessor
23
+ globals_dict["__SnailJsonObject"] = JsonObject
24
+ globals_dict["__SnailJsonPipelineWrapper"] = JsonPipelineWrapper
25
+ globals_dict["json"] = json
@@ -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 exc
13
+ return fallback_fn(exc)
snail/runtime/regex.py ADDED
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+
6
+ def regex_search(value, pattern):
7
+ return re.search(pattern, value)
8
+
9
+
10
+ def regex_compile(pattern):
11
+ return re.compile(pattern)
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import json as _json
4
+ import sys as _sys
5
+
6
+ from ..vendor import jmespath
7
+
8
+
9
+ class StructuredAccessor:
10
+ def __init__(self, query: str) -> None:
11
+ self.query = query
12
+
13
+ def __pipeline__(self, obj):
14
+ if not hasattr(obj, "__structured__"):
15
+ raise TypeError(
16
+ "Pipeline target must implement __structured__, "
17
+ f"got {type(obj).__name__}"
18
+ )
19
+ return obj.__structured__(self.query)
20
+
21
+
22
+ class JsonObject:
23
+ def __init__(self, data) -> None:
24
+ self.data = data
25
+
26
+ def __structured__(self, query: str):
27
+ return jmespath.search(query, self.data)
28
+
29
+ def __repr__(self) -> str:
30
+ return _json.dumps(self.data, indent=2)
31
+
32
+
33
+ class JsonPipelineWrapper:
34
+ """Wrapper for json() to support pipeline operator without blocking stdin."""
35
+
36
+ def __pipeline__(self, input_data):
37
+ return json(input_data)
38
+
39
+ def __structured__(self, query: str):
40
+ data = json(_sys.stdin)
41
+ return data.__structured__(query)
42
+
43
+ def __repr__(self) -> str:
44
+ data = json(_sys.stdin)
45
+ return repr(data)
46
+
47
+
48
+ def json(input_data=None):
49
+ """Parse JSON from various input sources."""
50
+ if input_data is None:
51
+ return JsonPipelineWrapper()
52
+
53
+ if isinstance(input_data, str):
54
+ try:
55
+ data = _json.loads(input_data)
56
+ except _json.JSONDecodeError:
57
+ with open(input_data, "r", encoding="utf-8") as handle:
58
+ data = _json.load(handle)
59
+ elif hasattr(input_data, "read"):
60
+ content = input_data.read()
61
+ if isinstance(content, bytes):
62
+ content = content.decode("utf-8")
63
+ data = _json.loads(content)
64
+ elif isinstance(input_data, (dict, list, int, float, bool)) or input_data is None:
65
+ data = input_data
66
+ else:
67
+ raise TypeError(
68
+ f"json() input must be JSON-compatible, got {type(input_data).__name__}"
69
+ )
70
+
71
+ return JsonObject(data)
@@ -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 __pipeline__(self, input_data):
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 __pipeline__(self, input_data):
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
File without changes
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,12 @@
1
+ from . import parser
2
+ from .visitor import Options
3
+
4
+ __version__ = "1.0.1"
5
+
6
+
7
+ def compile(expression):
8
+ return parser.Parser().parse(expression)
9
+
10
+
11
+ def search(expression, data, options=None):
12
+ return parser.Parser().parse(expression).search(data, options=options)
@@ -0,0 +1,90 @@
1
+ # AST nodes have this structure:
2
+ # {"type": <node type>", children: [], "value": ""}
3
+
4
+
5
+ def comparator(name, first, second):
6
+ return {'type': 'comparator', 'children': [first, second], 'value': name}
7
+
8
+
9
+ def current_node():
10
+ return {'type': 'current', 'children': []}
11
+
12
+
13
+ def expref(expression):
14
+ return {'type': 'expref', 'children': [expression]}
15
+
16
+
17
+ def function_expression(name, args):
18
+ return {'type': 'function_expression', 'children': args, 'value': name}
19
+
20
+
21
+ def field(name):
22
+ return {"type": "field", "children": [], "value": name}
23
+
24
+
25
+ def filter_projection(left, right, comparator):
26
+ return {'type': 'filter_projection', 'children': [left, right, comparator]}
27
+
28
+
29
+ def flatten(node):
30
+ return {'type': 'flatten', 'children': [node]}
31
+
32
+
33
+ def identity():
34
+ return {"type": "identity", 'children': []}
35
+
36
+
37
+ def index(index):
38
+ return {"type": "index", "value": index, "children": []}
39
+
40
+
41
+ def index_expression(children):
42
+ return {"type": "index_expression", 'children': children}
43
+
44
+
45
+ def key_val_pair(key_name, node):
46
+ return {"type": "key_val_pair", 'children': [node], "value": key_name}
47
+
48
+
49
+ def literal(literal_value):
50
+ return {'type': 'literal', 'value': literal_value, 'children': []}
51
+
52
+
53
+ def multi_select_dict(nodes):
54
+ return {"type": "multi_select_dict", "children": nodes}
55
+
56
+
57
+ def multi_select_list(nodes):
58
+ return {"type": "multi_select_list", "children": nodes}
59
+
60
+
61
+ def or_expression(left, right):
62
+ return {"type": "or_expression", "children": [left, right]}
63
+
64
+
65
+ def and_expression(left, right):
66
+ return {"type": "and_expression", "children": [left, right]}
67
+
68
+
69
+ def not_expression(expr):
70
+ return {"type": "not_expression", "children": [expr]}
71
+
72
+
73
+ def pipe(left, right):
74
+ return {'type': 'pipe', 'children': [left, right]}
75
+
76
+
77
+ def projection(left, right):
78
+ return {'type': 'projection', 'children': [left, right]}
79
+
80
+
81
+ def subexpression(children):
82
+ return {"type": "subexpression", 'children': children}
83
+
84
+
85
+ def slice(start, end, step):
86
+ return {"type": "slice", "children": [start, end, step]}
87
+
88
+
89
+ def value_projection(left, right):
90
+ return {'type': 'value_projection', 'children': [left, right]}
@@ -0,0 +1,19 @@
1
+ import sys
2
+ import inspect
3
+ from itertools import zip_longest
4
+
5
+
6
+ text_type = str
7
+ string_type = str
8
+
9
+
10
+ def with_str_method(cls):
11
+ # In python3, we don't need to do anything, we return a str type.
12
+ return cls
13
+
14
+ def with_repr_method(cls):
15
+ return cls
16
+
17
+ def get_methods(cls):
18
+ for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
19
+ yield name, method
@@ -0,0 +1,137 @@
1
+ from .compat import with_str_method
2
+
3
+
4
+ class JMESPathError(ValueError):
5
+ pass
6
+
7
+
8
+ @with_str_method
9
+ class ParseError(JMESPathError):
10
+ _ERROR_MESSAGE = "Invalid jmespath expression"
11
+
12
+ def __init__(self, lex_position, token_value, token_type, msg=_ERROR_MESSAGE):
13
+ super(ParseError, self).__init__(lex_position, token_value, token_type)
14
+ self.lex_position = lex_position
15
+ self.token_value = token_value
16
+ self.token_type = token_type.upper()
17
+ self.msg = msg
18
+ # Whatever catches the ParseError can fill in the full expression
19
+ self.expression = None
20
+
21
+ def __str__(self):
22
+ # self.lex_position +1 to account for the starting double quote char.
23
+ underline = " " * (self.lex_position + 1) + "^"
24
+ return (
25
+ "%s: Parse error at column %s, "
26
+ 'token "%s" (%s), for expression:\n"%s"\n%s'
27
+ % (
28
+ self.msg,
29
+ self.lex_position,
30
+ self.token_value,
31
+ self.token_type,
32
+ self.expression,
33
+ underline,
34
+ )
35
+ )
36
+
37
+
38
+ @with_str_method
39
+ class IncompleteExpressionError(ParseError):
40
+ def set_expression(self, expression):
41
+ self.expression = expression
42
+ self.lex_position = len(expression)
43
+ self.token_type = None
44
+ self.token_value = None
45
+
46
+ def __str__(self):
47
+ # self.lex_position +1 to account for the starting double quote char.
48
+ underline = " " * (self.lex_position + 1) + "^"
49
+ return 'Invalid jmespath expression: Incomplete expression:\n"%s"\n%s' % (
50
+ self.expression,
51
+ underline,
52
+ )
53
+
54
+
55
+ @with_str_method
56
+ class LexerError(ParseError):
57
+ def __init__(self, lexer_position, lexer_value, message, expression=None):
58
+ self.lexer_position = lexer_position
59
+ self.lexer_value = lexer_value
60
+ self.message = message
61
+ super(LexerError, self).__init__(lexer_position, lexer_value, message)
62
+ # Whatever catches LexerError can set this.
63
+ self.expression = expression
64
+
65
+ def __str__(self):
66
+ underline = " " * self.lexer_position + "^"
67
+ return "Bad jmespath expression: %s:\n%s\n%s" % (
68
+ self.message,
69
+ self.expression,
70
+ underline,
71
+ )
72
+
73
+
74
+ @with_str_method
75
+ class ArityError(ParseError):
76
+ def __init__(self, expected, actual, name):
77
+ self.expected_arity = expected
78
+ self.actual_arity = actual
79
+ self.function_name = name
80
+ self.expression = None
81
+
82
+ def __str__(self):
83
+ return "Expected %s %s for function %s(), received %s" % (
84
+ self.expected_arity,
85
+ self._pluralize("argument", self.expected_arity),
86
+ self.function_name,
87
+ self.actual_arity,
88
+ )
89
+
90
+ def _pluralize(self, word, count):
91
+ if count == 1:
92
+ return word
93
+ else:
94
+ return word + "s"
95
+
96
+
97
+ @with_str_method
98
+ class VariadictArityError(ArityError):
99
+ def __str__(self):
100
+ return "Expected at least %s %s for function %s(), received %s" % (
101
+ self.expected_arity,
102
+ self._pluralize("argument", self.expected_arity),
103
+ self.function_name,
104
+ self.actual_arity,
105
+ )
106
+
107
+
108
+ @with_str_method
109
+ class JMESPathTypeError(JMESPathError):
110
+ def __init__(self, function_name, current_value, actual_type, expected_types):
111
+ self.function_name = function_name
112
+ self.current_value = current_value
113
+ self.actual_type = actual_type
114
+ self.expected_types = expected_types
115
+
116
+ def __str__(self):
117
+ return (
118
+ "In function %s(), invalid type for value: %s, "
119
+ 'expected one of: %s, received: "%s"'
120
+ % (
121
+ self.function_name,
122
+ self.current_value,
123
+ self.expected_types,
124
+ self.actual_type,
125
+ )
126
+ )
127
+
128
+
129
+ class EmptyExpressionError(JMESPathError):
130
+ def __init__(self):
131
+ super(EmptyExpressionError, self).__init__(
132
+ "Invalid JMESPath expression: cannot be empty."
133
+ )
134
+
135
+
136
+ class UnknownFunctionError(JMESPathError):
137
+ pass