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
39 changes: 38 additions & 1 deletion src/specify_cli/workflows/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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