from core.builtin_concepts import BuiltinConcepts from core.builtin_helpers import expect_one, only_successful from core.concept import Concept, DoNotResolve, ConceptParts, InfiniteRecursionResolved CONCEPT_EVALUATION_STEPS = [ BuiltinConcepts.BEFORE_EVALUATION, BuiltinConcepts.EVALUATION, BuiltinConcepts.AFTER_EVALUATION] class SheerkaEvaluateConcept: def __init__(self, sheerka): self.sheerka = sheerka self.logger_name = "EvaluateConcept" @staticmethod def infinite_recursion_detected(context, concept): if concept is None: return False parent = context.get_parent() while parent is not None: if parent.who == context.who and parent.obj == concept: return True parent = parent.get_parent() return False @staticmethod def get_infinite_recursion_resolution(obj): if isinstance(obj, InfiniteRecursionResolved): return obj if isinstance(obj, Concept) and isinstance(obj.body, InfiniteRecursionResolved): return obj.body return None def manage_infinite_recursion(self, context): """ We look for the fist parent that has a body that means something We use the eval function with no local or global :param context: :return: """ parent = context concepts_found = set() while parent and parent.obj: if parent.who == context.who and parent.desc == context.desc: body = parent.obj.metadata.body try: return self.sheerka.ret(self.logger_name, True, InfiniteRecursionResolved(eval(body))) except Exception: pass concepts_found.add(parent.obj) parent = parent.get_parent() return self.sheerka.ret( self.logger_name, False, self.sheerka.new(BuiltinConcepts.CHICKEN_AND_EGG, body=concepts_found)) def initialize_concept_asts(self, context, concept: Concept): """ Updates the codes of the newly created concept Basically, it runs the parsers on all parts :param concept: :param context: :param logger: :return: """ def is_only_successful(r): # return False return context.sheerka.isinstance(r, BuiltinConcepts.RETURN_VALUE) and \ context.sheerka.isinstance(r.body, BuiltinConcepts.ONLY_SUCCESSFUL) steps = [BuiltinConcepts.BEFORE_PARSING, BuiltinConcepts.PARSING, BuiltinConcepts.AFTER_PARSING] for part_key in ConceptParts: if part_key in concept.compiled: continue source = getattr(concept.metadata, part_key.value) if source is None or not isinstance(source, str): continue if source.strip() == "": concept.compiled[part_key] = DoNotResolve(source) else: with context.push(desc=f"Initializing *compiled* for {part_key}") as sub_context: sub_context.add_inputs(source=source) to_parse = self.sheerka.ret(context.who, True, self.sheerka.new(BuiltinConcepts.USER_INPUT, body=source)) res = self.sheerka.execute(sub_context, to_parse, steps) only_success = only_successful(sub_context, res) concept.compiled[part_key] = only_success.body.body if is_only_successful(only_success) else res sub_context.add_values(return_values=res) for var_name, default_value in concept.metadata.variables: if var_name in concept.compiled: continue if default_value is None or not isinstance(default_value, str): continue if default_value.strip() == "": concept.compiled[var_name] = DoNotResolve(default_value) else: with context.push(desc=f"Initializing *compiled* for property {var_name}") as sub_context: sub_context.add_inputs(source=default_value) to_parse = self.sheerka.ret(context.who, True, self.sheerka.new(BuiltinConcepts.USER_INPUT, body=default_value)) res = self.sheerka.execute(sub_context, to_parse, steps) only_success = only_successful(sub_context, res) concept.compiled[var_name] = only_success.body.body if is_only_successful(only_success) else res sub_context.add_values(return_values=res) # Updates the cache of concepts when possible if self.sheerka.has_id(concept.id): self.sheerka.get_by_id(concept.id).compiled = concept.compiled def resolve(self, context, to_resolve, current_prop, current_concept, force_evaluation): if isinstance(to_resolve, DoNotResolve): return to_resolve.value # manage infinite loop if self.infinite_recursion_detected(context, current_concept): with context.push(desc="Infinite recursion detected", obj=current_concept) as sub_context: # I create a sub context in order to log what happened ret_val = self.manage_infinite_recursion(context) sub_context.add_values(return_values=ret_val) return ret_val.body desc = f"Evaluating {current_prop} (concept={current_concept})" context.log(desc, self.logger_name) with context.push(desc=desc, obj=current_concept) as sub_context: if force_evaluation: sub_context.local_hints.add(BuiltinConcepts.EVAL_BODY_REQUESTED) # when it's a concept, evaluate it if isinstance(to_resolve, Concept) and \ not context.sheerka.isinstance(to_resolve, BuiltinConcepts.RETURN_VALUE): evaluated = self.evaluate_concept(sub_context, to_resolve) sub_context.add_values(return_values=evaluated) if evaluated.key == to_resolve.key: return evaluated else: error = evaluated # otherwise, execute all return values to find out what is the value else: use_copy = [r for r in to_resolve] if hasattr(to_resolve, "__iter__") else to_resolve r = self.sheerka.execute(sub_context, use_copy, CONCEPT_EVALUATION_STEPS) one_r = expect_one(context, r) sub_context.add_values(return_values=one_r) if one_r.status: return one_r.value else: error = one_r.value return error if self.sheerka.isinstance(error, BuiltinConcepts.CHICKEN_AND_EGG) \ else self.sheerka.new(BuiltinConcepts.CONCEPT_EVAL_ERROR, body=error, concept=current_concept, property_name=current_prop) def resolve_list(self, context, list_to_resolve, current_prop, current_concept, force_evaluation): """When dealing with a list, there are two possibilities""" # It may be a list of ReturnValueConcept to execute (always the case for metadata) # or a list of single values (may be the case for properties) # in this latter case, all values are to be processed one by one and a list should be returned if len(list_to_resolve) == 0: return [] if self.sheerka.isinstance(list_to_resolve[0], BuiltinConcepts.RETURN_VALUE): return self.resolve(context, list_to_resolve, current_prop, current_concept, force_evaluation) res = [] for to_resolve in list_to_resolve: # sanity check if self.sheerka.isinstance(to_resolve, BuiltinConcepts.RETURN_VALUE): return self.sheerka.new(BuiltinConcepts.CONCEPT_EVAL_ERROR, body="Mix between real values and return values", concept=current_concept, property_name=current_prop) r = self.resolve(context, to_resolve, current_prop, current_concept, force_evaluation) if self.sheerka.isinstance(r, BuiltinConcepts.CONCEPT_EVAL_ERROR): return r res.append(r) return res def evaluate_concept(self, context, concept: Concept): """ Evaluation a concept It means that if the where clause is True, will evaluate the body :param context: :param concept: :return: value of the evaluation or error """ if concept.metadata.is_evaluated: return concept self.initialize_concept_asts(context, concept) # to make sure of the order, it don't use ConceptParts.get_parts() # variables must be evaluated first, body must be evaluated before where all_metadata_to_eval = self.choose_metadata_to_eval(context, concept) for metadata_to_eval in all_metadata_to_eval: if metadata_to_eval == "variables": for var_name in (v for v in concept.variables() if v in concept.compiled): prop_ast = concept.compiled[var_name] if isinstance(prop_ast, list): # Do not send the current concept for the properties resolved = self.resolve_list(context, prop_ast, var_name, None, True) else: # Do not send the current concept for the properties resolved = self.resolve(context, prop_ast, var_name, None, True) if isinstance(resolved, Concept) and not context.sheerka.is_success(resolved): resolved.set_value("concept", concept) # since current concept was not sent return resolved else: concept.set_value(var_name, resolved) else: part_key = ConceptParts(metadata_to_eval) # do not evaluate where when the body is a set # Indeed, the way that the where clause is expressed is not a valid python or concept code if part_key == ConceptParts.WHERE and self.sheerka.isaset(context, concept.body): continue if part_key in concept.compiled and concept.compiled[part_key] is not None: metadata_ast = concept.compiled[part_key] # if part_key is PRE, POST or WHERE, the concepts need to be evaluated # otherwise the predicates cannot be resolved force_concept_eval = False if part_key == ConceptParts.BODY else True resolved = self.resolve(context, metadata_ast, part_key, concept, force_concept_eval) if isinstance(resolved, Concept) and not context.sheerka.is_success(resolved): return resolved else: concept.set_value(part_key, self.get_infinite_recursion_resolution(resolved) or resolved) # # TODO : Validate the PRE condition # # validate where clause if ConceptParts.WHERE in concept.values: where_value = concept.get_value(ConceptParts.WHERE) if not (where_value is None or self.sheerka.objvalue(where_value)): return self.sheerka.new(BuiltinConcepts.WHERE_CLAUSE_FAILED, body=concept) # # TODO : Validate the POST condition # concept.init_key() # only does it if needed concept.metadata.is_evaluated = "body" in all_metadata_to_eval return concept def choose_metadata_to_eval(self, context, concept): if context.in_context(BuiltinConcepts.EVAL_BODY_REQUESTED): return ["pre", "post", "variables", "body", "where"] metadata = ["pre", "post"] if context.in_context(BuiltinConcepts.EVAL_WHERE_REQUESTED) or concept.metadata.need_validation: needed = self.needed_metadata(concept, ConceptParts.WHERE) for e in needed: if e not in metadata: metadata.append(e) if "where" not in metadata: metadata.append("where") return metadata def needed_metadata(self, concept, metadata): """ Tries to find out if the evaluation of the body is necessary It's a very basic approach that will need to be improved :param concept: :param metadata: :return: """ if metadata not in concept.compiled: return [] return_values = concept.compiled[metadata] if not isinstance(return_values, list): return [] needed = [] for return_value in return_values: if not self.sheerka.isinstance(return_value, BuiltinConcepts.RETURN_VALUE): continue if not return_value.status: continue if not self.sheerka.isinstance(return_value.body, BuiltinConcepts.PARSER_RESULT): continue if not isinstance(return_value.body.source, str): continue for var_name in (p[0] for p in concept.metadata.variables): if var_name in return_value.body.source: needed.append("variables") break if "self" in return_value.body.source: needed.append("body") return needed