comparison env/lib/python3.7/site-packages/cwltool/expression.py @ 0:26e78fe6e8c4 draft

"planemo upload commit c699937486c35866861690329de38ec1a5d9f783"
author shellac
date Sat, 02 May 2020 07:14:21 -0400
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:26e78fe6e8c4
1 """Parse CWL expressions."""
2 from __future__ import absolute_import
3
4 import copy
5 import re
6 from typing import (Any, Dict, List, Mapping, MutableMapping, MutableSequence, Optional,
7 Union)
8
9 import six
10 from six import string_types, u
11 from future.utils import raise_from
12 from typing_extensions import Text # pylint: disable=unused-import
13 # move to a regular typing import when Python 3.3-3.6 is no longer supported
14
15 from .sandboxjs import default_timeout, execjs, JavascriptException
16 from .errors import WorkflowException
17 from .utils import bytes2str_in_dicts, docker_windows_path_adjust, json_dumps
18
19
20 def jshead(engine_config, rootvars):
21 # type: (List[Text], Dict[Text, Any]) -> Text
22
23 # make sure all the byte strings are converted
24 # to str in `rootvars` dict.
25
26 return u"\n".join(
27 engine_config + [u"var {} = {};".format(k, json_dumps(v, indent=4))
28 for k, v in rootvars.items()])
29
30
31 # decode all raw strings to unicode
32 seg_symbol = r"""\w+"""
33 seg_single = r"""\['([^']|\\')+'\]"""
34 seg_double = r"""\["([^"]|\\")+"\]"""
35 seg_index = r"""\[[0-9]+\]"""
36 segments = r"(\.%s|%s|%s|%s)" % (seg_symbol, seg_single, seg_double, seg_index)
37 segment_re = re.compile(u(segments), flags=re.UNICODE)
38 param_str = r"\((%s)%s*\)$" % (seg_symbol, segments)
39 param_re = re.compile(u(param_str), flags=re.UNICODE)
40
41 JSON = Union[Dict[Any, Any], List[Any], Text, int, float, bool, None]
42
43
44 class SubstitutionError(Exception):
45 pass
46
47
48 def scanner(scan): # type: (Text) -> List[int]
49 DEFAULT = 0
50 DOLLAR = 1
51 PAREN = 2
52 BRACE = 3
53 SINGLE_QUOTE = 4
54 DOUBLE_QUOTE = 5
55 BACKSLASH = 6
56
57 i = 0
58 stack = [DEFAULT]
59 start = 0
60 while i < len(scan):
61 state = stack[-1]
62 c = scan[i]
63
64 if state == DEFAULT:
65 if c == '$':
66 stack.append(DOLLAR)
67 elif c == '\\':
68 stack.append(BACKSLASH)
69 elif state == BACKSLASH:
70 stack.pop()
71 if stack[-1] == DEFAULT:
72 return [i - 1, i + 1]
73 elif state == DOLLAR:
74 if c == '(':
75 start = i - 1
76 stack.append(PAREN)
77 elif c == '{':
78 start = i - 1
79 stack.append(BRACE)
80 else:
81 stack.pop()
82 elif state == PAREN:
83 if c == '(':
84 stack.append(PAREN)
85 elif c == ')':
86 stack.pop()
87 if stack[-1] == DOLLAR:
88 return [start, i + 1]
89 elif c == "'":
90 stack.append(SINGLE_QUOTE)
91 elif c == '"':
92 stack.append(DOUBLE_QUOTE)
93 elif state == BRACE:
94 if c == '{':
95 stack.append(BRACE)
96 elif c == '}':
97 stack.pop()
98 if stack[-1] == DOLLAR:
99 return [start, i + 1]
100 elif c == "'":
101 stack.append(SINGLE_QUOTE)
102 elif c == '"':
103 stack.append(DOUBLE_QUOTE)
104 elif state == SINGLE_QUOTE:
105 if c == "'":
106 stack.pop()
107 elif c == '\\':
108 stack.append(BACKSLASH)
109 elif state == DOUBLE_QUOTE:
110 if c == '"':
111 stack.pop()
112 elif c == '\\':
113 stack.append(BACKSLASH)
114 i += 1
115
116 if len(stack) > 1:
117 raise SubstitutionError(
118 "Substitution error, unfinished block starting at position {}: {}".format(start, scan[start:]))
119 else:
120 return []
121
122
123 def next_seg(parsed_string, remaining_string, current_value): # type: (Text, Text, JSON) -> JSON
124 if remaining_string:
125 m = segment_re.match(remaining_string)
126 if not m:
127 return current_value
128 next_segment_str = m.group(0)
129
130 key = None # type: Optional[Union[Text, int]]
131 if next_segment_str[0] == '.':
132 key = next_segment_str[1:]
133 elif next_segment_str[1] in ("'", '"'):
134 key = next_segment_str[2:-2].replace("\\'", "'").replace('\\"', '"')
135
136 if key is not None:
137 if isinstance(current_value, MutableSequence) and key == "length" and not remaining_string[m.end(0):]:
138 return len(current_value)
139 if not isinstance(current_value, MutableMapping):
140 raise WorkflowException("%s is a %s, cannot index on string '%s'" % (parsed_string, type(current_value).__name__, key))
141 if key not in current_value:
142 raise WorkflowException("%s does not contain key '%s'" % (parsed_string, key))
143 else:
144 try:
145 key = int(next_segment_str[1:-1])
146 except ValueError as v:
147 raise_from(WorkflowException(u(str(v))), v)
148 if not isinstance(current_value, MutableSequence):
149 raise WorkflowException("%s is a %s, cannot index on int '%s'" % (parsed_string, type(current_value).__name__, key))
150 if key >= len(current_value):
151 raise WorkflowException("%s list index %i out of range" % (parsed_string, key))
152
153 if isinstance(current_value, Mapping):
154 try:
155 return next_seg(parsed_string + remaining_string, remaining_string[m.end(0):], current_value[key])
156 except KeyError:
157 raise WorkflowException("%s doesn't have property %s" % (parsed_string, key))
158 elif isinstance(current_value, list) and isinstance(key, int):
159 try:
160 return next_seg(parsed_string + remaining_string, remaining_string[m.end(0):], current_value[key])
161 except KeyError:
162 raise WorkflowException("%s doesn't have property %s" % (parsed_string, key))
163 else:
164 raise WorkflowException("%s doesn't have property %s" % (parsed_string, key))
165 else:
166 return current_value
167
168
169 def evaluator(ex, # type: Text
170 jslib, # type: Text
171 obj, # type: Dict[Text, Any]
172 timeout, # type: float
173 fullJS=False, # type: bool
174 force_docker_pull=False, # type: bool
175 debug=False, # type: bool
176 js_console=False # type: bool
177 ):
178 # type: (...) -> JSON
179 match = param_re.match(ex)
180
181 expression_parse_exception = None
182 expression_parse_succeeded = False
183
184 if match is not None:
185 first_symbol = match.group(1)
186 first_symbol_end = match.end(1)
187
188 if first_symbol_end + 1 == len(ex) and first_symbol == "null":
189 return None
190 try:
191 if obj.get(first_symbol) is None:
192 raise WorkflowException("%s is not defined" % first_symbol)
193
194 return next_seg(first_symbol, ex[first_symbol_end:-1], obj[first_symbol])
195 except WorkflowException as werr:
196 expression_parse_exception = werr
197 else:
198 expression_parse_succeeded = True
199
200 if fullJS and not expression_parse_succeeded:
201 return execjs(
202 ex, jslib, timeout, force_docker_pull=force_docker_pull,
203 debug=debug, js_console=js_console)
204 else:
205 if expression_parse_exception is not None:
206 raise JavascriptException(
207 "Syntax error in parameter reference '%s': %s. This could be "
208 "due to using Javascript code without specifying "
209 "InlineJavascriptRequirement." % \
210 (ex[1:-1], expression_parse_exception))
211 else:
212 raise JavascriptException(
213 "Syntax error in parameter reference '%s'. This could be due "
214 "to using Javascript code without specifying "
215 "InlineJavascriptRequirement." % ex)
216
217
218 def interpolate(scan, # type: Text
219 rootvars, # type: Dict[Text, Any]
220 timeout=default_timeout, # type: float
221 fullJS=False, # type: bool
222 jslib="", # type: Text
223 force_docker_pull=False, # type: bool
224 debug=False, # type: bool
225 js_console=False, # type: bool
226 strip_whitespace=True # type: bool
227 ): # type: (...) -> JSON
228 if strip_whitespace:
229 scan = scan.strip()
230 parts = []
231 w = scanner(scan)
232 while w:
233 parts.append(scan[0:w[0]])
234
235 if scan[w[0]] == '$':
236 e = evaluator(scan[w[0] + 1:w[1]], jslib, rootvars, timeout,
237 fullJS=fullJS, force_docker_pull=force_docker_pull,
238 debug=debug, js_console=js_console)
239 if w[0] == 0 and w[1] == len(scan) and len(parts) <= 1:
240 return e
241 leaf = json_dumps(e, sort_keys=True)
242 if leaf[0] == '"':
243 leaf = leaf[1:-1]
244 parts.append(leaf)
245 elif scan[w[0]] == '\\':
246 e = scan[w[1] - 1]
247 parts.append(e)
248
249 scan = scan[w[1]:]
250 w = scanner(scan)
251 parts.append(scan)
252 return ''.join(parts)
253
254 def needs_parsing(snippet): # type: (Any) -> bool
255 return isinstance(snippet, string_types) \
256 and ("$(" in snippet or "${" in snippet)
257
258 def do_eval(ex, # type: Union[Text, Dict[Text, Text]]
259 jobinput, # type: Dict[Text, JSON]
260 requirements, # type: List[Dict[Text, Any]]
261 outdir, # type: Optional[Text]
262 tmpdir, # type: Optional[Text]
263 resources, # type: Dict[str, int]
264 context=None, # type: Any
265 timeout=default_timeout, # type: float
266 force_docker_pull=False, # type: bool
267 debug=False, # type: bool
268 js_console=False, # type: bool
269 strip_whitespace=True # type: bool
270 ): # type: (...) -> Any
271
272 runtime = copy.deepcopy(resources) # type: Dict[str, Any]
273 runtime["tmpdir"] = docker_windows_path_adjust(tmpdir) if tmpdir else None
274 runtime["outdir"] = docker_windows_path_adjust(outdir) if outdir else None
275
276 rootvars = {
277 u"inputs": jobinput,
278 u"self": context,
279 u"runtime": runtime}
280
281 # TODO: need to make sure the `rootvars dict`
282 # contains no bytes type in the first place.
283 if six.PY3:
284 rootvars = bytes2str_in_dicts(rootvars) # type: ignore
285
286 if isinstance(ex, string_types) and needs_parsing(ex):
287 fullJS = False
288 jslib = u""
289 for r in reversed(requirements):
290 if r["class"] == "InlineJavascriptRequirement":
291 fullJS = True
292 jslib = jshead(r.get("expressionLib", []), rootvars)
293 break
294
295 try:
296 return interpolate(ex,
297 rootvars,
298 timeout=timeout,
299 fullJS=fullJS,
300 jslib=jslib,
301 force_docker_pull=force_docker_pull,
302 debug=debug,
303 js_console=js_console,
304 strip_whitespace=strip_whitespace)
305
306 except Exception as e:
307 raise_from(WorkflowException("Expression evaluation error:\n%s" % Text(e)), e)
308 else:
309 return ex