|
| 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