Source code for ahbicht.expressions.expression_builder

"""
Module to create expressions from scratch.
"""

import re
from abc import ABC, abstractmethod
from typing import Generic, Optional, Protocol, Type, TypeVar, Union

from ahbicht.models.condition_nodes import (
    ConditionNode,
    EvaluatedComposition,
    EvaluatedFormatConstraint,
    Hint,
    RequirementConstraint,
    UnevaluatedFormatConstraint,
)
from ahbicht.models.enums import LogicalOperator

SupportedNodes = TypeVar("SupportedNodes")


[docs] class ExpressionBuilder(Generic[SupportedNodes], ABC): """ Class that helps to create expression strings. It separates the logical operation (connect two conditions with a logical operator) from the implementation which might differ depending on the condition type and other circumstances. """
[docs] @abstractmethod def get_expression(self) -> Optional[str]: """ Returns the expression string or none if there is no expression. :return: """ raise NotImplementedError("Has to be implemented by inheriting class.")
[docs] @abstractmethod def land(self, other: SupportedNodes) -> "ExpressionBuilder[SupportedNodes]": """ connects the expression with a logical and (LAND) :param other: condition or expression to be connected to the expression :return: """ raise NotImplementedError("Has to be implemented by inheriting class.")
[docs] @abstractmethod def lor(self, other: SupportedNodes) -> "ExpressionBuilder[SupportedNodes]": """ connects the expression with a logical or (LOR) :param other: condition or expression to be connected to the expression :return: """ raise NotImplementedError("Has to be implemented by inheriting class.")
[docs] @abstractmethod def xor(self, other: SupportedNodes) -> "ExpressionBuilder[SupportedNodes]": """ connects the expression with an exclusive or (XOR) :param other: condition or expression to be connected to the expression :return: """ raise NotImplementedError("Has to be implemented by inheriting class.")
TEffectiveFCExpressionBuilderArguments = Union[ # pylint:disable=invalid-name EvaluatedComposition, UnevaluatedFormatConstraint, Optional[str] ] # node types that have an effect on the built format constraint expression TUneffectiveFCExpressionBuilderArguments = Union[ # pylint:disable=invalid-name RequirementConstraint, EvaluatedComposition, Hint, Type[ConditionNode] ] # node types that are formally accepted as argument but don't # have any effect. Instead of checking which nodes contain format constraints all are put into the # FormatConstraintExpressionBuilder, but it only has an effect on those with format constraints. # Note that EvaluatedComposition is in both classes since they can have format constraints but don't have to. TSupportedFCExpressionBuilderArguments = Union[ # pylint:disable=invalid-name TEffectiveFCExpressionBuilderArguments, TUneffectiveFCExpressionBuilderArguments ]
[docs] class FormatConstraintExpressionBuilder(ExpressionBuilder[TSupportedFCExpressionBuilderArguments]): """ Class to create expressions that consist of FormatConstraints """ # the character `]` is escaped, although it's not necessary, just to avoid that it's being confused with group end _one_key_surrounded_by_brackets_pattern = re.compile(r"\((?P<body>\[\d+\])\)") # https://regex101.com/r/IauOei/1 # (?P<group_name>...) is a named group: https://docs.python.org/3/howto/regex.html#non-capturing-and-named-groups def __init__(self, init_condition_or_expression: TSupportedFCExpressionBuilderArguments) -> None: """ Start with a plain expression :param init_condition_or_expression: initial format constraint or existing expression """ self._expression: Optional[str] if isinstance(init_condition_or_expression, UnevaluatedFormatConstraint): # the condition key of the token in expression '[42]' is only '42' # so the get a valid expression, we add the square brackets self._expression = f"[{init_condition_or_expression.condition_key}]" elif ( isinstance(init_condition_or_expression, EvaluatedComposition) and init_condition_or_expression.format_constraints_expression ): self._expression = init_condition_or_expression.format_constraints_expression elif isinstance(init_condition_or_expression, str): self._expression = f"{init_condition_or_expression}" elif isinstance(init_condition_or_expression, (RequirementConstraint, EvaluatedComposition, Hint)): # requirement constraints and hints don't have any effect on the newly built format constraint expression # also evaluated compositions that don't have a format constraint expression self._expression = None else: # we should never come here self._expression = None
[docs] def get_expression(self) -> Optional[str]: # could add simplifications here return self._expression
[docs] def land(self, other: TSupportedFCExpressionBuilderArguments) -> "FormatConstraintExpressionBuilder": return self._connect(LogicalOperator.LAND, other)
[docs] def lor(self, other: TSupportedFCExpressionBuilderArguments) -> "FormatConstraintExpressionBuilder": return self._connect(LogicalOperator.LOR, other)
[docs] def xor(self, other: TSupportedFCExpressionBuilderArguments) -> "FormatConstraintExpressionBuilder": return self._connect(LogicalOperator.XOR, other)
def _connect( self, operator_character: LogicalOperator, other: TSupportedFCExpressionBuilderArguments ) -> "FormatConstraintExpressionBuilder": """ Connect the existing expression and the other part. :param operator_character: "X", "U" or "O" """ if self._expression: prefix = f"({self._expression}) {operator_character}" else: prefix = "" if isinstance(other, UnevaluatedFormatConstraint): self._expression = f"{prefix} [{other.condition_key}]" elif isinstance(other, EvaluatedComposition) and other.format_constraints_expression: self._expression = f"{prefix} ({other.format_constraints_expression})" elif isinstance(other, str): self._expression = f"{prefix} ({other})" elif isinstance(other, (RequirementConstraint, EvaluatedComposition, Hint)): # other types than the above don't affect the newly built format constraint expression (no effect, explicit) pass else: # all other types also have no effect (no effect, implicit) pass # we should never come here if self._expression: self._expression = self._expression.strip() self._expression = self._one_key_surrounded_by_brackets_pattern.sub(r"\g<body>", self._expression) return self
# pylint:disable=too-few-public-methods class _ClassesWithHintAttribute(Protocol): """ A class to be used in type hints. describes all classes that have a "hint" attribute """ hint: str ClassesWithHintAttribute = TypeVar("ClassesWithHintAttribute", bound=_ClassesWithHintAttribute)
[docs] class HintExpressionBuilder(ExpressionBuilder[ClassesWithHintAttribute]): """ Allows connecting hints with logical operations. """
[docs] @staticmethod def get_hint_text(hinty_object: Optional[_ClassesWithHintAttribute]) -> Optional[str]: """ get the hint from a Hint instance or plain string :param hinty_object: :return: hint if there is any, None otherwise """ if hinty_object is None: return None if isinstance(hinty_object, str): return hinty_object return getattr(hinty_object, "hint", None)
def __init__(self, init_condition: Optional[_ClassesWithHintAttribute]) -> None: """ Initialize by providing either a Hint Node or a hint string """ self._expression = HintExpressionBuilder.get_hint_text(init_condition)
[docs] def get_expression(self) -> Optional[str]: return self._expression
[docs] def land(self, other: Optional[_ClassesWithHintAttribute]) -> "HintExpressionBuilder[ClassesWithHintAttribute]": if other is not None: if self._expression: self._expression += f" und {HintExpressionBuilder.get_hint_text(other)}" else: self._expression = HintExpressionBuilder.get_hint_text(other) return self
[docs] def lor(self, other: Optional[_ClassesWithHintAttribute]) -> "HintExpressionBuilder[ClassesWithHintAttribute]": if other is not None: if self._expression: self._expression += f" oder {HintExpressionBuilder.get_hint_text(other)}" else: self._expression = HintExpressionBuilder.get_hint_text(other) return self
[docs] def xor(self, other: Optional[_ClassesWithHintAttribute]) -> "HintExpressionBuilder[ClassesWithHintAttribute]": if other is not None: if self._expression: self._expression = f"Entweder ({self._expression}) oder ({HintExpressionBuilder.get_hint_text(other)})" else: self._expression = HintExpressionBuilder.get_hint_text(other) return self
# This class is only used by the FormatConstraintTransformer. # That's why it only accepts EvaluatedFormatConstraints as input.
[docs] class FormatErrorMessageExpressionBuilder(ExpressionBuilder[EvaluatedFormatConstraint]): """ Class to build the error messages for the format constraint evaluation. """ def __init__(self, init_condition: EvaluatedFormatConstraint) -> None: self._expression = init_condition.error_message self.format_constraint_fulfilled = init_condition.format_constraint_fulfilled
[docs] def get_expression(self) -> Optional[str]: return self._expression
@staticmethod def _wrap_message(msg: Optional[str]) -> str: """ Wrap a message appropriately for combining with logical operators. Use parentheses for compound expressions, single quotes for simple messages. """ if not msg: return "''" # Check if this is already a compound expression (contains logical operators) if " oder " in msg or " und " in msg: return f"({msg})" return f"'{msg}'"
[docs] def land(self, other: EvaluatedFormatConstraint) -> "FormatErrorMessageExpressionBuilder": if other.format_constraint_fulfilled is True: # If a format constraint is connected with "logical and" to another format constraint which is fulfilled, # then the remaining expression/error message stays the same. pass else: if self._expression is None: self._expression = other.error_message else: left_part = self._wrap_message(self._expression) right_part = self._wrap_message(other.error_message) self._expression = f"{left_part} und {right_part}" return self
[docs] def lor(self, other: EvaluatedFormatConstraint) -> "FormatErrorMessageExpressionBuilder": if self.format_constraint_fulfilled is False and other.format_constraint_fulfilled is False: left_part = self._wrap_message(self._expression) right_part = self._wrap_message(other.error_message) self._expression = f"{left_part} oder {right_part}" else: self._expression = None return self
[docs] def xor(self, other: EvaluatedFormatConstraint) -> "FormatErrorMessageExpressionBuilder": if self.format_constraint_fulfilled is False and other.format_constraint_fulfilled is False: left_part = self._wrap_message(self._expression) right_part = self._wrap_message(other.error_message) self._expression = f"Entweder {left_part} oder {right_part}" elif self.format_constraint_fulfilled is True and other.format_constraint_fulfilled is True: self._expression = "Zwei exklusive Formatdefinitionen dürfen nicht gleichzeitig erfüllt sein" # TODO: Do we need to know which one? It's probably more work than benefit. else: self._expression = None return self