|
| 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 | + return cast( |
| 195 | + str, |
| 196 | + getattr(functions, function_name)( |
| 197 | + arguments, |
| 198 | + ctx, |
| 199 | + { |
| 200 | + "treat_as_optional": treat_as_optional, |
| 201 | + "wdl_apply_expr": wdl_apply_expr, |
| 202 | + }, |
| 203 | + ), |
| 204 | + ) |
| 205 | + |
| 206 | + raise WDLSourceLine(wdl_apply_expr, ConversionException).makeError( |
| 207 | + f"Function name '{function_name}' not yet handled." |
| 208 | + ) |
| 209 | + |
| 210 | + |
| 211 | +def get_expr_get(wdl_get_expr: WDL.Expr.Get, ctx: ConversionContext) -> str: |
| 212 | + """Translate WDL Get Expressions.""" |
| 213 | + member = wdl_get_expr.member |
| 214 | + if not member: |
| 215 | + return get_expr_ident(wdl_get_expr.expr, ctx) # type: ignore |
| 216 | + struct_name = get_expr(wdl_get_expr.expr, ctx) |
| 217 | + member_str = f"{struct_name}.{member}" |
| 218 | + return ( |
| 219 | + member_str |
| 220 | + if not isinstance(wdl_get_expr.type, WDL.Type.File) |
| 221 | + else f"{member_str}.path" |
| 222 | + ) |
| 223 | + |
| 224 | + |
| 225 | +def get_expr_ident(wdl_ident_expr: WDL.Expr.Ident, ctx: ConversionContext) -> str: |
| 226 | + """Translate WDL Ident Expressions.""" |
| 227 | + id_name = wdl_ident_expr.name |
| 228 | + ident_name = get_input(id_name) |
| 229 | + referee: Any = wdl_ident_expr.referee |
| 230 | + optional = wdl_ident_expr.type.optional |
| 231 | + if referee: |
| 232 | + with WDLSourceLine(referee, ConversionException): |
| 233 | + if isinstance(referee, WDL.Tree.Call): |
| 234 | + return id_name |
| 235 | + if referee.expr and ( |
| 236 | + wdl_ident_expr.name in ctx.optional_cwl_null |
| 237 | + or wdl_ident_expr.name not in ctx.non_static_values |
| 238 | + ): |
| 239 | + return get_expr(referee.expr, ctx) |
| 240 | + if optional and isinstance(wdl_ident_expr.type, WDL.Type.File): |
| 241 | + # To prevent null showing on the terminal for inputs of type File |
| 242 | + name_with_file_check = get_expr_name_with_is_file_check(wdl_ident_expr) |
| 243 | + return f'{ident_name} === null ? "" : {name_with_file_check}' |
| 244 | + return ( |
| 245 | + ident_name |
| 246 | + if not isinstance(wdl_ident_expr.type, WDL.Type.File) |
| 247 | + else f"{ident_name}.path" |
| 248 | + ) |
| 249 | + |
| 250 | + |
| 251 | +def get_expr_name_with_is_file_check(wdl_expr: WDL.Expr.Ident) -> str: |
| 252 | + """Extract name from WDL expr and check if it's a file path.""" |
| 253 | + if wdl_expr is None or not hasattr(wdl_expr, "name"): |
| 254 | + raise WDLSourceLine(wdl_expr, ConversionException).makeError( |
| 255 | + f"{type(wdl_expr)} has not attribute 'name'" |
| 256 | + ) |
| 257 | + expr_name = get_input(wdl_expr.name) |
| 258 | + is_file = isinstance(wdl_expr.type, WDL.Type.File) |
| 259 | + return expr_name if not is_file else f"{expr_name}.path" |
| 260 | + |
| 261 | + |
| 262 | +__all__ = ["get_expr", "get_expr_string", "translate_wdl_placeholder"] |
0 commit comments