bare-script 3.8.2__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.
@@ -0,0 +1,37 @@
1
+ # Licensed under the MIT License
2
+ # https://github.com/craigahobbs/bare-script-py/blob/main/LICENSE
3
+
4
+ """
5
+ bare-script package
6
+ """
7
+
8
+ from .data import \
9
+ add_calculated_field, \
10
+ aggregate_data, \
11
+ filter_data, \
12
+ join_data, \
13
+ sort_data, \
14
+ top_data, \
15
+ validate_data
16
+
17
+ from .model import \
18
+ lint_script, \
19
+ validate_expression, \
20
+ validate_script
21
+
22
+ from .options import \
23
+ fetch_http, \
24
+ fetch_read_only, \
25
+ fetch_read_write, \
26
+ log_stdout, \
27
+ url_file_relative
28
+
29
+ from .parser import \
30
+ BareScriptParserError, \
31
+ parse_expression, \
32
+ parse_script
33
+
34
+ from .runtime import \
35
+ BareScriptRuntimeError, \
36
+ evaluate_expression, \
37
+ execute_script
@@ -0,0 +1,12 @@
1
+ # Licensed under the MIT License
2
+ # https://github.com/craigahobbs/bare-script-py/blob/main/LICENSE
3
+
4
+ """
5
+ bare-script top-level script environment
6
+ """
7
+
8
+ from .bare import main
9
+
10
+
11
+ if __name__ == '__main__': # pragma: no cover
12
+ main()
bare_script/bare.py ADDED
@@ -0,0 +1,165 @@
1
+ # Licensed under the MIT License
2
+ # https://github.com/craigahobbs/bare-script-py/blob/main/LICENSE
3
+
4
+ """
5
+ bare-script command-line interface (CLI)
6
+ """
7
+
8
+ import argparse
9
+ from functools import partial
10
+ import importlib.resources
11
+ import os
12
+ import sys
13
+ import time
14
+
15
+ from .library import SYSTEM_GLOBAL_INCLUDES_NAME
16
+ from .model import lint_script
17
+ from .options import fetch_read_write, log_stdout, url_file_relative
18
+ from .parser import parse_expression, parse_script
19
+ from .runtime import evaluate_expression, execute_script
20
+ from .value import value_boolean
21
+
22
+
23
+ def main(argv=None):
24
+ """
25
+ BareScript command-line interface (CLI) main entry point
26
+ """
27
+
28
+ # Command line arguments
29
+ parser = argparse.ArgumentParser(prog='bare', description='The BareScript command-line interface')
30
+ parser.add_argument('file', nargs='*', action=_FileScriptAction, help='files to process')
31
+ parser.add_argument('-c', '--code', action=_InlineScriptAction, help='execute the BareScript code')
32
+ parser.add_argument('-d', '--debug', action='store_true', help='enable debug mode')
33
+ parser.add_argument('-m', '--markdown-up', action='store_true', help='run with MarkdownUp stubs')
34
+ parser.add_argument('-s', '--static', dest='static', action='store_const', const='s', help='perform static analysis')
35
+ parser.add_argument('-x', '--staticx', dest='static', action='store_const', const='x', help='perform static analysis with execution')
36
+ parser.add_argument('-v', '--var', nargs=2, action='append', metavar=('VAR', 'EXPR'), default = [],
37
+ help='set a global variable to an expression value')
38
+ args = parser.parse_args(args=argv)
39
+ if not args.scripts:
40
+ parser.print_help()
41
+ parser.exit()
42
+
43
+ status_code = 0
44
+ inline_count = 0
45
+ try:
46
+ # Evaluate the global variable expression arguments
47
+ globals_ = {}
48
+ for var_name, var_expr in args.var:
49
+ globals_[var_name] = evaluate_expression(parse_expression(var_expr))
50
+
51
+ # Get the scripts to run
52
+ scripts = args.scripts
53
+ ix_user_script = 0
54
+ if args.markdown_up:
55
+ scripts = [('code', 'include <markdownUp.bare>'), *scripts]
56
+ ix_user_script = 1
57
+
58
+ # Add unittest.bare argument globals
59
+ globals_['vUnittestReport'] = True
60
+ if args.static:
61
+ globals_['vUnittestDisabled'] = True
62
+
63
+ # Parse and execute all source files in order
64
+ for ix_script, [script_type, script_value] in enumerate(scripts):
65
+ # Get the script source
66
+ if script_type == 'file':
67
+ script_name = script_value
68
+ script_source = None
69
+ try:
70
+ script_source = fetch_read_write({'url': script_value})
71
+ except:
72
+ pass
73
+ if script_source is None:
74
+ raise ValueError(f'Failed to load "{script_value}"')
75
+ else:
76
+ inline_count += 1
77
+ inline_display = inline_count - ix_user_script
78
+ script_name = f'<string{inline_display if inline_display > 1 else ""}>'
79
+ script_source = script_value
80
+
81
+ # Parse the script source
82
+ script = parse_script(script_source, 1, script_name)
83
+
84
+ # Execute?
85
+ static_globals = None
86
+ if args.static != 's':
87
+ # Set the globals to use for static analysis (below)
88
+ if not args.static or ix_script < ix_user_script:
89
+ static_globals = globals_
90
+ else:
91
+ # Copy global to keep each script as isolated as possible
92
+ static_globals = dict(globals_)
93
+ global_includes = static_globals.get(SYSTEM_GLOBAL_INCLUDES_NAME)
94
+ if global_includes is not None and isinstance(global_includes, dict):
95
+ static_globals[SYSTEM_GLOBAL_INCLUDES_NAME] = dict(global_includes)
96
+
97
+ # Execute the script
98
+ time_begin = time.time()
99
+ result = execute_script(script, {
100
+ 'debug': args.debug or False,
101
+ 'fetchFn': _fetch_include,
102
+ 'globals': static_globals,
103
+ 'logFn': log_stdout,
104
+ 'systemPrefix': _FETCH_INCLUDE_PREFIX,
105
+ 'urlFn': partial(url_file_relative, script_value) if script_type == 'file' else None
106
+ })
107
+ if isinstance(result, (int, float)) and int(result) == result and 0 <= result <= 255:
108
+ status_code = int(result) or status_code
109
+ else:
110
+ status_code = (1 if value_boolean(result) else 0) or status_code
111
+
112
+ # Log script execution end with timing
113
+ if args.debug and ix_script >= ix_user_script:
114
+ time_end = time.time()
115
+ print(f'BareScript executed in {1000 * (time_end - time_begin):.1f} milliseconds')
116
+
117
+ # Run the bare-script linter?
118
+ if args.static and ix_script >= ix_user_script:
119
+ warnings = lint_script(script, static_globals)
120
+ if not warnings:
121
+ print(f'BareScript static analysis "{script_name}" ... OK')
122
+ else:
123
+ print(f'BareScript static analysis "{script_name}" ... {len(warnings)} warning{"s" if len(warnings) > 1 else ""}:')
124
+ for warning in warnings:
125
+ print(warning)
126
+ status_code = 1
127
+
128
+ # Stop on error status code
129
+ if status_code != 0 and not args.static:
130
+ break
131
+
132
+ except Exception as exc:
133
+ print(str(exc).strip())
134
+ status_code = 1
135
+
136
+ # Return the status code
137
+ sys.exit(status_code)
138
+
139
+
140
+ def _fetch_include(request):
141
+ # Is this a bare system include?
142
+ url = request['url']
143
+ if url.startswith(_FETCH_INCLUDE_PREFIX):
144
+ path = url[len(_FETCH_INCLUDE_PREFIX):]
145
+ with importlib.resources.files('bare_script.include').joinpath(path).open('rb') as cm_inc:
146
+ return cm_inc.read().decode(encoding='utf-8')
147
+
148
+ return fetch_read_write(request)
149
+
150
+
151
+ _FETCH_INCLUDE_PREFIX = f':bare-include:{os.sep}'
152
+
153
+
154
+ class _InlineScriptAction(argparse.Action):
155
+ def __call__(self, parser, namespace, values, option_string=None):
156
+ if 'scripts' not in namespace:
157
+ setattr(namespace, 'scripts', [])
158
+ namespace.scripts.append(('code', values))
159
+
160
+
161
+ class _FileScriptAction(argparse.Action):
162
+ def __call__(self, parser, namespace, values, option_string=None):
163
+ if 'scripts' not in namespace:
164
+ setattr(namespace, 'scripts', [])
165
+ namespace.scripts.extend(('file', value) for value in values)
bare_script/baredoc.py ADDED
@@ -0,0 +1,177 @@
1
+ # Licensed under the MIT License
2
+ # https://github.com/craigahobbs/bare-script-py/blob/main/LICENSE
3
+
4
+ """
5
+ bare-script documentation tool
6
+ """
7
+
8
+ import argparse
9
+ import json
10
+ import re
11
+ import sys
12
+
13
+
14
+ def main(argv=None):
15
+ """
16
+ BareScript documentation tool main entry point
17
+ """
18
+
19
+ # Command line arguments
20
+ parser = argparse.ArgumentParser(prog='baredoc', description='The BareScript documentation tool')
21
+ parser.add_argument('files', metavar='file', nargs='+', help='files to process')
22
+ parser.add_argument('-o', dest='output', metavar='file', default='-', help='write output to file (default is "-")')
23
+ args = parser.parse_args(args=argv)
24
+
25
+ # Parse each source file line-by-line
26
+ errors = []
27
+ funcs = {}
28
+ func = None
29
+ for file_ in args.files:
30
+ # Read the source file
31
+ source = None
32
+ try:
33
+ with open(file_, 'r', encoding='utf-8') as fh:
34
+ source = fh.read()
35
+ except:
36
+ pass
37
+ if source is None:
38
+ errors.append(f'Failed to load "{file_}"')
39
+ continue
40
+
41
+ # Split the source lines and process documentation comments
42
+ lines = _R_SPLIT.split(source)
43
+ for ix_line, line in enumerate(lines):
44
+ # function/group/doc/return documentation keywords?
45
+ match_key = _R_KEY.match(line)
46
+ if match_key is not None:
47
+ key = match_key['key']
48
+ text = match_key['text']
49
+ text_trim = text.strip()
50
+
51
+ # Keyword used outside of function?
52
+ if key != 'function' and func is None:
53
+ errors.append(f'{file_}:{ix_line + 1}: {key} keyword outside function')
54
+ continue
55
+
56
+ # Process the keyword
57
+ if key == 'group':
58
+ if text_trim == '':
59
+ errors.append(f'{file_}:{ix_line + 1}: Invalid function group name "{text_trim}"')
60
+ continue
61
+ if 'group' in func:
62
+ errors.append(f'{file_}:{ix_line + 1}: Function "{func["name"]}" group redefinition')
63
+ continue
64
+
65
+ # Set the function group
66
+ func['group'] = text_trim
67
+
68
+ elif key in ('doc', 'return'):
69
+ # Add the documentation line - don't add leading blank lines
70
+ func_doc = func.get(key)
71
+ if func_doc is not None or text_trim != '':
72
+ if func_doc is None:
73
+ func_doc = []
74
+ func[key] = func_doc
75
+ func_doc.append(text)
76
+
77
+ else:
78
+ # key == 'function'
79
+ if text_trim == '':
80
+ errors.append(f'{file_}:{ix_line + 1}: Invalid function name "{text_trim}"')
81
+ continue
82
+ if text_trim in funcs:
83
+ errors.append(f'{file_}:{ix_line + 1}: Function "{text_trim}" redefinition')
84
+ continue
85
+
86
+ # Add the function
87
+ func = {'name': text_trim}
88
+ funcs[text_trim] = func
89
+
90
+ continue
91
+
92
+ # arg keyword?
93
+ match_arg = _R_ARG.match(line)
94
+ if match_arg is not None:
95
+ name = match_arg['name']
96
+ text = match_arg['text']
97
+ text_trim = text.strip()
98
+
99
+ # Keyword used outside of function?
100
+ if func is None:
101
+ errors.append(f'{file_}:{ix_line + 1}: Function argument "{name}" outside function')
102
+ continue
103
+
104
+ # Get the function argument model, if it exists
105
+ func_args = func.get('args')
106
+ func_arg = None
107
+ if func_args is not None:
108
+ func_arg = next((find_arg for find_arg in func_args if find_arg['name'] == name), None)
109
+
110
+ # Ignore leading argument documentation blank lines
111
+ if func_arg is None and text_trim == '':
112
+ continue
113
+
114
+ # Add the fuction model arguments member, if necessary
115
+ if func_args is None:
116
+ func_args = []
117
+ func['args'] = func_args
118
+
119
+ # Add the function argument model, if necessary
120
+ if func_arg is None:
121
+ func_arg = {'name': name, 'doc': []}
122
+ func_args.append(func_arg)
123
+
124
+ # Add the function argument documentation line
125
+ func_arg['doc'].append(text)
126
+ continue
127
+
128
+ # Unknown documentation comment?
129
+ match_unknown = _R_UNKNOWN.match(line)
130
+ if match_unknown is not None:
131
+ unknown = match_unknown.group('unknown')
132
+ errors.append(f'{file_}:{ix_line + 1}: Invalid documentation comment "{unknown}"')
133
+ continue
134
+
135
+ # Create the library documentation model
136
+ library = {'functions': sorted(funcs.values(), key=lambda func: func['name'])}
137
+
138
+ # Validate the library documentation model
139
+ if len(library['functions']) == 0:
140
+ errors.append('error: No library functions')
141
+ for func in library['functions']:
142
+ func_name = func['name']
143
+ if 'group' not in func:
144
+ errors.append(f'error: Function "{func_name}" missing group')
145
+ if 'doc' not in func:
146
+ errors.append(f'error: Function "{func_name}" missing documentation')
147
+
148
+ # Errors?
149
+ if len(errors) != 0:
150
+ print('\n'.join(errors))
151
+ sys.exit(1)
152
+
153
+ # JSON-serialize the library documentation model
154
+ library_json = json.dumps(library, separators=(',', ':'), sort_keys=True)
155
+
156
+ # Output to stdout?
157
+ if args.output == '-':
158
+ print(library_json)
159
+ else:
160
+ # Output to file
161
+ success = False
162
+ try:
163
+ with open(args.output, 'w', encoding='utf-8') as fh:
164
+ fh.write(library_json)
165
+ success = True
166
+ except:
167
+ pass
168
+ if not success:
169
+ print(f'error: Failed to write "{args.output}"')
170
+ sys.exit(1)
171
+
172
+
173
+ # Library documentation regular expressions
174
+ _R_KEY = re.compile(r'^\s*(?:\/\/|#)\s*\$(?P<key>function|group|doc|return):\s?(?P<text>.*)$')
175
+ _R_ARG = re.compile(r'^\s*(?:\/\/|#)\s*\$arg\s+(?P<name>[A-Za-z_][A-Za-z0-9_]*(?:\.\.\.)?):\s?(?P<text>.*)$')
176
+ _R_UNKNOWN = re.compile(r'^\s*(?:\/\/|#)\s*\$(?P<unknown>[^:]+):')
177
+ _R_SPLIT = re.compile(r'\r?\n')