Skip to content
Merged
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
1 change: 1 addition & 0 deletions criteria/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 5 additions & 1 deletion criteria/src/lambdafeedback/criteria/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -61,4 +64,5 @@
"build_criteria_parser",
"build_quantity_criteria_parser",
"split_criteria",
"where_substitutions",
]
29 changes: 29 additions & 0 deletions criteria/src/lambdafeedback/criteria/where.py
Original file line number Diff line number Diff line change
@@ -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)]
26 changes: 26 additions & 0 deletions criteria/tests/test_criteria.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand Down
Loading