fix(workflows): evaluate 'in'/'not in' safely on a non-iterable right operand (#3447)#3468
fix(workflows): evaluate 'in'/'not in' safely on a non-iterable right operand (#3447)#3468Noor-ul-ain001 wants to merge 6 commits into
Conversation
… 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>
There was a problem hiding this comment.
Pull request overview
Fixes workflow expression evaluation so in / not in conditions don’t crash the run when the right-hand operand isn’t iterable (e.g., int, bool, float), aligning membership semantics with the existing “safe” behavior used for ordering comparisons.
Changes:
- Added
_safe_contains(left, right)to handleinsafely (returningFalseonNoneorTypeErrorinstead of raising). - Routed both
" in "and" not in "operator handling through_safe_containsto keep behavior consistent and avoid drift. - Added regression tests covering
in/not inagainst non-iterable right operands, plus a quick iterable containment sanity check.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/workflows/expressions.py |
Introduces _safe_contains and uses it for "in" / "not in" to prevent TypeError crashes on non-iterable RHS. |
tests/test_workflows.py |
Adds a focused regression test ensuring non-iterable RHS doesn’t crash and returns the expected boolean results. |
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: 1
- Review effort level: Low
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…r-non-iterable # Conflicts: # src/specify_cli/workflows/expressions.py
| return _safe_contains(left, right) | ||
| if op == " not in ": | ||
| return _safe_membership(left, right, negate=True) | ||
| return not _safe_contains(left, right) |
| def test_in_operator_non_iterable_right_operand(self): | ||
| """`in`/`not in` against a non-iterable right operand must not crash. |
mnriem
left a comment
There was a problem hiding this comment.
Please address Copilot feedback
github#3447 was fixed independently by github#3448 (merged first), which added the same _safe_membership helper this branch introduced. Per Copilot review: - Revert the redundant _safe_contains rename in expressions.py so the file matches main; the working membership guard already lives there. - Drop the duplicate test_in_operator_non_iterable_right_operand test and fold its only new coverage (not in against float/bool/None right operands, which the base test only checked for the int case) into the existing test_membership_against_non_iterable_is_false_not_error. Also merges latest upstream/main into the branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the review. Addressed the Copilot feedback: On the redundant refactor (main finding): You are right — #3447 was fixed independently by #3448, which merged first and added the same On the duplicate test: Removed On rebase: Merged the latest The remaining diff is now just that small test-hardening addition on top of the already-merged fix. The earlier docstring/indentation comments were resolved in prior commits. |
Summary
Closes #3447. The
in/not inoperators in_evaluate_simple_expression(src/specify_cli/workflows/expressions.py) only guardedright is not None:But
left in rightalso raisesTypeErrorfor any other non-iterable right operand (int, bool, float). So a workflow condition like{{ inputs.tag in inputs.count }}wherecountis a number leaked a rawTypeError: argument of type 'int' is not iterableand crashed the whole run, instead of evaluating like the None case right beside it.This is asymmetric with
_safe_compare, which already swallowsTypeErrorand returnsFalsefor the ordering operators (<,>, etc.).Before:
After: evaluates to
False(nothing is contained in a non-container), same as theright is Nonebranch.Changes
_safe_contains(left, right)helper mirroring_safe_compare: aNoneor non-containerrightmeans "nothing is contained", soin→Falseandnot in→Truerather than a rawTypeError.not innegates the result), so the two operators can't drift.Testing
test_in_operator_non_iterable_right_operand: assertsin/not inagainst int/bool/float/None right operands don't crash and return the None-branch result, and that genuine containment against iterables still works.TypeErrorpropagates).pytest tests/test_workflows.py→ 395 passed, 1 skipped. (The 11TestWorkflow*SymlinkGuardfailures are pre-existing on Windows without elevation and unrelated to this change.)🤖 Generated with Claude Code