Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion keep/step/step.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from collections.abc import Iterable
import time
from enum import Enum

Expand Down Expand Up @@ -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)"""
Expand Down
30 changes: 30 additions & 0 deletions tests/test_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Loading