import ast from dataclasses import dataclass import pytest from core.builtin_concepts_ids import BuiltinConcepts from core.concept import Concept from core.sheerka.services.SheerkaEvaluateRules import SheerkaEvaluateRules from core.sheerka.services.SheerkaExecute import ParserInput from core.sheerka.services.SheerkaRuleManager import CompiledCondition from evaluators.PythonEvaluator import PythonEvaluator from parsers.BaseParser import ErrorSink from parsers.ExpressionParser import ExpressionParser from parsers.PythonParser import PythonNode from sheerkapython.ExprToConditions import ExprToConditionsVisitor from tests.TestUsingMemoryBasedSheerka import TestUsingMemoryBasedSheerka from tests.core.test_SheerkaRuleManager import PYTHON_EVALUATOR_NAME @dataclass class Obj: value: object def __eq__(self, other): return isinstance(other, Obj) and self.value == other.value def __hash__(self): return hash(self.value) cmap = { "one": Concept("one", body="1"), "two": Concept("two", body="2"), "three": Concept("three", body="3"), "twenties": Concept("twenties", definition="'twenty' (one|two)=unit", body="20 + unit").def_var("unit"), "equals": Concept("x equals y", pre="is_question()", body="x == y").def_var("x").def_var("y"), "isa1": Concept("x is a y", pre="is_question()", body="isa(x, y)").def_var("x").def_var("y"), "isa2": Concept("x is a y", pre="is_question()", body="isinstance(x, y)").def_var("x").def_var("y"), "isan1": Concept("x is an y", pre="is_question()", body="isa(x , y)").def_var("x").def_var("y"), "isan2": Concept("x is an y", body="set_isa(x, y)").def_var("x").def_var("y"), "foo": Concept("foo"), "bar": Concept("bar"), "baz": Concept("baz"), } class TestExprToConditionsVisitor(TestUsingMemoryBasedSheerka): shared_ontology = None @classmethod def setup_class(cls): instance = cls() init_test_helper = instance.init_test(cache_only=False, ontology="#TestExprToConditionsVisitor#") sheerka, context, *updated = init_test_helper.with_concepts(*cmap.values(), create_new=True).unpack() for i, concept_name in enumerate(cmap): cmap[concept_name] = updated[i] global_context = instance.get_context(sheerka, global_truth=True) sheerka.set_isa(global_context, cmap["baz"], cmap["foo"]) cls.shared_ontology = sheerka.get_ontology(context) sheerka.pop_ontology(context) def initialize_test(self, concepts_map=None): if concepts_map is None: sheerka, context = self.init_test().unpack() sheerka.add_ontology(context, self.shared_ontology) else: sheerka, context, *updated = super().init_test().with_concepts(*concepts_map.values(), create_new=True).unpack() for i, concept_name in enumerate(concepts_map): concepts_map[concept_name] = updated[i] return sheerka, context @staticmethod def evaluate_conditions(context, conditions, namespace): with context.push(BuiltinConcepts.EXEC_CODE, None) as sub_context: sub_context.protected_hints.add(BuiltinConcepts.EVAL_BODY_REQUESTED) sub_context.protected_hints.add(BuiltinConcepts.EVAL_WHERE_REQUESTED) sub_context.protected_hints.add(BuiltinConcepts.EVAL_UNTIL_SUCCESS_REQUESTED) sub_context.protected_hints.add(BuiltinConcepts.EVAL_QUESTION_REQUESTED) sub_context.deactivate_push() evaluation_service = sub_context.sheerka.services[SheerkaEvaluateRules.NAME] return evaluation_service.evaluate_conditions(sub_context, conditions, namespace) @staticmethod def get_conditions_from_expression(context, expression, parser=None): parser = parser or ExpressionParser() error_sink = ErrorSink() parser_input = ParserInput(expression) parser.reset_parser_input(parser_input, error_sink) parsed = parser.parse_input(context, parser_input, error_sink) assert not error_sink.has_error known_variables = parser.known_variables if hasattr(parser, "known_variables") else None visitor = ExprToConditionsVisitor(context, known_variables=known_variables) conditions = visitor.get_conditions(parsed) return conditions @staticmethod def validate_condition(context, expression, condition, e_code, e_objects, e_variables, e_not_variables): sheerka = context.sheerka # check what was compiled if e_code is None: # manage cases where we only check for variable existence assert condition.evaluator_type is None assert condition.return_value is None else: ast_ = ast.parse(e_code, "", 'exec' if "\n" in e_code else 'eval') expected_python_node = PythonNode(e_code, ast_, expression) assert condition.evaluator_type == PYTHON_EVALUATOR_NAME assert sheerka.isinstance(condition.return_value, BuiltinConcepts.RETURN_VALUE) assert sheerka.objvalue(condition.return_value) == expected_python_node # check the objects if e_objects is not None: resolved_objects = {k: v.id for k, v in condition.objects.items()} resolved_expected_objects = {k: cmap[v].id for k, v in e_objects.items()} assert resolved_objects == resolved_expected_objects # check that variables detected if e_variables is not None: assert condition.variables == e_variables if e_not_variables is not None: assert condition.not_variables == e_not_variables def run_test_cases(self, context, conditions, test_suite): sheerka = context.sheerka for test_data in test_suite: namespace, expected_value = test_data for k, v in namespace.items(): if isinstance(v, str): try: namespace[k] = self.evaluate_from_source(context, v) except: pass res = self.evaluate_conditions(context, conditions, namespace) value = sheerka.objvalue(res[0].body) if res else False assert value == expected_value @staticmethod def evaluate_condition(context, expression, condition, objects): with context.push("Testing conditions SheerkaRuleManagerRulesCompilation", expression) as sub_context: sub_context.protected_hints.add(BuiltinConcepts.EVAL_BODY_REQUESTED) sub_context.protected_hints.add(BuiltinConcepts.EVAL_WHERE_REQUESTED) sub_context.protected_hints.add(BuiltinConcepts.EVAL_UNTIL_SUCCESS_REQUESTED) sub_context.protected_hints.add(BuiltinConcepts.EVAL_QUESTION_REQUESTED) sub_context.sheerka.add_many_to_short_term_memory(sub_context, objects) evaluator = PythonEvaluator() for c in condition.concepts_to_reset: c.get_hints().is_evaluated = False return evaluator.eval(sub_context, condition.return_value) @pytest.mark.parametrize("expression, e_code, e_variables, e_not_variables, test_suite", [ ("var", None, {"var"}, set(), [({"var": "value"}, True), ({}, False)]), ("True", "True", set(), set(), []), ("var.value", None, {"var.value"}, set(), []), ("not var", None, set(), {"var"}, [({"var": "value"}, False), ({}, True)]), ("not not var", None, {"var"}, set(), []), ("var and var2 and not var3", None, {"var", "var2"}, {"var3"}, []), ("var and var.value == 3", "var.value == 3", {"var"}, set(), [({"var": Obj(3)}, True)]), ("not v2 and v1.value == 3", "v1.value == 3", {"v1"}, {"v2"}, [({"v1": Obj(3)}, True), ({"v2": 0}, False)]), ("func(var)", "func(var)", {"var"}, set(), []), ("var in []", "var in []", {"var"}, set(), []), ("a + b", "a + b", {"a", "b"}, set(), []), ("foo x", "__o_00__", set(), set(), []), # foo is not a variable ("foo y", "evaluate_question(__o_00__, x=y)", {"y"}, set(), []), # foo is not a variable ("bar y", "call_concept(__o_00__, x=y)", {"y"}, set(), []), # bar is not a variable ]) def test_i_can_parse_and_manage_exists(self, expression, e_code, e_variables, e_not_variables, test_suite): sheerka, context, foo, bar = self.init_test().with_concepts( Concept("foo x", pre="is_question()").def_var("x"), Concept("bar x").def_var("x"), ).unpack() conditions = self.get_conditions_from_expression(context, expression) assert len(conditions) == 1 assert isinstance(conditions[0], CompiledCondition) condition = conditions[0] self.validate_condition(context, expression, condition, e_code, None, e_variables, e_not_variables) self.run_test_cases(context, conditions, test_suite) @pytest.mark.parametrize("expression, e_code, e_objects, e_variables, test_suite", [ # Concept ("1 equals 1", "evaluate_question(__o_00__)", {"__o_00__": "equals"}, set(), [({}, True)]), ("1 equals 2", "evaluate_question(__o_00__)", {"__o_00__": "equals"}, set(), [({}, False)]), ("one equals one", "evaluate_question(__o_00__)", {"__o_00__": "equals"}, set(), [({}, True)]), ("one equals two", "evaluate_question(__o_00__)", {"__o_00__": "equals"}, set(), [({}, False)]), ("one equals twenty one", "evaluate_question(__o_00__)", {"__o_00__": "equals"}, set(), [({}, False)]), ("self equals 1", "evaluate_question(__o_00__, x=self)", {"__o_00__": "equals"}, {"self"}, [ ({"self": 1}, True), ({"self": 2}, False)]), ("x equals 1", "evaluate_question(__o_00__, x=x)", {"__o_00__": "equals"}, {"x"}, [ ({"x": 1}, True), ({"x": 2}, False)]), ("one equals self", "evaluate_question(__o_00__, y=self)", {"__o_00__": "equals"}, {"self"}, [ ({"self": "one"}, True), ({"self": "two"}, False)]), ("self equals twenty two", "evaluate_question(__o_00__, x=self)", {"__o_00__": "equals"}, {"self"}, [ ({"self": "twenty two"}, True), ({"self": "two"}, False)] ), ("x equals 1 and y equals 2", "evaluate_question(__o_00__, x=x) and evaluate_question(__o_01__, x=y)", {"__o_00__": "equals", "__o_01__": "equals"}, {"x", "y"}, [({"x": 1, "y": 2}, True), ({"x": "0", "y": "0"}, False)] ), ("x equals y", "__o_00__", {"__o_00__": "equals"}, set(), []), ("func(self) equals twenty one", "evaluate_question(__o_00__, x=func(self))", {"__o_00__": "equals"}, {"self"}, [({"self": "twenty one", "func": lambda x: x}, True)]), ("func(self) equals twenty one + 1", "evaluate_question(__o_01__, x=func(self), y=__o_00__ + 1)", {"__o_01__": "equals", "__o_00__": "twenties"}, {"self"}, [({"self": "22", "func": lambda x: x}, True)]), # equality ("a == 10", "a == 10", {}, {"a"}, [({"a": 10}, True), ({"a": 20}, False), ({}, False)]), ("__ret.status == True", "__ret.status == True", {}, {"__ret"}, []), ("self == sheerka", "is_sheerka(self)", {}, {"self"}, [ ({"self": "sheerka"}, True), ({"self": "other"}, False), ]), ("self == BuiltinConcepts.TO_DICT", "self == BuiltinConcepts.TO_DICT", {}, {"self"}, [ ({"self": "BuiltinConcepts.TO_DICT"}, True), ({"self": "other"}, False), ]), # other Comparisons ("a + self > 10", "a + self > 10", {}, {"a", "self"}, [ ({"a": 10, "self": 1}, True), ({"a": 10, "self": 0}, False), ]), ("10 < one + self", "10 < __o_00__ + self", {"__o_00__": "one"}, {"self"}, [ ({"self": 10}, True), ({"self": 1}, False), ]), ("23 < twenty one + self", "23 < __o_00__ + self", {"__o_00__": "twenties"}, {"self"}, [ ({"self": 10}, True), ({"self": 1}, False), ]), ("a equals b and c equals d", "evaluate_question(__o_00__, x=a, y=b) and evaluate_question(__o_01__, x=c, y=d)", {"__o_00__": "equals", "__o_01__": "equals"}, {"a", "b", "c", "d"}, []), # simple expressions ("True", "True", {}, set(), [({}, True)]), ("False", "False", {}, set(), [({}, False)]), ("10 + 5", "10 + 5", {}, set(), [({}, 15)]), ("a + self", "a + self", {}, {"a", "self"}, [({"a": 10, "self": 5}, 15)]), ("a + twenty one", "a + __o_00__", {"__o_00__": "twenties"}, {"a"}, [({"a": 10}, 31)]), # functions ("isinstance('hello', str)", "isinstance('hello', str)", {}, set(), [({}, True)]), ("isinstance(a, str)", "isinstance(a, str)", {}, {"a"}, [({"a": "an_str"}, True), ({"a": 1}, False)]), ("f(BuiltinConcepts.TO_DICT)", "f(BuiltinConcepts.TO_DICT)", {}, set(), [({"f": lambda x: x}, "__TO_DICT")]) ]) def test_i_can_parse(self, expression, e_code, e_objects, e_variables, test_suite): sheerka, context = self.initialize_test() conditions = self.get_conditions_from_expression(context, expression) assert len(conditions) == 1 assert isinstance(conditions[0], CompiledCondition) condition = conditions[0] self.validate_condition(context, expression, condition, e_code, e_objects, e_variables, None) self.run_test_cases(context, conditions, test_suite) def test_i_can_force_variable(self): sheerka, context = self.initialize_test() parser = ExpressionParser(known_variables={"one"}) conditions = self.get_conditions_from_expression(context, "one < two", parser) python_source = conditions[0].return_value.body.body.source assert python_source == "one < __o_00__" resolved_objects = {k: v.id for k, v in conditions[0].objects.items()} resolved_expected_objects = {k: cmap[v].id for k, v in {"__o_00__": "two"}.items()} assert resolved_objects == resolved_expected_objects def test_i_can_manage_possible_variable_indication(self): my_map = { "isa": Concept("x is an int").def_var("x") # no pre condition } sheerka, context = self.initialize_test(my_map) parser = ExpressionParser(known_variables={"x"}) conditions = self.get_conditions_from_expression(context, "x is an int", parser) python_source = conditions[0].return_value.body.body.source assert python_source == "call_concept(__o_00__, x=x)" resolved_objects = {k: v.id for k, v in conditions[0].objects.items()} resolved_expected_objects = {k: my_map[v].id for k, v in {"__o_00__": "isa"}.items()} assert resolved_objects == resolved_expected_objects def test_i_can_parse_when_variables_are_missing(self): sheerka, context = self.initialize_test() expression = "x equals 1" e_code = "evaluate_question(__o_00__, x=x)" e_objects = {"__o_00__": "equals"} e_variables = {"x"} test_suite = [({}, False), ({"y": 1}, False)] conditions = self.get_conditions_from_expression(context, expression) assert len(conditions) == 1 assert isinstance(conditions[0], CompiledCondition) condition = conditions[0] self.validate_condition(context, expression, condition, e_code, e_objects, e_variables, None) self.run_test_cases(context, conditions, test_suite) def test_question_concept_is_chosen_other_non_question_concept(self): sheerka, context = self.initialize_test() expression = "a is an b" e_code = "evaluate_question(__o_00__, x=a, y=b)" e_objects = {"__o_00__": "isan1"} conditions = self.get_conditions_from_expression(context, expression) assert len(conditions) == 1 assert isinstance(conditions[0], CompiledCondition) condition = conditions[0] self.validate_condition(context, expression, condition, e_code, e_objects, None, None) def test_i_can_manage_when_multiple_concepts(self): sheerka, context = self.initialize_test() expression = "a is a b" conditions = self.get_conditions_from_expression(context, expression) assert len(conditions) == 2 condition = conditions[0] assert isinstance(condition, CompiledCondition) e_code = "evaluate_question(__o_00__, x=a, y=b)" e_objects = {"__o_00__": "isa1"} e_variables = {"a", "b"} self.validate_condition(context, expression, condition, e_code, e_objects, e_variables, None) condition = conditions[1] assert isinstance(condition, CompiledCondition) e_code = "evaluate_question(__o_01__, x=a, y=b)" e_objects = {"__o_01__": "isa2"} e_variables = {"a", "b"} self.validate_condition(context, expression, condition, e_code, e_objects, e_variables, None) # testing namespace = {"a": sheerka.new("foo"), "b": sheerka.new("bar")} res = self.evaluate_conditions(context, conditions, namespace) assert len(res) == 2 assert isinstance(res[0].value, bool) and not res[0].value assert isinstance(res[1].value, bool) and not res[1].value namespace = {"a": sheerka.new("foo"), "b": sheerka.new("foo")} res = self.evaluate_conditions(context, conditions, namespace) assert len(res) == 1 assert isinstance(res[0].value, bool) and res[0].value namespace = {"a": sheerka.new("baz"), "b": sheerka.new("foo")} res = self.evaluate_conditions(context, conditions, namespace) assert len(res) == 1 assert isinstance(res[0].value, bool) and res[0].value def test_i_can_manage_or_expressions(self): sheerka, context = self.initialize_test() expression = "isinstance(self, foo) or self is a bar" conditions = self.get_conditions_from_expression(context, expression) assert len(conditions) == 3 condition = conditions[0] assert isinstance(condition, CompiledCondition) e_code = "isinstance(self, __o_00__)" e_objects = {"__o_00__": "foo"} e_variables = {"self"} self.validate_condition(context, "isinstance(self, foo)", condition, e_code, e_objects, e_variables, None) condition = conditions[1] assert isinstance(condition, CompiledCondition) e_code = "evaluate_question(__o_01__, x=self)" e_objects = {"__o_01__": "isa1"} e_variables = {"self"} self.validate_condition(context, "self is a bar", condition, e_code, e_objects, e_variables, None) condition = conditions[2] assert isinstance(condition, CompiledCondition) e_code = "evaluate_question(__o_02__, x=self)" e_objects = {"__o_02__": "isa2"} e_variables = {"self"} self.validate_condition(context, "self is a bar", condition, e_code, e_objects, e_variables, None) def test_i_can_manage_multiple_concepts_melt_with_and_expressions(self): sheerka, context = self.initialize_test() expression = "isinstance(self, foo) and self is a bar" conditions = self.get_conditions_from_expression(context, expression) assert len(conditions) == 2 condition = conditions[0] assert isinstance(condition, CompiledCondition) e_code = "isinstance(self, __o_00__) and evaluate_question(__o_01__, x=self)" e_objects = {"__o_00__": "foo", "__o_01__": "isa1"} e_variables = {"self"} self.validate_condition(context, expression, condition, e_code, e_objects, e_variables, None) condition = conditions[1] assert isinstance(condition, CompiledCondition) e_code = "isinstance(self, __o_00__) and evaluate_question(__o_02__, x=self)" e_objects = {"__o_00__": "foo", "__o_02__": "isa2"} e_variables = {"self"} self.validate_condition(context, expression, condition, e_code, e_objects, e_variables, None) @pytest.mark.parametrize("expression, expected", [ ("self is a 'foo'", {"x is a y"}), ("set self is a 'foo'", set()), ]) def test_i_can_get_concept_to_reset(self, expression, expected): """ When compiled conditions, sometimes there are concepts to reset between two usages :param expression: :param expected: :return: """ concepts_map = { "isa": Concept("x is a y", pre="is_question()").def_var("x").def_var("y"), "set_isa": Concept("set x is a y").def_var("x").def_var("y"), } sheerka, context = self.initialize_test(concepts_map) conditions = self.get_conditions_from_expression(context, expression) assert len(conditions) == 1 assert {c.name for c in conditions[0].concepts_to_reset} == expected def test_i_can_reset_concepts_when_multiple_levels(self): """ When compiled conditions, sometimes there are concepts to reset between two usages :return: """ sheerka, context, is_instance, is_int, is_integer = self.init_concepts( Concept("x is an instance of y", pre="is_question()", body="isinstance(x, y)").def_var("x").def_var("y"), Concept("x is a int", pre="is_question()", body="x is an instance of int").def_var("x"), Concept("x is an integer", pre="is_question()", body="x is a int").def_var("x"), ) expression = "self is an integer" conditions = self.get_conditions_from_expression(context, expression) assert len(conditions) == 1 assert {c.name for c in conditions[0].concepts_to_reset} == {"x is an instance of y", "x is a int", "x is an integer"} # So I can evaluate multiple times res = self.evaluate_condition(context, expression, conditions[0], {'self': 10}) assert res.status assert sheerka.objvalue(res.body) res = self.evaluate_condition(context, expression, conditions[0], {'self': "string"}) assert res.status assert not sheerka.objvalue(res.body) def test_i_can_reset_concepts_when_multiple_levels_and_concept_node(self): """ When compiled conditions, sometimes there are concepts to reset between two usages :return: """ # in this example, x + 2 is an int won't be parsed as an ExactNodeConcept, but as a ConceptNode sheerka, context, is_int, is_integer = self.init_concepts( Concept("x is a int", pre="is_question()", body="isinstance(x, int)").def_var("x"), Concept("x is an integer", pre="is_question()", body="x + 2 is a int").def_var("x"), create_new=True ) expression = "self is an integer" conditions = self.get_conditions_from_expression(context, expression) assert len(conditions) == 1 assert set(c.name for c in conditions[0].concepts_to_reset) == {"x is a int", "x is an integer"} # So I can evaluate multiple times res = self.evaluate_condition(context, expression, conditions[0], {'self': 10}) assert res.status assert sheerka.objvalue(res.body) res = self.evaluate_condition(context, expression, conditions[0], {'self': "string"}) assert not res.status def test_long_name_concepts_are_not_considered_as_variables(self): sheerka, context, one, number = self.init_concepts( "one", "all numbers", ) sheerka.set_isa(context, one, number) expression = "all numbers < 5" conditions = self.get_conditions_from_expression(context, expression) assert len(conditions) == 1 assert conditions[0].return_value.body.body.source == '__o_00__ < 5'