From 665bbb7d2bd2021558a4a253e3cf7448b47aa033 Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Tue, 14 Jul 2026 22:13:19 +0500 Subject: [PATCH] fix(workflows): fan-out max_concurrency .inf falls back to sequential, not crash _run_fan_out coerces max_concurrency with int() inside except (TypeError, ValueError). int(float('inf')) raises OverflowError, which is not in that tuple, so a YAML 'max_concurrency: .inf' crashed the whole run with an uncaught OverflowError instead of the documented 'cannot be coerced -> sequential' fallback. Add OverflowError to the except tuple (nan already coerced via ValueError). Extends the existing invalid-value parametrization with float('inf')/nan (fails before on inf: OverflowError; passes after: sequential, all items in order). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/specify_cli/workflows/engine.py | 10 ++++++---- tests/test_workflows.py | 7 ++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/specify_cli/workflows/engine.py b/src/specify_cli/workflows/engine.py index ed52710bed..0c2a06f774 100644 --- a/src/specify_cli/workflows/engine.py +++ b/src/specify_cli/workflows/engine.py @@ -1197,9 +1197,9 @@ def _run_fan_out( already flipped), so the prefix never drops the actual halting item. ``max_concurrency`` is coerced with ``int()``; a value that cannot be - coerced (``None``, a non-numeric string, …) or that coerces to <= 1 runs - sequentially, while a numeric string like ``"4"`` or a float like ``4.0`` - is honored. + coerced (``None``, a non-numeric string, ``.inf``/``.nan``, …) or that + coerces to <= 1 runs sequentially, while a numeric string like ``"4"`` or + a float like ``4.0`` is honored. """ if not items: return [] @@ -1207,7 +1207,9 @@ def _run_fan_out( halting = (RunStatus.PAUSED, RunStatus.FAILED, RunStatus.ABORTED) try: workers = max(1, int(max_concurrency)) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): + # OverflowError: int(float("inf")) — a YAML ``max_concurrency: .inf`` + # would otherwise crash the whole run instead of falling back. workers = 1 # Never spin up more workers than there is work — bounds a user-controlled # max_concurrency from over-allocating threads. diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 8fefad740e..fb2af7921a 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -2662,8 +2662,13 @@ def on_item(item): results, _ = self._run(tmp_path, list(range(n)), n, on_item) assert results == [{"seen": i} for i in range(n)] - @pytest.mark.parametrize("bad", [0, -1, None, "abc", 1.0]) + @pytest.mark.parametrize( + "bad", [0, -1, None, "abc", 1.0, float("inf"), float("nan")] + ) def test_invalid_max_concurrency_coerces_to_sequential(self, tmp_path, bad): + # float("inf") -> int() raises OverflowError (not TypeError/ValueError); + # it must fall back to sequential like any other uncoercible value, not + # crash the run. results, _ = self._run(tmp_path, list(range(4)), bad) assert results == [{"seen": i} for i in range(4)]