Skip to content

Commit eb455f9

Browse files
committed
Move functions to its own module, load them dynamically in expr apply.
1 parent 9781f7b commit eb455f9

File tree

8 files changed

+627
-460
lines changed

8 files changed

+627
-460
lines changed

wdl2cwl/errors.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from typing import Any, Callable, Optional, Type, cast
66

77
import WDL
8-
from WDL._error_util import SourcePosition
98

109
# Inspired by https://github.com/common-workflow-language/schema_salad/blob/661fb0fa8c745ed70253dda93bd12002007f6b33/schema_salad/sourceline.py#L232
1110

@@ -46,7 +45,7 @@ def __exit__(
4645

4746
def makeLead(self) -> str:
4847
"""Caculate the error message prefix."""
49-
pos: SourcePosition = cast(SourcePosition, self.item.pos)
48+
pos: WDL.SourcePosition = cast(WDL.SourcePosition, self.item.pos)
5049
return f"{pos.uri}:{pos.line}:{pos.column}:"
5150

5251
def makeError(self, msg: str) -> Any:
@@ -69,3 +68,7 @@ def makeError(self, msg: str) -> Any:
6968
else:
7069
errs.append(f"{lead} {m}")
7170
return self.raise_type("\n".join(errs))
71+
72+
73+
class ConversionException(Exception):
74+
"""Error during conversion."""

wdl2cwl/expr.py

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
"""WDL Expressions (literal values, arithmetic, comparison, conditional, string interpolation, array, map, and functions)."""
2+
3+
from typing import Union, Any, cast
4+
5+
import WDL
6+
from wdl2cwl.errors import WDLSourceLine, ConversionException
7+
from wdl2cwl.util import get_input, ConversionContext
8+
9+
10+
def get_literal_name(
11+
expr: Union[WDL.Expr.Boolean, WDL.Expr.Int, WDL.Expr.Float, WDL.Expr.Array]
12+
) -> str:
13+
"""Translate WDL Boolean, Int, Float, or Array Expression."""
14+
# if the literal expr is used inside WDL.Expr.Apply
15+
# the literal value is what's needed
16+
parent = expr.parent # type: ignore[union-attr]
17+
if isinstance(parent, (WDL.Expr.Apply, WDL.Expr.IfThenElse)):
18+
return expr.literal.value # type: ignore
19+
raise WDLSourceLine(expr, ConversionException).makeError(
20+
f"The parent expression for {expr} is not WDL.Expr.Apply, but {parent}."
21+
)
22+
23+
24+
def get_expr_ifthenelse(
25+
wdl_ifthenelse: WDL.Expr.IfThenElse, ctx: ConversionContext
26+
) -> str:
27+
"""Translate WDL IfThenElse Expressions."""
28+
condition = get_expr(wdl_ifthenelse.condition, ctx)
29+
if_true = get_expr(wdl_ifthenelse.consequent, ctx)
30+
if_false = get_expr(wdl_ifthenelse.alternative, ctx)
31+
return f"{condition} ? {if_true} : {if_false}"
32+
33+
34+
def translate_wdl_placeholder(
35+
wdl_placeholder: WDL.Expr.Placeholder, ctx: ConversionContext
36+
) -> str:
37+
"""Translate WDL Expr Placeholder to a valid CWL command string."""
38+
cwl_command_str = ""
39+
expr = wdl_placeholder.expr
40+
if expr is None:
41+
raise WDLSourceLine(wdl_placeholder, ConversionException).makeError(
42+
f"Placeholder '{wdl_placeholder}' has no expr."
43+
)
44+
placeholder_expr = get_expr(expr, ctx)
45+
options = wdl_placeholder.options
46+
if options:
47+
if "true" in options:
48+
true_value = options["true"]
49+
false_value = options["false"]
50+
true_str = f'"{true_value}"' if '"' not in true_value else f"'{true_value}'"
51+
false_str = (
52+
f'"{false_value}"' if '"' not in false_value else f"'{false_value}'"
53+
)
54+
is_optional = False
55+
if isinstance(expr, WDL.Expr.Get):
56+
is_optional = expr.type.optional
57+
elif isinstance(expr, WDL.Expr.Apply):
58+
is_optional = expr.arguments[0].type.optional
59+
if not is_optional:
60+
cwl_command_str = f"$({placeholder_expr} ? {true_str} : {false_str})"
61+
else:
62+
cwl_command_str = (
63+
f"$({placeholder_expr} === null ? {false_str} : {true_str})"
64+
)
65+
elif "sep" in options:
66+
seperator = options["sep"]
67+
if isinstance(expr.type, WDL.Type.Array):
68+
item_type: WDL.Expr.Base = expr.type.item_type # type: ignore
69+
if isinstance(item_type, WDL.Type.String):
70+
cwl_command_str = f'$({placeholder_expr}.join("{seperator}"))'
71+
elif isinstance(item_type, WDL.Type.File):
72+
cwl_command_str = (
73+
f"$({placeholder_expr}.map("
74+
+ 'function(el) {return el.path}).join("'
75+
+ seperator
76+
+ '"))'
77+
)
78+
else:
79+
raise WDLSourceLine(wdl_placeholder, ConversionException).makeError(
80+
f"{wdl_placeholder} with separator and item type {item_type} is not yet handled"
81+
)
82+
else:
83+
raise WDLSourceLine(wdl_placeholder, ConversionException).makeError(
84+
f"{wdl_placeholder} with expr of type {expr.type} is not yet handled"
85+
)
86+
else:
87+
raise WDLSourceLine(wdl_placeholder, ConversionException).makeError(
88+
f"Placeholders with options {options} are not yet handled."
89+
)
90+
else:
91+
# for the one case where the $(input.some_input_name) is used within the placeholder_expr
92+
# we return the placeholder_expr without enclosing in another $()
93+
cwl_command_str = (
94+
f"$({placeholder_expr})"
95+
if placeholder_expr[-1] != ")"
96+
else placeholder_expr
97+
)
98+
# sometimes placeholders are used inside WDL.Expr.String.
99+
# with the parent and grand_parent we can confirm that we are in
100+
# the command string (WDL.Expr.String) and task (WDL.Tree.Task) respectively
101+
parent = wdl_placeholder.parent # type: ignore
102+
grand_parent = parent.parent
103+
return (
104+
cwl_command_str
105+
if isinstance(parent, WDL.Expr.String)
106+
and isinstance(grand_parent, WDL.Tree.Task)
107+
else cwl_command_str[2:-1]
108+
)
109+
110+
111+
def get_expr_string(wdl_expr_string: WDL.Expr.String, ctx: ConversionContext) -> str:
112+
"""Translate WDL String Expressions."""
113+
if wdl_expr_string.literal is not None:
114+
return f'"{wdl_expr_string.literal.value}"'
115+
string = ""
116+
parts = wdl_expr_string.parts
117+
for index, part in enumerate(parts[1:-1], start=1):
118+
if isinstance(
119+
part,
120+
(WDL.Expr.Placeholder, WDL.Expr.Apply, WDL.Expr.Get, WDL.Expr.Ident),
121+
):
122+
placeholder = get_expr(part, ctx)
123+
part = (
124+
"" if parts[index - 1] == '"' or parts[index - 1] == "'" else "' + " # type: ignore
125+
)
126+
part += placeholder
127+
part += (
128+
"" if parts[index + 1] == '"' or parts[index + 1] == "'" else " + '" # type: ignore
129+
)
130+
string += part
131+
# condition to determine if the opening and closing quotes should be added to string
132+
# for cases where a placeholder begins or ends a WDL.Expr.String
133+
if type(parts[1]) == str:
134+
string = "'" + string
135+
if type(parts[-2]) == str:
136+
string = string + "'"
137+
return string
138+
139+
140+
def get_expr_name(wdl_expr: WDL.Expr.Ident) -> str:
141+
"""Extract name from WDL expr."""
142+
if not hasattr(wdl_expr, "name"):
143+
raise WDLSourceLine(wdl_expr, ConversionException).makeError(
144+
f"{type(wdl_expr)} has not attribute 'name'"
145+
)
146+
return get_input(wdl_expr.name)
147+
148+
149+
def get_expr(wdl_expr: Any, ctx: ConversionContext) -> str:
150+
"""Translate WDL Expressions."""
151+
if isinstance(wdl_expr, WDL.Expr.Apply):
152+
return get_expr_apply(wdl_expr, ctx)
153+
elif isinstance(wdl_expr, WDL.Expr.Get):
154+
return get_expr_get(wdl_expr, ctx)
155+
elif isinstance(wdl_expr, WDL.Expr.IfThenElse):
156+
return get_expr_ifthenelse(wdl_expr, ctx)
157+
elif isinstance(wdl_expr, WDL.Expr.Placeholder):
158+
return translate_wdl_placeholder(wdl_expr, ctx)
159+
elif isinstance(wdl_expr, WDL.Expr.String):
160+
return get_expr_string(wdl_expr, ctx)
161+
elif isinstance(wdl_expr, WDL.Tree.Decl):
162+
return get_expr(wdl_expr.expr, ctx)
163+
elif isinstance(
164+
wdl_expr,
165+
(
166+
WDL.Expr.Boolean,
167+
WDL.Expr.Int,
168+
WDL.Expr.Float,
169+
WDL.Expr.Array,
170+
),
171+
):
172+
return get_literal_name(wdl_expr)
173+
else:
174+
raise WDLSourceLine(wdl_expr, ConversionException).makeError(
175+
f"The expression '{wdl_expr}' is not handled yet."
176+
)
177+
178+
179+
def get_expr_apply(wdl_apply_expr: WDL.Expr.Apply, ctx: ConversionContext) -> str:
180+
"""Translate WDL Apply Expressions."""
181+
# N.B: This import here avoids circular dependency error when loading the modules.
182+
from wdl2cwl import functions
183+
184+
function_name = wdl_apply_expr.function_name
185+
arguments = wdl_apply_expr.arguments
186+
if not arguments:
187+
raise WDLSourceLine(wdl_apply_expr, ConversionException).makeError(
188+
f"The '{wdl_apply_expr}' expression has no arguments."
189+
)
190+
treat_as_optional = wdl_apply_expr.type.optional
191+
192+
# Call the function if we have it in our wdl2cwl.functions module
193+
if hasattr(functions, function_name):
194+
kwargs = {
195+
"treat_as_optional": treat_as_optional,
196+
"wdl_apply_expr": wdl_apply_expr,
197+
}
198+
return cast(
199+
str,
200+
getattr(functions, function_name)(arguments, ctx, **kwargs),
201+
)
202+
203+
raise WDLSourceLine(wdl_apply_expr, ConversionException).makeError(
204+
f"Function name '{function_name}' not yet handled."
205+
)
206+
207+
208+
def get_expr_get(wdl_get_expr: WDL.Expr.Get, ctx: ConversionContext) -> str:
209+
"""Translate WDL Get Expressions."""
210+
member = wdl_get_expr.member
211+
if not member:
212+
return get_expr_ident(wdl_get_expr.expr, ctx) # type: ignore
213+
struct_name = get_expr(wdl_get_expr.expr, ctx)
214+
member_str = f"{struct_name}.{member}"
215+
return (
216+
member_str
217+
if not isinstance(wdl_get_expr.type, WDL.Type.File)
218+
else f"{member_str}.path"
219+
)
220+
221+
222+
def get_expr_ident(wdl_ident_expr: WDL.Expr.Ident, ctx: ConversionContext) -> str:
223+
"""Translate WDL Ident Expressions."""
224+
id_name = wdl_ident_expr.name
225+
ident_name = get_input(id_name)
226+
referee: Any = wdl_ident_expr.referee
227+
optional = wdl_ident_expr.type.optional
228+
if referee:
229+
with WDLSourceLine(referee, ConversionException):
230+
if isinstance(referee, WDL.Tree.Call):
231+
return id_name
232+
if referee.expr and (
233+
wdl_ident_expr.name in ctx.optional_cwl_null
234+
or wdl_ident_expr.name not in ctx.non_static_values
235+
):
236+
return get_expr(referee.expr, ctx)
237+
if optional and isinstance(wdl_ident_expr.type, WDL.Type.File):
238+
# To prevent null showing on the terminal for inputs of type File
239+
name_with_file_check = get_expr_name_with_is_file_check(wdl_ident_expr)
240+
return f'{ident_name} === null ? "" : {name_with_file_check}'
241+
return (
242+
ident_name
243+
if not isinstance(wdl_ident_expr.type, WDL.Type.File)
244+
else f"{ident_name}.path"
245+
)
246+
247+
248+
def get_expr_name_with_is_file_check(wdl_expr: WDL.Expr.Ident) -> str:
249+
"""Extract name from WDL expr and check if it's a file path."""
250+
if wdl_expr is None or not hasattr(wdl_expr, "name"):
251+
raise WDLSourceLine(wdl_expr, ConversionException).makeError(
252+
f"{type(wdl_expr)} has not attribute 'name'"
253+
)
254+
expr_name = get_input(wdl_expr.name)
255+
is_file = isinstance(wdl_expr.type, WDL.Type.File)
256+
return expr_name if not is_file else f"{expr_name}.path"
257+
258+
259+
__all__ = ["get_expr", "get_expr_string", "translate_wdl_placeholder"]

0 commit comments

Comments
 (0)