bare-script 0.9.0__py3-none-any.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.
bare_script/runtime.py ADDED
@@ -0,0 +1,345 @@
1
+ # Licensed under the MIT License
2
+ # https://github.com/craigahobbs/bare-script-py/blob/main/LICENSE
3
+
4
+ """
5
+ The BareScript runtime
6
+ """
7
+
8
+ import datetime
9
+ import functools
10
+
11
+ from .library import DEFAULT_MAX_STATEMENTS, EXPRESSION_FUNCTIONS, SCRIPT_FUNCTIONS, default_args
12
+ from .model import lint_script
13
+ from .options import url_file_relative
14
+ from .parser import BareScriptParserError, parse_script
15
+ from .value import round_number, value_boolean, value_compare, value_string
16
+
17
+
18
+ def execute_script(script, options=None):
19
+ """
20
+ Execute a BareScript model
21
+
22
+ :param script: The `BareScript model <https://craigahobbs.github.io/bare-script-py/model/#var.vName='BareScript'>`__
23
+ :type script: dict
24
+ :param options: The :class:`script execution options <ExecuteScriptOptions>`
25
+ :type options: dict or None, optional
26
+ :returns: The script result
27
+ :raises BareScriptRuntimeError: A script runtime error occurred
28
+ """
29
+
30
+ if options is None:
31
+ options = {}
32
+
33
+ # Create the global variable object, if necessary
34
+ globals_ = options.get('globals')
35
+ if globals_ is None:
36
+ globals_ = {}
37
+ options['globals'] = globals_
38
+
39
+ # Set the script function globals variables
40
+ for script_func_name, script_func in SCRIPT_FUNCTIONS.items():
41
+ if script_func_name not in globals_:
42
+ globals_[script_func_name] = script_func
43
+
44
+ # Execute the script
45
+ options['statementCount'] = 0
46
+ return _execute_script_helper(script['statements'], options, None)
47
+
48
+
49
+ def _execute_script_helper(statements, options, locals_):
50
+ globals_ = options['globals']
51
+
52
+ # Iterate each script statement
53
+ label_indexes = None
54
+ statements_length = len(statements)
55
+ ix_statement = 0
56
+ while ix_statement < statements_length:
57
+ statement = statements[ix_statement]
58
+ statement_key = next(iter(statement.keys()))
59
+
60
+ # Increment the statement counter
61
+ max_statements = options.get('maxStatements', DEFAULT_MAX_STATEMENTS)
62
+ if max_statements > 0:
63
+ options['statementCount'] = options.get('statementCount', 0) + 1
64
+ if options['statementCount'] > max_statements:
65
+ raise BareScriptRuntimeError(f'Exceeded maximum script statements ({max_statements})')
66
+
67
+ # Expression?
68
+ if statement_key == 'expr':
69
+ expr_value = evaluate_expression(statement['expr']['expr'], options, locals_, False)
70
+ expr_name = statement['expr'].get('name')
71
+ if expr_name is not None:
72
+ if locals_ is not None:
73
+ locals_[expr_name] = expr_value
74
+ else:
75
+ globals_[expr_name] = expr_value
76
+
77
+ # Jump?
78
+ elif statement_key == 'jump':
79
+ # Evaluate the expression (if any)
80
+ if 'expr' not in statement['jump'] or value_boolean(evaluate_expression(statement['jump']['expr'], options, locals_, False)):
81
+ # Find the label
82
+ if label_indexes is not None and statement['jump']['label'] in label_indexes:
83
+ ix_statement = label_indexes[statement['jump']['label']]
84
+ else:
85
+ ix_label = next(
86
+ (ix_stmt for ix_stmt, stmt in enumerate(statements) if stmt.get('label') == statement['jump']['label']),
87
+ -1
88
+ )
89
+ if ix_label == -1:
90
+ raise BareScriptRuntimeError(f"Unknown jump label \"{statement['jump']['label']}\"")
91
+ if label_indexes is None:
92
+ label_indexes = {}
93
+ label_indexes[statement['jump']['label']] = ix_label
94
+ ix_statement = ix_label
95
+
96
+ # Return?
97
+ elif statement_key == 'return':
98
+ if 'expr' in statement['return']:
99
+ return evaluate_expression(statement['return']['expr'], options, locals_, False)
100
+ return None
101
+
102
+ # Function?
103
+ elif statement_key == 'function':
104
+ globals_[statement['function']['name']] = functools.partial(_script_function, statement['function'])
105
+
106
+ # Include?
107
+ elif statement_key == 'include':
108
+ system_prefix = options.get('systemPrefix')
109
+ fetch_fn = options.get('fetchFn')
110
+ log_fn = options.get('logFn')
111
+ url_fn = options.get('urlFn')
112
+ for include in statement['include']['includes']:
113
+ url = include['url']
114
+
115
+ # Fixup system include URL
116
+ if include.get('system') and system_prefix is not None:
117
+ url = url_file_relative(system_prefix, url)
118
+ elif url_fn is not None:
119
+ url = url_fn(url)
120
+
121
+ # Fetch the URL
122
+ try:
123
+ script_text = fetch_fn({'url': url}) if fetch_fn is not None else None
124
+ except: # pylint: disable=bare-except
125
+ script_text = None
126
+ if script_text is None:
127
+ raise BareScriptRuntimeError(f'Include of "{url}" failed')
128
+
129
+ # Parse the include script
130
+ try:
131
+ script = parse_script(script_text)
132
+ except BareScriptParserError as exc:
133
+ raise BareScriptParserError(exc.error, exc.line, exc.column_number, exc.line_number, f'Included from "{url}"')
134
+
135
+ # Run the bare-script linter?
136
+ if log_fn is not None and options.get('debug'):
137
+ warnings = lint_script(script)
138
+ if warnings:
139
+ warning_prefix = f'BareScript: Include "{url}" static analysis...'
140
+ log_fn(f'{warning_prefix} {len(warnings)} warning${"s" if len(warnings) > 1 else ""}:')
141
+ for warning in warnings:
142
+ log_fn(f'BareScript: {warning}')
143
+
144
+ # Execute the include script
145
+ include_options = options.copy()
146
+ include_options['urlFn'] = functools.partial(url_file_relative, url)
147
+ _execute_script_helper(script['statements'], include_options, None)
148
+
149
+ # Increment the statement counter
150
+ ix_statement += 1
151
+
152
+ return None
153
+
154
+
155
+ # Runtime script function implementation
156
+ def _script_function(function, args, options):
157
+ func_locals = {}
158
+ func_args = function.get('args')
159
+ if func_args is not None:
160
+ args_length = len(args)
161
+ func_args_length = len(func_args)
162
+ ix_arg_last = function.get('lastArgArray', None) and (func_args_length - 1)
163
+ for ix_arg in range(func_args_length):
164
+ arg_name = func_args[ix_arg]
165
+ if ix_arg < args_length:
166
+ func_locals[arg_name] = args[ix_arg] if ix_arg != ix_arg_last else args[ix_arg:]
167
+ else:
168
+ func_locals[arg_name] = [] if ix_arg == ix_arg_last else None
169
+ return _execute_script_helper(function['statements'], options, func_locals)
170
+
171
+
172
+ def evaluate_expression(expr, options=None, locals_=None, builtins=True):
173
+ """
174
+ Evaluate an expression model
175
+
176
+ :param script: The `expression model <https://craigahobbs.github.io/bare-script-py/model/#var.vName='Expression'>`__
177
+ :type script: dict
178
+ :param options: The :class:`script execution options <ExecuteScriptOptions>`
179
+ :type options: dict or None, optional
180
+ :param locals_: The local variables
181
+ :type locals_: dict or None, optional
182
+ :param builtins: If true, include the
183
+ `built-in expression functions <https://craigahobbs.github.io/bare-script-py/library/expression.html>`__
184
+ :type builtins: bool, optional
185
+ :returns: The expression result
186
+ :raises BareScriptRuntimeError: A script runtime error occurred
187
+ """
188
+
189
+ expr_key, = expr.keys()
190
+ globals_ = options.get('globals') if options is not None else None
191
+
192
+ # Number
193
+ if expr_key == 'number':
194
+ return expr['number']
195
+
196
+ # String
197
+ if expr_key == 'string':
198
+ return expr['string']
199
+
200
+ # Variable
201
+ if expr_key == 'variable':
202
+ # Keywords
203
+ if expr['variable'] == 'null':
204
+ return None
205
+ if expr['variable'] == 'false':
206
+ return False
207
+ if expr['variable'] == 'true':
208
+ return True
209
+
210
+ # Get the local or global variable value or None if undefined
211
+ if locals_ is not None and expr['variable'] in locals_:
212
+ return locals_[expr['variable']]
213
+ else:
214
+ return globals_.get(expr['variable']) if globals_ is not None else None
215
+
216
+ # Function
217
+ if expr_key == 'function':
218
+ # "if" built-in function?
219
+ func_name = expr['function']['name']
220
+ if func_name == 'if':
221
+ value_expr, true_expr, false_expr = default_args(expr['function'].get('args', ()), (None, None, None))
222
+ value = evaluate_expression(value_expr, options, locals_, builtins) if value_expr else False
223
+ result_expr = true_expr if value_boolean(value) else false_expr
224
+ return evaluate_expression(result_expr, options, locals_, builtins) if result_expr else None
225
+
226
+ # Compute the function arguments
227
+ func_args = [evaluate_expression(arg, options, locals_, builtins) for arg in expr['function']['args']] \
228
+ if 'args' in expr['function'] else None
229
+
230
+ # Global/local function?
231
+ if locals_ is not None and func_name in locals_:
232
+ func_value = locals_[func_name]
233
+ elif globals_ is not None and func_name in globals_:
234
+ func_value = globals_[func_name]
235
+ else:
236
+ func_value = EXPRESSION_FUNCTIONS.get(func_name) if builtins else None
237
+ if func_value is not None:
238
+ # Call the function
239
+ try:
240
+ return func_value(func_args, options)
241
+ except BareScriptRuntimeError:
242
+ raise
243
+ except Exception as error: # pylint: disable=broad-exception-caught
244
+ # Log and return null
245
+ if options is not None and 'logFn' in options and options.get('debug'):
246
+ options['logFn'](f'BareScript: Function "{func_name}" failed with error: {error}')
247
+ return None
248
+
249
+ raise BareScriptRuntimeError(f'Undefined function "{func_name}"')
250
+
251
+ # Binary expression
252
+ if expr_key == 'binary':
253
+ bin_op = expr['binary']['op']
254
+ left_value = evaluate_expression(expr['binary']['left'], options, locals_, builtins)
255
+
256
+ # Short-circuiting binary operators
257
+ if bin_op == '&&':
258
+ if not value_boolean(left_value):
259
+ return left_value
260
+ else:
261
+ return evaluate_expression(expr['binary']['right'], options, locals_, builtins)
262
+ elif bin_op == '||':
263
+ if value_boolean(left_value):
264
+ return left_value
265
+ else:
266
+ return evaluate_expression(expr['binary']['right'], options, locals_, builtins)
267
+
268
+ # Non-short-circuiting binary operators
269
+ right_value = evaluate_expression(expr['binary']['right'], options, locals_, builtins)
270
+ if bin_op == '+':
271
+ if isinstance(left_value, (int, float)) and isinstance(right_value, (int, float)):
272
+ return left_value + right_value
273
+ elif isinstance(left_value, str) and isinstance(right_value, str):
274
+ return left_value + right_value
275
+ elif isinstance(left_value, str):
276
+ return left_value + value_string(right_value)
277
+ elif isinstance(right_value, str):
278
+ return value_string(left_value) + right_value
279
+ elif isinstance(left_value, datetime.datetime) and isinstance(right_value, (int, float)):
280
+ return left_value + datetime.timedelta(milliseconds=right_value)
281
+ elif isinstance(left_value, (int, float)) and isinstance(right_value, datetime.datetime):
282
+ return right_value + datetime.timedelta(milliseconds=left_value)
283
+ elif bin_op == '-':
284
+ if isinstance(left_value, (int, float)) and isinstance(right_value, (int, float)):
285
+ return left_value - right_value
286
+ elif isinstance(left_value, datetime.datetime) and isinstance(right_value, datetime.datetime):
287
+ return round_number((left_value - right_value).total_seconds() * 1000, 0)
288
+ elif bin_op == '*':
289
+ if isinstance(left_value, (int, float)) and isinstance(right_value, (int, float)):
290
+ return left_value * right_value
291
+ elif bin_op == '/':
292
+ if isinstance(left_value, (int, float)) and isinstance(right_value, (int, float)):
293
+ return left_value / right_value
294
+ elif bin_op == '==':
295
+ return value_compare(left_value, right_value) == 0
296
+ elif bin_op == '!=':
297
+ return value_compare(left_value, right_value) != 0
298
+ elif bin_op == '<=':
299
+ return value_compare(left_value, right_value) <= 0
300
+ elif bin_op == '<':
301
+ return value_compare(left_value, right_value) < 0
302
+ elif bin_op == '>=':
303
+ return value_compare(left_value, right_value) >= 0
304
+ elif bin_op == '>':
305
+ return value_compare(left_value, right_value) > 0
306
+ elif bin_op == '%':
307
+ if isinstance(left_value, (int, float)) and isinstance(right_value, (int, float)):
308
+ return left_value % right_value
309
+ else:
310
+ # bin_op == '**':
311
+ if isinstance(left_value, (int, float)) and isinstance(right_value, (int, float)):
312
+ return left_value ** right_value
313
+
314
+ # Invalid operation values
315
+ return None
316
+
317
+ # Unary expression
318
+ if expr_key == 'unary':
319
+ unary_op = expr['unary']['op']
320
+ value = evaluate_expression(expr['unary']['expr'], options, locals_, builtins)
321
+ if unary_op == '!':
322
+ return not value_boolean(value)
323
+ else:
324
+ # unary_op == '-'
325
+ if isinstance(value, (int, float)):
326
+ return -value
327
+
328
+ # Invalid operation value
329
+ return None
330
+
331
+ # Expression group
332
+ # expr_key == 'group'
333
+ return evaluate_expression(expr['group'], options, locals_, builtins)
334
+
335
+
336
+ class BareScriptRuntimeError(Exception):
337
+ """
338
+ A BareScript runtime error
339
+
340
+ :param message: The runtime error message
341
+ :type message: str
342
+ """
343
+
344
+ def __init__(self, message):
345
+ super().__init__(message)
bare_script/value.py ADDED
@@ -0,0 +1,199 @@
1
+ # Licensed under the MIT License
2
+ # https://github.com/craigahobbs/bare-script-py/blob/main/LICENSE
3
+
4
+ """
5
+ BareScript value utilities
6
+ """
7
+
8
+ import datetime
9
+ import re
10
+
11
+ from schema_markdown import JSONEncoder
12
+
13
+
14
+ def round_number(value, digits):
15
+ """
16
+ Round a number
17
+
18
+ :param value: The number to round
19
+ :type value: int or float
20
+ :param digits: The number of digits of precision
21
+ :type digits: int
22
+ :return: The rounded number
23
+ :rtype: float
24
+ """
25
+
26
+ multiplier = 10 ** digits
27
+ return int(value * multiplier + (0.5 if value >= 0 else -0.5)) / multiplier
28
+
29
+
30
+ def value_type(value):
31
+ """
32
+ Get a value's type string
33
+
34
+ :param value: The value
35
+ :return: The type string ('array', 'boolean', 'datetime', 'function', 'null', 'number', 'object', 'regex', 'string')
36
+ :rtype: str
37
+ """
38
+
39
+ if value is None:
40
+ return 'null'
41
+ elif isinstance(value, str):
42
+ return 'string'
43
+ elif isinstance(value, bool):
44
+ return 'boolean'
45
+ elif isinstance(value, (int, float)):
46
+ return 'number'
47
+ elif isinstance(value, datetime.datetime):
48
+ return 'datetime'
49
+ elif isinstance(value, dict):
50
+ return 'object'
51
+ elif isinstance(value, list):
52
+ return 'array'
53
+ elif callable(value):
54
+ return 'function'
55
+ elif isinstance(value, REGEX_TYPE):
56
+ return 'regex'
57
+
58
+ # Unknown value type
59
+ return None
60
+
61
+ REGEX_TYPE = type(re.compile(''))
62
+
63
+
64
+ def value_string(value):
65
+ """
66
+ Get a value's string representation
67
+
68
+ :param value: The value
69
+ :return: The value as a string
70
+ :rtype: str
71
+ """
72
+
73
+ if value is None:
74
+ return 'null'
75
+ elif isinstance(value, str):
76
+ return value
77
+ elif isinstance(value, bool):
78
+ return 'true' if value else 'false'
79
+ elif isinstance(value, int):
80
+ return str(value)
81
+ elif isinstance(value, float):
82
+ return R_NUMBER_CLEANUP.sub('', str(value))
83
+ elif isinstance(value, datetime.datetime):
84
+ return value.isoformat()
85
+ elif isinstance(value, (dict)):
86
+ return value_json(value)
87
+ elif isinstance(value, (list)):
88
+ return value_json(value)
89
+ elif callable(value):
90
+ return '<function>'
91
+ elif isinstance(value, REGEX_TYPE):
92
+ return '<regex>'
93
+
94
+ # Unknown value type
95
+ return '<unknown>'
96
+
97
+ R_NUMBER_CLEANUP = re.compile(r'\.0*$')
98
+
99
+
100
+ def value_json(value, indent=None):
101
+ """
102
+ Get a value's JSON string representation
103
+
104
+ :param value: The value
105
+ :param indent: The JSON indent
106
+ :type indent: int
107
+ :return: The value as a JSON string
108
+ :rtype: str
109
+ """
110
+
111
+ if indent is not None and indent > 0:
112
+ return JSONEncoder(allow_nan=False, indent=indent, separators=(',', ': '), sort_keys=True).encode(value)
113
+ else:
114
+ return JSONEncoder(allow_nan=False, separators=(',', ':'), sort_keys=True).encode(value)
115
+
116
+
117
+ def value_boolean(value):
118
+ """
119
+ Interpret the value as a boolean
120
+
121
+ :param value: The value
122
+ :return: The value as a boolean
123
+ :rtype: bool
124
+ """
125
+
126
+ if value is None:
127
+ return False
128
+ elif isinstance(value, str):
129
+ return value != ''
130
+ elif isinstance(value, bool):
131
+ return value
132
+ elif isinstance(value, (int, float)):
133
+ return value != 0
134
+ elif isinstance(value, datetime.datetime):
135
+ return True
136
+ elif isinstance(value, dict):
137
+ return True
138
+ elif isinstance(value, list):
139
+ return len(value) != 0
140
+ elif callable(value):
141
+ return True
142
+ elif isinstance(value, REGEX_TYPE):
143
+ return True
144
+
145
+ # Unknown value type
146
+ return True
147
+
148
+
149
+ def value_is(value1, value2):
150
+ """
151
+ Test if one value is the same object as another
152
+
153
+ :param value1: The first value
154
+ :param value2: The second value
155
+ :return: True if values are the same object, false otherwise
156
+ :rtype: bool
157
+ """
158
+
159
+ if isinstance(value1, (int, float)) and not isinstance(value1, bool) and \
160
+ isinstance(value2, (int, float)) and not isinstance(value2, bool):
161
+ return value1 == value2
162
+
163
+ return value1 is value2
164
+
165
+
166
+ def value_compare(left, right):
167
+ """
168
+ Compare two values
169
+
170
+ :param left: The left value
171
+ :param right: The right value
172
+ :return: -1 if the left value is less than the right value, 0 if equal, and 1 if greater than
173
+ :rtype: int
174
+ """
175
+
176
+ if left is None:
177
+ return 0 if right is None else -1
178
+ elif right is None:
179
+ return 1
180
+ if isinstance(left, str) and isinstance(right, str):
181
+ return -1 if left < right else (0 if left == right else 1)
182
+ elif isinstance(left, bool) and isinstance(right, bool):
183
+ return -1 if left < right else (0 if left == right else 1)
184
+ elif isinstance(left, (int, float)) and not isinstance(left, bool) and \
185
+ isinstance(right, (int, float)) and not isinstance(right, bool):
186
+ return -1 if left < right else (0 if left == right else 1)
187
+ elif isinstance(left, datetime.datetime) and isinstance(right, datetime.datetime):
188
+ return -1 if left < right else (0 if left == right else 1)
189
+ elif isinstance(left, list) and isinstance(right, list):
190
+ for ix in range(min(len(left), len(right))):
191
+ item_compare = value_compare(left[ix], right[ix])
192
+ if item_compare != 0:
193
+ return item_compare
194
+ return -1 if len(left) < len(right) else (0 if len(left) == len(right) else 1)
195
+
196
+ # Invalid comparison - compare by type name
197
+ type1 = value_type(left) or 'unknown'
198
+ type2 = value_type(right) or 'unknown'
199
+ return -1 if type1 < type2 else (0 if type1 == type2 else 1)
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Craig A. Hobbs
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.