Add option parsing utils and tests

This commit is contained in:
relhamdi
2026-05-22 17:41:55 +02:00
parent b158698139
commit d24b38d5b0
2 changed files with 136 additions and 0 deletions
+64
View File
@@ -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
+72
View File
@@ -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")