Evaluate a template string with ``{{ ... }}`` expressions. If the entire string is a single expression, returns the raw value (preserving type). Otherwise, substitutes each expression inline and returns a string. Parameters ---------- template: The template string
(template: str, context: Any)
| 432 | |
| 433 | |
| 434 | def evaluate_expression(template: str, context: Any) -> Any: |
| 435 | """Evaluate a template string with ``{{ ... }}`` expressions. |
| 436 | |
| 437 | If the entire string is a single expression, returns the raw value |
| 438 | (preserving type). Otherwise, substitutes each expression inline |
| 439 | and returns a string. |
| 440 | |
| 441 | Parameters |
| 442 | ---------- |
| 443 | template: |
| 444 | The template string (e.g., ``"{{ steps.plan.output.task_count }}"`` |
| 445 | or ``"Processed {{ inputs.spec }}"``. |
| 446 | context: |
| 447 | A ``StepContext`` or compatible object. |
| 448 | |
| 449 | Returns |
| 450 | ------- |
| 451 | The resolved value (any type for single-expression templates, |
| 452 | string for multi-expression or mixed templates). |
| 453 | """ |
| 454 | if not isinstance(template, str): |
| 455 | return template |
| 456 | |
| 457 | namespace = _build_namespace(context) |
| 458 | |
| 459 | # Single expression: return typed value (preserving type). |
| 460 | # |
| 461 | # The fast path must fire only when the whole template is one ``{{ ... }}`` |
| 462 | # block. Neither ``fullmatch`` nor a match-span check on ``_EXPR_PATTERN`` |
| 463 | # can decide this reliably: the non-greedy body stops at the first ``}}``, |
| 464 | # so ``fullmatch`` over-expands ``"{{ a }} {{ b }}"`` to garbage (returning |
| 465 | # ``None`` and bypassing interpolation, issue #3208), while a span check |
| 466 | # trips over a literal ``}}`` inside a string argument such as |
| 467 | # ``{{ inputs.text | contains('}}') }}`` and mis-routes it to interpolation |
| 468 | # (coercing its typed return to ``str``). ``_is_single_expression`` scans |
| 469 | # for a block-closing ``}}`` outside string literals, so both cases resolve |
| 470 | # correctly. |
| 471 | stripped = template.strip() |
| 472 | if _is_single_expression(stripped): |
| 473 | return _evaluate_simple_expression(stripped[2:-2].strip(), namespace) |
| 474 | |
| 475 | # Multi-expression: string interpolation |
| 476 | def _replacer(m: re.Match[str]) -> str: |
| 477 | val = _evaluate_simple_expression(m.group(1).strip(), namespace) |
| 478 | return str(val) if val is not None else "" |
| 479 | |
| 480 | return _EXPR_PATTERN.sub(_replacer, template) |
| 481 | |
| 482 | |
| 483 | def evaluate_condition(condition: str, context: Any) -> bool: |