fix: interpolate multi-expression templates instead of returning None (#3208)#3228
fix: interpolate multi-expression templates instead of returning None (#3208)#3228Noor-ul-ain001 wants to merge 5 commits into
Conversation
…github#3208) `evaluate_expression` returned None for templates containing two or more `{{ }}` blocks with no surrounding literal text, e.g. `"{{ context.run_id }} {{ inputs.issue }}"`. The single-expression fast path used `_EXPR_PATTERN.fullmatch()`, but `fullmatch` defeats the pattern's non-greedy `(.+?)` body: for two adjacent expressions it still matches, capturing everything between the first `{{` and the last `}}` (`"context.run_id }} {{ inputs.issue"`) as the body. That garbage failed dot-path resolution and returned None directly, bypassing the `sub()` interpolation path that would have resolved each expression. Downstream this surfaced as the literal string "None" reaching commands. Guard the fast path on `stripped.count("{{") == 1` so only genuine single-expression templates take the typed return; multi-expression templates fall through to `sub()` and interpolate correctly. Add regression tests for two expressions separated by a space and for adjacent expressions with no separator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a workflow expression-evaluator edge case where templates containing multiple {{ ... }} blocks (and no non-whitespace surrounding text) could incorrectly take the single-expression fullmatch() fast path and return None, causing downstream commands to receive the literal string "None".
Changes:
- Adjust
evaluate_expression()to only use the typed single-expression fast path when the template is truly a single expression, otherwise fall back to interpolation. - Add regression tests covering multi-expression templates with whitespace-only separation and back-to-back expressions.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/workflows/expressions.py |
Guards the typed fullmatch() fast path so multi-expression templates interpolate correctly instead of collapsing to None. |
tests/test_workflows.py |
Adds regression tests to ensure multi-expression templates interpolate as expected. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Low
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
Please address Copilot feedback. If not applicable, please explain why. And please resolve test & lint errors |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
Please address Copilot feedback. If not applicable, please explain why |
…eral {{ keep their type
The previous `stripped.count("{{") == 1` guard misclassified a genuine
single expression whose string argument contains a literal `{{` (e.g.
`{{ inputs.text | contains('{{') }}`) as multi-expression, routing it
through `sub()` interpolation and coercing the typed (bool/int/list)
return value to a string -- breaking the type-preservation the docstring
promises (Copilot review on github#3228).
Anchor a single match at the start and require it to consume the whole
stripped string instead. The non-greedy body stops at the first `}}`, so
a two-block template fails the span check (falls through to interpolation,
fixing github#3208) while a lone expression -- including one with a `{{` inside
a string literal -- matches to the end and keeps its typed value.
Add a regression test for the literal-brace single-expression case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the Copilot feedback in 53c899a. Main finding (the substantive one): the Fix: replaced the count heuristic with a match-span guard: match = _EXPR_PATTERN.match(stripped)
if match and match.end() == len(stripped):
return _evaluate_simple_expression(match.group(1).strip(), namespace)The pattern's non-greedy body stops at the first
This is strictly more correct than both the original Added a regression test Test-file findings (docstring wording + indentation) were already handled in the earlier autofix commits. Tests/lint: all 34 |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
What
evaluate_expressionreturnedNonefor any template containing two or more{{ }}blocks with no surrounding literal text, e.g."{{ context.run_id }} {{ inputs.issue }}". Downstream this surfaced as the literal string"None"reaching commands (the reporter hit "no issue was provided" despiteissue: "23"being in the run inputs).Fixes #3208.
Root cause
The single-expression fast path used
_EXPR_PATTERN.fullmatch(), butfullmatchdefeats the pattern's non-greedy(.+?)body. For"{{ a }} {{ b }}"it still matches, expanding the body to capture everything between the first{{and the last}}— i.e."a }} {{ b". That garbage body fails dot-path resolution and returnsNonedirectly, bypassing thesub()interpolation path that would have resolved each expression separately.fullmatch{{ inputs.issue }}inputs.issuebash x "{{ inputs.issue }}"sub(){{ a }} {{ b }}"a }} {{ b"How
Guard the fast path on
stripped.count("{{") == 1so only genuine single-expression templates take the typed-return path; multi-expression templates fall through to the existingsub()interpolation, which is already correct. Single-expression type preservation is unchanged.Tests
Added two regression tests to
TestExpressions:test_multi_expression_no_surrounding_text—"{{ context.run_id }} {{ inputs.issue }}"→"47c5eb4b 23"test_multi_expression_adjacent_no_separator—"{{ inputs.a }}{{ inputs.b }}"→"foobar"Verified both fail on
main(returnNone) and pass with the fix. All 31TestExpressionstests pass;ruffclean. (The 5 symlink-test failures on my local Windows run pre-exist onmain— a symlink-privilege limitation unrelated to this change; they pass on Linux CI.)🤖 Generated with Claude Code