mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
fix(workflows): don't crash on membership test against a non-iterable (#3448)
* fix(workflows): don't crash on membership test against a non-iterable
the `in` / `not in` operators in _evaluate_simple_expression only guarded
`right is not None`, so `left in right` still raised a raw TypeError when the
right operand was any other non-iterable (int, bool, float). a condition like
`{{ inputs.tag in inputs.count }}` where count is a number crashed the whole
workflow run instead of evaluating.
nothing is contained in a non-iterable, so treat membership as False (`not in`
as True) via a new _safe_membership helper that swallows TypeError. this
generalizes the old None guard and mirrors _safe_compare, which already
catches TypeError for the ordering operators.
added a regression test; confirmed it fails on the pre-fix code (raw
TypeError) and that genuine list/substring membership still works.
* address review: float membership case + broaden _safe_membership docstring
- add a float right-operand assertion so the test matches its comment (was
claiming float coverage while only exercising int/bool/None).
- reword the _safe_membership docstring to describe TypeError generally
(non-iterable right is the common case, but also e.g. an unhashable left
against a set) rather than implying only the right operand matters.
This commit is contained in:
@@ -460,6 +460,34 @@ class TestExpressions:
|
||||
assert evaluate_expression("{{ inputs.s | contains('ab') }}", ctx2) is True
|
||||
assert evaluate_expression("{{ inputs.missing | default('a|b') }}", ctx2) == "a|b"
|
||||
|
||||
def test_membership_against_non_iterable_is_false_not_error(self):
|
||||
from specify_cli.workflows.expressions import (
|
||||
evaluate_condition,
|
||||
evaluate_expression,
|
||||
)
|
||||
from specify_cli.workflows.base import StepContext
|
||||
|
||||
# A non-iterable right operand (int, bool, None, float) makes a raw
|
||||
# `x in y` raise TypeError in Python. The evaluator must treat it as
|
||||
# "not contained" (False, and `not in` as True) instead of leaking the
|
||||
# TypeError and crashing the whole workflow run. This generalizes the
|
||||
# previous `right is not None` guard and mirrors _safe_compare, which
|
||||
# already swallows TypeError for the ordering operators.
|
||||
ctx = StepContext(inputs={"tag": "x", "count": 5, "ratio": 1.5, "flag": True})
|
||||
assert evaluate_expression("{{ inputs.tag in inputs.count }}", ctx) is False
|
||||
assert evaluate_expression("{{ inputs.tag not in inputs.count }}", ctx) is True
|
||||
assert evaluate_expression("{{ 'a' in inputs.ratio }}", ctx) is False
|
||||
assert evaluate_expression("{{ 'a' in inputs.flag }}", ctx) is False
|
||||
assert evaluate_expression("{{ inputs.tag in inputs.missing }}", ctx) is False
|
||||
# A condition that would otherwise crash the run now evaluates cleanly.
|
||||
assert evaluate_condition("{{ inputs.tag in inputs.count }}", ctx) is False
|
||||
|
||||
# Regression: genuine membership over a real iterable still works.
|
||||
ok = StepContext(inputs={"items": ["x", "y"], "s": "xyz"})
|
||||
assert evaluate_expression("{{ 'x' in inputs.items }}", ok) is True
|
||||
assert evaluate_expression("{{ 'z' not in inputs.items }}", ok) is True
|
||||
assert evaluate_expression("{{ 'y' in inputs.s }}", ok) is True
|
||||
|
||||
def test_filter_default(self):
|
||||
from specify_cli.workflows.expressions import evaluate_expression
|
||||
from specify_cli.workflows.base import StepContext
|
||||
|
||||
Reference in New Issue
Block a user