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
+121 -63
View File
@@ -1,14 +1,14 @@
import ast
import copy
import traceback
from functools import partial, update_wrapper
from dataclasses import dataclass, field
import core.ast.nodes
from core.sheerka.services.SheerkaFilter import Pipe
import core.utils
from core.ast.visitors import UnreferencedNamesVisitor
from core.builtin_concepts import BuiltinConcepts, ParserResultConcept
from core.concept import ConceptParts, Concept
from core.sheerka.services.SheerkaFilter import Pipe
from evaluators.BaseEvaluator import OneReturnValueEvaluator
from parsers.PythonParser import PythonNode
@@ -35,6 +35,13 @@ class Expando:
setattr(self, k, v)
@dataclass
class PythonEvalError:
error: Exception
traceback: str = field(repr=False)
concepts: dict = field(repr=False)
class PythonEvaluator(OneReturnValueEvaluator):
NAME = "Python"
@@ -54,37 +61,59 @@ class PythonEvaluator(OneReturnValueEvaluator):
def eval(self, context, return_value):
sheerka = context.sheerka
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)
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])
context.log(f"Evaluating python node {node}.", self.name)
# get globals
my_globals = self.get_globals(context, node)
context.log(f"globals={my_globals}", self.name)
# Do not evaluate if the ast refers to a concept (leave it to ConceptEvaluator)
# TODO: Remove this section when this check will be implemented in the AFTER_PARSING step
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
if isinstance(node.ast_, ast.Expression):
context.log("Evaluating using 'eval'.", 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)
# get globals
my_globals = self.get_globals(context, node)
context.log(f"globals={my_globals}", self.name)
context.log(f"{evaluated=}", self.name)
return sheerka.ret(self.name, True, evaluated, parents=[return_value])
all_possible_globals = self.get_all_possible_globals(context, my_globals)
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:
context.log_error(error, who=self.name, exc=traceback.format_exc())
error = sheerka.new(BuiltinConcepts.ERROR, body=error)
return sheerka.ret(self.name, False, error, parents=[return_value])
break # in this first version, we stop once a success is found
except Exception as ex:
if concepts_entries is None:
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):
my_globals = {
@@ -94,9 +123,9 @@ class PythonEvaluator(OneReturnValueEvaluator):
# has to be the first, to allow override
method_from_sheerka = self.update_globals_with_sheerka_methods(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
my_globals.update(self.globals)
@@ -125,20 +154,23 @@ class PythonEvaluator(OneReturnValueEvaluator):
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:
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():
prop_value = context.obj.get_value(prop_name)
if isinstance(prop_value, Concept):
my_locals[prop_name] = context.sheerka.objvalue(prop_value)
else:
my_locals[prop_name] = prop_value
my_globals[prop_name] = context.obj.get_value(prop_name)
my_globals["self"] = context.obj
my_locals["self"] = context.obj.body
def update_globals_with_node(self, my_locals, context, node):
def update_globals_with_node(self, my_globals, context, node, already_known):
"""
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_)
unreferenced_names_visitor = UnreferencedNamesVisitor(context.sheerka)
unreferenced_names_visitor.visit(node_concept)
@@ -146,14 +178,15 @@ class PythonEvaluator(OneReturnValueEvaluator):
for name in unreferenced_names_visitor.names:
context.log(f"Resolving '{name}'.", self.name)
# get the concept
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)
concept = self.resolve_concept(context, node.concepts[name])
elif name in my_locals:
context.log(f"Using value from property.", self.name)
elif name in already_known:
context.log(f"Already known. Skipping.", self.name)
continue
else:
context.log(f"Instantiating new concept with {name}.", self.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)
continue
# evaluate it if needed
if concept.metadata.is_evaluated:
context.log(f"Concept {name} is already evaluated.", self.name)
else:
context.log(f"Evaluating '{concept}'", self.name)
with context.push(self.name, desc=f"Evaluating '{concept}'", obj=concept) as sub_context:
@@ -177,7 +210,49 @@ class PythonEvaluator(OneReturnValueEvaluator):
continue
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
def resolve_concept(context, concept_hint):
@@ -189,30 +264,13 @@ class PythonEvaluator(OneReturnValueEvaluator):
return None
new_instance = context.sheerka.new_from_template(concept, concept.key)
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
# We mark the concept as already evaluated, so it's body will not be evaluated
new_instance.metadata.is_evaluated = True
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
def expr_to_expression(expr):
expr.lineno = 0