Skip to content

fix(workflows): evaluate 'in'/'not in' safely on a non-iterable right operand (#3447)#3468

Open
Noor-ul-ain001 wants to merge 1 commit into
github:mainfrom
Noor-ul-ain001:fix/3447-in-operator-non-iterable
Open

fix(workflows): evaluate 'in'/'not in' safely on a non-iterable right operand (#3447)#3468
Noor-ul-ain001 wants to merge 1 commit into
github:mainfrom
Noor-ul-ain001:fix/3447-in-operator-non-iterable

Conversation

@Noor-ul-ain001

Copy link
Copy Markdown
Contributor

Summary

Closes #3447. The in / not in operators in _evaluate_simple_expression (src/specify_cli/workflows/expressions.py) only guarded right is not None:

if op == " in ":
    return left in right if right is not None else False

But left in right also raises TypeError for any other non-iterable right operand (int, bool, float). So a workflow condition like {{ inputs.tag in inputs.count }} where count is a number leaked a raw TypeError: argument of type 'int' is not iterable and crashed the whole run, instead of evaluating like the None case right beside it.

This is asymmetric with _safe_compare, which already swallows TypeError and returns False for the ordering operators (<, >, etc.).

Before:

evaluate_condition("{{ inputs.tag in inputs.count }}", StepContext(inputs={"tag":"x","count":5}))
# TypeError: argument of type 'int' is not iterable  (crashes the run)

After: evaluates to False (nothing is contained in a non-container), same as the right is None branch.

Changes

  • Add a _safe_contains(left, right) helper mirroring _safe_compare: a None or non-container right means "nothing is contained", so inFalse and not inTrue rather than a raw TypeError.
  • Route both membership branches through it (not in negates the result), so the two operators can't drift.

Testing

  • Added test_in_operator_non_iterable_right_operand: asserts in/not in against int/bool/float/None right operands don't crash and return the None-branch result, and that genuine containment against iterables still works.
  • Test-the-test: confirmed the new test fails without the source change (raw TypeError propagates).
  • pytest tests/test_workflows.py → 395 passed, 1 skipped. (The 11 TestWorkflow*SymlinkGuard failures are pre-existing on Windows without elevation and unrelated to this change.)

🤖 Generated with Claude Code

… operand (github#3447)

The `in` / `not in` operators in `_evaluate_simple_expression` only guarded
`right is not None`, but `left in right` also raises `TypeError` for any other
non-iterable right operand (int, bool, float). So a workflow condition like
`{{ inputs.tag in inputs.count }}` where `count` is a number leaked a raw
`TypeError: argument of type 'int' is not iterable` and crashed the whole run,
instead of evaluating like the None case beside it.

This was asymmetric with `_safe_compare`, which already swallows `TypeError`
and returns False for the ordering operators.

Add a `_safe_contains` helper (mirroring `_safe_compare`) that treats both a
None and a non-container right operand as "nothing is contained": `in` -> False,
`not in` -> True. Add a regression test covering int/bool/float/None right
operands and asserting genuine containment against iterables still works.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Noor-ul-ain001 Noor-ul-ain001 requested a review from mnriem as a code owner July 11, 2026 05:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

workflow 'in' operator crashes with raw TypeError on a non-iterable right operand

1 participant