Skip to content

Commit b117eac

Browse files
committed
implement: Read a comma inside a quoted selector pattern as part of the pattern (t57)
1 parent 387490b commit b117eac

3 files changed

Lines changed: 48 additions & 5 deletions

File tree

‎docs/source/spec.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ any block. A constraint names one of four attributes:
8484
SELECTOR,` requires the block to follow the first block that the named selector matches, which is
8585
how a spec tells the solutions at the end of a problem sheet from the questions at the front.
8686

87+
A comma separates the `after` clause from the rest of the selector only outside quotes. A comma
88+
inside a quoted pattern, such as `Para text~'^(Sheet|Note),|^Marks'`, is part of the pattern. One
89+
line holds one selector: write two selectors as a list under the key, as above.
90+
8791
Write a regular expression in single quotes. YAML reads `\(` inside double quotes as an escape
8892
sequence and reports an error, and single quotes pass the backslash through.
8993

‎in2lambda/spec/__init__.py‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -294,16 +294,18 @@ def _selector(text: Any, line: int) -> Selector:
294294
if not isinstance(text, str):
295295
raise _refuse(line, f"A selector is a line of text, which {text!r} is not.")
296296
head, tail = _split(text.strip())
297-
if head.strip().startswith("after "):
298-
after = _clause(head.strip()[len("after ") :], line)
297+
head = head.strip()
298+
if head.startswith("after "):
299+
after = _clause(head[len("after ") :], line)
299300
return _clause(tail.strip() if tail else "", line, after)
300301
if tail is not None:
301302
raise _refuse(
302303
line,
303-
"A selector's comma separates its `after` clause from the rest, and "
304-
f"{text!r} holds no `after` clause.",
304+
f"{text.strip()!r} names two selectors on one line; write them as a YAML "
305+
f"list under the role:\n - {head}\n - {tail.strip()}\n"
306+
"A selector's comma separates its `after` clause from the rest.",
305307
)
306-
return _clause(head.strip(), line)
308+
return _clause(head, line)
307309

308310

309311
def _selectors(value: Any, line: int, item_lines: list[int]) -> list[Selector]:

‎tests/test_spec.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import in2lambda
2323
import in2lambda.draft
24+
import in2lambda.spec
2425
from in2lambda.main import cli
2526
from in2lambda.source import SourceError
2627

@@ -386,6 +387,42 @@ def test_a_spec_calling_a_predicate_nothing_holds_is_refused(
386387
assert (tmp_path / "source.draft.json").read_bytes() == written
387388

388389

390+
def test_a_comma_inside_a_quoted_pattern_is_part_of_the_pattern() -> None:
391+
"""A regex alternating over labels holds commas, which are not the after separator."""
392+
spec = in2lambda.spec.load(
393+
"ignore: Para text~'^(Sheet|Note),|^Marks'\n"
394+
"question: Para\n"
395+
"layout: PartsOneSol\n"
396+
)
397+
398+
assert spec.ignore[0].constraints[0].wanted.pattern == "^(Sheet|Note),|^Marks"
399+
400+
401+
def test_two_selectors_written_on_one_line_are_refused_with_the_list_to_write() -> None:
402+
"""A comma outside quotes reads as an after clause, which two block types are not."""
403+
with pytest.raises(in2lambda.spec.BadSpec) as refusal:
404+
in2lambda.spec.load(
405+
"ignore: Header, Table\nquestion: Para\nlayout: PartsOneSol\n"
406+
)
407+
408+
assert "names two selectors on one line" in str(refusal.value)
409+
assert "- Header" in str(refusal.value)
410+
assert "- Table" in str(refusal.value)
411+
412+
413+
def test_a_comma_outside_quotes_still_separates_an_after_clause() -> None:
414+
"""The documented ``after SELECTOR, rest`` form, which the quotes must not break."""
415+
spec = in2lambda.spec.load(
416+
"question: Para\n"
417+
"solution: after Header text=Solutions, Para\n"
418+
"layout: PartsOneSol\n"
419+
)
420+
421+
assert spec.solution[0].after.type == "Header"
422+
assert spec.solution[0].after.constraints[0].wanted == "Solutions"
423+
assert spec.solution[0].type == "Para"
424+
425+
389426
def test_a_spec_that_is_not_there_is_refused_rather_than_left_to_the_file_system(
390427
tmp_path: Path, monkeypatch
391428
) -> None:

0 commit comments

Comments
 (0)