diff --git a/_concepts.txt b/_concepts.txt index 9612afd..a60879f 100644 --- a/_concepts.txt +++ b/_concepts.txt @@ -39,4 +39,7 @@ seventeen isa number eighteen isa number nineteen isa number twenty isa number -def concept twenties from bnf twenty number where number < 10 as twenty + number \ No newline at end of file +def concept twenties from bnf twenty number where number < 10 as twenty + number +def concept thirty as 30 +def concept forty as 40 + diff --git a/src/core/builtin_concepts.py b/src/core/builtin_concepts.py index 2981f07..9470844 100644 --- a/src/core/builtin_concepts.py +++ b/src/core/builtin_concepts.py @@ -51,6 +51,7 @@ class BuiltinConcepts(Enum): REDUCE_REQUESTED = "reduce requested" # remove meaningless error when possible NOT_A_SET = "not a set" # the concept has no entry in sets WHERE_CLAUSE_FAILED = "where clause failed" # failed to validate where clause during evaluation + CHICKEN_AND_EGG = "chicken and egg" # infinite recursion when declaring concept NODE = "node" GENERIC_NODE = "generic node" @@ -92,7 +93,8 @@ BuiltinErrors = [str(e) for e in { BuiltinConcepts.CONCEPT_EVAL_ERROR, BuiltinConcepts.CONCEPT_ALREADY_IN_SET, BuiltinConcepts.NOT_A_SET, - BuiltinConcepts.WHERE_CLAUSE_FAILED + BuiltinConcepts.WHERE_CLAUSE_FAILED, + BuiltinConcepts.CHICKEN_AND_EGG }] """ diff --git a/src/core/builtin_helpers.py b/src/core/builtin_helpers.py index 0144ae9..a7492cf 100644 --- a/src/core/builtin_helpers.py +++ b/src/core/builtin_helpers.py @@ -4,7 +4,6 @@ import logging import core.ast.nodes from core.ast.nodes import CallNodeConcept, GenericNodeConcept from core.ast.visitors import UnreferencedNamesVisitor - from core.builtin_concepts import BuiltinConcepts @@ -100,11 +99,22 @@ def expect_one(context, return_values, logger=None): return_values[0], parents=return_values) else: - return sheerka.ret( - context.who, - False, - sheerka.new(BuiltinConcepts.TOO_MANY_ERRORS, body=return_values), - parents=return_values) + # test if only one evaluator in error + from evaluators.OneErrorEvaluator import OneErrorEvaluator + one_error_evaluator = OneErrorEvaluator() + reduce_requested = sheerka.ret(context.who, True, sheerka.new(BuiltinConcepts.REDUCE_REQUESTED)) + if one_error_evaluator.matches(context, return_values + [reduce_requested]): + return sheerka.ret( + context.who, + False, + one_error_evaluator.eval(context, return_values).body, + parents=return_values) + else: + return sheerka.ret( + context.who, + False, + sheerka.new(BuiltinConcepts.TOO_MANY_ERRORS, body=return_values), + parents=return_values) def get_names(sheerka, concept_node): @@ -210,5 +220,3 @@ def _extract_predicates(sheerka, node, variables_to_include, variables_to_exclud predicates.append(res) return predicates - - diff --git a/src/core/concept.py b/src/core/concept.py index b67b507..68e780f 100644 --- a/src/core/concept.py +++ b/src/core/concept.py @@ -242,24 +242,24 @@ class Concept: def body(self): return self.values[ConceptParts.BODY] if ConceptParts.BODY in self.values else None - def add_codes(self, codes): - """ - Gets the ASTs for 'where', 'pre', 'post' and 'body' - There ASTs are know when the concept is freshly parsed. - So the values are kept in cache. - - For concepts loaded from sdp, these ASTs must be created again - TODO : Seems to be a service method. Can be put somewhere else - :param codes: - :return: - """ - if codes is None: - return - - for key in codes: - self.compiled[key] = codes[key] - - return self + # def add_codes(self, codes): + # """ + # Gets the ASTs for 'where', 'pre', 'post' and 'body' + # There ASTs are know when the concept is freshly parsed. + # So the values are kept in cache. + # + # For concepts loaded from sdp, these ASTs must be created again + # TODO : Seems to be a service method. Can be put somewhere else + # :param codes: + # :return: + # """ + # if codes is None: + # return + # + # for key in codes: + # self.compiled[key] = codes[key] + # + # return self def get_digest(self): """ @@ -406,3 +406,12 @@ class DoNotResolve: set concept.compiled[BODY] to DoNotResolve(value) """ value: object + + +@dataclass() +class InfiniteRecursionResolved: + """This class is used to when we managed to break an infinite recursion concept definition""" + value: object + + def get_value(self): + return self.value diff --git a/src/core/sheerka/ExecutionContext.py b/src/core/sheerka/ExecutionContext.py index f2b4c7f..bb74d6c 100644 --- a/src/core/sheerka/ExecutionContext.py +++ b/src/core/sheerka/ExecutionContext.py @@ -222,6 +222,8 @@ class ExecutionContext: to_str = self.return_value_to_str(r) logger.debug(f"[{self._id:2}]" + self._tab + "-> " + to_str) + def get_parent(self): + return self._parent @staticmethod def return_value_to_str(r): diff --git a/src/core/sheerka/Services/SheerkaDump.py b/src/core/sheerka/Services/SheerkaDump.py index f04f499..7e8b876 100644 --- a/src/core/sheerka/Services/SheerkaDump.py +++ b/src/core/sheerka/Services/SheerkaDump.py @@ -1,6 +1,7 @@ from core.builtin_concepts import BuiltinConcepts from core.concept import Concept -from sdp.sheerkaDataProvider import SheerkaDataProvider +from core.sheerka.ExecutionContext import ExecutionContext +from sdp.sheerkaDataProvider import SheerkaDataProvider, Event import pprint import os @@ -28,7 +29,7 @@ class SheerkaDump: defs = self.sheerka.sdp.get(self.sheerka.CONCEPTS_DEFINITIONS_ENTRY) self.sheerka.log.info(defs) - def dump_desc(self, *concept_names): + def dump_desc(self, *concept_names, eval=False): first = True for concept_name in concept_names: if isinstance(concept_name, Concept): @@ -43,13 +44,20 @@ class SheerkaDump: concepts = [concepts] for c in concepts: + if eval: + event = Event(f"Evaluating {c}", "") + context = ExecutionContext("dump_desc", event, self.sheerka) + evaluated = self.sheerka.evaluate_concept(context, c) + value = evaluated.body if evaluated.key == c.key else evaluated + if not first: self.sheerka.log.info("") self.sheerka.log.info(f"name : {c.name}") self.sheerka.log.info(f"bnf : {c.metadata.definition}") self.sheerka.log.info(f"key : {c.key}") self.sheerka.log.info(f"body : {c.metadata.body}") - self.sheerka.log.info(f"value : {c.body}") + if eval: + self.sheerka.log.info(f"value : {value}") self.sheerka.log.info(f"digest : {c.get_digest()}") if self.sheerka.isaset(c): diff --git a/src/core/sheerka/Services/SheerkaEvaluateConcept.py b/src/core/sheerka/Services/SheerkaEvaluateConcept.py index 8a33da5..d0d2579 100644 --- a/src/core/sheerka/Services/SheerkaEvaluateConcept.py +++ b/src/core/sheerka/Services/SheerkaEvaluateConcept.py @@ -1,5 +1,5 @@ from core.builtin_concepts import BuiltinConcepts -from core.concept import Concept, DoNotResolve, ConceptParts +from core.concept import Concept, DoNotResolve, ConceptParts, InfiniteRecursionResolved import core.builtin_helpers CONCEPT_EVALUATION_STEPS = [ @@ -13,6 +13,55 @@ class SheerkaEvaluateConcept: self.sheerka = sheerka self.logger_name = self.evaluate_concept.__name__ + @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, logger=None): """ Updates the codes of the newly created concept @@ -75,6 +124,15 @@ class SheerkaEvaluateConcept: 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 + sub_context.log_new(logger) + 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(logger, desc, self.logger_name) with context.push(desc=desc, obj=current_concept) as sub_context: @@ -92,7 +150,8 @@ class SheerkaEvaluateConcept: # otherwise, execute all return values to find out what is the value else: - r = self.sheerka.execute(sub_context, to_resolve, CONCEPT_EVALUATION_STEPS, logger) + 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, logger) one_r = core.builtin_helpers.expect_one(context, r) sub_context.add_values(return_values=one_r) if one_r.status: @@ -100,10 +159,11 @@ class SheerkaEvaluateConcept: else: error = one_r.value - return self.sheerka.new(BuiltinConcepts.CONCEPT_EVAL_ERROR, - body=error, - concept=current_concept, - property_name=current_prop) + 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, logger): """When dealing with a list, there are two possibilities""" @@ -168,7 +228,7 @@ class SheerkaEvaluateConcept: else: # Do not send the current concept for the properties resolved = self.resolve(context, prop_ast, prop_name, None, logger) - if context.sheerka.isinstance(resolved, BuiltinConcepts.CONCEPT_EVAL_ERROR): + if isinstance(resolved, Concept) and not context.sheerka.is_success(resolved): resolved.set_prop("concept", concept) # since current concept was not sent return resolved else: @@ -178,10 +238,10 @@ class SheerkaEvaluateConcept: if part_key in concept.compiled and concept.compiled[part_key] is not None: metadata_ast = concept.compiled[part_key] resolved = self.resolve(context, metadata_ast, part_key, concept, logger) - if context.sheerka.isinstance(resolved, BuiltinConcepts.CONCEPT_EVAL_ERROR): + if isinstance(resolved, Concept) and not context.sheerka.is_success(resolved): return resolved else: - concept.values[part_key] = resolved + concept.values[part_key] = self.get_infinite_recursion_resolution(resolved) or resolved # validate where clause if concept.metadata.where is not None: diff --git a/src/core/sheerka/Sheerka.py b/src/core/sheerka/Sheerka.py index b70778b..feb037c 100644 --- a/src/core/sheerka/Sheerka.py +++ b/src/core/sheerka/Sheerka.py @@ -249,11 +249,12 @@ class Sheerka(Concept): if not self.skip_builtins_in_db: self.sdp.save_result(self, execution_context) - # hack to save valid concept definition - if not self.during_restore: - if len(ret) == 1 and ret[0].status and self.isinstance(ret[0].value, BuiltinConcepts.NEW_CONCEPT): - with open(CONCEPTS_FILE, "a") as f: - f.write(text + "\n") + # hack to save valid concept definition + # if not self.during_restore: + # if len(ret) == 1 and ret[0].status and self.isinstance(ret[0].value, BuiltinConcepts.NEW_CONCEPT): + # with open(CONCEPTS_FILE, "a") as f: + # f.write(text + "\n") + return ret def execute(self, execution_context, return_values, execution_steps, logger=None): @@ -364,19 +365,26 @@ class Sheerka(Concept): concept_key = str(concept_key) # first search in cache - result = self.cache_by_key[concept_key] if concept_key in self.cache_by_key else \ - self.sdp.get_safe(self.CONCEPTS_ENTRY, concept_key) + if concept_key in self.cache_by_key: + result = self.cache_by_key[concept_key] + else: + result = self.sdp.get_safe(self.CONCEPTS_ENTRY, concept_key) + if result is None: + metadata = [("key", concept_key), ("id", concept_id)] if concept_id else ("key", concept_key) + result = self._get_unknown(metadata) - if result and (concept_id is None or not isinstance(result, list)): + self.cache_by_key[concept_key] = result + for r in (result if isinstance(result, list) else [result]): + if r.id: + self.cache_by_id[r.id] = r + + if not (isinstance(result, list) and concept_id): return result - if isinstance(result, list): - if concept_id: - for c in result: - if c.id == concept_id: - return c - else: - return result + # result is a list, but we have the concept_id to discriminate + for c in result: + if c.id == concept_id: + return c metadata = [("key", concept_key), ("id", concept_id)] if concept_id else ("key", concept_key) return self._get_unknown(metadata) @@ -386,10 +394,15 @@ class Sheerka(Concept): return ErrorConcept("Concept id is undefined.") # first search in cache - result = self.cache_by_id[concept_id] if concept_id in self.cache_by_id else \ - self.sdp.get_safe(self.CONCEPTS_BY_ID_ENTRY, concept_id) + if concept_id in self.cache_by_id: + result = self.cache_by_id[concept_id] + else: + result = self.sdp.get_safe(self.CONCEPTS_BY_ID_ENTRY, concept_id) + if result is None: + result = self._get_unknown(('id', concept_id)) + self.cache_by_id[concept_id] = result - return result or self._get_unknown(('id', concept_id)) + return result def get_concepts_definitions(self, context): if self.concepts_definitions_cache: diff --git a/src/evaluators/AddConceptEvaluator.py b/src/evaluators/AddConceptEvaluator.py index bfbd262..b1abfce 100644 --- a/src/evaluators/AddConceptEvaluator.py +++ b/src/evaluators/AddConceptEvaluator.py @@ -6,8 +6,8 @@ from evaluators.BaseEvaluator import OneReturnValueEvaluator from parsers.BaseParser import NotInitializedNode from parsers.ConceptLexerParser import ParsingExpression, ParsingExpressionVisitor from parsers.DefaultParser import DefConceptNode - from parsers.PythonParser import PythonNode +import core.utils class ConceptOrRuleNameVisitor(ParsingExpressionVisitor): @@ -66,7 +66,7 @@ class AddConceptEvaluator(OneReturnValueEvaluator): setattr(concept.metadata, prop, source) # try to find what can be a property - concept_name = [part.value for part in def_concept_node.name.tokens] + concept_name = [part.value for part in core.utils.strip_tokens(def_concept_node.name.tokens, True)] for p in self.get_props(sheerka, part_ret_val, concept_name): props_found.add(p) @@ -108,11 +108,12 @@ class AddConceptEvaluator(OneReturnValueEvaluator): # Case of python code # if isinstance(ret_value.value, ParserResultConcept) and isinstance(ret_value.value.value, PythonNode): - python_node = ret_value.value.value - as_concept_node = python_to_concept(python_node.ast_) - variables = get_names(sheerka, as_concept_node) - variables = filter(lambda x: x in concept_name, variables) - return list(variables) + if len(concept_name) > 1: + python_node = ret_value.value.value + as_concept_node = python_to_concept(python_node.ast_) + variables = get_names(sheerka, as_concept_node) + variables = filter(lambda x: x in concept_name, variables) + return list(variables) # # case of concept diff --git a/tests/BaseTest.py b/tests/BaseTest.py index d3ae48a..0ac8234 100644 --- a/tests/BaseTest.py +++ b/tests/BaseTest.py @@ -48,7 +48,7 @@ class BaseTest: @staticmethod def pretval(concept, source=None, parser="parsers.name"): - """ParserResult ret_val""" + """ParserResult ret_val (p stands for ParserResult)""" return ReturnValueConcept( "some_name", True, diff --git a/tests/core/test_SheerkaCreateNewConcept.py b/tests/core/test_SheerkaCreateNewConcept.py index b8cfbd3..bbe22b6 100644 --- a/tests/core/test_SheerkaCreateNewConcept.py +++ b/tests/core/test_SheerkaCreateNewConcept.py @@ -1,5 +1,5 @@ from core.builtin_concepts import BuiltinConcepts -from core.concept import PROPERTIES_TO_SERIALIZE +from core.concept import PROPERTIES_TO_SERIALIZE, Concept from sdp.sheerkaDataProvider import SheerkaDataProvider from tests.TestUsingMemoryBasedSheerka import TestUsingMemoryBasedSheerka @@ -80,12 +80,10 @@ class TestSheerkaCreateNewConcept(TestUsingMemoryBasedSheerka): sheerka.cache_by_key = {} # reset the cache loaded = sheerka.get(concept.key) - assert loaded is not None assert loaded == concept # I can also get it by its id loaded = sheerka.sdp.get(sheerka.CONCEPTS_BY_ID_ENTRY, concept.id) - assert loaded is not None assert loaded == concept def test_i_can_get_a_concept_by_its_id(self): @@ -96,7 +94,6 @@ class TestSheerkaCreateNewConcept(TestUsingMemoryBasedSheerka): sheerka.cache_by_key = {} # reset the cache loaded = sheerka.get_by_id(concept.id) - assert loaded is not None assert loaded == concept def test_i_can_get_list_of_concept_when_same_key_when_no_cache(self): @@ -178,3 +175,12 @@ class TestSheerkaCreateNewConcept(TestUsingMemoryBasedSheerka): result = sheerka.get(concept1.key, "wrong id") assert sheerka.isinstance(result, BuiltinConcepts.UNKNOWN_CONCEPT) + + def test_concept_that_references_itself_is_correctly_created(self): + sheerka = self.get_sheerka() + concept = Concept("foo", body="foo") + + res = sheerka.create_new_concept(self.get_context(sheerka), concept) + + assert res.status + diff --git a/tests/core/test_SheerkaEvaluateConcept.py b/tests/core/test_SheerkaEvaluateConcept.py index b31185e..55b44b0 100644 --- a/tests/core/test_SheerkaEvaluateConcept.py +++ b/tests/core/test_SheerkaEvaluateConcept.py @@ -1,6 +1,6 @@ import pytest from core.builtin_concepts import BuiltinConcepts, ReturnValueConcept, ParserResultConcept -from core.concept import Concept, simplec, DoNotResolve, ConceptParts, Property +from core.concept import Concept, simplec, DoNotResolve, ConceptParts, Property, InfiniteRecursionResolved from parsers.PythonParser import PythonNode from tests.TestUsingMemoryBasedSheerka import TestUsingMemoryBasedSheerka @@ -369,3 +369,91 @@ class TestSheerkaEvaluateConcept(TestUsingMemoryBasedSheerka): evaluated = sheerka.evaluate_concept(self.get_context(sheerka), concept) assert sheerka.isinstance(evaluated, BuiltinConcepts.WHERE_CLAUSE_FAILED) assert evaluated.body == concept + + def test_i_can_detect_infinite_recursion_with_numeric_constant(self): + sheerka = self.get_sheerka() + + one_str = Concept("one", body="1") + one_digit = Concept("1", body="one") + + sheerka.add_in_cache(one_str) + sheerka.add_in_cache(one_digit) + + evaluated = sheerka.evaluate_concept(self.get_context(sheerka), one_digit) + assert evaluated.key == one_digit.key + assert evaluated.body == InfiniteRecursionResolved(1) + + evaluated = sheerka.evaluate_concept(self.get_context(sheerka), one_str) + assert evaluated.key == one_str.key + assert evaluated.body == InfiniteRecursionResolved(1) + + def test_i_can_detect_infinite_recursion_with_boolean_constant(self): + sheerka = self.get_sheerka() + + true_str = Concept("true", body="True") + true_bool = Concept("True", body="true") + + sheerka.add_in_cache(true_str) + sheerka.add_in_cache(true_bool) + + evaluated = sheerka.evaluate_concept(self.get_context(sheerka), true_str) + assert evaluated.key == true_str.key + assert evaluated.body == InfiniteRecursionResolved(True) + + evaluated = sheerka.evaluate_concept(self.get_context(sheerka), true_bool) + assert evaluated.key == true_bool.key + assert evaluated.body == InfiniteRecursionResolved(True) + + def test_i_can_detect_infinite_recursion_with_constant_with_more_concepts(self): + sheerka = self.get_sheerka() + + c1 = sheerka.add_in_cache(Concept("one", body="1")) + c2 = sheerka.add_in_cache(Concept("1", body="2")) + c3 = sheerka.add_in_cache(Concept("2", body="3")) + c4 = sheerka.add_in_cache(Concept("3", body="one")) + + for concept in (c1, c2, c3, c4): + evaluated = sheerka.evaluate_concept(self.get_context(sheerka), concept) + assert evaluated.key == concept.key + assert evaluated.body == InfiniteRecursionResolved(3) + + def test_i_can_detect_infinite_recursion_when_no_constant(self): + sheerka = self.get_sheerka() + + foo = sheerka.add_in_cache(Concept("foo", body="bar")) + bar = sheerka.add_in_cache(Concept("bar", body="baz")) + baz = sheerka.add_in_cache(Concept("baz", body="qux")) + qux = sheerka.add_in_cache(Concept("qux", body="foo")) + + evaluated = sheerka.evaluate_concept(self.get_context(sheerka), foo) + assert sheerka.isinstance(evaluated, BuiltinConcepts.CHICKEN_AND_EGG) + assert evaluated.body == {foo, bar, baz, qux} + + evaluated = sheerka.evaluate_concept(self.get_context(sheerka), bar) + assert sheerka.isinstance(evaluated, BuiltinConcepts.CHICKEN_AND_EGG) + assert evaluated.body == {foo, bar, baz, qux} + + evaluated = sheerka.evaluate_concept(self.get_context(sheerka), baz) + assert sheerka.isinstance(evaluated, BuiltinConcepts.CHICKEN_AND_EGG) + assert evaluated.body == {foo, bar, baz, qux} + + evaluated = sheerka.evaluate_concept(self.get_context(sheerka), qux) + assert sheerka.isinstance(evaluated, BuiltinConcepts.CHICKEN_AND_EGG) + assert evaluated.body == {foo, bar, baz, qux} + + def test_i_can_detect_auto_recursion(self): + sheerka = self.get_sheerka() + + foo = sheerka.add_in_cache(Concept("foo", body="foo")) + + evaluated = sheerka.evaluate_concept(self.get_context(sheerka), foo) + assert sheerka.isinstance(evaluated, BuiltinConcepts.CHICKEN_AND_EGG) + assert evaluated.body == {foo} + + def test_i_can_manage_auto_recursion_when_constant(self): + sheerka = self.get_sheerka() + + one = Concept("1", body="1") + evaluated = sheerka.evaluate_concept(self.get_context(sheerka), one) + assert evaluated.key == one.key + assert evaluated.body == 1 diff --git a/tests/core/test_sheerka.py b/tests/core/test_sheerka.py index 0d93abb..80e8eac 100644 --- a/tests/core/test_sheerka.py +++ b/tests/core/test_sheerka.py @@ -242,3 +242,41 @@ class TestSheerka(TestUsingFileBasedSheerka): # only test a random one, it will be the same for the others sheerka = self.get_sheerka() assert not sheerka.is_success(sheerka.new(BuiltinConcepts.TOO_MANY_SUCCESS)) + + def test_cache_is_updated_after_get(self): + sheerka = self.get_sheerka() + + # updated when by_key returns one element + sheerka.create_new_concept(self.get_context(sheerka), Concept("foo", body="1")) + sheerka.reset_cache() + sheerka.get("foo") + assert "foo" in sheerka.cache_by_key + assert "1001" in sheerka.cache_by_id + + # updated when by_key returns two elements + sheerka.create_new_concept(self.get_context(sheerka), Concept("foo", body="2")) + sheerka.reset_cache() + sheerka.get("foo") + assert "foo" in sheerka.cache_by_key + assert "1001" in sheerka.cache_by_id + assert "1002" in sheerka.cache_by_id + + # updated when by_id + sheerka.reset_cache() + sheerka.get_by_id("1001") + assert "1001" in sheerka.cache_by_id + assert "foo" not in sheerka.cache_by_key # cache_by_key not updated as "1001" is not the only one + + def test_i_can_get_by_key_several_times(self): + sheerka = self.get_sheerka() + sheerka.create_new_concept(self.get_context(sheerka), Concept("foo", body="1")) + sheerka.create_new_concept(self.get_context(sheerka), Concept("foo", body="2")) + + sheerka.reset_cache() + sheerka.get("foo", "1001") # only one element requested. But the cache must be updated with two elements + + # let's check it + concepts = sheerka.get("foo") + assert len(concepts) == 2 + assert concepts[0].id == "1001" + assert concepts[1].id == "1002" diff --git a/tests/evaluators/test_AddConceptEvaluator.py b/tests/evaluators/test_AddConceptEvaluator.py index 13e7b54..a05d13a 100644 --- a/tests/evaluators/test_AddConceptEvaluator.py +++ b/tests/evaluators/test_AddConceptEvaluator.py @@ -69,9 +69,8 @@ class TestAddConceptEvaluator(TestUsingMemoryBasedSheerka): return ReturnValueConcept(BaseParser.PREFIX + "some_name", True, ParserResultConcept(value=concept)) @pytest.mark.parametrize("ret_val, expected", [ - ( - ReturnValueConcept(BaseParser.PREFIX + "some_name", True, ParserResultConcept(value=DefConceptNode([]))), - True), + (ReturnValueConcept(BaseParser.PREFIX + "some_name", True, ParserResultConcept(value=DefConceptNode([]))), + True), (ReturnValueConcept(BaseParser.PREFIX + "some_name", False, ParserResultConcept(value=DefConceptNode([]))), False), (ReturnValueConcept(BaseParser.PREFIX + "some_name", True, "not a ParserResultConcept"), False), @@ -133,11 +132,17 @@ class TestAddConceptEvaluator(TestUsingMemoryBasedSheerka): assert from_db.compiled == {} # ast is not saved in db - def test_i_can_get_props_from_python_node(self): + def test_i_can_get_props_from_python_node_when_long_name(self): ret_val = self.get_concept_part("isinstance(a, str)") context = self.get_context() - assert AddConceptEvaluator.get_props(context.sheerka, ret_val, ["a"]) == ["a"] + assert AddConceptEvaluator.get_props(context.sheerka, ret_val, ["a", "b"]) == ["a"] + + def test_i_cannot_get_props_from_python_node_when_name_has_only_one_token(self): + ret_val = self.get_concept_part("isinstance(a, str)") + context = self.get_context() + + assert AddConceptEvaluator.get_props(context.sheerka, ret_val, ["a"]) == [] def test_i_can_get_props_from_another_concept(self): concept = Concept("hello").def_prop("a").def_prop("b") @@ -153,3 +158,16 @@ class TestAddConceptEvaluator(TestUsingMemoryBasedSheerka): ret_val = self.get_return_value("mult (('+'|'-') add)?", parsing_expression) assert AddConceptEvaluator.get_props(self.get_context(), ret_val, []) == ["add", "mult"] + + def test_concept_that_references_itself_is_correctly_created(self): + context = self.get_context() + def_concept_as_return_value = self.get_def_concept("foo", body="foo") + + ret_val = AddConceptEvaluator().eval(context, def_concept_as_return_value) + + assert ret_val.status + new_concept = ret_val.body.body + assert new_concept.name == 'foo' + assert new_concept.metadata.body == 'foo' + assert new_concept.props == {} + assert new_concept.metadata.props == [] diff --git a/tests/evaluators/test_ConceptEvaluator.py b/tests/evaluators/test_ConceptEvaluator.py index 4589ce7..64408b2 100644 --- a/tests/evaluators/test_ConceptEvaluator.py +++ b/tests/evaluators/test_ConceptEvaluator.py @@ -103,7 +103,7 @@ class TestAddConceptEvaluator(TestUsingMemoryBasedSheerka): assert context.sheerka.isinstance(result.value, BuiltinConcepts.CONCEPT_EVAL_ERROR) assert result.value.concept == concept_plus assert result.value.property_name == "b" - assert context.sheerka.isinstance(result.value.error, BuiltinConcepts.TOO_MANY_ERRORS) + assert context.sheerka.isinstance(result.value.error, BuiltinConcepts.ERROR) def test_that_error_concepts_are_transformed_into_errors_only_if_the_key_is_different(self): context = self.get_context() diff --git a/tests/non_reg/test_sheerka_non_reg.py b/tests/non_reg/test_sheerka_non_reg.py index 7cde5e3..f4b0f05 100644 --- a/tests/non_reg/test_sheerka_non_reg.py +++ b/tests/non_reg/test_sheerka_non_reg.py @@ -671,3 +671,28 @@ as: res = sheerka.evaluate_user_input("eval twenty three") assert len(res) > 1 + + def test_i_can_detect_when_only_one_evaluator_is_in_error(self): + sheerka = self.get_sheerka() + + sheerka.evaluate_user_input("def concept 1 as one") + res = sheerka.evaluate_user_input("1") + assert len(res) == 1 + assert not res[0].status + assert sheerka.isinstance(res[0].body, BuiltinConcepts.CONCEPT_EVAL_ERROR) + + def test_i_can_manage_some_type_of_infinite_recursion(self): + sheerka = self.get_sheerka() + + sheerka.evaluate_user_input("def concept one as 1") + sheerka.evaluate_user_input("def concept 1 as one") + res = sheerka.evaluate_user_input("one + 1") + assert len(res) == 1 + assert res[0].status + assert res[0].body == 2 + + res = sheerka.evaluate_user_input("1 + 1") + assert len(res) == 1 + assert res[0].status + assert res[0].body == 2 +