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
37 changes: 36 additions & 1 deletion src/specify_cli/workflows/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,41 @@ def _find_top_level(text: str, token: str) -> int:
return -1


def _is_single_list_literal(expr: str) -> bool:
"""Return ``True`` only when *expr* is exactly one bracketed list
literal -- the opening ``[`` closes at the FINAL character, not partway
through the string.

``expr.startswith("[") and expr.endswith("]")`` alone also matches a
list literal immediately followed by an index suffix, e.g.
``[1,2,3][1]`` (meant as "index 1 of [1,2,3]", i.e. 2). Naively
stripping the outer brackets from that string yields the garbage
``1,2,3][1``, which then silently evaluates to ``[1, 2, None]`` instead
of raising or resolving the index -- the same "grabs the wrong span"
failure mode the string-literal check above guards against, just never
given the same treatment for brackets.
"""
if not (expr.startswith("[") and expr.endswith("]")):
return False
quote: str | None = None
depth = 0
n = len(expr)
for i, ch in enumerate(expr):
if quote is not None:
if ch == quote:
quote = None
continue
if ch in ("'", '"'):
quote = ch
elif ch in "([{":
depth += 1
elif ch in ")]}":
depth -= 1
if depth == 0:
return i == n - 1
return False


def _apply_filter(value: Any, filter_expr: str, namespace: dict[str, Any]) -> Any:
"""Apply a single pipe filter segment to *value*.

Expand Down Expand Up @@ -647,7 +682,7 @@ def _evaluate_simple_expression(expr: str, namespace: dict[str, Any]) -> Any:
return None

# List literal (simple)
if expr.startswith("[") and expr.endswith("]"):
if _is_single_list_literal(expr):
inner = expr[1:-1].strip()
if not inner:
return []
Expand Down
23 changes: 23 additions & 0 deletions tests/test_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,29 @@ def test_list_literal_ignores_trailing_and_empty_commas(self):
# …but an intentional empty-string element is still preserved.
assert evaluate_expression("{{ ['', 'a'] }}", ctx) == ["", "a"]

def test_list_literal_followed_by_index_is_not_misparsed_as_one_literal(self):
"""A list literal immediately followed by an index suffix, e.g.
``[1,2,3][1]``, both starts with ``[`` and ends with ``]`` -- the
same shape as a genuine single list literal. Naively stripping the
outer brackets from ``[1,2,3][1]`` yields ``1,2,3][1``, which then
silently evaluates to ``[1, 2, None]`` instead of raising or
resolving the index. It must not be misclassified as one literal;
falling through to unresolvable (``None``) is safe, unlike silently
returning a wrong-looking list.
"""
from specify_cli.workflows.expressions import evaluate_expression
from specify_cli.workflows.base import StepContext

ctx = StepContext()
assert evaluate_expression("{{ [1,2,3][1] }}", ctx) is None
assert evaluate_expression("{{ [1,2][0] }}", ctx) is None
# Genuine list literals -- including ones a bracket-depth scan must
# still recognize as ending exactly at the final character -- are
# unaffected.
assert evaluate_expression("{{ [1, 2, 3] }}", ctx) == [1, 2, 3]
assert evaluate_expression("{{ [[1, 2], 3] }}", ctx) == [[1, 2], 3]
assert evaluate_expression("{{ ['a]', 'b'] }}", ctx) == ["a]", "b"]

def test_operator_splitting_is_quote_aware(self):
from specify_cli.workflows.expressions import (
evaluate_condition,
Expand Down