snail-lang 0.2.0__cp310-abi3-macosx_11_0_arm64.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.
@@ -0,0 +1,366 @@
1
+ import math
2
+ import json
3
+
4
+ from . import exceptions
5
+ from .compat import string_type as STRING_TYPE
6
+ from .compat import get_methods
7
+
8
+
9
+ # python types -> jmespath types
10
+ TYPES_MAP = {
11
+ "bool": "boolean",
12
+ "list": "array",
13
+ "dict": "object",
14
+ "NoneType": "null",
15
+ "unicode": "string",
16
+ "str": "string",
17
+ "float": "number",
18
+ "int": "number",
19
+ "long": "number",
20
+ "OrderedDict": "object",
21
+ "_Projection": "array",
22
+ "_Expression": "expref",
23
+ }
24
+
25
+
26
+ # jmespath types -> python types
27
+ REVERSE_TYPES_MAP = {
28
+ "boolean": ("bool",),
29
+ "array": ("list", "_Projection"),
30
+ "object": (
31
+ "dict",
32
+ "OrderedDict",
33
+ ),
34
+ "null": ("NoneType",),
35
+ "string": ("unicode", "str"),
36
+ "number": ("float", "int", "long"),
37
+ "expref": ("_Expression",),
38
+ }
39
+
40
+
41
+ def signature(*arguments):
42
+ def _record_signature(func):
43
+ func.signature = arguments
44
+ return func
45
+
46
+ return _record_signature
47
+
48
+
49
+ class FunctionRegistry(type):
50
+ def __init__(cls, name, bases, attrs):
51
+ cls._populate_function_table()
52
+ super(FunctionRegistry, cls).__init__(name, bases, attrs)
53
+
54
+ def _populate_function_table(cls):
55
+ function_table = {}
56
+ # Any method with a @signature decorator that also
57
+ # starts with "_func_" is registered as a function.
58
+ # _func_max_by -> max_by function.
59
+ for name, method in get_methods(cls):
60
+ if not name.startswith("_func_"):
61
+ continue
62
+ signature = getattr(method, "signature", None)
63
+ if signature is not None:
64
+ function_table[name[6:]] = {
65
+ "function": method,
66
+ "signature": signature,
67
+ }
68
+ cls.FUNCTION_TABLE = function_table
69
+
70
+
71
+ class Functions(metaclass=FunctionRegistry):
72
+ FUNCTION_TABLE = {}
73
+
74
+ def call_function(self, function_name, resolved_args):
75
+ try:
76
+ spec = self.FUNCTION_TABLE[function_name]
77
+ except KeyError:
78
+ raise exceptions.UnknownFunctionError(
79
+ "Unknown function: %s()" % function_name
80
+ )
81
+ function = spec["function"]
82
+ signature = spec["signature"]
83
+ self._validate_arguments(resolved_args, signature, function_name)
84
+ return function(self, *resolved_args)
85
+
86
+ def _validate_arguments(self, args, signature, function_name):
87
+ if signature and signature[-1].get("variadic"):
88
+ if len(args) < len(signature):
89
+ raise exceptions.VariadictArityError(
90
+ len(signature), len(args), function_name
91
+ )
92
+ elif len(args) != len(signature):
93
+ raise exceptions.ArityError(len(signature), len(args), function_name)
94
+ return self._type_check(args, signature, function_name)
95
+
96
+ def _type_check(self, actual, signature, function_name):
97
+ for i in range(len(signature)):
98
+ allowed_types = signature[i]["types"]
99
+ if allowed_types:
100
+ self._type_check_single(actual[i], allowed_types, function_name)
101
+
102
+ def _type_check_single(self, current, types, function_name):
103
+ # Type checking involves checking the top level type,
104
+ # and in the case of arrays, potentially checking the types
105
+ # of each element.
106
+ allowed_types, allowed_subtypes = self._get_allowed_pytypes(types)
107
+ # We're not using isinstance() on purpose.
108
+ # The type model for jmespath does not map
109
+ # 1-1 with python types (booleans are considered
110
+ # integers in python for example).
111
+ actual_typename = type(current).__name__
112
+ if actual_typename not in allowed_types:
113
+ raise exceptions.JMESPathTypeError(
114
+ function_name,
115
+ current,
116
+ self._convert_to_jmespath_type(actual_typename),
117
+ types,
118
+ )
119
+ # If we're dealing with a list type, we can have
120
+ # additional restrictions on the type of the list
121
+ # elements (for example a function can require a
122
+ # list of numbers or a list of strings).
123
+ # Arrays are the only types that can have subtypes.
124
+ if allowed_subtypes:
125
+ self._subtype_check(current, allowed_subtypes, types, function_name)
126
+
127
+ def _get_allowed_pytypes(self, types):
128
+ allowed_types = []
129
+ allowed_subtypes = []
130
+ for t in types:
131
+ type_ = t.split("-", 1)
132
+ if len(type_) == 2:
133
+ type_, subtype = type_
134
+ allowed_subtypes.append(REVERSE_TYPES_MAP[subtype])
135
+ else:
136
+ type_ = type_[0]
137
+ allowed_types.extend(REVERSE_TYPES_MAP[type_])
138
+ return allowed_types, allowed_subtypes
139
+
140
+ def _subtype_check(self, current, allowed_subtypes, types, function_name):
141
+ if len(allowed_subtypes) == 1:
142
+ # The easy case, we know up front what type
143
+ # we need to validate.
144
+ allowed_subtypes = allowed_subtypes[0]
145
+ for element in current:
146
+ actual_typename = type(element).__name__
147
+ if actual_typename not in allowed_subtypes:
148
+ raise exceptions.JMESPathTypeError(
149
+ function_name, element, actual_typename, types
150
+ )
151
+ elif len(allowed_subtypes) > 1 and current:
152
+ # Dynamic type validation. Based on the first
153
+ # type we see, we validate that the remaining types
154
+ # match.
155
+ first = type(current[0]).__name__
156
+ for subtypes in allowed_subtypes:
157
+ if first in subtypes:
158
+ allowed = subtypes
159
+ break
160
+ else:
161
+ raise exceptions.JMESPathTypeError(
162
+ function_name, current[0], first, types
163
+ )
164
+ for element in current:
165
+ actual_typename = type(element).__name__
166
+ if actual_typename not in allowed:
167
+ raise exceptions.JMESPathTypeError(
168
+ function_name, element, actual_typename, types
169
+ )
170
+
171
+ @signature({"types": ["number"]})
172
+ def _func_abs(self, arg):
173
+ return abs(arg)
174
+
175
+ @signature({"types": ["array-number"]})
176
+ def _func_avg(self, arg):
177
+ if arg:
178
+ return sum(arg) / float(len(arg))
179
+ else:
180
+ return None
181
+
182
+ @signature({"types": [], "variadic": True})
183
+ def _func_not_null(self, *arguments):
184
+ for argument in arguments:
185
+ if argument is not None:
186
+ return argument
187
+
188
+ @signature({"types": []})
189
+ def _func_to_array(self, arg):
190
+ if isinstance(arg, list):
191
+ return arg
192
+ else:
193
+ return [arg]
194
+
195
+ @signature({"types": []})
196
+ def _func_to_string(self, arg):
197
+ if isinstance(arg, STRING_TYPE):
198
+ return arg
199
+ else:
200
+ return json.dumps(arg, separators=(",", ":"), default=str)
201
+
202
+ @signature({"types": []})
203
+ def _func_to_number(self, arg):
204
+ if isinstance(arg, (list, dict, bool)):
205
+ return None
206
+ elif arg is None:
207
+ return None
208
+ elif isinstance(arg, (int, float)):
209
+ return arg
210
+ else:
211
+ try:
212
+ return int(arg)
213
+ except ValueError:
214
+ try:
215
+ return float(arg)
216
+ except ValueError:
217
+ return None
218
+
219
+ @signature({"types": ["array", "string"]}, {"types": []})
220
+ def _func_contains(self, subject, search):
221
+ return search in subject
222
+
223
+ @signature({"types": ["string", "array", "object"]})
224
+ def _func_length(self, arg):
225
+ return len(arg)
226
+
227
+ @signature({"types": ["string"]}, {"types": ["string"]})
228
+ def _func_ends_with(self, search, suffix):
229
+ return search.endswith(suffix)
230
+
231
+ @signature({"types": ["string"]}, {"types": ["string"]})
232
+ def _func_starts_with(self, search, suffix):
233
+ return search.startswith(suffix)
234
+
235
+ @signature({"types": ["array", "string"]})
236
+ def _func_reverse(self, arg):
237
+ if isinstance(arg, STRING_TYPE):
238
+ return arg[::-1]
239
+ else:
240
+ return list(reversed(arg))
241
+
242
+ @signature({"types": ["number"]})
243
+ def _func_ceil(self, arg):
244
+ return math.ceil(arg)
245
+
246
+ @signature({"types": ["number"]})
247
+ def _func_floor(self, arg):
248
+ return math.floor(arg)
249
+
250
+ @signature({"types": ["string"]}, {"types": ["array-string"]})
251
+ def _func_join(self, separator, array):
252
+ return separator.join(array)
253
+
254
+ @signature({"types": ["expref"]}, {"types": ["array"]})
255
+ def _func_map(self, expref, arg):
256
+ result = []
257
+ for element in arg:
258
+ result.append(expref.visit(expref.expression, element))
259
+ return result
260
+
261
+ @signature({"types": ["array-number", "array-string"]})
262
+ def _func_max(self, arg):
263
+ if arg:
264
+ return max(arg)
265
+ else:
266
+ return None
267
+
268
+ @signature({"types": ["object"], "variadic": True})
269
+ def _func_merge(self, *arguments):
270
+ merged = {}
271
+ for arg in arguments:
272
+ merged.update(arg)
273
+ return merged
274
+
275
+ @signature({"types": ["array-number", "array-string"]})
276
+ def _func_min(self, arg):
277
+ if arg:
278
+ return min(arg)
279
+ else:
280
+ return None
281
+
282
+ @signature({"types": ["array-string", "array-number"]})
283
+ def _func_sort(self, arg):
284
+ return list(sorted(arg))
285
+
286
+ @signature({"types": ["array-number"]})
287
+ def _func_sum(self, arg):
288
+ return sum(arg)
289
+
290
+ @signature({"types": ["object"]})
291
+ def _func_keys(self, arg):
292
+ # To be consistent with .values()
293
+ # should we also return the indices of a list?
294
+ return list(arg.keys())
295
+
296
+ @signature({"types": ["object"]})
297
+ def _func_values(self, arg):
298
+ return list(arg.values())
299
+
300
+ @signature({"types": []})
301
+ def _func_type(self, arg):
302
+ if isinstance(arg, STRING_TYPE):
303
+ return "string"
304
+ elif isinstance(arg, bool):
305
+ return "boolean"
306
+ elif isinstance(arg, list):
307
+ return "array"
308
+ elif isinstance(arg, dict):
309
+ return "object"
310
+ elif isinstance(arg, (float, int)):
311
+ return "number"
312
+ elif arg is None:
313
+ return "null"
314
+
315
+ @signature({"types": ["array"]}, {"types": ["expref"]})
316
+ def _func_sort_by(self, array, expref):
317
+ if not array:
318
+ return array
319
+ # sort_by allows for the expref to be either a number of
320
+ # a string, so we have some special logic to handle this.
321
+ # We evaluate the first array element and verify that it's
322
+ # either a string of a number. We then create a key function
323
+ # that validates that type, which requires that remaining array
324
+ # elements resolve to the same type as the first element.
325
+ required_type = self._convert_to_jmespath_type(
326
+ type(expref.visit(expref.expression, array[0])).__name__
327
+ )
328
+ if required_type not in ["number", "string"]:
329
+ raise exceptions.JMESPathTypeError(
330
+ "sort_by", array[0], required_type, ["string", "number"]
331
+ )
332
+ keyfunc = self._create_key_func(expref, [required_type], "sort_by")
333
+ return list(sorted(array, key=keyfunc))
334
+
335
+ @signature({"types": ["array"]}, {"types": ["expref"]})
336
+ def _func_min_by(self, array, expref):
337
+ keyfunc = self._create_key_func(expref, ["number", "string"], "min_by")
338
+ if array:
339
+ return min(array, key=keyfunc)
340
+ else:
341
+ return None
342
+
343
+ @signature({"types": ["array"]}, {"types": ["expref"]})
344
+ def _func_max_by(self, array, expref):
345
+ keyfunc = self._create_key_func(expref, ["number", "string"], "max_by")
346
+ if array:
347
+ return max(array, key=keyfunc)
348
+ else:
349
+ return None
350
+
351
+ def _create_key_func(self, expref, allowed_types, function_name):
352
+ def keyfunc(x):
353
+ result = expref.visit(expref.expression, x)
354
+ actual_typename = type(result).__name__
355
+ jmespath_type = self._convert_to_jmespath_type(actual_typename)
356
+ # allowed_types is in term of jmespath types, not python types.
357
+ if jmespath_type not in allowed_types:
358
+ raise exceptions.JMESPathTypeError(
359
+ function_name, result, jmespath_type, allowed_types
360
+ )
361
+ return result
362
+
363
+ return keyfunc
364
+
365
+ def _convert_to_jmespath_type(self, pyobject):
366
+ return TYPES_MAP.get(pyobject, "unknown")
@@ -0,0 +1,258 @@
1
+ import string
2
+ import warnings
3
+ from json import loads
4
+
5
+ from .exceptions import LexerError, EmptyExpressionError
6
+
7
+
8
+ class Lexer(object):
9
+ START_IDENTIFIER = set(string.ascii_letters + "_")
10
+ VALID_IDENTIFIER = set(string.ascii_letters + string.digits + "_")
11
+ VALID_NUMBER = set(string.digits)
12
+ WHITESPACE = set(" \t\n\r")
13
+ SIMPLE_TOKENS = {
14
+ ".": "dot",
15
+ "*": "star",
16
+ "]": "rbracket",
17
+ ",": "comma",
18
+ ":": "colon",
19
+ "@": "current",
20
+ "(": "lparen",
21
+ ")": "rparen",
22
+ "{": "lbrace",
23
+ "}": "rbrace",
24
+ }
25
+
26
+ def tokenize(self, expression):
27
+ self._initialize_for_expression(expression)
28
+ while self._current is not None:
29
+ if self._current in self.SIMPLE_TOKENS:
30
+ yield {
31
+ "type": self.SIMPLE_TOKENS[self._current],
32
+ "value": self._current,
33
+ "start": self._position,
34
+ "end": self._position + 1,
35
+ }
36
+ self._next()
37
+ elif self._current in self.START_IDENTIFIER:
38
+ start = self._position
39
+ buff = self._current
40
+ while self._next() in self.VALID_IDENTIFIER:
41
+ buff += self._current
42
+ yield {
43
+ "type": "unquoted_identifier",
44
+ "value": buff,
45
+ "start": start,
46
+ "end": start + len(buff),
47
+ }
48
+ elif self._current in self.WHITESPACE:
49
+ self._next()
50
+ elif self._current == "[":
51
+ start = self._position
52
+ next_char = self._next()
53
+ if next_char == "]":
54
+ self._next()
55
+ yield {
56
+ "type": "flatten",
57
+ "value": "[]",
58
+ "start": start,
59
+ "end": start + 2,
60
+ }
61
+ elif next_char == "?":
62
+ self._next()
63
+ yield {
64
+ "type": "filter",
65
+ "value": "[?",
66
+ "start": start,
67
+ "end": start + 2,
68
+ }
69
+ else:
70
+ yield {
71
+ "type": "lbracket",
72
+ "value": "[",
73
+ "start": start,
74
+ "end": start + 1,
75
+ }
76
+ elif self._current == "'":
77
+ yield self._consume_raw_string_literal()
78
+ elif self._current == "|":
79
+ yield self._match_or_else("|", "or", "pipe")
80
+ elif self._current == "&":
81
+ yield self._match_or_else("&", "and", "expref")
82
+ elif self._current == "`":
83
+ yield self._consume_literal()
84
+ elif self._current in self.VALID_NUMBER:
85
+ start = self._position
86
+ buff = self._consume_number()
87
+ yield {
88
+ "type": "number",
89
+ "value": int(buff),
90
+ "start": start,
91
+ "end": start + len(buff),
92
+ }
93
+ elif self._current == "-":
94
+ # Negative number.
95
+ start = self._position
96
+ buff = self._consume_number()
97
+ if len(buff) > 1:
98
+ yield {
99
+ "type": "number",
100
+ "value": int(buff),
101
+ "start": start,
102
+ "end": start + len(buff),
103
+ }
104
+ else:
105
+ raise LexerError(
106
+ lexer_position=start,
107
+ lexer_value=buff,
108
+ message="Unknown token '%s'" % buff,
109
+ )
110
+ elif self._current == '"':
111
+ yield self._consume_quoted_identifier()
112
+ elif self._current == "<":
113
+ yield self._match_or_else("=", "lte", "lt")
114
+ elif self._current == ">":
115
+ yield self._match_or_else("=", "gte", "gt")
116
+ elif self._current == "!":
117
+ yield self._match_or_else("=", "ne", "not")
118
+ elif self._current == "=":
119
+ if self._next() == "=":
120
+ yield {
121
+ "type": "eq",
122
+ "value": "==",
123
+ "start": self._position - 1,
124
+ "end": self._position,
125
+ }
126
+ self._next()
127
+ else:
128
+ if self._current is None:
129
+ # If we're at the EOF, we never advanced
130
+ # the position so we don't need to rewind
131
+ # it back one location.
132
+ position = self._position
133
+ else:
134
+ position = self._position - 1
135
+ raise LexerError(
136
+ lexer_position=position,
137
+ lexer_value="=",
138
+ message="Unknown token '='",
139
+ )
140
+ else:
141
+ raise LexerError(
142
+ lexer_position=self._position,
143
+ lexer_value=self._current,
144
+ message="Unknown token %s" % self._current,
145
+ )
146
+ yield {"type": "eof", "value": "", "start": self._length, "end": self._length}
147
+
148
+ def _consume_number(self):
149
+ start = self._position
150
+ buff = self._current
151
+ while self._next() in self.VALID_NUMBER:
152
+ buff += self._current
153
+ return buff
154
+
155
+ def _initialize_for_expression(self, expression):
156
+ if not expression:
157
+ raise EmptyExpressionError()
158
+ self._position = 0
159
+ self._expression = expression
160
+ self._chars = list(self._expression)
161
+ self._current = self._chars[self._position]
162
+ self._length = len(self._expression)
163
+
164
+ def _next(self):
165
+ if self._position == self._length - 1:
166
+ self._current = None
167
+ else:
168
+ self._position += 1
169
+ self._current = self._chars[self._position]
170
+ return self._current
171
+
172
+ def _consume_until(self, delimiter):
173
+ # Consume until the delimiter is reached,
174
+ # allowing for the delimiter to be escaped with "\".
175
+ start = self._position
176
+ buff = ""
177
+ self._next()
178
+ while self._current != delimiter:
179
+ if self._current == "\\":
180
+ buff += "\\"
181
+ self._next()
182
+ if self._current is None:
183
+ # We're at the EOF.
184
+ raise LexerError(
185
+ lexer_position=start,
186
+ lexer_value=self._expression[start:],
187
+ message="Unclosed %s delimiter" % delimiter,
188
+ )
189
+ buff += self._current
190
+ self._next()
191
+ # Skip the closing delimiter.
192
+ self._next()
193
+ return buff
194
+
195
+ def _consume_literal(self):
196
+ start = self._position
197
+ lexeme = self._consume_until("`").replace("\\`", "`")
198
+ try:
199
+ # Assume it is valid JSON and attempt to parse.
200
+ parsed_json = loads(lexeme)
201
+ except ValueError:
202
+ try:
203
+ # Invalid JSON values should be converted to quoted
204
+ # JSON strings during the JEP-12 deprecation period.
205
+ parsed_json = loads('"%s"' % lexeme.lstrip())
206
+ warnings.warn(
207
+ "deprecated string literal syntax", PendingDeprecationWarning
208
+ )
209
+ except ValueError:
210
+ raise LexerError(
211
+ lexer_position=start,
212
+ lexer_value=self._expression[start:],
213
+ message="Bad token %s" % lexeme,
214
+ )
215
+ token_len = self._position - start
216
+ return {
217
+ "type": "literal",
218
+ "value": parsed_json,
219
+ "start": start,
220
+ "end": token_len,
221
+ }
222
+
223
+ def _consume_quoted_identifier(self):
224
+ start = self._position
225
+ lexeme = '"' + self._consume_until('"') + '"'
226
+ try:
227
+ token_len = self._position - start
228
+ return {
229
+ "type": "quoted_identifier",
230
+ "value": loads(lexeme),
231
+ "start": start,
232
+ "end": token_len,
233
+ }
234
+ except ValueError as e:
235
+ error_message = str(e).split(":")[0]
236
+ raise LexerError(
237
+ lexer_position=start, lexer_value=lexeme, message=error_message
238
+ )
239
+
240
+ def _consume_raw_string_literal(self):
241
+ start = self._position
242
+ lexeme = self._consume_until("'").replace("\\'", "'")
243
+ token_len = self._position - start
244
+ return {"type": "literal", "value": lexeme, "start": start, "end": token_len}
245
+
246
+ def _match_or_else(self, expected, match_type, else_type):
247
+ start = self._position
248
+ current = self._current
249
+ next_char = self._next()
250
+ if next_char == expected:
251
+ self._next()
252
+ return {
253
+ "type": match_type,
254
+ "value": current + next_char,
255
+ "start": start,
256
+ "end": start + 1,
257
+ }
258
+ return {"type": else_type, "value": current, "start": start, "end": start}