summary
evaluate_expression raises ValueError on a multi-expression template when any block has a literal }} inside a quoted filter argument, e.g. default('}}'). it should interpolate.
root cause
there are two paths in evaluate_expression:
_EXPR_PATTERN = re.compile(r"\{\{(.+?)\}\}")
...
return _EXPR_PATTERN.sub(_replacer, template)
the non-greedy (.+?)}} body stops at the first }} regardless of quoting. so in a multi-expression template, a block whose argument contains a literal }} gets captured truncated (e.g. inputs.missing | default(' instead of inputs.missing | default('}}')). that truncated body reaches the filter parser malformed and raises ValueError.
so #3208/#3228 fixed this class for a lone expression but left the interpolation path exposed.
reproduction
from specify_cli.workflows.expressions import evaluate_expression
from specify_cli.workflows.base import StepContext
ctx = StepContext(inputs={"name": "Bob", "missing": None})
# raises ValueError, should return "Bob: }}"
evaluate_expression("{{ inputs.name }}: {{ inputs.missing | default('}}') }}", ctx)
# raises ValueError, should return "}} / Bob"
evaluate_expression("{{ inputs.missing | default('}}') }} / {{ inputs.name }}", ctx)
the single-expression equivalent already works, because it takes the quote-aware path:
# returns "}}" fine
evaluate_expression("{{ inputs.missing | default('}}') }}", ctx)
impact
default('}}'), default('{{'), contains('}}') etc. inside a template that also has literal text or another expression around it will hard-error at evaluation. it's a narrow trigger (you need the brace literal inside a filter arg and more than one block), but it's a crash rather than a wrong value, and the fix for the sibling path is already in the tree.
proposed fix
replace the _EXPR_PATTERN.sub interpolation with a quote-aware scan that finds each block's closing }} outside string literals, mirroring the exact logic _is_single_expression already uses. a literal }} in a plain resolved value (not an expression) stays untouched.
happy to open a PR (have one ready with regression tests).
summary
evaluate_expressionraisesValueErroron a multi-expression template when any block has a literal}}inside a quoted filter argument, e.g.default('}}'). it should interpolate.root cause
there are two paths in
evaluate_expression:_is_single_expression, which is quote-aware (added in [Bug]: expression evaluator returns None for multi-expression templates without surrounding text #3208 / fix: interpolate multi-expression templates instead of returning None (#3208) #3228 after a few review rounds specifically to handle a literal{{or}}inside a string argument).the non-greedy
(.+?)}}body stops at the first}}regardless of quoting. so in a multi-expression template, a block whose argument contains a literal}}gets captured truncated (e.g.inputs.missing | default('instead ofinputs.missing | default('}}')). that truncated body reaches the filter parser malformed and raisesValueError.so #3208/#3228 fixed this class for a lone expression but left the interpolation path exposed.
reproduction
the single-expression equivalent already works, because it takes the quote-aware path:
impact
default('}}'),default('{{'),contains('}}')etc. inside a template that also has literal text or another expression around it will hard-error at evaluation. it's a narrow trigger (you need the brace literal inside a filter arg and more than one block), but it's a crash rather than a wrong value, and the fix for the sibling path is already in the tree.proposed fix
replace the
_EXPR_PATTERN.subinterpolation with a quote-aware scan that finds each block's closing}}outside string literals, mirroring the exact logic_is_single_expressionalready uses. a literal}}in a plain resolved value (not an expression) stays untouched.happy to open a PR (have one ready with regression tests).