Skip to content

Commit 82252b7

Browse files
committed
Move expr code from main.py into functions.py, utils.py, and expr.py modules
1 parent 0db1555 commit 82252b7

File tree

8 files changed

+675
-528
lines changed

8 files changed

+675
-528
lines changed

wdl2cwl/errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,7 @@ def makeError(self, msg: str) -> Any:
6969
else:
7070
errs.append(f"{lead} {m}")
7171
return self.raise_type("\n".join(errs))
72+
73+
74+
class ConversionException(Exception):
75+
"""Error during conversion."""

wdl2cwl/expr.py

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
"""WDL Expressions (literal values, arithmetic, comparison, conditional, string interpolation, array, map, and functions)."""
2+
3+
import re
4+
from typing import Any, cast, Optional, Union, Tuple
5+
6+
import WDL
7+
from wdl2cwl.errors import WDLSourceLine, ConversionException
8+
from wdl2cwl.util import get_input, nice_quote, ConversionContext
9+
10+
11+
def get_expr_ifthenelse(
12+
wdl_ifthenelse: WDL.Expr.IfThenElse, ctx: ConversionContext
13+
) -> str:
14+
"""Translate WDL IfThenElse Expressions."""
15+
condition = get_expr(wdl_ifthenelse.condition, ctx)
16+
if_true = get_expr(wdl_ifthenelse.consequent, ctx)
17+
if_false = get_expr(wdl_ifthenelse.alternative, ctx)
18+
return f"{condition} ? {if_true} : {if_false}"
19+
20+
21+
def translate_wdl_placeholder(
22+
wdl_placeholder: WDL.Expr.Placeholder, ctx: ConversionContext
23+
) -> str:
24+
"""Translate WDL Expr Placeholder to a valid CWL expression."""
25+
expr = wdl_placeholder.expr
26+
placeholder_expr = get_expr(expr, ctx)
27+
options = wdl_placeholder.options
28+
if options:
29+
if "true" in options:
30+
true_str = nice_quote(options["true"])
31+
false_str = nice_quote(options["false"])
32+
test_str = f"{placeholder_expr} ? {true_str} : {false_str}"
33+
is_optional = False
34+
if isinstance(expr, WDL.Expr.Get):
35+
is_optional = expr.type.optional
36+
elif isinstance(expr, WDL.Expr.Apply):
37+
is_optional = (
38+
expr.arguments[0].type.optional
39+
and expr.function_name != "defined" # optimization
40+
)
41+
if not is_optional:
42+
return test_str
43+
else:
44+
if "default" in options:
45+
return (
46+
f"{placeholder_expr} === null ? "
47+
f"{nice_quote(options['default'])} : {test_str}"
48+
)
49+
return f'{placeholder_expr} === null ? "" : {test_str}'
50+
elif "sep" in options:
51+
separator = options["sep"]
52+
assert isinstance(expr.type, WDL.Type.Array)
53+
item_type = expr.type.item_type
54+
if isinstance(item_type, WDL.Type.File):
55+
pl_holder_str = (
56+
placeholder_expr + ".map(function(el) {return el.path})"
57+
f'.join("{separator}")'
58+
)
59+
else:
60+
pl_holder_str = f'{placeholder_expr}.join("{separator}")'
61+
if "default" in options and (expr.type.optional or item_type.optional):
62+
return (
63+
f"{placeholder_expr} === null ? "
64+
f"{nice_quote(options['default'])} : {pl_holder_str}"
65+
)
66+
return pl_holder_str
67+
# options must contain only "default", no "sep" or "true"/"false"
68+
return (
69+
f"{placeholder_expr} === null ? "
70+
f"{nice_quote(options['default'])} : {placeholder_expr}"
71+
)
72+
return placeholder_expr
73+
74+
75+
def get_expr_string(wdl_expr_string: WDL.Expr.String, ctx: ConversionContext) -> str:
76+
"""Translate WDL String Expressions."""
77+
if wdl_expr_string.literal is not None:
78+
return str(wdl_expr_string.literal)
79+
parts = wdl_expr_string.parts
80+
q = cast(str, parts[0])[0]
81+
string = (
82+
f"{q}{parts[1]}{q}" if isinstance(parts[1], str) else get_expr(parts[1], ctx)
83+
)
84+
if parts[2:-1]:
85+
string += " + " + " + ".join(
86+
f"{q}{part}{q}" if isinstance(part, str) else get_expr(part, ctx)
87+
for part in parts[2:-1]
88+
)
89+
return string
90+
91+
92+
def get_expr_name(wdl_expr: WDL.Expr.Ident) -> str:
93+
"""Extract name from WDL expr."""
94+
return get_input(wdl_expr.name)
95+
96+
97+
def get_expr(wdl_expr: WDL.Expr.Base, ctx: ConversionContext) -> str:
98+
"""Translate WDL Expressions."""
99+
if isinstance(wdl_expr, WDL.Expr.Apply):
100+
return get_expr_apply(wdl_expr, ctx)
101+
elif isinstance(wdl_expr, WDL.Expr.Get):
102+
return get_expr_get(wdl_expr, ctx)
103+
elif isinstance(wdl_expr, WDL.Expr.IfThenElse):
104+
return get_expr_ifthenelse(wdl_expr, ctx)
105+
elif isinstance(wdl_expr, WDL.Expr.Placeholder):
106+
return translate_wdl_placeholder(wdl_expr, ctx)
107+
elif isinstance(wdl_expr, WDL.Expr.String):
108+
return get_expr_string(wdl_expr, ctx)
109+
elif isinstance(wdl_expr, WDL.Expr.Boolean) and wdl_expr.literal:
110+
return str(wdl_expr.literal) # "true" not "True"
111+
elif (
112+
isinstance(
113+
wdl_expr,
114+
(
115+
WDL.Expr.Boolean,
116+
WDL.Expr.Int,
117+
WDL.Expr.Float,
118+
),
119+
)
120+
and wdl_expr.literal
121+
):
122+
return str(wdl_expr.literal.value)
123+
elif isinstance(wdl_expr, WDL.Expr.Array):
124+
return "[ " + ", ".join(get_expr(item, ctx) for item in wdl_expr.items) + " ]"
125+
elif isinstance(wdl_expr, WDL.Expr.Map):
126+
return (
127+
"{ "
128+
+ ", ".join(
129+
f"{get_expr(key, ctx)}: {get_expr(value, ctx)}"
130+
for key, value in wdl_expr.items
131+
)
132+
+ " }"
133+
)
134+
else: # pragma: no cover
135+
raise WDLSourceLine(wdl_expr, ConversionException).makeError(
136+
f"The expression '{wdl_expr}' is not handled yet."
137+
)
138+
139+
140+
_BINARY_OPS = {
141+
"_gt": ">",
142+
"_lor": "||",
143+
"_neq": "!==",
144+
"_lt": "<",
145+
"_mul": "*",
146+
"_eqeq": "===",
147+
"_div": "/",
148+
"_sub": "-",
149+
}
150+
151+
_SINGLE_ARG_FN = { # implemented elsewhere, just return the argument
152+
"read_string",
153+
"read_float",
154+
"glob",
155+
"read_int",
156+
"read_boolean",
157+
"read_tsv",
158+
"read_lines",
159+
}
160+
161+
162+
def get_expr_apply(wdl_apply_expr: WDL.Expr.Apply, ctx: ConversionContext) -> str:
163+
"""Translate WDL Apply Expressions."""
164+
# N.B: This import here avoids circular dependency error when loading the modules.
165+
from wdl2cwl import functions
166+
167+
function_name = wdl_apply_expr.function_name
168+
arguments = wdl_apply_expr.arguments
169+
if not arguments:
170+
raise WDLSourceLine(wdl_apply_expr, ConversionException).makeError(
171+
f"The '{wdl_apply_expr}' expression has no arguments."
172+
)
173+
treat_as_optional = wdl_apply_expr.type.optional
174+
175+
if function_name in _BINARY_OPS:
176+
left_operand, right_operand = arguments
177+
left_operand_expr = get_expr(left_operand, ctx)
178+
right_operand_expr = get_expr(right_operand, ctx)
179+
return f"{left_operand_expr} {_BINARY_OPS[function_name]} {right_operand_expr}"
180+
elif function_name in _SINGLE_ARG_FN:
181+
only_arg = arguments[0]
182+
return get_expr(only_arg, ctx)
183+
elif hasattr(functions, function_name):
184+
# Call the function if we have it in our wdl2cwl.functions module
185+
kwargs = {
186+
"treat_as_optional": treat_as_optional,
187+
"wdl_apply_expr": wdl_apply_expr,
188+
}
189+
return cast(
190+
str,
191+
getattr(functions, function_name)(arguments, ctx, **kwargs),
192+
)
193+
raise WDLSourceLine(wdl_apply_expr, ConversionException).makeError(
194+
f"Function name '{function_name}' not yet handled."
195+
)
196+
197+
198+
def get_expr_get(wdl_get_expr: WDL.Expr.Get, ctx: ConversionContext) -> str:
199+
"""Translate WDL Get Expressions."""
200+
member = wdl_get_expr.member
201+
202+
if not member:
203+
return get_expr_ident(wdl_get_expr.expr, ctx) # type: ignore[arg-type]
204+
struct_name = get_expr(wdl_get_expr.expr, ctx)
205+
member_str = f"{struct_name}.{member}"
206+
return (
207+
member_str
208+
if not isinstance(wdl_get_expr.type, WDL.Type.File)
209+
else f"{member_str}.path"
210+
)
211+
212+
213+
def get_expr_ident(wdl_ident_expr: WDL.Expr.Ident, ctx: ConversionContext) -> str:
214+
"""Translate WDL Ident Expressions."""
215+
id_name = wdl_ident_expr.name
216+
referee = wdl_ident_expr.referee
217+
optional = wdl_ident_expr.type.optional
218+
if referee:
219+
with WDLSourceLine(referee, ConversionException):
220+
if isinstance(referee, WDL.Tree.Call):
221+
return id_name
222+
if referee.expr and (
223+
wdl_ident_expr.name in ctx.optional_cwl_null
224+
or wdl_ident_expr.name not in ctx.non_static_values
225+
):
226+
return get_expr(referee.expr, ctx)
227+
ident_name = get_input(id_name)
228+
if optional and isinstance(wdl_ident_expr.type, WDL.Type.File):
229+
# To prevent null showing on the terminal for inputs of type File
230+
name_with_file_check = get_expr_name_with_is_file_check(wdl_ident_expr)
231+
return f'{ident_name} === null ? "" : {name_with_file_check}'
232+
return (
233+
ident_name
234+
if not isinstance(wdl_ident_expr.type, WDL.Type.File)
235+
else f"{ident_name}.path"
236+
)
237+
238+
239+
def get_expr_name_with_is_file_check(wdl_expr: WDL.Expr.Ident) -> str:
240+
"""Extract name from WDL expr and check if it's a file path."""
241+
expr_name = get_input(wdl_expr.name)
242+
is_file = isinstance(wdl_expr.type, WDL.Type.File)
243+
return expr_name if not is_file else f"{expr_name}.path"
244+
245+
246+
def get_literal_value(expr: WDL.Expr.Base) -> Optional[Any]:
247+
"""Recursively get a literal value."""
248+
literal = expr.literal
249+
if literal:
250+
if hasattr(expr.parent, "type") and isinstance(expr.parent.type, WDL.Type.File): # type: ignore[attr-defined]
251+
return {"class": "File", "path": literal.value}
252+
value = literal.value
253+
if isinstance(expr.type, WDL.Type.Map):
254+
return {key.value: val.value for key, val in value}
255+
if isinstance(value, list):
256+
result = []
257+
for item in value:
258+
if hasattr(expr.parent, "type") and isinstance(expr.parent.type.item_type, WDL.Type.File): # type: ignore[attr-defined]
259+
result.append({"class": "File", "path": item.value})
260+
else:
261+
result.append(item.value)
262+
return result
263+
return value
264+
return None
265+
266+
267+
def get_step_input_expr(
268+
wf_expr: Union[WDL.Expr.Get, WDL.Expr.String], ctx: ConversionContext
269+
) -> Tuple[str, Optional[str]]:
270+
"""
271+
Get name of expression referenced in workflow call inputs.
272+
273+
Returns a tuple of the source plus any needed "valueFrom" expression.
274+
"""
275+
with WDLSourceLine(wf_expr, ConversionException):
276+
if isinstance(wf_expr, WDL.Expr.String):
277+
return get_expr_string(wf_expr, ctx)[1:-1], None
278+
elif isinstance(wf_expr, WDL.Expr.Get):
279+
if isinstance(wf_expr.expr, WDL.Expr.Ident):
280+
member = None
281+
id_name = wf_expr.expr.name
282+
referee = wf_expr.expr.referee
283+
if referee and isinstance(referee, WDL.Tree.Scatter):
284+
scatter_name, value_from = get_step_input_expr(referee.expr, ctx) # type: ignore[arg-type]
285+
ctx.scatter_names.append(scatter_name)
286+
return scatter_name, value_from
287+
return id_name, None
288+
elif isinstance(wf_expr.expr, WDL.Expr.Get):
289+
member = str(wf_expr.member)
290+
ident = cast(WDL.Expr.Ident, wf_expr.expr.expr)
291+
id_name = ident.name
292+
elif isinstance(wf_expr, WDL.Expr.Apply):
293+
expr_str = get_expr(wf_expr, ctx)
294+
if expr_str.count("inputs") == 1:
295+
id_name = re.match(r"inputs\.*?[ \.](.*?)[. ]", expr_str).groups()[0]
296+
value_from = "self" + expr_str.partition(f"inputs.{id_name}")[2]
297+
return id_name, value_from
298+
else:
299+
return get_literal_value(wf_expr), None
300+
return id_name, f"self.{member}" if member else None
301+
302+
303+
__all__ = [
304+
"get_expr",
305+
"get_expr_string",
306+
"get_step_input_expr",
307+
"translate_wdl_placeholder",
308+
]

0 commit comments

Comments
 (0)