Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨Parse Repeatability in Packages: [123P4..5] (without using it yet) #120

Merged
merged 23 commits into from
Feb 25, 2022
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e9ad672
add repeatability to condition parser
hf-kklein Feb 2, 2022
9e2e072
parse the repeatability in the package rule, handle invalid n..m
hf-kklein Feb 2, 2022
451e723
parse repeatability
hf-kklein Feb 2, 2022
1767df8
more n testcases
hf-kklein Feb 2, 2022
373cec4
Merge branch 'main' into wiederholbarkeiten
hf-kklein Feb 3, 2022
9862159
use only 1 point for validation n<=m, no duplicated check
hf-kklein Feb 4, 2022
9d6af3c
disable pylint. we're fine with not using the repeatability yet
hf-kklein Feb 4, 2022
417e126
Merge remote-tracking branch 'origin/wiederholbarkeiten' into wiederh…
hf-kklein Feb 4, 2022
355f582
Merge branch 'main' into wiederholbarkeiten
hf-kklein Feb 10, 2022
f638be2
use attrs 21.4.0 syntax in Repeatability
hf-kklein Feb 10, 2022
52e2797
Merge branch 'main' into wiederholbarkeiten
hf-kklein Feb 10, 2022
4788375
Merge branch 'main' into wiederholbarkeiten
hf-kklein Feb 18, 2022
16a1632
remove condition that is never true
hf-kklein Feb 25, 2022
c36a767
Update src/ahbicht/mapping_results.py
hf-kklein Feb 25, 2022
b4f35d2
more comments on token list
hf-kklein Feb 25, 2022
c45beba
Merge remote-tracking branch 'origin/wiederholbarkeiten' into wiederh…
hf-kklein Feb 25, 2022
69b6720
Merge branch 'main' into wiederholbarkeiten
hf-kklein Feb 25, 2022
6fca6e4
isort .
hf-kklein Feb 25, 2022
71d8bfd
align code and docstring
hf-kklein Feb 25, 2022
ccb1221
✅ (Failing) test for repeatability 1...1
hf-aschloegl Feb 25, 2022
941b77c
"fix" ;)
hf-kklein Feb 25, 2022
04063ad
better docstring
hf-kklein Feb 25, 2022
00feb2c
pylint
hf-kklein Feb 25, 2022
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
3 changes: 1 addition & 2 deletions src/ahbicht/expressions/ahb_expression_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@ def parse_ahb_expression_to_single_requirement_indicator_expressions(ahb_express
PREFIX_OPERATOR: "X"i | "O"i | "U"i
MODAL_MARK: /M(uss)?|S(oll)?|K(ann)?/i
// Matches if it looks like a condition expression, but does not yet check if it is a syntactically valid one:
CONDITION_EXPRESSION: /(?!\BU\B)[\[\]\(\)U∧O∨X⊻\d\sP]+/i
CONDITION_EXPRESSION: /(?!\BU\B)[\[\]\(\)U∧O∨X⊻\d\sP\.]+/i
"""
# todo: implement wiederholbarkeiten
# Regarding the negative lookahead in the condition expression regex see examples https://regex101.com/r/6fFHD4/1
# and CTRL+F for "Mus[2]" in the unittest that fails if you remove the lookahead.
parser = Lark(grammar, start="ahb_expression")
Expand Down
9 changes: 4 additions & 5 deletions src/ahbicht/expressions/condition_expression_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def parse_condition_expression_to_tree(condition_expression: str) -> Tree[Token]
:return parsed_tree: Tree
"""

grammar = """
grammar = r"""
?expression: expression "O"i expression -> or_composition
| expression "∨" expression -> or_composition
| expression "X"i expression -> xor_composition
Expand All @@ -38,29 +38,28 @@ def parse_condition_expression_to_tree(condition_expression: str) -> Tree[Token]
| package
| condition
?brackets: "(" expression ")"
package: "[" PACKAGE_KEY "]" // a rule for packages
package: "[" PACKAGE_KEY REPEATABILITY? "]" // a rule for packages
condition: "[" CONDITION_KEY "]" // a rule for condition keys
CONDITION_KEY: INT // a TERMINAL for all the remaining ints (lower priority)
REPEATABILITY: /\d+\.{2}[1-9]\d*/ // a terminal for repetitions n..m with n>=0 and m>n
hf-kklein marked this conversation as resolved.
Show resolved Hide resolved
PACKAGE_KEY: INT "P" // a TERMINAL for all INTs followed by "P" (high priority)
%import common.INT
%import common.WS
%ignore WS // WS = whitespace
"""
# todo: add wiederholbarkeiten https://github.com/Hochfrequenz/ahbicht/issues/96
parser = Lark(grammar, start="expression")
try:
parsed_tree = parser.parse(condition_expression)
except (UnexpectedEOF, UnexpectedCharacters, TypeError) as eof:
raise SyntaxError(
"""Please make sure that:
* all conditions have the form [INT]
* all packages have the form [INTP]
* all packages have the form [INTPn..m]
* no conditions are empty
* all compositions are combined by operators 'U'/'O'/'X' or without an operator
* all open brackets are closed again and vice versa
"""
) from eof
# todo: implement wiederholbarkeiten
return parsed_tree


Expand Down
32 changes: 24 additions & 8 deletions src/ahbicht/expressions/expression_resolver.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""
This module makes it possible to parse expressions including all their subexpressions, if present.
for example ahb_expressions which contain condition_expressions or condition_expressions which contain packages.
Parsing expressions that are nested into other expressions is refered to as "resolving".
Parsing expressions that are nested into other expressions is referred to as "resolving".
"""
import asyncio
import inspect
from typing import Awaitable, List, Union
from typing import Awaitable, List, Optional, Union

import inject
from lark import Token, Transformer, Tree
Expand All @@ -14,6 +14,7 @@
from ahbicht.expressions.ahb_expression_parser import parse_ahb_expression_to_single_requirement_indicator_expressions
from ahbicht.expressions.condition_expression_parser import parse_condition_expression_to_tree
from ahbicht.expressions.package_expansion import PackageResolver
from ahbicht.mapping_results import Repeatability, parse_repeatability


async def parse_expression_including_unresolved_subexpressions(
Expand Down Expand Up @@ -101,11 +102,26 @@ def package(self, tokens: List[Token]) -> Awaitable[Tree]:
"""
try to resolve the package using the injected PackageResolver
"""
return self._package_async(tokens)

async def _package_async(self, tokens: List[Token]) -> Tree[Token]:
resolved_package = await self._resolver.get_condition_expression(tokens[0].value)
# The grammar guarantees that there is always exactly 1 package_key token/terminal.
# But the repeatability token is optional, so the list repeatability_tokens might contain 0 or 1 entries
# They all come in the same `tokens` list which we split in the following two lines.
package_key_token = [t for t in tokens if t.type == "PACKAGE_KEY"][0]
repeatability_tokens = [t for t in tokens if t.type == "REPEATABILITY"]
hf-aschloegl marked this conversation as resolved.
Show resolved Hide resolved
# pylint: disable=unused-variable
# we parse the repeatability, but we don't to anything with it, yet.
repeatability: Optional[Repeatability]
if len(repeatability_tokens) == 1:
repeatability = parse_repeatability(repeatability_tokens[0].value)
else:
repeatability = None
Comment on lines +115 to +116
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

der fall len(repeatability_tokens)>1 solte nie auftreten, vong grammar her

return self._package_async(package_key_token)

async def _package_async(self, package_key_token: Token) -> Tree[Token]:
resolved_package = await self._resolver.get_condition_expression(package_key_token.value)
if not resolved_package.has_been_resolved_successfully():
raise NotImplementedError(f"The package '{tokens[0].value}' could not be resolved by {self._resolver}")
raise NotImplementedError(
f"The package '{package_key_token.value}' could not be resolved by {self._resolver}"
)
# the package_expression is not None because that's the definition of "has been resolved successfully"
return parse_condition_expression_to_tree(resolved_package.package_expression) # type:ignore[arg-type]
tree_result = parse_condition_expression_to_tree(resolved_package.package_expression) # type:ignore[arg-type]
return tree_result
2 changes: 1 addition & 1 deletion src/ahbicht/expressions/package_expansion.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
from abc import ABC, abstractmethod
from typing import Mapping, Optional

# pylint:disable=too-few-public-methods
from maus.edifact import EdifactFormat, EdifactFormatVersion

from ahbicht.mapping_results import PackageKeyConditionExpressionMapping


# pylint:disable=too-few-public-methods
class PackageResolver(ABC):
"""
A package resolver provides condition expressions for given package keys.
Expand Down
56 changes: 55 additions & 1 deletion src/ahbicht/mapping_results.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""
This module contains classes that are returned by mappers, meaning they contain a mapping.
"""
from typing import Optional
import re
from typing import Match, Optional

import attrs
from marshmallow import Schema, fields, post_load
Expand Down Expand Up @@ -88,3 +89,56 @@ def deserialize(self, data, **kwargs) -> PackageKeyConditionExpressionMapping:
Converts the barely typed data dictionary into an actual :class:`.PackageKeyConditionExpressionMapping`
"""
return PackageKeyConditionExpressionMapping(**data)


# pylint:disable=unused-argument
def check_max_greater_or_equal_than_min(instance: "Repeatability", attribute, value):
"""
assert that 0<=min<max
"""
if not 0 <= instance.min_occurrences < instance.max_occurrences:
raise ValueError(f"0≤n<m is not fulfilled for n={instance.min_occurrences}, m={instance.max_occurrences}")


# pylint:disable=too-few-public-methods
@attrs.define(auto_attribs=True, kw_only=True)
class Repeatability:
"""
describes how often a segment/code must be used when a "repeatability" is provided with packages
"""

min_occurrences: int = attrs.field(
validator=attrs.validators.and_(attrs.validators.instance_of(int), check_max_greater_or_equal_than_min)
)
"""
how often the segment/code has to be repeated (lower, inclusive bound); may be 0 for optional packages
"""

max_occurrences: int = attrs.field(
validator=attrs.validators.and_(attrs.validators.instance_of(int), check_max_greater_or_equal_than_min)
)
"""
how often the segment/coode may be repeated at most (upper, inclusive bound).
This is inclusive meaning that [123P0..1] leads to max_occurrences==1
"""

def is_optional(self) -> bool:
"""
returns true if the package used together with this repeatability is optional
"""
return self.min_occurrences == 0


_repeatability_pattern = re.compile(r"^(?P<min>\d+)\.{2}(?P<max>\d+)$") #: a pattern to match "n..m" repeatabilities


def parse_repeatability(repeatability_string: str) -> Repeatability:
"""
parses the given string as repeatability; f.e. `17..23` is parsed as min=17, max=23
"""
match: Optional[Match[str]] = _repeatability_pattern.match(repeatability_string)
if match is None:
raise ValueError(f"The given string '{repeatability_string}' could not be parsed as repeatability")
min_repeatability = int(match["min"])
max_repeatability = int(match["max"])
return Repeatability(min_occurrences=min_repeatability, max_occurrences=max_repeatability)
2 changes: 1 addition & 1 deletion unittests/test_ahb_expression_evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def side_effect_rc_evaluation(condition_expression):
SyntaxError,
"""Please make sure that:
* all conditions have the form [INT]
* all packages have the form [INTP]
* all packages have the form [INTPn..m]
* no conditions are empty
* all compositions are combined by operators 'U'/'O'/'X' or without an operator
* all open brackets are closed again and vice versa
Expand Down
19 changes: 18 additions & 1 deletion unittests/test_condition_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,23 @@ def test_parse_valid_expression_to_tree_with_format_constraints(self, expression
],
),
),
pytest.param(
# nested brackets
"[10P1..5]U([1]O[2])",
Tree(
"and_composition",
[
Tree(Token("RULE", "package"), [Token("PACKAGE_KEY", "10P"), Token("REPEATABILITY", "1..5")]),
Tree(
"or_composition",
[
Tree(Token("RULE", "condition"), [Token("CONDITION_KEY", "1")]),
Tree(Token("RULE", "condition"), [Token("CONDITION_KEY", "2")]),
],
),
],
),
),
],
)
def test_parse_valid_expression_with_brackets_to_tree(self, expression: str, expected_tree: Tree):
Expand Down Expand Up @@ -484,7 +501,7 @@ def test_parse_invalid_expression(self, expression: str):

assert """Please make sure that:
* all conditions have the form [INT]
* all packages have the form [INTP]
* all packages have the form [INTPn..m]
* no conditions are empty
* all compositions are combined by operators 'U'/'O'/'X' or without an operator
* all open brackets are closed again and vice versa
Expand Down
2 changes: 1 addition & 1 deletion unittests/test_expression_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ async def test_expression_resolver_failing(self, expression: str):

assert """Please make sure that:
* all conditions have the form [INT]
* all packages have the form [INTP]
* all packages have the form [INTPn..m]
* no conditions are empty
* all compositions are combined by operators 'U'/'O'/'X' or without an operator
* all open brackets are closed again and vice versa
Expand Down
22 changes: 22 additions & 0 deletions unittests/test_package_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ def inject_package_resolver(self, request: SubRequest):
"unexpanded_expression, expected_expanded_expression",
[
pytest.param("[123P]", "[1] U ([2] O [3])"),
pytest.param("[123P7..8]", "[1] U ([2] O [3])"),
pytest.param("[123P10..11]", "[1] U ([2] O [3])"),
pytest.param("[123P0..5]", "[1] U ([2] O [3])"),
pytest.param("[17] U [123P]", "[17] U ([1] U ([2] O [3]))"),
],
)
Expand All @@ -47,3 +50,22 @@ async def test_correct_injection(
assert actual_tree is not None
expected_expanded_tree = parse_condition_expression_to_tree(expected_expanded_expression)
assert actual_tree == expected_expanded_tree

@pytest.mark.parametrize(
"inject_package_resolver",
[{"123P": "[1] U ([2] O [3])"}],
indirect=True,
)
@pytest.mark.parametrize(
"unexpanded_expression, error_message",
[
pytest.param("[123P8..7]", "0≤n<m is not fulfilled for n=8, m=7"),
],
)
async def test_invalid_package_repeatability(
self, inject_package_resolver, unexpanded_expression: str, error_message: str
):
unexpanded_tree = parse_condition_expression_to_tree(unexpanded_expression)
with pytest.raises(ValueError) as invalid_repeatability_error:
_ = await expand_packages(parsed_tree=unexpanded_tree)
assert error_message in str(invalid_repeatability_error)
2 changes: 1 addition & 1 deletion unittests/test_requirement_constraint_evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ async def test_evaluate_valid_ahb_expression(
SyntaxError,
"""Please make sure that:
* all conditions have the form [INT]
* all packages have the form [INTP]
* all packages have the form [INTPn..m]
* no conditions are empty
* all compositions are combined by operators 'U'/'O'/'X' or without an operator
* all open brackets are closed again and vice versa
Expand Down
29 changes: 29 additions & 0 deletions unittests/test_utility_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import pytest # type:ignore[import]

from ahbicht.mapping_results import Repeatability, parse_repeatability
from ahbicht.utility_functions import gather_if_necessary

pytestmark = pytest.mark.asyncio
Expand Down Expand Up @@ -36,3 +37,31 @@ class TestUtilityFunctions:
async def test_gather_if_necessary(self, mixed_input: List[Union[T, Awaitable[T]]], expected_result: List[T]):
actual = await gather_if_necessary(mixed_input)
assert actual == expected_result

@pytest.mark.parametrize(
"candidate, expected_result",
[
pytest.param("0..1", Repeatability(min_occurrences=0, max_occurrences=1)),
pytest.param("1..1", Repeatability(min_occurrences=1, max_occurrences=1)),
hf-aschloegl marked this conversation as resolved.
Show resolved Hide resolved
pytest.param("1..2", Repeatability(min_occurrences=1, max_occurrences=2)),
pytest.param("71..89", Repeatability(min_occurrences=71, max_occurrences=89)),
],
)
def test_parse_repeatability(self, candidate: str, expected_result: Repeatability):
actual = parse_repeatability(candidate)
assert actual == expected_result

@pytest.mark.parametrize(
"invalid_candidate",
[
pytest.param("1..0"),
pytest.param("77..76"),
pytest.param("0..0"),
pytest.param("-1..1"),
pytest.param("bullshit"),
pytest.param(""),
],
)
def test_parse_repeatability_failures(self, invalid_candidate):
with pytest.raises(ValueError):
_ = parse_repeatability(invalid_candidate)