From 91d569ec1780162b059e928ab364f518811008b4 Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Wed, 24 Jun 2026 02:13:42 +0500 Subject: [PATCH 1/2] fix(workflows): preserve commas inside quoted list-literal elements The simple-expression evaluator parsed a list literal with a naive `inner.split(",")`, which splits on commas inside quoted strings (and nested brackets). So `{{ ["a, b", "c"] }}` evaluated to three items (`["a", "b", "c"]`) instead of two, silently corrupting `fan-out` `items:` and any list expression that contains a comma inside a quoted element. Split list-literal elements on top-level commas only, ignoring commas inside quotes or nested brackets, via a small `_split_top_level_commas` helper. Plain and empty lists are unchanged. Add tests covering quoted commas, nested lists, and the existing plain/empty cases. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/specify_cli/workflows/expressions.py | 39 +++++++++++++++++++++++- tests/test_workflows.py | 14 +++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/specify_cli/workflows/expressions.py b/src/specify_cli/workflows/expressions.py index ca10b24d1b..b7ed17e801 100644 --- a/src/specify_cli/workflows/expressions.py +++ b/src/specify_cli/workflows/expressions.py @@ -146,6 +146,40 @@ def _build_namespace(context: Any) -> dict[str, Any]: return ns +def _split_top_level_commas(text: str) -> list[str]: + """Split *text* on commas that are not inside quotes or nested brackets. + + Used for list-literal elements so a quoted element containing a comma + (e.g. ``["a, b", "c"]``) is not split mid-string, and nested lists/calls + (e.g. ``[[1, 2], 3]``) are kept intact. + """ + parts: list[str] = [] + buf: list[str] = [] + quote: str | None = None + depth = 0 + for ch in text: + if quote is not None: + buf.append(ch) + if ch == quote: + quote = None + elif ch in ("'", '"'): + quote = ch + buf.append(ch) + elif ch in "([{": + depth += 1 + buf.append(ch) + elif ch in ")]}": + depth = max(0, depth - 1) + buf.append(ch) + elif ch == "," and depth == 0: + parts.append("".join(buf)) + buf = [] + else: + buf.append(ch) + parts.append("".join(buf)) + return parts + + def _evaluate_simple_expression(expr: str, namespace: dict[str, Any]) -> Any: """Evaluate a simple expression against the namespace. @@ -291,7 +325,10 @@ def _evaluate_simple_expression(expr: str, namespace: dict[str, Any]) -> Any: inner = expr[1:-1].strip() if not inner: return [] - items = [_evaluate_simple_expression(i.strip(), namespace) for i in inner.split(",")] + items = [ + _evaluate_simple_expression(i.strip(), namespace) + for i in _split_top_level_commas(inner) + ] return items # Variable reference (dot-path) diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 512b354158..3f867e5ce1 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -268,6 +268,20 @@ def test_boolean_or(self): ctx = StepContext(inputs={"a": False, "b": True}) assert evaluate_expression("{{ inputs.a or inputs.b }}", ctx) is True + def test_list_literal_preserves_quoted_commas(self): + from specify_cli.workflows.expressions import evaluate_expression + from specify_cli.workflows.base import StepContext + + ctx = StepContext() + # commas inside a quoted element must not split it + assert evaluate_expression('{{ ["a, b", "c"] }}', ctx) == ["a, b", "c"] + assert evaluate_expression('{{ ["x, y, z"] }}', ctx) == ["x, y, z"] + # plain and empty lists still parse correctly + assert evaluate_expression("{{ [1, 2, 3] }}", ctx) == [1, 2, 3] + assert evaluate_expression("{{ [] }}", ctx) == [] + # nested list elements stay intact + assert evaluate_expression('{{ [["a", "b"], "c"] }}', ctx) == [["a", "b"], "c"] + def test_filter_default(self): from specify_cli.workflows.expressions import evaluate_expression from specify_cli.workflows.base import StepContext From 70b6ef783917797d050fb691c67089a076556d4c Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Wed, 24 Jun 2026 18:15:47 +0500 Subject: [PATCH 2/2] test(workflows): cover single-quoted and nested list literals Address review: extend the list-literal regression test to assert single-quoted elements with commas and nested lists parse correctly, alongside the existing double-quoted cases. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/test_workflows.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 3f867e5ce1..4780a512c9 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -273,14 +273,18 @@ def test_list_literal_preserves_quoted_commas(self): from specify_cli.workflows.base import StepContext ctx = StepContext() - # commas inside a quoted element must not split it + # commas inside a double-quoted element must not split it assert evaluate_expression('{{ ["a, b", "c"] }}', ctx) == ["a, b", "c"] assert evaluate_expression('{{ ["x, y, z"] }}', ctx) == ["x, y, z"] + # single-quoted elements are handled the same way + assert evaluate_expression("{{ ['a, b', 'c'] }}", ctx) == ["a, b", "c"] + assert evaluate_expression("{{ ['p, q, r'] }}", ctx) == ["p, q, r"] # plain and empty lists still parse correctly assert evaluate_expression("{{ [1, 2, 3] }}", ctx) == [1, 2, 3] assert evaluate_expression("{{ [] }}", ctx) == [] - # nested list elements stay intact + # nested lists (commas inside the inner brackets) stay intact assert evaluate_expression('{{ [["a", "b"], "c"] }}', ctx) == [["a", "b"], "c"] + assert evaluate_expression("{{ [[1, 2], [3, 4]] }}", ctx) == [[1, 2], [3, 4]] def test_filter_default(self): from specify_cli.workflows.expressions import evaluate_expression