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

🎨💥Remove entered_input (used by FC evaluation) from lots of signatures; Use context var instead #199

Merged
merged 4 commits into from
Jul 4, 2022
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
33 changes: 20 additions & 13 deletions minimal_working_example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"# omit this import and apply in the real code\n",
"import nest_asyncio\n",
"\n",
"from ahbicht.content_evaluation import fc_evaluators\n",
"from ahbicht.mapping_results import PackageKeyConditionExpressionMapping\n",
"\n",
"nest_asyncio.apply() # can be omitted outside jupyter"
Expand All @@ -29,12 +30,12 @@
"name": "stdout",
"output_type": "stream",
"text": [
"Tree(Token('RULE', 'ahb_expression'), [Tree('single_requirement_indicator_expression', [Token('MODAL_MARK', 'Muss'), Tree('and_composition', [Tree('and_composition', [Tree(Token('RULE', 'condition'), [Token('CONDITION_KEY', '2')]), Tree('then_also_composition', [Tree('and_composition', [Tree('or_composition', [Tree(Token('RULE', 'condition'), [Token('CONDITION_KEY', '3')]), Tree(Token('RULE', 'condition'), [Token('CONDITION_KEY', '4')])]), Tree(Token('RULE', 'package'), [Token('PACKAGE_KEY', '123P')])]), Tree(Token('RULE', 'condition'), [Token('CONDITION_KEY', '901')])])]), Tree(Token('RULE', 'condition'), [Token('CONDITION_KEY', '555')])])])])\n"
"Tree(Token('RULE', 'ahb_expression'), [Tree('single_requirement_indicator_expression', [Token('MODAL_MARK', 'Muss'), Tree('and_composition', [Tree(Token('RULE', 'condition'), [Token('CONDITION_KEY', '2')]), Tree('and_composition', [Tree('then_also_composition', [Tree('and_composition', [Tree('or_composition', [Tree(Token('RULE', 'condition'), [Token('CONDITION_KEY', '3')]), Tree(Token('RULE', 'condition'), [Token('CONDITION_KEY', '4')])]), Tree(Token('RULE', 'package'), [Token('PACKAGE_KEY', '123P')])]), Tree(Token('RULE', 'condition'), [Token('CONDITION_KEY', '901')])]), Tree(Token('RULE', 'condition'), [Token('CONDITION_KEY', '555')])])])])])\n"
]
},
{
"data": {
"text/plain": "{'type': 'ahb_expression',\n 'children': [{'tree': {'type': 'single_requirement_indicator_expression',\n 'children': [{'tree': None,\n 'token': {'value': 'Muss', 'type': 'MODAL_MARK'}},\n {'tree': {'type': 'and_composition',\n 'children': [{'tree': {'type': 'and_composition',\n 'children': [{'tree': {'type': 'condition',\n 'children': [{'tree': None,\n 'token': {'value': '2', 'type': 'CONDITION_KEY'}}]},\n 'token': None},\n {'tree': {'type': 'then_also_composition',\n 'children': [{'tree': {'type': 'and_composition',\n 'children': [{'tree': {'type': 'or_composition',\n 'children': [{'tree': {'type': 'condition',\n 'children': [{'tree': None,\n 'token': {'value': '3', 'type': 'CONDITION_KEY'}}]},\n 'token': None},\n {'tree': {'type': 'condition',\n 'children': [{'tree': None,\n 'token': {'value': '4', 'type': 'CONDITION_KEY'}}]},\n 'token': None}]},\n 'token': None},\n {'tree': {'type': 'package',\n 'children': [{'tree': None,\n 'token': {'value': '123P', 'type': 'PACKAGE_KEY'}}]},\n 'token': None}]},\n 'token': None},\n {'tree': {'type': 'condition',\n 'children': [{'tree': None,\n 'token': {'value': '901', 'type': 'CONDITION_KEY'}}]},\n 'token': None}]},\n 'token': None}]},\n 'token': None},\n {'tree': {'type': 'condition',\n 'children': [{'tree': None,\n 'token': {'value': '555', 'type': 'CONDITION_KEY'}}]},\n 'token': None}]},\n 'token': None}]},\n 'token': None}]}"
"text/plain": "{'children': [{'tree': {'children': [{'tree': None,\n 'token': {'value': 'Muss', 'type': 'MODAL_MARK'}},\n {'tree': {'children': [{'tree': {'children': [{'tree': None,\n 'token': {'value': '2', 'type': 'CONDITION_KEY'}}],\n 'type': 'condition'},\n 'token': None},\n {'tree': {'children': [{'tree': {'children': [{'tree': {'children': [{'tree': {'children': [{'tree': {'children': [{'tree': None,\n 'token': {'value': '3', 'type': 'CONDITION_KEY'}}],\n 'type': 'condition'},\n 'token': None},\n {'tree': {'children': [{'tree': None,\n 'token': {'value': '4', 'type': 'CONDITION_KEY'}}],\n 'type': 'condition'},\n 'token': None}],\n 'type': 'or_composition'},\n 'token': None},\n {'tree': {'children': [{'tree': None,\n 'token': {'value': '123P', 'type': 'PACKAGE_KEY'}}],\n 'type': 'package'},\n 'token': None}],\n 'type': 'and_composition'},\n 'token': None},\n {'tree': {'children': [{'tree': None,\n 'token': {'value': '901', 'type': 'CONDITION_KEY'}}],\n 'type': 'condition'},\n 'token': None}],\n 'type': 'then_also_composition'},\n 'token': None},\n {'tree': {'children': [{'tree': None,\n 'token': {'value': '555', 'type': 'CONDITION_KEY'}}],\n 'type': 'condition'},\n 'token': None}],\n 'type': 'and_composition'},\n 'token': None}],\n 'type': 'and_composition'},\n 'token': None}],\n 'type': 'single_requirement_indicator_expression'},\n 'token': None}],\n 'type': 'ahb_expression'}"
},
"execution_count": 2,
"metadata": {},
Expand Down Expand Up @@ -68,7 +69,7 @@
"outputs": [
{
"data": {
"text/plain": "{'ahb_expression': [{'single_requirement_indicator_expression': ['Muss',\n {'and_composition': [{'and_composition': ['2',\n {'then_also_composition': [{'and_composition': [{'or_composition': ['3',\n '4']},\n '123P']},\n '901']}]},\n '555']}]}]}"
"text/plain": "{'ahb_expression': [{'single_requirement_indicator_expression': ['Muss',\n {'and_composition': ['2',\n {'and_composition': [{'then_also_composition': [{'and_composition': [{'or_composition': ['3',\n '4']},\n '123P']},\n '901']},\n '555']}]}]}]}"
},
"execution_count": 3,
"metadata": {},
Expand Down Expand Up @@ -196,7 +197,9 @@
" The result is always an EvaluatedFormatConstraint.\n",
" \"\"\"\n",
" # insert your own logic here\n",
" if entered_input[\"data\"].lower() == entered_input[\"data\"]:\n",
" if not entered_input:\n",
" return EvaluatedFormatConstraint(False, f\"The input is empty but expected lower case.\")\n",
" if entered_input.lower() == entered_input:\n",
" return EvaluatedFormatConstraint(True, None)\n",
" return EvaluatedFormatConstraint(\n",
" False, f\"The input '{entered_input['data']}' does not obey format constraint 901.\"\n",
Expand Down Expand Up @@ -275,7 +278,6 @@
" \"2\": ConditionFulfilledValue.FULFILLED,\n",
" \"3\": ConditionFulfilledValue.UNFULFILLED,\n",
" \"4\": ConditionFulfilledValue.FULFILLED,\n",
" \"data\": \"some lower case stuff which will be checked in 901\",\n",
"}\n",
"\n",
"my_evaluatable_data = EvaluatableData(\n",
Expand Down Expand Up @@ -331,8 +333,16 @@
"\n",
"# But later on we can provide AHBicht with the content evaluation results ...\n",
"# Providing the content evaluation results to find out, if a line in the AHB is actually required,\n",
"# is called expression evaluation\n",
"expression_evaluation_result = await evaluate_ahb_expression_tree(tree, entered_input=hardcoded_content_evaluations)"
"# is called expression evaluation.\n",
"\n",
"# If there's any text or user input associated with the field whose expression/tree we evaluated,\n",
"# then we can set a context variable that will be used to e.g. evaluate a format constraint.\n",
"# This is relevant if you use AHBicht for validation (does my edifact message obey the AHB?).\n",
"# If you use the MAUS data model to model the AHB and message data, this will be set under the hood.\n",
"fc_evaluators.text_to_be_evaluated_by_format_constraint.set(\"it's all lower case. should be fine for 901\")\n",
"# Now the context variable is set and ready to be used by the FC evaluator.\n",
"\n",
"expression_evaluation_result = await evaluate_ahb_expression_tree(tree)"
],
"metadata": {
"collapsed": false,
Expand Down Expand Up @@ -414,7 +424,7 @@
" edifact_format=EdifactFormat.UTILMD,\n",
" edifact_format_version=EdifactFormatVersion.FV2104,\n",
") # this does all the magic, no need to manually define classes\n",
"evaluated = await evaluate_ahb_expression_tree(tree, entered_input=\"\")\n",
"evaluated = await evaluate_ahb_expression_tree(tree)\n",
"print(evaluated)"
],
"metadata": {
Expand Down Expand Up @@ -482,7 +492,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"[(ContentEvaluationResult(hints={'555': 'Hinweis 555'}, format_constraints={'901': EvaluatedFormatConstraint(format_constraint_fulfilled=True, error_message=None)}, requirement_constraints={'2': <ConditionFulfilledValue.FULFILLED: 'FULFILLED'>, '3': <ConditionFulfilledValue.FULFILLED: 'FULFILLED'>, '4': <ConditionFulfilledValue.FULFILLED: 'FULFILLED'>}, packages={}, id=None), AhbExpressionEvaluationResult(requirement_indicator=<ModalMark.MUSS: 'MUSS'>, requirement_constraint_evaluation_result=RequirementConstraintEvaluationResult(requirement_constraints_fulfilled=True, requirement_is_conditional=True, format_constraints_expression='[901]', hints='Hinweis 555'), format_constraint_evaluation_result=FormatConstraintEvaluationResult(format_constraints_fulfilled=True, error_message=None))), (ContentEvaluationResult(hints={'555': 'Hinweis 555'}, format_constraints={'901': EvaluatedFormatConstraint(format_constraint_fulfilled=True, error_message=None)}, requirement_constraints={'2': <ConditionFulfilledValue.FULFILLED: 'FULFILLED'>, '3': <ConditionFulfilledValue.FULFILLED: 'FULFILLED'>, '4': <ConditionFulfilledValue.UNFULFILLED: 'UNFULFILLED'>}, packages={}, id=None), AhbExpressionEvaluationResult(requirement_indicator=<ModalMark.MUSS: 'MUSS'>, requirement_constraint_evaluation_result=RequirementConstraintEvaluationResult(requirement_constraints_fulfilled=True, requirement_is_conditional=True, format_constraints_expression='[901]', hints='Hinweis 555'), format_constraint_evaluation_result=FormatConstraintEvaluationResult(format_constraints_fulfilled=True, error_message=None)))]\n"
"[(ContentEvaluationResult(hints={'555': 'Hinweis 555'}, format_constraints={'901': EvaluatedFormatConstraint(format_constraint_fulfilled=True, error_message=None)}, requirement_constraints={'2': <ConditionFulfilledValue.FULFILLED: 'FULFILLED'>, '3': <ConditionFulfilledValue.FULFILLED: 'FULFILLED'>, '4': <ConditionFulfilledValue.FULFILLED: 'FULFILLED'>}, packages={}, id=None), AhbExpressionEvaluationResult(requirement_indicator=<ModalMark.MUSS: 'MUSS'>, requirement_constraint_evaluation_result=RequirementConstraintEvaluationResult(requirement_constraints_fulfilled=True, requirement_is_conditional=True, format_constraints_expression='[901]', hints='foo'), format_constraint_evaluation_result=FormatConstraintEvaluationResult(format_constraints_fulfilled=True, error_message=None))), (ContentEvaluationResult(hints={'555': 'Hinweis 555'}, format_constraints={'901': EvaluatedFormatConstraint(format_constraint_fulfilled=True, error_message=None)}, requirement_constraints={'2': <ConditionFulfilledValue.FULFILLED: 'FULFILLED'>, '3': <ConditionFulfilledValue.FULFILLED: 'FULFILLED'>, '4': <ConditionFulfilledValue.UNFULFILLED: 'UNFULFILLED'>}, packages={}, id=None), AhbExpressionEvaluationResult(requirement_indicator=<ModalMark.MUSS: 'MUSS'>, requirement_constraint_evaluation_result=RequirementConstraintEvaluationResult(requirement_constraints_fulfilled=True, requirement_is_conditional=True, format_constraints_expression='[901]', hints='foo'), format_constraint_evaluation_result=FormatConstraintEvaluationResult(format_constraints_fulfilled=True, error_message=None)))]\n"
]
}
],
Expand All @@ -497,10 +507,7 @@
" )\n",
" try:\n",
" expression_evaluation_result = await evaluate_ahb_expression_tree(\n",
" await parse_expression_including_unresolved_subexpressions(\n",
" expression=\"Muss [2] U ([3] O [4])[901] U [555]\"\n",
" ),\n",
" entered_input=\"\",\n",
" await parse_expression_including_unresolved_subexpressions(expression=\"Muss [2] U ([3] O [4])[901] U [555]\")\n",
" )\n",
" except NotImplementedError:\n",
" # there are cases that don't make any sense and won't occur out in the wild. These are mostly related to neutral elements where no neutral elements are expected. We can just ignore them\n",
Expand Down
46 changes: 33 additions & 13 deletions src/ahbicht/content_evaluation/fc_evaluators.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,39 @@
import asyncio
import inspect
from abc import ABC
from contextvars import ContextVar
from typing import Coroutine, Dict, List, Optional

from ahbicht.content_evaluation.evaluators import Evaluator
from ahbicht.content_evaluation.german_strom_and_gas_tag import has_no_utc_offset, is_xtag_limit
from ahbicht.evaluation_results import FormatConstraintEvaluationResult
from ahbicht.expressions.condition_nodes import EvaluatedFormatConstraint

text_to_be_evaluated_by_format_constraint: ContextVar[Optional[str]] = ContextVar(
"text_to_be_evaluated_by_format_constraint", default=None
)
"""
This context variable holds the text that is to be analysed/evaluated by the format constraint evaluator.
It will always return the "correct" value in your context. You only have to manually set this context variable if you're
evaluating an expression outside the validation framework.
The conceptual difference to the EvaluatableData which are dependency injected using the EvaluatableDataProvider is,
that the data evaluated in a format constraint (via the context variable) vary over the time span of one validation run.
The EvaluatableData are stable in that regard.
"""

# The idea behind the context variable is to avoid passing the string/text to be evaluated by FC evaluators through many
# layers of code (including lark classes like transformers where we formerly had the string as constructor argument).
# Now it needs to be set once and only once when the entered input is determined (e.g. when reading user input).
# Then we can forget about it, it does not bloat our function signatures (where in >90% of the cases it has just been
# forwarded to the next layer of code).
# The context variable also greatly simplifies the debugging/error analysis.
# Instead of tracing the entered input all the way down from the ahbicht API to the FC Evaluator method,
# you now just need to watch the two places in the code where the values are actually set:
# 1. in the ahbicht code base in the validation
# 2. in custom code using ahbicht(?)
# The single evaluation methods of the FC evaluators (e.g. "def evaluate_987") are still provided with the value as a
# usual function argument to prevent the methods from needing to access the context variable themselves.


class FcEvaluator(Evaluator, ABC):
"""
Expand Down Expand Up @@ -77,23 +103,21 @@ def evaluate_935(self, entered_input: str) -> EvaluatedFormatConstraint:
"""
return is_xtag_limit(entered_input, "Gas")

async def evaluate_single_format_constraint(
self, condition_key: str, entered_input: Optional[str]
) -> EvaluatedFormatConstraint:
async def evaluate_single_format_constraint(self, condition_key: str) -> EvaluatedFormatConstraint:
"""
Evaluates the format constraint with the given key.
:param condition_key: key of the condition, e.g. "950"
:param entered_input: the entered input whose format should be checked, e.g. "12345678913"
:return: If the format constraint is fulfilled and an optional error_message.
"""
text_to_be_evaluated = text_to_be_evaluated_by_format_constraint.get()
evaluation_method = self.get_evaluation_method(condition_key)
if evaluation_method is None:
raise NotImplementedError(f"There is no content_evaluation method for format constraint '{condition_key}'")
result: EvaluatedFormatConstraint
if inspect.iscoroutinefunction(evaluation_method):
result = await evaluation_method(entered_input)
result = await evaluation_method(text_to_be_evaluated)
else:
result = evaluation_method(entered_input)
result = evaluation_method(text_to_be_evaluated)
try:
if result.format_constraint_fulfilled is False and result.error_message is None:
result.error_message = f"Condition [{condition_key}] has to be fulfilled."
Expand All @@ -106,14 +130,12 @@ async def evaluate_single_format_constraint(
raise attribute_error
return result

async def evaluate_format_constraints(
self, condition_keys: List[str], entered_input: Optional[str]
) -> Dict[str, EvaluatedFormatConstraint]:
async def evaluate_format_constraints(self, condition_keys: List[str]) -> Dict[str, EvaluatedFormatConstraint]:
"""
Evaluate the entered_input in regard to all the formats provided in condition_keys.
"""
tasks: List[Coroutine] = [
self.evaluate_single_format_constraint(condition_key, entered_input) for condition_key in condition_keys
self.evaluate_single_format_constraint(condition_key) for condition_key in condition_keys
]
results: List[EvaluatedFormatConstraint] = await asyncio.gather(*tasks)

Expand All @@ -135,9 +157,7 @@ def __init__(self, results: Dict[str, EvaluatedFormatConstraint]):
self._results: Dict[str, EvaluatedFormatConstraint] = results

# pylint: disable=unused-argument
async def evaluate_single_format_constraint(
self, condition_key: str, entered_input: Optional[str]
) -> EvaluatedFormatConstraint:
async def evaluate_single_format_constraint(self, condition_key: str) -> EvaluatedFormatConstraint:
try:
return self._results[condition_key]
except KeyError as key_error:
Expand Down
Loading