Add option parsing utils and tests
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import re
|
||||
from itertools import product
|
||||
|
||||
|
||||
def clean_prompt(text: str) -> str:
|
||||
@@ -13,3 +14,66 @@ def clean_prompt(text: str) -> str:
|
||||
text = re.sub(r"^\s*,\s*", "", text, flags=re.MULTILINE) # Leading comma
|
||||
text = re.sub(r"\n\s*\n", "\n", text) # Empty lines
|
||||
return text.strip()
|
||||
|
||||
|
||||
def parse_options(raw: str) -> list[str]:
|
||||
"""
|
||||
Parse the options field into a flat list of options.
|
||||
|
||||
Supports:
|
||||
- Plain lines: direct options
|
||||
- @combine / @end blocks: cartesian product of --- separated lists
|
||||
- Empty lines: empty option (displayed as --)
|
||||
- --- outside a block: treated as a literal option
|
||||
"""
|
||||
lines = raw.split("\n")
|
||||
result = []
|
||||
in_combine = False
|
||||
current_block: list[list[str]] = []
|
||||
current_list: list[str] = []
|
||||
|
||||
for i, line in enumerate(lines, 1):
|
||||
stripped = line.strip()
|
||||
|
||||
if stripped == "@combine":
|
||||
if in_combine:
|
||||
raise ValueError(
|
||||
f"parse_options: @combine at line {i} opened inside an unclosed @combine block."
|
||||
)
|
||||
in_combine = True
|
||||
current_block = []
|
||||
current_list = []
|
||||
|
||||
elif stripped == "@end":
|
||||
if not in_combine:
|
||||
raise ValueError(
|
||||
f"parse_options: @end at line {i} without matching @combine."
|
||||
)
|
||||
# Save last list
|
||||
current_block.append(current_list)
|
||||
current_list = []
|
||||
in_combine = False
|
||||
|
||||
# Generate cartesian product
|
||||
for combo in product(*current_block):
|
||||
result.append(", ".join(combo))
|
||||
|
||||
elif stripped == "---":
|
||||
if in_combine:
|
||||
# Separator between lists in a combine block
|
||||
current_block.append(current_list)
|
||||
current_list = []
|
||||
else:
|
||||
# Outside a block, treat as literal option
|
||||
result.append("---")
|
||||
|
||||
else:
|
||||
if in_combine:
|
||||
current_list.append(line)
|
||||
else:
|
||||
result.append(line)
|
||||
|
||||
if in_combine:
|
||||
raise ValueError("parse_options: unclosed @combine block — missing @end.")
|
||||
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import pytest
|
||||
from src.utils import parse_options
|
||||
|
||||
# --- Plain options ---
|
||||
|
||||
|
||||
def test_plain_options():
|
||||
result = parse_options("from front\nfrom behind")
|
||||
assert result == ["from front", "from behind"]
|
||||
|
||||
|
||||
def test_empty_line():
|
||||
result = parse_options("from front\n\nfrom behind")
|
||||
assert result == ["from front", "", "from behind"]
|
||||
|
||||
|
||||
def test_separator_outside_block():
|
||||
result = parse_options("from front\n---\nfrom behind")
|
||||
assert result == ["from front", "---", "from behind"]
|
||||
|
||||
|
||||
# --- Combine blocks ---
|
||||
|
||||
|
||||
def test_combine_two_lists():
|
||||
raw = "before\n@combine\na\nb\n---\nc\nd\n@end\nafter"
|
||||
result = parse_options(raw)
|
||||
assert "before" in result
|
||||
assert "after" in result
|
||||
assert "a, c" in result
|
||||
assert "a, d" in result
|
||||
assert "b, c" in result
|
||||
assert "b, d" in result
|
||||
assert len(result) == 6 # before + 4 combos + after
|
||||
|
||||
|
||||
def test_combine_three_lists():
|
||||
raw = "@combine\na\n---\nb\n---\nc\n@end"
|
||||
result = parse_options(raw)
|
||||
assert result == ["a, b, c"]
|
||||
|
||||
|
||||
def test_combine_empty_line_in_list():
|
||||
raw = "@combine\na\n\n---\nb\n@end"
|
||||
result = parse_options(raw)
|
||||
assert ", b" in result # empty + b
|
||||
assert "a, b" in result
|
||||
|
||||
|
||||
def test_multiple_combine_blocks():
|
||||
raw = "@combine\na\n---\nb\n@end\n@combine\nc\n---\nd\n@end"
|
||||
result = parse_options(raw)
|
||||
assert "a, b" in result
|
||||
assert "c, d" in result
|
||||
|
||||
|
||||
# --- Validation errors ---
|
||||
|
||||
|
||||
def test_combine_without_end():
|
||||
with pytest.raises(ValueError, match="unclosed @combine"):
|
||||
parse_options("@combine\na\n---\nb")
|
||||
|
||||
|
||||
def test_end_without_combine():
|
||||
with pytest.raises(ValueError, match="without matching @combine"):
|
||||
parse_options("@end")
|
||||
|
||||
|
||||
def test_nested_combine():
|
||||
with pytest.raises(ValueError, match="opened inside an unclosed"):
|
||||
parse_options("@combine\na\n@combine\nb\n@end\n@end")
|
||||
Reference in New Issue
Block a user