In PythonEvaluator, I now evaluate concept and/or concept body

This commit is contained in:
2020-06-11 17:36:43 +02:00
parent 9eae784581
commit c43a3ef946
6 changed files with 226 additions and 102 deletions
+1
View File
@@ -340,6 +340,7 @@ class Sheerka(Concept):
self.cache_manager.clear() self.cache_manager.clear()
self.printer_handler.reset() self.printer_handler.reset()
self.sdp.reset() self.sdp.reset()
self.locals = {}
def evaluate_user_input(self, text: str, user_name="kodjo"): def evaluate_user_input(self, text: str, user_name="kodjo"):
""" """
+28
View File
@@ -214,6 +214,34 @@ def product(a, b):
return res return res
def dict_product(a, b):
"""
Cartesian product like where a and b are list of dictionaries
>>> a = [{"a": "a", "b":"b", "c":"c"}]
>>> b = [{"d":"d1"}, {"d":"d2"}]
>>>
>>> assert dict_product(a, b) == [{"a": "a", "b":"b", "c":"c", "d":"d1"}, {"a": "a", "b":"b", "c":"c", "d":"d2"}]
:param a:
:param b:
:return:
"""
if a is None or len(a) == 0:
return b
if b is None or len(b) == 0:
return a
res = []
for item_a in a:
for item_b in b:
items = item_a.copy()
items.update(item_b)
res.append(items)
return res
def strip_quotes(text): def strip_quotes(text):
if not isinstance(text, str): if not isinstance(text, str):
return text return text
+121 -63
View File
@@ -1,14 +1,14 @@
import ast import ast
import copy import copy
import traceback import traceback
from functools import partial, update_wrapper from dataclasses import dataclass, field
import core.ast.nodes import core.ast.nodes
from core.sheerka.services.SheerkaFilter import Pipe
import core.utils import core.utils
from core.ast.visitors import UnreferencedNamesVisitor from core.ast.visitors import UnreferencedNamesVisitor
from core.builtin_concepts import BuiltinConcepts, ParserResultConcept from core.builtin_concepts import BuiltinConcepts, ParserResultConcept
from core.concept import ConceptParts, Concept from core.concept import ConceptParts, Concept
from core.sheerka.services.SheerkaFilter import Pipe
from evaluators.BaseEvaluator import OneReturnValueEvaluator from evaluators.BaseEvaluator import OneReturnValueEvaluator
from parsers.PythonParser import PythonNode from parsers.PythonParser import PythonNode
@@ -35,6 +35,13 @@ class Expando:
setattr(self, k, v) setattr(self, k, v)
@dataclass
class PythonEvalError:
error: Exception
traceback: str = field(repr=False)
concepts: dict = field(repr=False)
class PythonEvaluator(OneReturnValueEvaluator): class PythonEvaluator(OneReturnValueEvaluator):
NAME = "Python" NAME = "Python"
@@ -54,37 +61,59 @@ class PythonEvaluator(OneReturnValueEvaluator):
def eval(self, context, return_value): def eval(self, context, return_value):
sheerka = context.sheerka sheerka = context.sheerka
node = return_value.value.value node = return_value.value.value
try:
context.log(f"Evaluating python node {node}.", self.name)
# Do not evaluate if the ast refers to a concept (leave it to ConceptEvaluator) context.log(f"Evaluating python node {node}.", self.name)
if isinstance(node.ast_, ast.Expression) and isinstance(node.ast_.body, ast.Name):
c = context.sheerka.resolve(node.ast_.body.id)
if c is not None:
context.log("It's a simple concept. Not for me.", self.name)
not_for_me = context.sheerka.new(BuiltinConcepts.NOT_FOR_ME, body=node)
return sheerka.ret(self.name, False, not_for_me, parents=[return_value])
# get globals # Do not evaluate if the ast refers to a concept (leave it to ConceptEvaluator)
my_globals = self.get_globals(context, node) # TODO: Remove this section when this check will be implemented in the AFTER_PARSING step
context.log(f"globals={my_globals}", self.name) if isinstance(node.ast_, ast.Expression) and isinstance(node.ast_.body, ast.Name):
c = context.sheerka.resolve(node.ast_.body.id)
if c is not None:
context.log("It's a simple concept. Not for me.", self.name)
not_for_me = context.sheerka.new(BuiltinConcepts.NOT_FOR_ME, body=node)
return sheerka.ret(self.name, False, not_for_me, parents=[return_value])
# eval # get globals
if isinstance(node.ast_, ast.Expression): my_globals = self.get_globals(context, node)
context.log("Evaluating using 'eval'.", self.name) context.log(f"globals={my_globals}", self.name)
compiled = compile(node.ast_, "<string>", "eval")
evaluated = eval(compiled, my_globals, sheerka.locals)
else:
context.log("Evaluating using 'exec'.", self.name)
evaluated = self.exec_with_return(node.ast_, my_globals, sheerka.locals)
context.log(f"{evaluated=}", self.name) all_possible_globals = self.get_all_possible_globals(context, my_globals)
return sheerka.ret(self.name, True, evaluated, parents=[return_value]) concepts_entries = None
evaluated = BuiltinConcepts.NOT_INITIALIZED
errors = []
for globals_ in all_possible_globals:
try:
# eval
if isinstance(node.ast_, ast.Expression):
context.log("Evaluating using 'eval'.", self.name)
compiled = compile(node.ast_, "<string>", "eval")
evaluated = eval(compiled, globals_, sheerka.locals)
else:
context.log("Evaluating using 'exec'.", self.name)
evaluated = self.exec_with_return(node.ast_, globals_, sheerka.locals)
except Exception as error: break # in this first version, we stop once a success is found
context.log_error(error, who=self.name, exc=traceback.format_exc()) except Exception as ex:
error = sheerka.new(BuiltinConcepts.ERROR, body=error) if concepts_entries is None:
return sheerka.ret(self.name, False, error, parents=[return_value]) concepts_entries = self.get_concepts_entries_from_globals(my_globals)
errors.append(PythonEvalError(ex,
traceback.format_exc(),
self.get_concepts_values_from_globals(globals_, concepts_entries)))
if evaluated == BuiltinConcepts.NOT_INITIALIZED:
if len(errors) == 1:
context.log_error(errors[0].error, who=self.name, exc=errors[0].traceback)
one_error = sheerka.new(BuiltinConcepts.ERROR, body=errors[0])
return sheerka.ret(self.name, False, one_error, parents=[return_value])
if len(errors) > 1:
for eval_error in errors:
context.log_error(eval_error.error, who=self.name, exc=eval_error.traceback)
too_many_errors = sheerka.new(BuiltinConcepts.TOO_MANY_ERRORS, body=errors)
return sheerka.ret(self.name, False, too_many_errors, parents=[return_value])
context.log(f"{evaluated=}", self.name)
return sheerka.ret(self.name, True, evaluated, parents=[return_value])
def get_globals(self, context, node): def get_globals(self, context, node):
my_globals = { my_globals = {
@@ -94,9 +123,9 @@ class PythonEvaluator(OneReturnValueEvaluator):
# has to be the first, to allow override # has to be the first, to allow override
method_from_sheerka = self.update_globals_with_sheerka_methods(my_globals, context) method_from_sheerka = self.update_globals_with_sheerka_methods(my_globals, context)
self.update_globals_with_context(my_globals, context) self.update_globals_with_context(my_globals, context)
self.update_globals_with_node(my_globals, context, node) already_know = set(my_globals.keys())
self.update_globals_with_node(my_globals, context, node, already_know)
if self.globals: # when extra values are given. Add them if self.globals: # when extra values are given. Add them
my_globals.update(self.globals) my_globals.update(self.globals)
@@ -125,20 +154,23 @@ class PythonEvaluator(OneReturnValueEvaluator):
return methods_from_sheerka # to allow access using prefix "sheerka." return methods_from_sheerka # to allow access using prefix "sheerka."
def update_globals_with_context(self, my_locals, context): def update_globals_with_context(self, my_globals, context):
if context.obj: if context.obj:
context.log(f"Concept '{context.obj}' is in context. Adding it and its properties to locals.", self.name) context.log(f"Concept '{context.obj}' is in context. Adding it and its properties to locals.", self.name)
for prop_name in context.obj.variables(): for prop_name in context.obj.variables():
prop_value = context.obj.get_value(prop_name) my_globals[prop_name] = context.obj.get_value(prop_name)
if isinstance(prop_value, Concept): my_globals["self"] = context.obj
my_locals[prop_name] = context.sheerka.objvalue(prop_value)
else:
my_locals[prop_name] = prop_value
my_locals["self"] = context.obj.body def update_globals_with_node(self, my_globals, context, node, already_known):
"""
def update_globals_with_node(self, my_locals, context, node): Try to find concepts using the names that appear in the AST of the node.
:param my_globals: dictionary to update
:param context:
:param node:
:param already_known: if the name is in this list, do no try to instantiate it again
:return:
"""
node_concept = core.ast.nodes.python_to_concept(node.ast_) node_concept = core.ast.nodes.python_to_concept(node.ast_)
unreferenced_names_visitor = UnreferencedNamesVisitor(context.sheerka) unreferenced_names_visitor = UnreferencedNamesVisitor(context.sheerka)
unreferenced_names_visitor.visit(node_concept) unreferenced_names_visitor.visit(node_concept)
@@ -146,14 +178,15 @@ class PythonEvaluator(OneReturnValueEvaluator):
for name in unreferenced_names_visitor.names: for name in unreferenced_names_visitor.names:
context.log(f"Resolving '{name}'.", self.name) context.log(f"Resolving '{name}'.", self.name)
# get the concept
if name in node.concepts: if name in node.concepts:
# use it, even if it already in already_known
# This concept take precedence other the outer world
context.log(f"Using value from node.", self.name) context.log(f"Using value from node.", self.name)
concept = self.resolve_concept(context, node.concepts[name]) concept = self.resolve_concept(context, node.concepts[name])
elif name in already_known:
elif name in my_locals: context.log(f"Already known. Skipping.", self.name)
context.log(f"Using value from property.", self.name)
continue continue
else: else:
context.log(f"Instantiating new concept with {name}.", self.name) context.log(f"Instantiating new concept with {name}.", self.name)
concept = self.resolve_concept(context, name) concept = self.resolve_concept(context, name)
@@ -162,9 +195,9 @@ class PythonEvaluator(OneReturnValueEvaluator):
context.log(f"Concept '{name}' is not found or cannot be instantiated. Skipping.", self.name) context.log(f"Concept '{name}' is not found or cannot be instantiated. Skipping.", self.name)
continue continue
# evaluate it if needed
if concept.metadata.is_evaluated: if concept.metadata.is_evaluated:
context.log(f"Concept {name} is already evaluated.", self.name) context.log(f"Concept {name} is already evaluated.", self.name)
else: else:
context.log(f"Evaluating '{concept}'", self.name) context.log(f"Evaluating '{concept}'", self.name)
with context.push(self.name, desc=f"Evaluating '{concept}'", obj=concept) as sub_context: with context.push(self.name, desc=f"Evaluating '{concept}'", obj=concept) as sub_context:
@@ -177,7 +210,49 @@ class PythonEvaluator(OneReturnValueEvaluator):
continue continue
concept = evaluated concept = evaluated
my_locals[name] = context.sheerka.objvalue(concept) my_globals[name] = concept
@staticmethod
def get_all_possible_globals(context, my_globals):
"""
From a dictionary of globals (str, obj)
Creates as many globals as there are combination between a concept and its body
Example:
if the entry 'foo': Concept("foo", body="something")
2 globals will be created
one with foo: Concept("foo") # we keep the concept as an object
one with foo: 'something' # we substitute its value
:param context:
:param my_globals:
:return:
"""
# first pass, get all the non concept or concept with no body
# Note that we consider that all concepts are evaluated
# In the future, it may be a good optimisation to defer the evaluation of the body
# until the python evaluation fails
fixed_values = {}
concepts_with_body = {}
for k, v in my_globals.items():
if not isinstance(v, Concept) or context.sheerka.objvalue(v) == v:
fixed_values[k] = v
else:
concepts_with_body[k] = v
# make the product the rest as cartesian product
res = [fixed_values]
for k, v in concepts_with_body.items():
res = core.utils.dict_product(res, [{k: context.sheerka.objvalue(v)}, {k: v}])
return res
@staticmethod
def get_concepts_entries_from_globals(my_globals):
return [k for k, v in my_globals.items() if isinstance(v, Concept)]
@staticmethod
def get_concepts_values_from_globals(my_globals, names):
return {name: my_globals[name] for name in names}
@staticmethod @staticmethod
def resolve_concept(context, concept_hint): def resolve_concept(context, concept_hint):
@@ -189,30 +264,13 @@ class PythonEvaluator(OneReturnValueEvaluator):
return None return None
new_instance = context.sheerka.new_from_template(concept, concept.key) new_instance = context.sheerka.new_from_template(concept, concept.key)
if isinstance(concept_hint, tuple): if isinstance(concept_hint, tuple):
# It's means that it comes from PythonParser which have found a concept token (c:xxx:) # It's means that it was requested by PythonParser which have found a concept token (c:xxx:)
# So a concept was explicitly required, not its value # So a concept was explicitly required, not its value
# We mark the concept as already evaluated, so it's body will not be evaluated # We mark the concept as already evaluated, so it's body will not be evaluated
new_instance.metadata.is_evaluated = True new_instance.metadata.is_evaluated = True
return new_instance return new_instance
@staticmethod
def resolve_name(to_resolve):
"""
Try to match
__C__concept_key__C__
or
__C__concept_key__concept_id__C__
:param to_resolve:
:return:
"""
key, id_, use_concept = core.utils.decode_concept(to_resolve)
if key or id_:
return key, id_, use_concept
else:
return to_resolve, None, False
@staticmethod @staticmethod
def expr_to_expression(expr): def expr_to_expression(expr):
expr.lineno = 0 expr.lineno = 0
+18 -1
View File
@@ -1,7 +1,6 @@
import core.utils import core.utils
import pytest import pytest
from core.concept import ConceptParts, Concept from core.concept import ConceptParts, Concept
from core.tokenizer import Token, TokenKind from core.tokenizer import Token, TokenKind
@@ -212,3 +211,21 @@ def test_decode_concept_key_id():
assert core.utils.decode_concept("__C__KEY_key__ID_id__C__") == ("key", "id") assert core.utils.decode_concept("__C__KEY_key__ID_id__C__") == ("key", "id")
assert core.utils.decode_concept("__C__KEY_00None00__ID_id__C__") == (None, "id") assert core.utils.decode_concept("__C__KEY_00None00__ID_id__C__") == (None, "id")
assert core.utils.decode_concept("__C__KEY_key__ID_00None00__C__") == ("key", None) assert core.utils.decode_concept("__C__KEY_key__ID_00None00__C__") == ("key", None)
@pytest.mark.parametrize("a,b,expected", [
([], [], []),
([{"a": "a", "b": "b"}], [], [{"a": "a", "b": "b"}]),
([], [{"a": "a", "b": "b"}], [{"a": "a", "b": "b"}]),
([{"a": "a", "b": "b"}], [{"d": "d1"}, {"d": "d2"}], [{"a": "a", "b": "b", "d": "d1"},
{"a": "a", "b": "b", "d": "d2"}]),
([{"d": "d1"}, {"d": "d2"}], [{"a": "a", "b": "b"}], [{"a": "a", "b": "b", "d": "d1"},
{"a": "a", "b": "b", "d": "d2"}]),
([{"a": "a", "b": "b"}], [{"d": "d", "e": "e"}], [{"a": "a", "b": "b", "d": "d", "e": "e"}]),
([{"a": "a"}, {"b": "b"}], [{"d": "d"}, {"e": "e"}], [{"a": "a", "d": "d"},
{"a": "a", "e": "e"},
{"b": "b", "d": "d"},
{"b": "b", "e": "e"}])
])
def test_dict_product(a, b, expected):
assert core.utils.dict_product(a, b) == expected
+57 -38
View File
@@ -1,10 +1,10 @@
import pytest import pytest
from core.builtin_concepts import ReturnValueConcept, ParserResultConcept, BuiltinConcepts from core.builtin_concepts import ReturnValueConcept, ParserResultConcept, BuiltinConcepts
from core.concept import Concept, DEFINITION_TYPE_DEF from core.concept import Concept, CB
from core.sheerka.services.SheerkaExecute import ParserInput from core.sheerka.services.SheerkaExecute import ParserInput
from evaluators.PythonEvaluator import PythonEvaluator from evaluators.PythonEvaluator import PythonEvaluator, PythonEvalError
from parsers.PythonParser import PythonNode, PythonParser from parsers.PythonParser import PythonNode, PythonParser
from tests.TestUsingMemoryBasedSheerka import TestUsingMemoryBasedSheerka from tests.TestUsingMemoryBasedSheerka import TestUsingMemoryBasedSheerka
@@ -63,7 +63,7 @@ class TestPythonEvaluator(TestUsingMemoryBasedSheerka):
Concept("foo").def_var("prop", "'a'"), Concept("foo").def_var("prop", "'a'"),
Concept("foo", body="bar") Concept("foo", body="bar")
]) ])
def test_i_cannot_eval_simple_concept(self, concept): def test_simple_concepts_are_not_for_me(self, concept):
context = self.get_context() context = self.get_context()
context.sheerka.add_in_cache(Concept("foo")) context.sheerka.add_in_cache(Concept("foo"))
@@ -73,7 +73,7 @@ class TestPythonEvaluator(TestUsingMemoryBasedSheerka):
assert not evaluated.status assert not evaluated.status
assert context.sheerka.isinstance(evaluated.value, BuiltinConcepts.NOT_FOR_ME) assert context.sheerka.isinstance(evaluated.value, BuiltinConcepts.NOT_FOR_ME)
def test_i_can_eval_expression_with_that_references_concepts(self): def test_i_can_eval_ast_expression_that_references_concepts(self):
""" """
I can test modules with variables I can test modules with variables
:return: :return:
@@ -87,7 +87,7 @@ class TestPythonEvaluator(TestUsingMemoryBasedSheerka):
assert evaluated.status assert evaluated.status
assert evaluated.value == 3 assert evaluated.value == 3
def test_i_can_eval_module_with_that_references_concepts(self): def test_i_can_eval_ast_module_that_references_concepts(self):
""" """
I can test modules with variables I can test modules with variables
:return: :return:
@@ -101,13 +101,12 @@ class TestPythonEvaluator(TestUsingMemoryBasedSheerka):
assert evaluated.status assert evaluated.status
assert evaluated.value == Concept("foo").init_key() assert evaluated.value == Concept("foo").init_key()
def test_i_can_eval_module_with_that_references_concepts_with_body(self): def test_i_can_eval_ast_module_that_references_concepts_with_body(self):
""" """
I can test modules with variables I can test modules with variables
:return: :return:
""" """
context = self.get_context() sheerka, context, foo = self.init_concepts(Concept("foo", body="2"))
context.sheerka.add_in_cache(Concept("foo", body="2"))
parsed = PythonParser().parse(context, ParserInput("def a(b):\n return b\na(foo)")) parsed = PythonParser().parse(context, ParserInput("def a(b):\n return b\na(foo)"))
evaluated = PythonEvaluator().eval(context, parsed) evaluated = PythonEvaluator().eval(context, parsed)
@@ -127,15 +126,6 @@ class TestPythonEvaluator(TestUsingMemoryBasedSheerka):
assert evaluated.status assert evaluated.status
assert evaluated.value == "foo" assert evaluated.value == "foo"
# sanity, does not work otherwise
parsed = PythonParser().parse(context, ParserInput("get_concept_name(foo)"))
python_evaluator = PythonEvaluator()
python_evaluator.globals["get_concept_name"] = get_concept_name
evaluated = python_evaluator.eval(context, parsed)
assert not evaluated.status
assert evaluated.body.body.args[0] == "'int' object has no attribute 'name'"
def test_i_can_call_function_with_complex_concepts(self): def test_i_can_call_function_with_complex_concepts(self):
sheerka, context, plus, mult = self.init_concepts( sheerka, context, plus, mult = self.init_concepts(
self.from_def_concept("plus", "a plus b", ["a", "b"]), self.from_def_concept("plus", "a plus b", ["a", "b"]),
@@ -163,23 +153,52 @@ class TestPythonEvaluator(TestUsingMemoryBasedSheerka):
assert evaluated.status assert evaluated.status
assert evaluated.body == 10 assert evaluated.body == 10
# @pytest.mark.parametrize("text, concept_key, concept_id, use_concept", [ def test_i_can_get_all_possibles_globals(self):
# ("__C__key__C__", "key", None, False), sheerka, context, foo = self.init_concepts(Concept("foo", body="foo").auto_init())
# ("__C__key__id__C__", "key", "id", False), python_evaluator = PythonEvaluator()
# ("__C__USE_CONCEPT__key__id__C__", "key", "id", True), my_globals = {
# ("__C__USE_CONCEPT__key__id__C__", "key", "id", True), "a": "a string",
# ]) "b": self.test_i_can_get_all_possibles_globals,
# def test_i_can_resolve_name(self, text, concept_key, concept_id, use_concept): "foo": foo
# context = self.get_context() }
# assert PythonEvaluator().resolve_name(context, text) == (concept_key, concept_id, use_concept)
# all_globals = python_evaluator.get_all_possible_globals(context, my_globals)
# @pytest.mark.parametrize("text", [ assert len(all_globals) == 2
# "__C__", assert all_globals[0]["foo"] == 'foo'
# "__C__key", assert all_globals[1]["foo"] == CB(foo, "foo") # body is evaluated
# "__C__key____",
# "__C____", def test_i_can_detect_one_error(self):
# "__C__USE_CONCEPT__", sheerka, context, foo = self.init_concepts("foo")
# ])
# def test_i_cannot_resolve_name(self, text): parsed = PythonParser().parse(context, ParserInput("foo + 1"))
# context = self.get_context() evaluated = PythonEvaluator().eval(context, parsed)
# assert PythonEvaluator().resolve_name(context, text) is None
assert not evaluated.status
assert context.sheerka.isinstance(evaluated.value, BuiltinConcepts.ERROR)
error = evaluated.body.body
assert isinstance(error, PythonEvalError)
assert isinstance(error.error, TypeError)
assert error.error.args[0] == "unsupported operand type(s) for +: 'Concept' and 'int'"
assert error.concepts == {'foo': foo}
def test_i_can_detect_multiple_errors(self):
sheerka, context, foo = self.init_concepts(Concept("foo", body="'string'"))
parsed = PythonParser().parse(context, ParserInput("foo + 1"))
evaluated = PythonEvaluator().eval(context, parsed)
assert not evaluated.status
assert context.sheerka.isinstance(evaluated.value, BuiltinConcepts.TOO_MANY_ERRORS)
error0 = evaluated.body.body[0]
assert isinstance(error0, PythonEvalError)
assert isinstance(error0.error, TypeError)
assert error0.error.args[0] == 'can only concatenate str (not "int") to str'
assert error0.concepts == {'foo': 'string'}
error1 = evaluated.body.body[1]
assert isinstance(error1, PythonEvalError)
assert isinstance(error1.error, TypeError)
assert error1.error.args[0] == "unsupported operand type(s) for +: 'Concept' and 'int'"
assert error1.concepts == {'foo': CB(foo, 'string')}
+1
View File
@@ -921,6 +921,7 @@ as:
assert not res[0].status assert not res[0].status
class TestSheerkaNonRegFile(TestUsingFileBasedSheerka): class TestSheerkaNonRegFile(TestUsingFileBasedSheerka):
def test_i_can_def_several_concepts(self): def test_i_can_def_several_concepts(self):
sheerka = self.get_sheerka() sheerka = self.get_sheerka()