diff --git a/keep/step/step.py b/keep/step/step.py index 20b508d722..8d9bac7ed5 100644 --- a/keep/step/step.py +++ b/keep/step/step.py @@ -1,4 +1,5 @@ import logging +from collections.abc import Iterable import time from enum import Enum @@ -127,7 +128,17 @@ def _get_foreach_items(self) -> list | list[list]: foreach_items.append(items) if not foreach_items: return [] - return len(foreach_items) == 1 and foreach_items[0] or zip(*foreach_items) + if len(foreach_items) == 1: + # A single reference resolves to whatever it points at: normally + # the list to iterate, but also legitimately a scalar such as a + # count of 0. The old `X and Y or Z` idiom routed falsy values + # into zip(), which raised TypeError on non-iterables; a scalar + # (falsy or not) is instead iterated once. + value = foreach_items[0] + if isinstance(value, Iterable): + return value + return [value] + return zip(*foreach_items) def _run_foreach(self): """Evaluate the action for each item, when using the `foreach` attribute (see foreach.md)""" diff --git a/tests/test_steps.py b/tests/test_steps.py index 1975a7b138..b6d6d94203 100644 --- a/tests/test_steps.py +++ b/tests/test_steps.py @@ -145,3 +145,33 @@ def test_continue_on_error_explicit_false(): {}, ) assert step.continue_on_error is False + + +@pytest.mark.parametrize( + "resolved,expected", + [ + (0, [0]), # falsy scalar: used to hit zip(0) -> TypeError (#6721) + (False, [False]), + (None, [None]), + (5, [5]), # truthy non-iterable also crashed before via for-loop + ([1, 2], [1, 2]), # iterables keep their semantics + (["a", "b"], ["a", "b"]), + ([], []), + ], +) +def test_get_foreach_items_single_reference(sample_step, resolved, expected): + sample_step.config["foreach"] = "{{ steps.check-count.results }}" + sample_step.context_manager.get_full_context = Mock( + return_value={"steps": {"check-count": {"results": resolved}}} + ) + + assert sample_step._get_foreach_items() == expected + + +def test_get_foreach_items_multiple_references_zip(sample_step): + sample_step.config["foreach"] = "{{ steps.a.results }} && {{ steps.b.results }}" + sample_step.context_manager.get_full_context = Mock( + return_value={"steps": {"a": {"results": [1, 2]}, "b": {"results": ["x", "y"]}}} + ) + + assert list(sample_step._get_foreach_items()) == [(1, "x"), (2, "y")]