Source code for multilevel_py.constraints

import math
from collections import defaultdict
from collections.abc import Iterable
from datetime import time, date, timedelta, datetime
from inspect import signature
from types import FunctionType
from typing import Callable, Any, Union, Tuple, Collection, List
from multilevel_py.exceptions import InvalidInstantiationOrderException, \
    NotAClabjectException, InvalidPropValueConstraintException, TypeSpecificConstraintRemovalException


[docs]class BaseConstraint: """ Abstract Base of all kind of clabject constraints """ def __init__(self, name: str, violation_reason=""): self.name = name self.violation_reason = violation_reason
[docs]class PropValueConstraint(BaseConstraint): """ A callable constraint imposed on the values of clabject props """
[docs] def __init__(self, name: str, eval_value_func: Callable[[Any], str], eval_on_init: bool): """ Args: name: the name of the constraint eval_value_func: an evaluation function, that returns an empty string for valid prop values and a non empty string for prop values that violate the constraint eval_on_init: a value indicating whether the constr should be evaluated on (re) init """ super(PropValueConstraint, self).__init__(name=name) self.eval_value_func = eval_value_func self.eval_on_init = eval_on_init self.type_specific = False
def __call__(self, prop_value: Any) -> bool: self.violation_reason = "" if self.eval_value_func(prop_value): self.violation_reason = self.eval_value_func(prop_value) return False return True
[docs]class ClabjectStateConstraint(BaseConstraint): """ A callable constraint that is evaluated on the current clabject state """
[docs] def __init__(self, name: str, eval_clabject_func: Callable[[Any], str]): """ Args: name: The name of the constraint eval_clabject_func: an evaluation func with of the following structure: Callable(current_clabject) -> str """ super(ClabjectStateConstraint, self).__init__(name=name) self.eval_clabject_func = eval_clabject_func
def __call__(self, current_clabject: Any) -> bool: self.violation_reason = "" if self.eval_clabject_func(current_clabject): self.violation_reason = self.eval_clabject_func(current_clabject) return False return True
[docs]def prop_constraint_py_isinstance_functional(expected_type, eval_on_init=True) -> PropValueConstraint: """ Generate prop value constraints using Pythons traditional isinstance facility Args: expected_type: the expected type or class object eval_on_init: value indicating whether the constr should be evaluated on (re) init Returns: a parameterised, callable PropValueConstraint object """ def eval_value_func(value) -> str: if not isinstance(value, expected_type): return "{VALUE} is not of the expected type {TYPE}".format( VALUE=str(value), TYPE=expected_type.__name__ ) else: return "" name = "is_of_{TYPE}".format(TYPE=expected_type.__name__) return PropValueConstraint(name=name, eval_value_func=eval_value_func, eval_on_init=eval_on_init)
is_str_constraint = prop_constraint_py_isinstance_functional(str) is_int_constraint = prop_constraint_py_isinstance_functional(int) is_float_constraint = prop_constraint_py_isinstance_functional(float) is_bool_constraint = prop_constraint_py_isinstance_functional(bool) is_function_constraint = prop_constraint_py_isinstance_functional(FunctionType) is_date_constraint = prop_constraint_py_isinstance_functional(date) is_time_constraint = prop_constraint_py_isinstance_functional(time) is_datetime_constraint = prop_constraint_py_isinstance_functional(datetime) is_timedelta_constraint = prop_constraint_py_isinstance_functional(timedelta) is_set_constraint = prop_constraint_py_isinstance_functional(set) Instantiation_order_type = Union[int, Tuple[int, int], None]
[docs]def prop_constraint_ml_instance_of_th_order_functional(expected_clabject_type, instantiation_order: Instantiation_order_type = None, eval_on_init=True): """ Generate prop value constraints using the instance_mechanism provided by the multilevel_py package Args: expected_clabject_type: the expected_clabject_type values are supposed to be instances of instantiation_order: the number of permissible instantiation steps between value and expected_clabject_type, if None instances of any order are allowed, if int only this exact number of instantiation steps is tolerated, if (min, max) tuple the inclusive interval of integers between min and max is tolerated eval_on_init: value indicating whether the constr should be evaluated on (re) init Returns: a parameterised, callable PropValueConstraint object """ from multilevel_py.core import is_clabject from multilevel_py.core import Clabject def check_int_avove_zero(value) -> bool: if not isinstance(value, int): return False elif value < 1: return False else: return True if instantiation_order is None: min_inst_steps = 1 max_inst_steps = math.inf order_str = "any" elif isinstance(instantiation_order, int): if not check_int_avove_zero(instantiation_order): raise InvalidInstantiationOrderException(provided_instantiation_order=instantiation_order) else: min_inst_steps = instantiation_order max_inst_steps = instantiation_order order_str = str(instantiation_order) elif type(instantiation_order) is tuple: if not len(instantiation_order) == 2: raise InvalidInstantiationOrderException(provided_instantiation_order=instantiation_order) else: if not (check_int_avove_zero(instantiation_order[0]) and check_int_avove_zero(instantiation_order[1])): raise InvalidInstantiationOrderException(provided_instantiation_order=instantiation_order) else: min_inst_steps = instantiation_order[0] max_inst_steps = instantiation_order[1] order_str = "_".join(["between", str(min_inst_steps), "and", str(max_inst_steps)]) def eval_value_func(value) -> str: if not is_clabject(value): return str(NotAClabjectException(obj=value)) inst_steps = 0 current_clab = value while inst_steps < min_inst_steps: # check chain end or premature instance if current_clab == Clabject or current_clab == expected_clabject_type: return "Instantiation order of given clabject is to low." current_clab = current_clab.instance_of() inst_steps += 1 # section of instantiation chain that is valid while current_clab != Clabject and inst_steps <= max_inst_steps: if current_clab == expected_clabject_type: return "" current_clab = current_clab.instance_of() inst_steps += 1 return "Instantiation order of given clabject value is to high" name = "{ORDER}_order_ml_instance_of_{TYPE}".format(ORDER=order_str, TYPE=str(expected_clabject_type.__name__)) return PropValueConstraint(name=name, eval_value_func=eval_value_func, eval_on_init=True)
[docs]def prop_constraint_is_th_order_instance_of_clabject_set_functional(expected_values: set, order: int = 1, eval_on_init=True): """ Generates a PropValueConstraint that checks whether the given value is an (given) order instance of one of the specified clabjects Args: expected_values: a set of clabjects order: an integer, describing how often the instance_of operation should be applied eval_on_init: value indicating whether the constr should be evaluated on (re) init Returns: a parameterised, callable PropValueConstraint object """ from multilevel_py.core import is_clabject def eval_value_func(value: Any): if not is_clabject(value): return str(NotAClabjectException(value)) inst_order = order if order else 1 while inst_order > 0: value = value.instance_of() inst_order = inst_order - 1 if value in expected_values: return "" else: return "The given clabject value is not a {ORDER} order instance of any of the accepted clabjects".format( ORDER=order ) name = "{ORDER}_order_instance_of_clabject_set_{CLABJECTS}".format(ORDER=order, CLABJECTS={c.__name__ for c in expected_values}) return PropValueConstraint(name=name, eval_value_func=eval_value_func, eval_on_init=eval_on_init)
[docs]def prop_constraint_value_is_collection_functional(eval_on_init=True): """ Generates a PropValueConstraint that checks whether the property value is a valid collection Args: eval_on_init: value indicating whether the constr should be evaluated on (re) init """ def eval_func(value: Any) -> str: res = "" if not isinstance(value, Iterable) or isinstance(value, str): res = str(value) + "is not a collection" return res name = "is_collection" return PropValueConstraint(name=name, eval_value_func=eval_func, eval_on_init=eval_on_init)
is_collection_constraint = prop_constraint_value_is_collection_functional(eval_on_init=True)
[docs]def prop_constraint_collection_member_functional( member_constr_func: PropValueConstraint, filter_func: Callable[[Collection], Collection] = None, eval_on_init=True): """ Generates a PropValueConstraint that is applied on (selected) members of a collection prop value Args: member_constr_func: A PropValueConstraint that each collection member must satisfy filter_func: A filter function that takes the whole collection and returns only the members, the member_constr_func should be checked on eval_on_init: value indicating whether the constr should be evaluated on (re) init Returns: a parameterised, callable PropValueConstraint object """ def eval_func(collection_value): res_str = "" eval_collection = filter_func(collection_value) if filter_func else collection_value for member in eval_collection: if not member_constr_func(member): res_str += "Member {MEMBER} failed for reason: {REASON}".format( MEMBER=str(member), REASON=member_constr_func.violation_reason) res_str += "\n" return res_str name = "_".join(["check", member_constr_func.name, "on_collection_members"]) return PropValueConstraint(name=name, eval_value_func=eval_func, eval_on_init=eval_on_init)
[docs]def prop_constraint_collection_multiplicity_functional(min_member_number: int, max_member_number: int, eval_on_init=False): def eval_func(collection_value) -> str: res_str = "" if not hasattr(collection_value, "__len__"): res_str += "The given value {VALUE} is no collection".format(VALUE=str(collection_value)) if not min_member_number <= len(collection_value) <= max_member_number: res_str += "The collection holds {N} members".format(N=len(collection_value)) return res_str name = "collection_multiplicity_between_{MIN}_and_{MAX}".format(MIN=min_member_number, MAX=max_member_number) return PropValueConstraint(name=name, eval_value_func=eval_func, eval_on_init=eval_on_init)
[docs]def prop_value_can_be_bound_as_method_functional(eval_on_init=True): """ Args: eval_on_init: value indicating whether the constr should be evaluated on (re) init Returns: a PropValueConstraint that determines whether the provided value can serve as method """ def eval_func(value) -> str: res_str = "" if not isinstance(value, FunctionType): res_str = "The given value is not a function" else: sig = signature(value) # Methods must take obj. they bind to as first param if len(sig.parameters) == 0: res_str = "The given signature of the given function takes no parameters" return res_str name = "can_be_method_constraint" return PropValueConstraint(name=name, eval_value_func=eval_func, eval_on_init=eval_on_init)
value_can_be_bound_as_method_constraint = prop_value_can_be_bound_as_method_functional(eval_on_init=True)
[docs]def prop_constraint_or_functional(constraint_a: PropValueConstraint, constraint_b: PropValueConstraint): """ Args: constraint_a: the fst constraint constraint_b: the snd constraint Returns: a combined PropValueConstraint, in which at least one of constraint_a and constraint_b must be fulfilled """ def eval_func(value): constraint_a(value) constraint_b(value) res = "" if constraint_a.violation_reason and constraint_b.violation_reason: res += constraint_a.violation_reason res += constraint_b.violation_reason return res name = constraint_a.name + "_OR_" + constraint_b.name eval_on_init = constraint_a.eval_on_init and constraint_b.eval_on_init # conservative return PropValueConstraint(name=name, eval_value_func=eval_func, eval_on_init=eval_on_init)
[docs]def prop_constraint_and_functional(constraint_a: PropValueConstraint, constraint_b: PropValueConstraint, eval_on_init=False): """ Args: constraint_a: the fst constraint constraint_b: the snd constraint eval_on_init: (bool) value indicating whether the constr should be evaluated on (re) init Returns: a combined PropValue Constraint, in which both constraint_a, constraint_b must be fulfilled """ def eval_func(value): constraint_a(value) constraint_b(value) res = "" if constraint_a.violation_reason or constraint_b.violation_reason: res += constraint_a.violation_reason res += constraint_b.violation_reason return res name = constraint_a.name + "_And_" + constraint_b.name eval_on_init = eval_on_init return PropValueConstraint(name=name, eval_value_func=eval_func, eval_on_init=eval_on_init)
class _EmptyValueClass(object): def __str__(self): return "EmptyValue" EmptyValue = _EmptyValueClass()
[docs]def prop_constraint_optional_value_functional(constraint: PropValueConstraint): """ Args: constraint: a PropValueConstraint Returns: a relaxed constraint of the given PropValueConstraint by tolerating also values of None as valid init value """ def eval_func(value): if value == EmptyValue: return "" else: constraint(value) if constraint.violation_reason: return constraint.violation_reason + " or an EmptyValue" else: return "" name = constraint.name + "_OR_Empty" eval_on_init = constraint.eval_on_init return PropValueConstraint(name=name, eval_value_func=eval_func, eval_on_init=eval_on_init)
[docs]def eval_negative_value_func(value) -> str: if value < 0: return "The value must not be < 0" else: return ""
is_not_negative_constraint = PropValueConstraint(name="is_not_negative_constraint", eval_value_func=eval_negative_value_func, eval_on_init=True) is_not_negative_int_constraint = prop_constraint_and_functional(is_not_negative_constraint, is_int_constraint)
[docs]def prop_constraint_value_in_set_functional(expected_set: set, eval_on_init=True): """ Generate PropValueConstraint that evaluates whether the given value is in the expected set Args: expected_set: a set of permitted values eval_on_init: (bool) value indicating whether the constr should be evaluated on (re) init Returns: a callable prop_constr instance """ def eval_func(value): if value in expected_set: return "" else: return "The Value {VAL} is not in the expected set {SET}".format(VAL=value, SET=expected_set) return constraint.violation_reason return PropValueConstraint(name="is_in_set_constr", eval_value_func=eval_func, eval_on_init=eval_on_init)
[docs]class ConstrViolationDictFactory(defaultdict): """ call ConstrViolationsFactory(list) to create dict like objects """
[docs] def add_violations(self, violations: dict) -> None: """ Args: violations: a dict with structure <prop_name: str> => <violation_reason: str> Returns: Nothing, updates the violationDict object """ for constr_holder, constr in violations.items(): if isinstance(constr, list): for c in constr: self[constr_holder].append(c) else: self[constr_holder].append(constr)
[docs]def create_violated_constraint_dict(): """ Returns: an empty constraint_violation dict """ from multilevel_py.constraints import ConstrViolationDictFactory all_violated_constraints = ConstrViolationDictFactory(list) return all_violated_constraints
[docs]class ReInitPropConstr:
[docs] def __init__(self, del_constr: List[PropValueConstraint] = [], add_constr: List[PropValueConstraint] = []): """ Forces the reinitialisation of a property value under updated constraints, if set on a clabject via :meth:`multilevel_py.core.MetaClabject.require_re_init_on_next_step` Args: del_constr: list of constraints to remove for the next re instantiation of the prop add_constr: list of constraints to validate for the next instantiation step """ for constr in add_constr: if not isinstance(constr, PropValueConstraint): raise InvalidPropValueConstraintException(constr=constr) for constr in del_constr: if not isinstance(constr, PropValueConstraint): raise InvalidPropValueConstraintException(constr=constr) if constr.type_specific: raise TypeSpecificConstraintRemovalException(constr=constr) self.del_constr = del_constr self.add_constr = add_constr
def __bool__(self): return True