From 7fa509555da37b7e5e16439c81b02fef4c2b4dfe Mon Sep 17 00:00:00 2001 From: Kodjo Sossouvi Date: Sat, 16 Nov 2019 18:11:29 +0100 Subject: [PATCH] Managing concept properties in ConceptEvaluator --- core/builtin_concepts.py | 41 +++- core/concept.py | 2 +- core/sheerka.py | 81 +++++-- core/utils.py | 12 +- docs/blog.rst | 75 +++++- evaluators/ConceptEvaluator.py | 38 ++- evaluators/DuplicateConceptEvaluator.py | 2 +- evaluators/MutipleSameSuccessEvaluator.py | 62 +++++ ...ersEvaluator.py => OneSuccessEvaluator.py} | 11 +- evaluators/PythonEvaluator.py | 14 +- tests/test_ConceptEvaluator.py | 217 +++++++++++++++++ tests/test_MultipleSameSuccessEvaluator.py | 223 ++++++++++++++++++ tests/test_sheerka.py | 87 ++++++- 13 files changed, 808 insertions(+), 57 deletions(-) create mode 100644 evaluators/MutipleSameSuccessEvaluator.py rename evaluators/{ParsersEvaluator.py => OneSuccessEvaluator.py} (83%) create mode 100644 tests/test_ConceptEvaluator.py create mode 100644 tests/test_MultipleSameSuccessEvaluator.py diff --git a/core/builtin_concepts.py b/core/builtin_concepts.py index d4db061..f493251 100644 --- a/core/builtin_concepts.py +++ b/core/builtin_concepts.py @@ -23,9 +23,13 @@ class BuiltinConcepts(Enum): INVALID_RETURN_VALUE = 14 # the return value of an evaluator is not correct BEFORE_PARSING = 15 # activated before evaluation by the parsers PARSING = 16 # activated during the parsing. It contains the text to parse - AFTER_PARSING = 17 # activated when the parsing process seems to be finished - CONCEPT_ALREADY_DEFINED = 18 # when you try to add the same concept twice - NOP = 19 # no operation concept. Does nothing + AFTER_PARSING = 17 # after parsing + BEFORE_EVALUATION = 18 # before evalution + EVALUATION = 19 # activated when the parsing process seems to be finished + AFTER_EVALUATION = 20 # activated when the parsing process seems to be finished + CONCEPT_ALREADY_DEFINED = 21 # when you try to add the same concept twice + NOP = 22 # no operation concept. Does nothing + PROPERTY_EVAL_ERROR = 23 """ @@ -198,11 +202,34 @@ class BeforeParsingConcept(Concept): super().__init__(BuiltinConcepts.BEFORE_PARSING, True, True, BuiltinConcepts.BEFORE_PARSING) -class ParsingConcept(Concept): +class EvaluationConcept(Concept): def __init__(self): - super().__init__(BuiltinConcepts.PARSING, True, True, BuiltinConcepts.PARSING) + super().__init__(BuiltinConcepts.EVALUATION, True, True, BuiltinConcepts.EVALUATION) -class AfterParsingConcept(Concept): +class AfterEvaluationConcept(Concept): def __init__(self): - super().__init__(BuiltinConcepts.AFTER_PARSING, True, True, BuiltinConcepts.AFTER_PARSING) + super().__init__(BuiltinConcepts.AFTER_EVALUATION, True, True, BuiltinConcepts.AFTER_EVALUATION) + + +class PropertyEvalError(Concept): + def __init__(self, property_name=None, concept=None, error=None): + super().__init__(BuiltinConcepts.PROPERTY_EVAL_ERROR, True, False, BuiltinConcepts.PROPERTY_EVAL_ERROR) + self.set_prop("concept", concept) + self.set_prop("error", error) + self.body = property_name + + def __repr__(self): + return f"PropertyEvalError(property={self.property_name}, concept={self.concept}), error={self.error})" + + @property + def concept(self): + return self.props["concept"].value + + @property + def error(self): + return self.props["error"].value + + @property + def property_name(self): + return self.body diff --git a/core/concept.py b/core/concept.py index 515a639..80ae686 100644 --- a/core/concept.py +++ b/core/concept.py @@ -69,7 +69,7 @@ class Concept: # check the attributes for prop in self.props_to_serialize: if getattr(self, prop) != getattr(other, prop): - print(prop) + # print(prop) # use full to know which id does not match return False # check the props (Concept variables) diff --git a/core/sheerka.py b/core/sheerka.py index d2c1a2c..cd22466 100644 --- a/core/sheerka.py +++ b/core/sheerka.py @@ -150,19 +150,24 @@ class Sheerka(Concept): evt_digest = self.sdp.save_event(Event(text)) exec_context = ExecutionContext(self.key, evt_digest, self) - before_parsing = self.ret(self.eval.__name__, True, self.new(BuiltinConcepts.BEFORE_PARSING)) + # Before parsing + before_parsing = self.new(BuiltinConcepts.BEFORE_PARSING) return_values = self.process(exec_context, [], [before_parsing]) - return_values = core.utils.remove_from_list(return_values, [before_parsing]) + return_values = core.utils.remove_from_list(return_values, lambda x: x.value == before_parsing) + # parse parsing_results = self.parse(exec_context, text) return_values.extend(parsing_results) - processing_parsing = self.ret(self.eval.__name__, True, self.new(BuiltinConcepts.PARSING)) - return_values = self.process(exec_context, return_values, [processing_parsing]) - return_values = core.utils.remove_from_list(return_values, [processing_parsing]) - after_parsing = self.ret(self.eval.__name__, True, self.new(BuiltinConcepts.AFTER_PARSING)) - return_values = self.process(exec_context, return_values, [after_parsing]) - return_values = core.utils.remove_from_list(return_values, [after_parsing]) + # evaluate + evaluating = self.new(BuiltinConcepts.EVALUATION) + return_values = self.process(exec_context, return_values, [evaluating]) + return_values = core.utils.remove_from_list(return_values, lambda x: x.value == evaluating) + + # post evaluation + after_evaluation = self.new(BuiltinConcepts.AFTER_EVALUATION) + return_values = self.process(exec_context, return_values, [after_evaluation]) + return_values = core.utils.remove_from_list(return_values, lambda x: x.value == after_evaluation) return return_values @@ -207,17 +212,17 @@ class Sheerka(Concept): result.append(res) return result - def process(self, context, return_values, contextual_concepts=None): - contextual_concepts_values = [c.value for c in contextual_concepts] if contextual_concepts else [] - log.debug(f"Processing parsing result. context concept={contextual_concepts_values}") + def process(self, context, return_values, initial_concepts=None): + log.debug(f"Processing parsing result. context concept={initial_concepts}") # return_values must be a list if not isinstance(return_values, list): return_values = [return_values] # adds contextual concepts - if contextual_concepts: - return_values.extend(contextual_concepts) + if initial_concepts: + for concept in initial_concepts: + return_values.append(self.ret(context.who, True, concept)) # group the evaluators by priority and sort them # The first one to be applied will be the one with the highest priority @@ -261,6 +266,8 @@ class Sheerka(Concept): else: if evaluator.matches(context, original_items): results = evaluator.eval(context, original_items) + if results is None: + continue if not isinstance(results, list): results = [results] for result in results: @@ -277,6 +284,24 @@ class Sheerka(Concept): return return_values + def chain_process(self, context, return_values, initial_concepts): + """ + Executes process for all initial contexts + :param context: + :param return_values: + :param initial_concepts: + :return: + """ + for concept in initial_concepts: + if isinstance(concept, BuiltinConcepts): + concept = self.new(BuiltinConcepts) + + init = [self.ret(context.who, True, concept)] + return_values = self.process(context, return_values, [init]) + return_values = core.utils.remove_from_list(return_values, lambda x: x.value == init) + + return return_values + def create_new_concept(self, context, concept): """ Adds a new concept to the system @@ -319,13 +344,14 @@ class Sheerka(Concept): for part_key in ConceptParts: source = getattr(concept, part_key.value) if source is None or not isinstance(source, str) or source == "": - # the only sources that I am sure to parse are strings # I refuse empty strings for performance, I don't want to handle useless NOPConcepts continue + else: + concept.codes[part_key] = self.parse(context, source) - ret_val = self.expect_one(context, self.parse(context, source)) - concept.codes[part_key] = ret_val + for prop in concept.props: + concept.codes[prop] = self.parse(context, concept.props[prop].value) def add_in_cache(self, concept): """ @@ -334,7 +360,16 @@ class Sheerka(Concept): :param concept: :return: """ + + # sanity check + if concept.key is None: + concept.init_key() + + if concept.key is None: + raise KeyError() + self.concepts_cache[concept.key] = concept + return concept def get(self, concept_key): """ @@ -455,7 +490,7 @@ class Sheerka(Concept): base_class = core.utils.get_class("parsers.BaseParser.BaseParser") for c in core.utils.get_classes_recursive("parsers"): - #if issubclass(c, base_class) and c != base_class: + # if issubclass(c, base_class) and c != base_class: res.append(c) return res @@ -470,9 +505,11 @@ class ExecutionContext: """ To keep track of the execution of a request """ - who: object - event_digest: str - sheerka: Sheerka + who: object # who is asking + event_digest: str # what was the (original) trigger + sheerka: Sheerka # sheerka + desc: str = None # human description of what is going on + obj: Concept = None # what is the subject of the execution context (if known) - def push(self, who): - return ExecutionContext(who, self.event_digest, self.sheerka) + def push(self, who, desc=None, obj=None): + return ExecutionContext(who, self.event_digest, self.sheerka, desc=desc, obj=obj) diff --git a/core/utils.py b/core/utils.py index 2e629a1..551f89b 100644 --- a/core/utils.py +++ b/core/utils.py @@ -106,7 +106,6 @@ def get_classes_from_package(package_name): def get_sub_classes(package_name, base_class_name): - pkg = __import__(package_name) prefix = pkg.__name__ + "." for (module_loader, name, ispkg) in pkgutil.iter_modules(pkg.__path__, prefix): @@ -123,8 +122,13 @@ def remove_from_list(lst, to_remove): :param to_remove: :return: """ - for item in to_remove: - if item in lst: - lst.remove(item) + + flagged = [] + for item in lst: + if to_remove(item): + flagged.append(item) + + for item in flagged: + lst.remove(item) return lst diff --git a/docs/blog.rst b/docs/blog.rst index 8ecc310..c02cc0a 100644 --- a/docs/blog.rst +++ b/docs/blog.rst @@ -396,4 +396,77 @@ I must choose between expressing my ideas in this blog and coding. I have plenty of ideas that I would like to express, sometimes just to put the idea down, but I lack of time. It would be great if I can find a tool that will allow me to just to dictate my words. I know that there are plenty out there, I need to spend some time to test -them and choose one. \ No newline at end of file +them and choose one. + +2019-11-15 +********** + +Managing concepts resolutions +""""""""""""""""""""""""""""" +I am a little stuck on the algorithm I must use to derive (resolve) concepts. This is +one of this day I strongly regret to have someone I can discuss with :-( + +Let's write the problem down, sometimes, it helps figure out the best approach. + +:: + + def concept one as 1 + one + +The concept is first define (it returns the number 1), and then it's called. +During the call + +1. During parsing, + Both Python parser and concept parser will recognize 'one' +2. During Evaluation, + * Python Evaluator will fail (one is not know by python) + * Concept Evaluator will success. My question is what should it return ? + +The two option are: +1. Python node, to let the Python Evaluator work and return one, in the next row +2. Returns '1' directly + +I as write it down, it is obvious that it must return 1, since the purpose of any +evaluation is to give a result, not the path to find the result. + +Plus, if don"t resolve the body in the Concept Evaluator, I will loose where the +'1' comes from. + +I don't know if I was clear. I don't even know if I will be able to re-read myself. +But I think that I have my solution. + + +2019-11-16 +********** + +ExactConceptParser limitation +""""""""""""""""""""""""""""" + +From the beginning, my simplest example is to show that addition can be simply +explained to Sheerka + +:: + + def concept a plus b as a + b + def concept one as 1 + def concept two as 2 + one plus two + +The :code:`one plus two` is perfectly recognized, and the result is 3. +:code:`two plus one` also work (with the correct response). + +But I was quite surprised to see that :code:`one plus one` was not recognized !! + +Indeed, the **ExactConceptParser** looks for :code:`__var0__ plus __var1__`. So +the first operand and the second have to be different. + +It's unexpected :-( + +Do I need to enhance the parser to recognize it, or no I need to build another parser ? + +If I tell the parser that :code:`a plus b`, how do I handle the cases where 'a 'and 'b' +MUST be different ? How I handle when the explicitly have to be the same ? + +I seems that the purpose of the **ExactConceptParser** is to find exact match. +I need another way to express that 'a' and 'b' can be the same. + diff --git a/evaluators/ConceptEvaluator.py b/evaluators/ConceptEvaluator.py index 58a6b46..76abea6 100644 --- a/evaluators/ConceptEvaluator.py +++ b/evaluators/ConceptEvaluator.py @@ -1,4 +1,4 @@ -from core.builtin_concepts import ParserResultConcept +from core.builtin_concepts import ParserResultConcept, BuiltinConcepts from core.concept import Concept, ConceptParts from evaluators.BaseEvaluator import OneReturnValueEvaluator import logging @@ -9,6 +9,8 @@ log = logging.getLogger(__name__) class ConceptEvaluator(OneReturnValueEvaluator): + evaluation_steps = [BuiltinConcepts.EVALUATION, BuiltinConcepts.AFTER_EVALUATION] + def __init__(self): super().__init__("Concept Evaluator", 50) @@ -31,12 +33,32 @@ class ConceptEvaluator(OneReturnValueEvaluator): # TODO; check pre # if pre is not true, return Concept with a false value - if ConceptParts.BODY in concept.codes: - body = concept.codes[ConceptParts.BODY] - if body is None: - return None # nothing to do + # Evaluate the properties + for prop in concept.props: + sub_context = context.push(self.name, f"Evaluating property '{prop}'", concept) + res = self.evaluate_parsing(sheerka, sub_context, concept.codes[prop]) + if res.status: + concept.set_prop(prop, res.value) + else: + return sheerka.ret( + self.name, + False, + sheerka.new(BuiltinConcepts.PROPERTY_EVAL_ERROR, body=prop, concept=concept, error=res.value), + parents=[return_value]) - return sheerka.ret(self.name, True, body.value, parents=[return_value]) - - else: + # Evaluate body + if ConceptParts.BODY not in concept.codes: return sheerka.ret(self.name, True, concept, parents=[return_value]) + + body = concept.codes[ConceptParts.BODY] + if body is None: + return None # seems weird + + sub_context = context.push(self.name, "Evaluating body", concept) + res = self.evaluate_parsing(sheerka, sub_context, body) + return sheerka.ret(self.name, res.status, res.value, parents=[return_value]) + + def evaluate_parsing(self, sheerka, context, parsing_result): + res = sheerka.chain_process(context, parsing_result, self.evaluation_steps) + res = sheerka.expect_one(context, res) + return res diff --git a/evaluators/DuplicateConceptEvaluator.py b/evaluators/DuplicateConceptEvaluator.py index 8922359..b2cb4c8 100644 --- a/evaluators/DuplicateConceptEvaluator.py +++ b/evaluators/DuplicateConceptEvaluator.py @@ -19,7 +19,7 @@ class DuplicateConceptEvaluator(AllReturnValuesEvaluator): only_parsers = True for ret in return_values: - if sheerka.isinstance(ret.value, BuiltinConcepts.PARSING): + if sheerka.isinstance(ret.value, BuiltinConcepts.AFTER_EVALUATION): if ret.status: parsing = True elif ret.who == "Evaluators:Add new Concept": diff --git a/evaluators/MutipleSameSuccessEvaluator.py b/evaluators/MutipleSameSuccessEvaluator.py new file mode 100644 index 0000000..4424e78 --- /dev/null +++ b/evaluators/MutipleSameSuccessEvaluator.py @@ -0,0 +1,62 @@ +from core.builtin_concepts import BuiltinConcepts +from core.concept import Concept +from evaluators.BaseEvaluator import AllReturnValuesEvaluator, BaseEvaluator +import logging + +from parsers.BaseParser import BaseParser + +log = logging.getLogger(__name__) + + +class MultipleSameSuccessEvaluator(AllReturnValuesEvaluator): + """ + Used to filter the responses + It has a low priority to let other evaluators try to resolve the errors + """ + + def __init__(self): + super().__init__("Parsers Evaluator", 10) + self.success = [] + + def matches(self, context, return_values): + sheerka = context.sheerka + after_evaluation = False + nb_successful_evaluators = 0 + only_parsers_in_error = True + unlisted = False + + for ret in return_values: + + if sheerka.isinstance(ret.value, BuiltinConcepts.AFTER_EVALUATION): + if ret.status: + after_evaluation = True + + elif ret.who.startswith(BaseEvaluator.PREFIX): + if ret.status: + nb_successful_evaluators += 1 + self.success.append(ret.value) + elif ret.who.startswith(BaseParser.PREFIX): + if ret.status: + only_parsers_in_error = False + else: + unlisted = True + + return after_evaluation and nb_successful_evaluators > 1 and only_parsers_in_error and not unlisted + + def eval(self, context, return_values): + reference = self.get_value(self.success[0]) + + for return_value in self.success[1:]: + actual = self.get_value(return_value) + if actual != reference: + return None + + sheerka = context.sheerka + return sheerka.ret(self.name, True, reference, parents=return_values) + + @staticmethod + def get_value(obj): + if not isinstance(obj, Concept): + return obj + + return obj if obj.body is None else obj.body diff --git a/evaluators/ParsersEvaluator.py b/evaluators/OneSuccessEvaluator.py similarity index 83% rename from evaluators/ParsersEvaluator.py rename to evaluators/OneSuccessEvaluator.py index 3b2e6c2..b59290e 100644 --- a/evaluators/ParsersEvaluator.py +++ b/evaluators/OneSuccessEvaluator.py @@ -1,5 +1,4 @@ from core.builtin_concepts import BuiltinConcepts -from core.concept import Concept from evaluators.BaseEvaluator import AllReturnValuesEvaluator import logging @@ -8,7 +7,7 @@ from parsers.BaseParser import BaseParser log = logging.getLogger(__name__) -class ParsersEvaluator(AllReturnValuesEvaluator): +class OneSuccessEvaluator(AllReturnValuesEvaluator): """ Used to filter the responses It has a low priority to let other evaluators try to resolve the errors @@ -20,13 +19,13 @@ class ParsersEvaluator(AllReturnValuesEvaluator): def matches(self, context, return_values): sheerka = context.sheerka - after_parsing = False + after_evaluation = False nb_successful_evaluators = 0 only_parsers = True for ret in return_values: - if sheerka.isinstance(ret.value, BuiltinConcepts.AFTER_PARSING): + if sheerka.isinstance(ret.value, BuiltinConcepts.AFTER_EVALUATION): if ret.status: - after_parsing = True + after_evaluation = True elif ret.who.startswith(self.PREFIX): if ret.status: nb_successful_evaluators += 1 @@ -35,7 +34,7 @@ class ParsersEvaluator(AllReturnValuesEvaluator): if not ret.who.startswith(BaseParser.PREFIX): only_parsers = False - return after_parsing and nb_successful_evaluators == 1 and only_parsers + return after_evaluation and nb_successful_evaluators == 1 and only_parsers def eval(self, context, return_values): sheerka = context.sheerka diff --git a/evaluators/PythonEvaluator.py b/evaluators/PythonEvaluator.py index 93173c1..1ec22a7 100644 --- a/evaluators/PythonEvaluator.py +++ b/evaluators/PythonEvaluator.py @@ -13,8 +13,8 @@ class PythonEvaluator(OneReturnValueEvaluator): def matches(self, context, return_value): return return_value.status and \ - isinstance(return_value.value, ParserResultConcept) and \ - isinstance(return_value.value.value, PythonNode) + isinstance(return_value.value, ParserResultConcept) and \ + isinstance(return_value.value.value, PythonNode) def eval(self, context, return_value): sheerka = context.sheerka @@ -23,10 +23,18 @@ class PythonEvaluator(OneReturnValueEvaluator): try: log.debug(f"Evaluating python node {node}") compiled = compile(node.ast_, "", "eval") - evaluated = eval(compiled, {}, {"sheerka": context.sheerka}) + evaluated = eval(compiled, {}, self.get_locals(context)) return sheerka.ret(self.name, True, evaluated, parents=[return_value]) except Exception as error: error = sheerka.new(BuiltinConcepts.ERROR, body=error) return sheerka.ret(self.name, False, error, parents=[return_value]) else: return sheerka.ret(self.name, False, sheerka.new(BuiltinConcepts.ERROR), parents=[return_value]) + + @staticmethod + def get_locals(context): + my_locals = {"sheerka": context.sheerka} + if context.obj: + for prop_name, prop_value in context.obj.props.items(): + my_locals[prop_name] = prop_value.value + return my_locals diff --git a/tests/test_ConceptEvaluator.py b/tests/test_ConceptEvaluator.py new file mode 100644 index 0000000..e9d33f1 --- /dev/null +++ b/tests/test_ConceptEvaluator.py @@ -0,0 +1,217 @@ +import os +import shutil +from os import path + +import pytest + +from core.builtin_concepts import ReturnValueConcept, ParserResultConcept, BuiltinConcepts +from core.concept import Concept +from core.sheerka import Sheerka, ExecutionContext +from evaluators.ConceptEvaluator import ConceptEvaluator +from parsers.BaseParser import BaseParser + +tests_root = path.abspath("../build/tests") +root_folder = "init_folder" + + +@pytest.fixture(autouse=True) +def init_test(): + if path.exists(tests_root): + shutil.rmtree(tests_root) + + if not path.exists(tests_root): + os.makedirs(tests_root) + current_pwd = os.getcwd() + os.chdir(tests_root) + + yield None + os.chdir(current_pwd) + + +def get_context(): + sheerka = Sheerka() + sheerka.initialize(root_folder) + return ExecutionContext("test", "xxx", sheerka) + + +@pytest.mark.parametrize("ret_val, expected", [ + (ReturnValueConcept(BaseParser.PREFIX + "some_name", True, Concept()), True), + (ReturnValueConcept(BaseParser.PREFIX + "some_name", False, Concept()), False), + (ReturnValueConcept("Not a parser", True, Concept()), False), + (ReturnValueConcept(BaseParser.PREFIX + "some_name", True, "not a concept"), False), + (ReturnValueConcept(BaseParser.PREFIX + "some_name", True, ParserResultConcept()), False), +]) +def test_i_can_match(ret_val, expected): + context = get_context() + assert ConceptEvaluator().matches(context, ret_val) == expected + + +def test_concept_is_returned_when_no_body(): + context = get_context() + concept = Concept(name="one").init_key() + + evaluator = ConceptEvaluator() + item = ReturnValueConcept(BaseParser.PREFIX + "some_name", True, concept) + result = evaluator.eval(context, item) + + assert result.who == evaluator.name + assert result.status + assert result.value == concept + assert result.parents == [item] + + +def test_body_is_evaluated_when_python_body(): + context = get_context() + concept = Concept(name="one", body="1").init_key() + + evaluator = ConceptEvaluator() + item = ReturnValueConcept(BaseParser.PREFIX + "some_name", True, concept) + result = evaluator.eval(context, item) + + assert result.who == evaluator.name + assert result.status + assert result.value == 1 + assert result.parents == [item] + + +def test_body_is_evaluated_when_concept_body(): + context = get_context() + concept_one = Concept(name="one").init_key() + context.sheerka.add_in_cache(concept_one) + concept_un = Concept(name="un", body="one").init_key() + + evaluator = ConceptEvaluator() + item = ReturnValueConcept(BaseParser.PREFIX + "some_name", True, concept_un) + result = evaluator.eval(context, item) + + assert result.who == evaluator.name + assert result.status + assert result.value == concept_one + assert result.parents == [item] + + +def test_body_is_evaluated_when_concept_body_with_a_body(): + context = get_context() + concept_one = Concept(name="one", body="1").init_key() + context.sheerka.add_in_cache(concept_one) + concept_un = Concept(name="un", body="one").init_key() + + evaluator = ConceptEvaluator() + item = ReturnValueConcept(BaseParser.PREFIX + "some_name", True, concept_un) + result = evaluator.eval(context, item) + + assert result.who == evaluator.name + assert result.status + assert result.value == 1 + assert result.parents == [item] + + +def test_i_can_evaluate_longer_chains(): + context = get_context() + context.sheerka.add_in_cache(Concept(name="a", body="'a'").init_key()) + context.sheerka.add_in_cache(Concept(name="b", body="a").init_key()) + context.sheerka.add_in_cache(Concept(name="c", body="b").init_key()) + concept_d = context.sheerka.add_in_cache(Concept(name="d", body="c").init_key()) + + evaluator = ConceptEvaluator() + item = ReturnValueConcept(BaseParser.PREFIX + "some_name", True, concept_d) + result = evaluator.eval(context, item) + + assert result.status + assert result.value == 'a' + + +def test_i_can_evaluate_longer_chains_2(): + context = get_context() + concept_a = context.sheerka.add_in_cache(Concept(name="a").init_key()) + context.sheerka.add_in_cache(Concept(name="b", body="a").init_key()) + context.sheerka.add_in_cache(Concept(name="c", body="b").init_key()) + concept_d = context.sheerka.add_in_cache(Concept(name="d", body="c").init_key()) + + evaluator = ConceptEvaluator() + item = ReturnValueConcept(BaseParser.PREFIX + "some_name", True, concept_d) + result = evaluator.eval(context, item) + + assert result.status + assert result.value == concept_a + + +def test_i_can_recognize_concept_properties(): + """ + The concept 'plus' has some properties. + Let's check if they are recognized as concepts + :return: + """ + context = get_context() + concept_one = context.sheerka.add_in_cache(Concept(name="one").init_key()) + concept_two = context.sheerka.add_in_cache(Concept(name="two").init_key()) + concept_plus = context.sheerka.add_in_cache(Concept(name="a plus b") + .set_prop("a", "one") + .set_prop("b", "two").init_key()) + + evaluator = ConceptEvaluator() + item = ReturnValueConcept(BaseParser.PREFIX + "some_name", True, concept_plus) + result = evaluator.eval(context, item) + + assert result.status + assert context.sheerka.isinstance(result.value, concept_plus) + assert result.value.props["a"].value == concept_one + assert result.value.props["b"].value == concept_two + + +def test_i_can_recognize_concept_properties_with_body(): + """ + The concept 'plus' has some properties. + Let's check if they are recognized as concepts with Python code + :return: + """ + context = get_context() + context.sheerka.add_in_cache(Concept(name="one", body="1").init_key()) + context.sheerka.add_in_cache(Concept(name="two", body="2").init_key()) + concept_plus = context.sheerka.add_in_cache(Concept(name="a plus b") + .set_prop("a", "one") + .set_prop("b", "two").init_key()) + + evaluator = ConceptEvaluator() + item = ReturnValueConcept(BaseParser.PREFIX + "some_name", True, concept_plus) + result = evaluator.eval(context, item) + + assert result.status + assert context.sheerka.isinstance(result.value, concept_plus) + assert result.value.props["a"].value == 1 + assert result.value.props["b"].value == 2 + + +def test_i_can_recognize_concept_properties_with_body_when_concept_has_a_body(): + context = get_context() + context.sheerka.add_in_cache(Concept(name="one", body="1").init_key()) + context.sheerka.add_in_cache(Concept(name="two", body="2").init_key()) + concept_plus = context.sheerka.add_in_cache(Concept(name="a plus b", body="a + b") + .set_prop("a", "one") + .set_prop("b", "two").init_key()) + + evaluator = ConceptEvaluator() + item = ReturnValueConcept(BaseParser.PREFIX + "some_name", True, concept_plus) + result = evaluator.eval(context, item) + + assert result.status + assert result.value == 3 + + +def test_i_cannot_recognize_a_concept_if_one_of_the_prop_is_unknown(): + context = get_context() + context.sheerka.add_in_cache(Concept(name="one").init_key()) + concept_plus = context.sheerka.add_in_cache(Concept(name="a plus b") + .set_prop("a", "one") + .set_prop("b", "two").init_key()) + + evaluator = ConceptEvaluator() + item = ReturnValueConcept(BaseParser.PREFIX + "some_name", True, concept_plus) + result = evaluator.eval(context, item) + + assert not result.status + assert context.sheerka.isinstance(result.value, BuiltinConcepts.PROPERTY_EVAL_ERROR) + assert result.value.property_name == "b" + assert context.sheerka.isinstance(result.value.error, BuiltinConcepts.TOO_MANY_ERRORS) + assert result.value.concept == concept_plus + diff --git a/tests/test_MultipleSameSuccessEvaluator.py b/tests/test_MultipleSameSuccessEvaluator.py new file mode 100644 index 0000000..e6243b5 --- /dev/null +++ b/tests/test_MultipleSameSuccessEvaluator.py @@ -0,0 +1,223 @@ +import os +import shutil +from os import path + +import pytest + +from core.builtin_concepts import ReturnValueConcept, BuiltinConcepts +from core.concept import Concept +from core.sheerka import Sheerka, ExecutionContext +from evaluators.BaseEvaluator import BaseEvaluator +from evaluators.MutipleSameSuccessEvaluator import MultipleSameSuccessEvaluator +from parsers.BaseParser import BaseParser + +tests_root = path.abspath("../build/tests") +root_folder = "init_folder" + + +@pytest.fixture(autouse=True) +def init_test(): + if path.exists(tests_root): + shutil.rmtree(tests_root) + + if not path.exists(tests_root): + os.makedirs(tests_root) + current_pwd = os.getcwd() + os.chdir(tests_root) + + yield None + os.chdir(current_pwd) + + +def get_context(): + sheerka = Sheerka() + sheerka.initialize(root_folder) + return ExecutionContext("test", "xxx", sheerka) + + +def test_i_can_match_and_eval(): + context = get_context() + sheerka = context.sheerka + + return_values = [ + ReturnValueConcept(BaseParser.PREFIX + "some_name", False, "Not relevant"), + ReturnValueConcept(BaseParser.PREFIX + "some_name2", False, "Not relevant"), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="1", body="value")), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="2", body="value")), + ReturnValueConcept("some_name", True, sheerka.new(BuiltinConcepts.AFTER_EVALUATION)) + ] + + evaluator = MultipleSameSuccessEvaluator() + assert evaluator.matches(context, return_values) + + evaluated = evaluator.eval(context, return_values) + assert evaluated.status + assert evaluated.value == "value" + + +def test_i_can_match_and_eval_when_no_body(): + context = get_context() + sheerka = context.sheerka + + return_values = [ + ReturnValueConcept(BaseParser.PREFIX + "some_name", False, "Not relevant"), + ReturnValueConcept(BaseParser.PREFIX + "some_name2", False, "Not relevant"), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="1")), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="1")), + ReturnValueConcept("some_name", True, sheerka.new(BuiltinConcepts.AFTER_EVALUATION)) + ] + + evaluator = MultipleSameSuccessEvaluator() + assert evaluator.matches(context, return_values) + + evaluated = evaluator.eval(context, return_values) + assert evaluated.status + assert evaluated.value == Concept(name="1") + + +def test_i_can_match_and_eval_when_value_is_not_a_concept(): + context = get_context() + sheerka = context.sheerka + + return_values = [ + ReturnValueConcept(BaseParser.PREFIX + "some_name", False, "Not relevant"), + ReturnValueConcept(BaseParser.PREFIX + "some_name2", False, "Not relevant"), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, "value"), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, "value"), + ReturnValueConcept("some_name", True, sheerka.new(BuiltinConcepts.AFTER_EVALUATION)) + ] + + evaluator = MultipleSameSuccessEvaluator() + assert evaluator.matches(context, return_values) + + evaluated = evaluator.eval(context, return_values) + assert evaluated.status + assert evaluated.value == "value" + + +def test_i_can_match_even_if_the_value_are_not_the_same_but_eval_will_fail(): + context = get_context() + sheerka = context.sheerka + + return_values = [ + ReturnValueConcept(BaseParser.PREFIX + "some_name", False, "Not relevant"), + ReturnValueConcept(BaseParser.PREFIX + "some_name2", False, "Not relevant"), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="1", body="value")), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="2", body="value2")), + ReturnValueConcept("some_name", True, sheerka.new(BuiltinConcepts.AFTER_EVALUATION)) + ] + + evaluator = MultipleSameSuccessEvaluator() + assert evaluator.matches(context, return_values) + assert evaluator.eval(context, return_values) is None + + +def test_i_can_match_even_if_the_value_are_not_the_same_but_eval_will_fail_when_no_body(): + context = get_context() + sheerka = context.sheerka + + return_values = [ + ReturnValueConcept(BaseParser.PREFIX + "some_name", False, "Not relevant"), + ReturnValueConcept(BaseParser.PREFIX + "some_name2", False, "Not relevant"), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="1")), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="2")), + ReturnValueConcept("some_name", True, sheerka.new(BuiltinConcepts.AFTER_EVALUATION)) + ] + + evaluator = MultipleSameSuccessEvaluator() + assert evaluator.matches(context, return_values) + assert evaluator.eval(context, return_values) is None + + +def test_i_can_match_if_no_parser(): + context = get_context() + sheerka = context.sheerka + + return_values = [ + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="1", body="value")), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="2", body="value")), + ReturnValueConcept("some_name", True, sheerka.new(BuiltinConcepts.AFTER_EVALUATION)) + ] + + assert MultipleSameSuccessEvaluator().matches(context, return_values) + + +def test_i_cannot_match_if_not_after_evaluation(): + context = get_context() + sheerka = context.sheerka + + return_values = [ + ReturnValueConcept(BaseParser.PREFIX + "some_name", False, "Not relevant"), + ReturnValueConcept(BaseParser.PREFIX + "some_name2", False, "Not relevant"), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="1", body="value")), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="2", body="value")), + ReturnValueConcept("some_name", True, Concept(name="2", body="value")), + # ReturnValueConcept("some_name", True, sheerka.new(BuiltinConcepts.AFTER_EVALUATION)) + ] + + assert not MultipleSameSuccessEvaluator().matches(context, return_values) + + +def test_i_cannot_match_if_only_one_successful_evaluator(): + context = get_context() + sheerka = context.sheerka + + return_values = [ + ReturnValueConcept(BaseParser.PREFIX + "some_name", False, "Not relevant"), + ReturnValueConcept(BaseParser.PREFIX + "some_name2", False, "Not relevant"), + # ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="1", body="value")), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="2", body="value")), + ReturnValueConcept("some_name", True, Concept(name="2", body="value")), + ReturnValueConcept("some_name", True, sheerka.new(BuiltinConcepts.AFTER_EVALUATION)) + ] + + assert not MultipleSameSuccessEvaluator().matches(context, return_values) + + +def test_i_cannot_match_if_at_least_one_parser_is_successful(): + context = get_context() + sheerka = context.sheerka + + return_values = [ + ReturnValueConcept(BaseParser.PREFIX + "some_name", False, "Not relevant"), + ReturnValueConcept(BaseParser.PREFIX + "some_name2", True, "Not relevant"), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="1", body="value")), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="2", body="value")), + ReturnValueConcept("some_name", True, sheerka.new(BuiltinConcepts.AFTER_EVALUATION)) + ] + + assert not MultipleSameSuccessEvaluator().matches(context, return_values) + + +def test_i_cannot_match_if_i_have_unlisted_return_value_in_success(): + context = get_context() + sheerka = context.sheerka + + return_values = [ + ReturnValueConcept(BaseParser.PREFIX + "some_name", False, "Not relevant"), + ReturnValueConcept(BaseParser.PREFIX + "some_name2", False, "Not relevant"), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="1", body="value")), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="2", body="value")), + ReturnValueConcept("some_name", True, sheerka.new(BuiltinConcepts.AFTER_EVALUATION)), + ReturnValueConcept("some_name", True, "not relevant"), + + ] + + assert not MultipleSameSuccessEvaluator().matches(context, return_values) + + +def test_i_cannot_match_if_i_have_unlisted_return_value_in_error(): + context = get_context() + sheerka = context.sheerka + + return_values = [ + ReturnValueConcept(BaseParser.PREFIX + "some_name", False, "Not relevant"), + ReturnValueConcept(BaseParser.PREFIX + "some_name2", False, "Not relevant"), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="1", body="value")), + ReturnValueConcept(BaseEvaluator.PREFIX + "some_name", True, Concept(name="2", body="value")), + ReturnValueConcept("some_name", True, sheerka.new(BuiltinConcepts.AFTER_EVALUATION)), + ReturnValueConcept("some_name", False, "not relevant"), + + ] + + assert not MultipleSameSuccessEvaluator().matches(context, return_values) diff --git a/tests/test_sheerka.py b/tests/test_sheerka.py index a3c8eb7..47f89d8 100644 --- a/tests/test_sheerka.py +++ b/tests/test_sheerka.py @@ -341,7 +341,7 @@ def test_i_can_use_expect_one_when_not_a_list_false(): ("1 + 1", 2), ("sheerka.test()", 'I have access to Sheerka !') ]) -def test_i_can_eval_simple_python_expressions(text, expected): +def test_i_can_eval_python_expressions_with_no_variable(text, expected): sheerka = get_sheerka() res = sheerka.eval(text) @@ -351,9 +351,9 @@ def test_i_can_eval_simple_python_expressions(text, expected): assert res[0].value == expected -def test_i_can_eval_simple_concept(): +def test_i_can_eval_concept_with_python_body(): sheerka = get_sheerka() - concept = Concept(name="one", body="1").init_key() + concept = Concept(name="one", body="1") sheerka.add_in_cache(concept) text = "one" @@ -363,6 +363,46 @@ def test_i_can_eval_simple_concept(): assert res[0].value == 1 +def test_i_can_eval_concept_with_concept_body(): + sheerka = get_sheerka() + concept_one = Concept(name="one") + concept_un = Concept(name="un", body="one") + sheerka.add_in_cache(concept_one) + sheerka.add_in_cache(concept_un) + + res = sheerka.eval("un") + return_value = res[0].value + assert len(res) == 1 + assert res[0].status + assert sheerka.isinstance(return_value, concept_one) + + +def test_i_can_eval_concept_with_no_body(): + sheerka = get_sheerka() + concept = Concept(name="one") + sheerka.add_in_cache(concept) + + text = "one" + res = sheerka.eval(text) + assert len(res) == 1 + assert res[0].status + assert res[0].value == concept + assert id(res[0].value) != id(concept) + + +def test_is_unique_property_is_used_when_evaluating(): + sheerka = get_sheerka() + concept = Concept(name="one", is_unique=True) + sheerka.add_in_cache(concept) + + text = "one" + res = sheerka.eval(text) + assert len(res) == 1 + assert res[0].status + assert res[0].value == concept + assert id(res[0].value) == id(concept) + + def test_i_can_eval_def_concept_request(): text = """ def concept a + b @@ -405,7 +445,7 @@ def test_i_can_eval_def_concept_part_when_one_part_is_a_ref_of_another_concept() sheerka = get_sheerka() # concept 'a plus b' is known - concept_a_plus_b = Concept(name="a plus b").set_prop("a").set_prop("b").init_key() + concept_a_plus_b = Concept(name="a plus b").set_prop("a").set_prop("b") sheerka.add_in_cache(concept_a_plus_b) res = sheerka.eval("def concept a xx b as a plus b") @@ -461,6 +501,45 @@ def test_i_can_eval_a_empty_input(text): assert sheerka.isinstance(res[0].value, BuiltinConcepts.NOP) +def test_i_can_eval_concept_with_variable(): + sheerka = get_sheerka() + concept_hello = Concept(name="hello a").set_prop("a") + concept_foo = Concept(name="foo") + sheerka.add_in_cache(concept_hello) + sheerka.add_in_cache(concept_foo) + + res = sheerka.eval("hello foo") + return_value = res[0].value + assert len(res) == 1 + assert res[0].status + assert sheerka.isinstance(return_value, concept_hello) + assert return_value.props["a"].value == concept_foo + + +def test_i_can_eval_concept_with_variable_and_python_as_body(): + sheerka = get_sheerka() + sheerka.add_in_cache(Concept(name="hello a", body="'hello ' + a").set_prop("a")) + sheerka.add_in_cache(Concept(name="foo", body="'foo'")) + + res = sheerka.eval("hello foo") + assert len(res) == 1 + assert res[0].status + assert res[0].value, "hello foo" + + +def test_i_can_eval_duplicate_concepts_with_same_value(): + sheerka = get_sheerka() + + sheerka.add_in_cache(Concept(name="hello a", body="'hello ' + a").set_prop("a")) + sheerka.add_in_cache(Concept(name="hello foo", body="'hello foo'")) + sheerka.add_in_cache(Concept(name="foo", body="'foo'")) + + res = sheerka.eval("hello foo") + assert len(res) == 1 + assert res[0].status + assert res[0].value, "hello foo" + + def get_sheerka(): sheerka = Sheerka() sheerka.initialize(root_folder)