From eeb5b9b917b7dcc00a4980e80fab3f2ad16eb333 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Fri, 16 Oct 2026 11:06:49 +0100 Subject: [PATCH] Add `where_substitutions` to extract substitutions from `where` criteria, raise `CriteriaSyntaxError` for unsupported relations, and update tests, module init, and changelog. --- criteria/CHANGELOG.md | 1 + .../src/lambdafeedback/criteria/__init__.py | 6 +++- criteria/src/lambdafeedback/criteria/where.py | 29 +++++++++++++++++++ criteria/tests/test_criteria.py | 26 +++++++++++++++++ 4 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 criteria/src/lambdafeedback/criteria/where.py diff --git a/criteria/CHANGELOG.md b/criteria/CHANGELOG.md index d865b3f..08c20fb 100644 --- a/criteria/CHANGELOG.md +++ b/criteria/CHANGELOG.md @@ -64,6 +64,7 @@ Behaviour differences to be aware of when adopting: - `build_quantity_criteria_parser(reserved_expressions, token_list=, productions=)` parses criteria on physical quantities (`response matches answer`, `dimension(response) matches dimension(answer)`, and `=`, `<=`, `>=`, `<`, `>` between quantities). Its defaults are `quantity_grammar.QUANTITY_TOKENS` / `QUANTITY_PRODUCTIONS`. It was `generate_criteria_parser` in compareExpressions' physical-quantity context, and its parse trees are unchanged. It takes the same arguments as `build_criteria_parser`, so either can fill the same slot. `reserved_expressions` isn't used yet: only `response` and `answer` scan as quantities. - `split_criteria(criteria)` splits a string of criteria on the commas outside `()`, `[]` and `{}`, and strips each criterion. It was `split_criteria` in compareExpressions' `evaluation.py`. Unlike that version, it raises `CriteriaSyntaxError` (new, a `CriteriaError` and `ValueError`) when a bracket is closed before it is opened, or left open. The old version stopped splitting at the first such bracket instead. +- `where_substitutions(substitutions)` gives `[(name, expression), ...]` for the substitution part of a `where` criterion (the second child of a `WHERE` node), with the expressions as text. It replaces the node-reading half of compareExpressions' `parse_where_substitutions`; the caller now parses the expressions, so this package doesn't need `expression_parsing`. The grammar also accepts other relations after `where` (`where x > 2`, `where x proportional to y`). compareExpressions' `where_substitution_nodes` returned the node itself for these, instead of a list, and this function raises `CriteriaSyntaxError` for them. A `SEPARATOR` that mixes in such a relation (`where a = 2; x > 3`) used to be read as substitutions, and also raises now. ### Changed diff --git a/criteria/src/lambdafeedback/criteria/__init__.py b/criteria/src/lambdafeedback/criteria/__init__.py index 6e053a7..9d32edc 100644 --- a/criteria/src/lambdafeedback/criteria/__init__.py +++ b/criteria/src/lambdafeedback/criteria/__init__.py @@ -2,7 +2,9 @@ - :func:`build_criteria_parser` parses criteria such as ``response = answer where a = 2``; :func:`split_criteria` splits a - comma-separated string of criteria into them. + comma-separated string of criteria into them, and + :func:`where_substitutions` reads the substitutions of a ``where`` + criterion. - :func:`build_quantity_criteria_parser` parses criteria on physical quantities, such as ``response matches answer``. - :class:`CriteriaGraph` links evaluations to the criteria they establish, @@ -32,6 +34,7 @@ from .quantity_grammar import build_quantity_criteria_parser from .split import split_criteria from .tree import CriteriaTree +from .where import where_substitutions # Library convention: callers decide where log records go (``logging.basicConfig(level=logging.DEBUG)`` shows them). logging.getLogger(__name__).addHandler(logging.NullHandler()) @@ -61,4 +64,5 @@ "build_criteria_parser", "build_quantity_criteria_parser", "split_criteria", + "where_substitutions", ] diff --git a/criteria/src/lambdafeedback/criteria/where.py b/criteria/src/lambdafeedback/criteria/where.py new file mode 100644 index 0000000..7094c3b --- /dev/null +++ b/criteria/src/lambdafeedback/criteria/where.py @@ -0,0 +1,29 @@ +"""The substitutions of a ``where`` criterion, such as ``response = answer where a = 2; b = 3``.""" + +from __future__ import annotations + +from lambdafeedback.slr_parsing import ExprNode + +from .errors import CriteriaSyntaxError + + +def _substitution(node: ExprNode) -> tuple[str, str]: + """``(name, expression)`` for a substitution such as ``a = 2``.""" + if node.label != "EQUALITY": + raise CriteriaSyntaxError(f"`{node.content_string()}` is not a substitution such as `a = 2`.") + name, expression = node.children + return name.content_string(), expression.content_string() + + +def where_substitutions(substitutions: ExprNode) -> list[tuple[str, str]]: + """``[(name, expression), ...]`` for the substitution part of a ``where`` criterion. + + ``substitutions`` is the second child of a ``WHERE`` node from + :func:`.build_criteria_parser`: one ``EQUALITY`` or a ``SEPARATOR`` of + them. The expressions are returned as text, for the caller to parse. + Raises :class:`CriteriaSyntaxError` for any other relation, such as + ``where x > 2``. + """ + if substitutions.label == "SEPARATOR": + return [_substitution(node) for node in substitutions.children] + return [_substitution(substitutions)] diff --git a/criteria/tests/test_criteria.py b/criteria/tests/test_criteria.py index 3e7e4db..e6daf20 100644 --- a/criteria/tests/test_criteria.py +++ b/criteria/tests/test_criteria.py @@ -16,6 +16,7 @@ build_criteria_parser, build_quantity_criteria_parser, split_criteria, + where_substitutions, ) from lambdafeedback.slr_parsing import append_last, create_node, infix, join, proceed @@ -106,6 +107,31 @@ def test_unbalanced_brackets_raise(self, criteria): split_criteria(criteria) +class TestWhereSubstitutions: + @pytest.mark.parametrize( + "criterion, expected", + [ + ("response = answer where a = 2", [("a", "2")]), + ("response = answer where a = 2; b = x+1", [("a", "2"), ("b", "x+1")]), + ("response = answer where a = 2; b = 3; c = 4", [("a", "2"), ("b", "3"), ("c", "4")]), + ], + ) + def test_substitutions(self, criterion, expected): + assert where_substitutions(_parse(criterion).children[1]) == expected + + @pytest.mark.parametrize( + "criterion", + [ + "response = answer where x > 2", + "response = answer where x proportional to y", + "response = answer where a = 2; x > 3", + ], + ) + def test_other_relations_raise(self, criterion): + with pytest.raises(CriteriaSyntaxError): + where_substitutions(_parse(criterion).children[1]) + + class TestCriteriaGraph: def test_build_and_serialise(self): g = CriteriaGraph("root")