diff --git a/src/client.py b/src/client.py
index 826ca58..7b92d29 100644
--- a/src/client.py
+++ b/src/client.py
@@ -7,7 +7,7 @@ from os import path
import prompt_toolkit
import requests
-from prompt_toolkit import HTML, print_formatted_text, prompt
+from prompt_toolkit import ANSI, print_formatted_text, prompt
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.history import FileHistory
from requests import ConnectionError, HTTPError
@@ -16,6 +16,18 @@ from constants import CLIENT_OPERATION_QUIT, EXIT_COMMANDS
connect_regex = re.compile("connect\(['\"](.*?)['\"]\s*,\s*['\"](.*?)['\"]\)")
+CONSOLE_COLORS_MAP = {
+ "reset": "\u001b[0m",
+ "black": "\u001b[30m",
+ "red": "\u001b[31m",
+ "green": "\u001b[32m",
+ "yellow": "\u001b[33m",
+ "blue": "\u001b[34m",
+ "magenta": "\u001b[35m",
+ "cyan": "\u001b[36m",
+ "white": "\u001b[37m",
+}
+
@dataclass
class TestResponse:
@@ -116,7 +128,11 @@ class SheerkaClient:
as_json = response.json()
# Print the response and loop
- self.print_info(as_json['response'])
+ if as_json['status']:
+ self.print_info(as_json['response'])
+ else:
+ self.print_error(as_json['response'])
+
if as_json['command'] == CLIENT_OPERATION_QUIT:
break
else:
@@ -142,7 +158,7 @@ class SheerkaClient:
:return:
:rtype:
"""
- print_formatted_text(HTML(f'{message}'))
+ print_formatted_text(ANSI(f'{CONSOLE_COLORS_MAP["red"]}{message}{CONSOLE_COLORS_MAP["reset"]}'))
@staticmethod
def print_info(message: str):
diff --git a/src/common/ast_utils.py b/src/common/ast_utils.py
new file mode 100644
index 0000000..ce33575
--- /dev/null
+++ b/src/common/ast_utils.py
@@ -0,0 +1,106 @@
+import ast
+
+from caching.FastCache import FastCache
+from common.global_symbols import NotFound
+
+
+class UnreferencedNamesVisitor(ast.NodeVisitor):
+ """
+ Try to find symbols that will be requested by the ast
+ It can be variable names, but also function names
+ """
+
+ cache = FastCache()
+
+ def __init__(self, context):
+ self.context = context
+ self.names = set()
+
+ def get_names(self, node):
+ key = self.__class__.__name__, node
+ names = UnreferencedNamesVisitor.cache.get(key)
+ if names is NotFound:
+ self.visit(node)
+ UnreferencedNamesVisitor.cache.put(key, self.names)
+ return self.names
+
+ return names
+
+ def visit_Name(self, node):
+ self.names.add(node.id)
+
+ def visit_For(self, node: ast.For):
+ self.visit_selected(node, ["body", "orelse"])
+
+ def visit_selected(self, node, to_visit):
+ """Called if no explicit visitor function exists for a node."""
+ for field in to_visit:
+ value = getattr(node, field)
+ if isinstance(value, list):
+ for item in value:
+ if isinstance(item, ast.AST):
+ self.visit(item)
+ elif isinstance(value, ast.AST):
+ self.visit(value)
+
+
+class UnreferencedVariablesVisitor(UnreferencedNamesVisitor):
+ """
+ Try to find variables names that will be requested by the ast
+ This visitor do not yield function names
+ """
+
+ def visit_Call(self, node: ast.Call):
+ self.visit_selected(node, ["args", "keywords"])
+
+ def visit_keyword(self, node: ast.keyword):
+ self.names.add(node.arg)
+ self.visit_selected(node, ["value"])
+
+
+class NamesWithAttributesVisitor(ast.NodeVisitor):
+ """
+ Looks for all attributes for a given name
+ >>> ast_ = ast.parse("foo.bar.baz", "", mode="exec")
+ >>> assert NamesWithAttributesVisitor().get_sequences(ast_, "foo") == [["foo", "bar", "baz"]]
+
+ It parses all expressions / statements
+ >>> ast_ = ast.parse("foo.bar.baz; one.two.three; foo.bar", "", mode="exec")
+ >>> assert NamesWithAttributesVisitor().get_sequences(ast_, "foo") == [["foo", "bar", "baz"], ["foo", "bar"]]
+ """
+
+ def __init__(self):
+ self.sequences = []
+ self.temp = []
+ self.to_lookup = None
+
+ def get_sequences(self, ast_, to_lookup):
+ self.to_lookup = to_lookup
+ self.visit(ast_)
+ return self.sequences
+
+ def visit_Attribute(self, node: ast.Attribute):
+ self.temp.append(node.attr)
+ if isinstance(node.value, ast.Attribute):
+ self.visit_Attribute(node.value)
+ if isinstance(node.value, ast.Subscript):
+ self.visit_Subscript(node.value)
+ elif isinstance(node.value, ast.Name):
+ self.visit_Name(node.value)
+
+ def visit_Subscript(self, node: ast.Subscript):
+ # TODO manage the index when it will be needed
+ # using node.slice
+ if isinstance(node.value, ast.Attribute):
+ self.visit_Attribute(node.value)
+ if isinstance(node.value, ast.Subscript):
+ self.visit_Subscript(node.value)
+ elif isinstance(node.value, ast.Name):
+ self.visit_Name(node.value)
+
+ def visit_Name(self, node: ast.Name):
+ if node.id == self.to_lookup:
+ self.temp.append(node.id)
+ self.temp.reverse()
+ self.sequences.append(self.temp.copy())
+ self.temp.clear()
diff --git a/src/common/utils.py b/src/common/utils.py
index cda0832..d3541f6 100644
--- a/src/common/utils.py
+++ b/src/common/utils.py
@@ -1,6 +1,7 @@
import importlib
import pkgutil
from copy import deepcopy
+from typing import Any
from common.global_symbols import CustomType
@@ -227,6 +228,25 @@ def unstr_concept(concept_repr, prefix='c:'):
return key if key != "" else None, c_id if c_id != "" else None
+def encode_concept(t: tuple | Any, wrapper="C"):
+ """
+ Given a tuple of concept id, concept id
+ Create a valid Python identifier that can be parsed back
+
+ >>> assert encode_concept(("key", "id")) == "__C__KEY_key__ID_id__C__"
+ >>> assert encode_concept((None, "id")) == "__C__KEY_00None00__ID_id__C__"
+ >>> assert encode_concept(("key", None)) == "__C__KEY_key__ID_00None00__C__"
+
+ :param t:
+ :param wrapper:
+ :return:
+ """
+
+ key, id_ = (t[0], t[1]) if isinstance(t, tuple) else (t.key, t.id)
+ sanitized_key = "".join(c if c.isalnum() else "0" for c in key) if key else "00None00"
+ return f"__{wrapper}__KEY_{sanitized_key}__ID_{id_ or '00None00'}__{wrapper}__"
+
+
def compute_hash(obj):
"""
Helper to get the hash from collection
@@ -291,3 +311,55 @@ def to_dict(items, get_attr):
res.setdefault(get_attr(item), []).append(item)
return res
+
+
+def get_text_from_tokens(tokens, custom_switcher=None, tracker=None):
+ """
+ Create the source code, from the list of token
+ :param tokens: list of tokens
+ :param custom_switcher: to override the behaviour (the return value) of some token
+ :param tracker: keep track of the original token value when custom switched
+ :return:
+ """
+ if tokens is None:
+ return ""
+
+ if not hasattr(tokens, "__iter__"):
+ tokens = [tokens]
+
+ switcher = custom_switcher or {}
+
+ res = ""
+ for token in tokens:
+ value = switcher.get(token.type, lambda t: t.str_value)(token)
+ res += value
+ if tracker is not None and token.type in custom_switcher:
+ tracker[value] = token
+ 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
diff --git a/src/core/BuiltinConcepts.py b/src/core/BuiltinConcepts.py
index 32c60b3..3196d63 100644
--- a/src/core/BuiltinConcepts.py
+++ b/src/core/BuiltinConcepts.py
@@ -5,3 +5,4 @@ class BuiltinConcepts:
UNKNOWN_CONCEPT = "__UNKNOWN_CONCEPT"
USER_INPUT = "__USER_INPUT"
PARSER_INPUT = "__PARSER_INPUT"
+ PYTHON_CODE = "__PYTHON_CODE"
diff --git a/src/core/ExecutionContext.py b/src/core/ExecutionContext.py
index c5315ff..e606eed 100644
--- a/src/core/ExecutionContext.py
+++ b/src/core/ExecutionContext.py
@@ -5,7 +5,7 @@ import time
from core.Event import Event
-class ExecutionContextActions:
+class ContextActions:
TESTING = "Testing"
INIT_SHEERKA = "Init Sheerka"
EVALUATE_USER_INPUT = "Evaluate user input"
@@ -18,9 +18,16 @@ class ExecutionContextActions:
EVALUATION = "Evaluation"
AFTER_EVALUATION = "After Evaluation"
+ EVALUATING_CONCEPT = "Evaluating concept"
+ BUILD_CONCEPT = "Building all attributes"
+ BUILD_CONCEPT_ATTR = "Building one attribute"
+ EVAL_CONCEPT = "Evaluating all attributes"
+ EVAL_CONCEPT_ATTR = "Evaluating one attribute"
+
class ContextHint:
REDUCE_CONCEPTS = "Reduce Concepts" # to tell the process to only keep the meaningful results
+ EXPRESSION_ONLY_REQUESTED = "Expression Only"
ids = {} # keep track of the next execution context id, for a given event id
@@ -51,7 +58,7 @@ class ExecutionContext:
who: str,
event: Event,
sheerka,
- action: ExecutionContextActions,
+ action: ContextActions,
action_context: object,
desc: str = None,
logger=None,
@@ -102,6 +109,10 @@ class ExecutionContext:
def long_id(self):
return f"{self.event.get_digest()}:{self._id}"
+ @property
+ def medium_id(self):
+ return f"{self.event.get_digest()[:8]}:{self._id}"
+
@property
def id(self):
return self._id
@@ -143,7 +154,7 @@ class ExecutionContext:
def push(self,
who: str,
- action: ExecutionContextActions,
+ action: ContextActions,
action_context: object,
desc: str = None,
logger=None):
@@ -162,6 +173,9 @@ class ExecutionContext:
self._children.append(child)
return child
+ def get_parent(self):
+ return self._parent
+
def get_children(self, level=-1):
"""
recursively look for children
@@ -173,6 +187,30 @@ class ExecutionContext:
if level != 1:
yield from child.get_children(level - 1)
+ def get_parents(self, level=-1):
+ """
+ recursively look for parent
+ :return:
+ :rtype:
+ """
+ if level == 0 or self._parent is None:
+ return
+
+ yield self._parent
+ yield from self._parent.get_parents(level - 1)
+
+ def in_context(self, hint: ContextHint):
+ return hint in self.protected_hints or \
+ hint in self.global_hints or \
+ hint in self.private_hints
+
+ def get_from_short_term_memory(self, key):
+ return self.sheerka.get_from_short_term_memory(self, key)
+
+ def log(self, message: str, who: str = None):
+ """Send debug information to logger"""
+ pass
+
def __enter__(self):
self._start = time.time_ns()
return self
diff --git a/src/core/Sheerka.py b/src/core/Sheerka.py
index 8c54413..01f1475 100644
--- a/src/core/Sheerka.py
+++ b/src/core/Sheerka.py
@@ -10,21 +10,21 @@ from caching.Cache import Cache
from caching.IncCache import IncCache
from common.utils import get_logger_name, get_sub_classes, import_module_and_sub_module
from core.BuiltinConcepts import BuiltinConcepts
-from core.ErrorContext import ErrorContext
from core.Event import Event
-from core.ExecutionContext import ContextHint, ExecutionContext, ExecutionContextActions
+from core.ExecutionContext import ContextHint, ExecutionContext, ContextActions
from core.ReturnValue import ReturnValue
from core.concept import Concept, ConceptMetadata
+from core.error import ErrorContext
from ontologies.SheerkaOntologyManager import SheerkaOntologyManager
from server.authentication import User
EXECUTE_STEPS = [
- ExecutionContextActions.BEFORE_PARSING,
- ExecutionContextActions.PARSING,
- ExecutionContextActions.AFTER_PARSING,
- ExecutionContextActions.BEFORE_EVALUATION,
- ExecutionContextActions.EVALUATION,
- ExecutionContextActions.AFTER_EVALUATION
+ ContextActions.BEFORE_PARSING,
+ ContextActions.PARSING,
+ ContextActions.AFTER_PARSING,
+ ContextActions.BEFORE_EVALUATION,
+ ContextActions.EVALUATION,
+ ContextActions.AFTER_EVALUATION
]
@@ -122,6 +122,7 @@ class Sheerka:
self.om = SheerkaOntologyManager(self, root_folder)
# self.builtin_cache, self.builtin_cache_by_class_name = self.get_builtins_classes_as_dict()
+ self.initialize_bind_methods()
self.initialize_caching()
self.initialize_evaluators()
self.initialize_services()
@@ -133,7 +134,7 @@ class Sheerka:
with ExecutionContext(self.name,
event,
self,
- ExecutionContextActions.INIT_SHEERKA,
+ ContextActions.INIT_SHEERKA,
None,
desc="Initializing Sheerka.") as exec_context:
if self.om.current_sdp().first_time:
@@ -165,6 +166,14 @@ class Sheerka:
return res
+ def initialize_bind_methods(self):
+ """
+ Add some methods to the list of available methods
+ :return:
+ :rtype:
+ """
+ self.bind_service_method(self.name, self.echo, False)
+
@staticmethod
def initialize_logging(is_debug, root_folder):
if is_debug:
@@ -206,9 +215,9 @@ class Sheerka:
"""
self.init_log.info("Initializing services")
- import_module_and_sub_module('core.services')
- base_class = "core.services.BaseService.BaseService"
- services = [service(self) for service in get_sub_classes("core.services", base_class)]
+ import_module_and_sub_module('services')
+ base_class = "services.BaseService.BaseService"
+ services = [service(self) for service in get_sub_classes("services", base_class)]
services.sort(key=attrgetter("order"))
for service in services:
if hasattr(service, "initialize"):
@@ -282,7 +291,7 @@ class Sheerka:
with ExecutionContext(user.email,
event,
self,
- ExecutionContextActions.EVALUATE_USER_INPUT,
+ ContextActions.EVALUATE_USER_INPUT,
command,
desc=f"Evaluating '{command}'",
global_hints=self.global_context_hints.copy()) as exec_context:
@@ -322,3 +331,14 @@ class Sheerka:
return a.id == b[3:-1]
return a.key == b
+
+ def echo(self, msg):
+ """
+ test function
+ :param msg:
+ :type msg:
+ :return:
+ :rtype:
+ """
+
+ return msg
diff --git a/src/core/concept.py b/src/core/concept.py
index 0c86681..bcd5afd 100644
--- a/src/core/concept.py
+++ b/src/core/concept.py
@@ -14,7 +14,7 @@ class ConceptDefaultProps:
RET = "#ret#"
-DefaultProps = [v for k, v in ConceptDefaultProps.__dict__.items() if not k.startswith("_")]
+ConceptDefaultPropsAttrs = [v for k, v in ConceptDefaultProps.__dict__.items() if not k.startswith("_")]
class DefinitionType:
@@ -49,6 +49,9 @@ class ConceptMetadata:
digest: str = None
all_attrs: tuple = None
+ def get_metadata(self):
+ return self
+
@dataclass
class ConceptRuntimeInfo:
@@ -57,7 +60,7 @@ class ConceptRuntimeInfo:
They are related to the instance of the concept
"""
is_evaluated: bool = False # True is the concept is evaluated by sheerka.eval_concept()
- need_validation: bool = False # True if the properties of the concept need to be validated
+ need_validation: bool = True # True if the properties of the concept need to be validated
recognized_by: str = None # RECOGNIZED_BY_ID, RECOGNIZED_BY_NAME, RECOGNIZED_BY_KEY (from Sheerka.py)
def copy(self):
diff --git a/src/core/ErrorContext.py b/src/core/error.py
similarity index 51%
rename from src/core/ErrorContext.py
rename to src/core/error.py
index a8ad3fa..b2cca36 100644
--- a/src/core/ErrorContext.py
+++ b/src/core/error.py
@@ -1,9 +1,26 @@
+from dataclasses import dataclass
+
from common.utils import compute_hash
from core.ExecutionContext import ExecutionContext
class SheerkaException(Exception):
- pass
+ def get_error_msg(self) -> str:
+ pass
+
+
+class MethodAccessError(SheerkaException):
+ def __init__(self, method_name):
+ self.method_name = method_name
+
+ def get_error_msg(self) -> str:
+ return f"Cannot access method '{self.method_name}'"
+
+
+@dataclass
+class ErrorObj:
+ def get_error_msg(self) -> str:
+ pass
class ErrorContext:
@@ -18,7 +35,7 @@ class ErrorContext:
self.parents = None
def __repr__(self):
- return f"Error(who={self.who}, context_id={self.context.long_id}, value={self.value})"
+ return f"Error(who={self.who}, context_id={self.context.medium_id}, value={self.value})"
def __eq__(self, other):
if id(self) == id(other):
@@ -33,3 +50,16 @@ class ErrorContext:
def __hash__(self):
return hash((self.who, self.context.id, compute_hash(self.value)))
+
+ def get_error_msg(self):
+ value_as_list = self.value if isinstance(self.value, list) else [self.value]
+ temp = []
+ for value in value_as_list:
+ if isinstance(value, str):
+ temp.append(value)
+ elif isinstance(value, (SheerkaException, ErrorObj)):
+ temp.append(value.get_error_msg())
+ else:
+ temp.append(repr(value))
+
+ return ", ".join(temp)
diff --git a/src/core/python_fragment.py b/src/core/python_fragment.py
new file mode 100644
index 0000000..0c625ae
--- /dev/null
+++ b/src/core/python_fragment.py
@@ -0,0 +1,59 @@
+import ast
+import copy
+
+
+class PythonFragment:
+
+ def __init__(self, source_code, ast_tree=None, original_source=None, namespace=None):
+ self.source_code = source_code # what was parsed
+ self.original_source = original_source or source_code # to remember source before concepts id replacements
+ self.ast_tree = ast_tree # if ast_ else ast.parse(source, mode="eval") if source else None
+ self.namespace = namespace or {} # when objects (mainly concepts or rules) are recognized in the expression
+ self._compiled = None
+ self.ast_str = self.get_dump()
+
+ def __repr__(self):
+ ast_type = "expr" if isinstance(self.ast_tree, ast.Expression) else "module"
+ return "PythonNode(" + ast_type + "='" + self.source_code + "')"
+
+ def __eq__(self, other):
+ if not isinstance(other, PythonFragment):
+ return False
+
+ return self.source_code == other.source_code and self.original_source == other.original_source
+
+ def __hash__(self):
+ return hash((self.source_code, self.original_source))
+
+ def get_dump(self):
+ if not self.ast_tree:
+ return None
+
+ dump = ast.dump(self.ast_tree)
+ for to_remove in [", ctx=Load()", ", kind=None", ", type_ignores=[]"]:
+ dump = dump.replace(to_remove, "")
+ return dump
+
+ def get_compiled(self):
+ if self._compiled is None:
+ if isinstance(self.ast_tree, ast.Expression):
+ self._compiled = compile(self.ast_tree, "", "eval")
+ else:
+ # in case of module, if the last expr is an expression, we want to be able to return its value
+ if isinstance(self.ast_tree.body[-1], ast.Expr):
+ init_ast = copy.deepcopy(self.ast_tree)
+ init_ast.body = self.ast_tree.body[:-1]
+ last_ast = copy.deepcopy(self.ast_tree)
+ last_ast_as_expression = self.expr_to_expression(last_ast.body[0])
+ self._compiled = [compile(init_ast, "", "exec"),
+ compile(last_ast_as_expression, "", "eval")]
+ else:
+ self._compiled = compile(self.ast_tree, "", "exec")
+
+ return self._compiled
+
+ @staticmethod
+ def expr_to_expression(expr):
+ expr.lineno = 0
+ expr.col_offset = 0
+ return ast.Expression(expr.value, lineno=0, col_offset=0)
diff --git a/src/evaluators/CreateParserInput.py b/src/evaluators/CreateParserInput.py
index c9ec92f..ac5c42c 100644
--- a/src/evaluators/CreateParserInput.py
+++ b/src/evaluators/CreateParserInput.py
@@ -1,6 +1,6 @@
from core.BuiltinConcepts import BuiltinConcepts
-from core.ErrorContext import ErrorContext
-from core.ExecutionContext import ExecutionContext, ExecutionContextActions
+from core.error import ErrorContext
+from core.ExecutionContext import ExecutionContext, ContextActions
from core.ReturnValue import ReturnValue
from evaluators.base_evaluator import EvaluatorEvalResult, EvaluatorMatchResult, OneReturnValueEvaluator
from parsers.ParserInput import ParserInput
@@ -10,7 +10,7 @@ class CreateParserInput(OneReturnValueEvaluator):
NAME = "CreateParserInput"
def __init__(self):
- super().__init__(self.NAME, ExecutionContextActions.BEFORE_EVALUATION, 50)
+ super().__init__(self.NAME, ContextActions.BEFORE_PARSING, 50)
def matches(self, context: ExecutionContext, return_value: ReturnValue) -> EvaluatorMatchResult:
if return_value.status and \
@@ -26,5 +26,5 @@ class CreateParserInput(OneReturnValueEvaluator):
return EvaluatorEvalResult([new_ret_val], [return_value])
else:
error = ErrorContext(self.NAME, context, parser_input)
- new_ret_val = ReturnValue(self.NAME, False, error, parents=[return_value])
- return EvaluatorEvalResult([new_ret_val], [return_value])
+ error_ret_val = ReturnValue(self.NAME, False, error, parents=[return_value])
+ return EvaluatorEvalResult([error_ret_val], [return_value])
diff --git a/src/evaluators/PythonEvaluator.py b/src/evaluators/PythonEvaluator.py
new file mode 100644
index 0000000..75f4775
--- /dev/null
+++ b/src/evaluators/PythonEvaluator.py
@@ -0,0 +1,31 @@
+from core.BuiltinConcepts import BuiltinConcepts
+from core.ExecutionContext import ExecutionContext, ContextActions
+from core.ReturnValue import ReturnValue
+from core.error import ErrorContext
+from evaluators.base_evaluator import EvaluatorEvalResult, EvaluatorMatchResult, OneReturnValueEvaluator
+
+
+class PythonEvaluator(OneReturnValueEvaluator):
+ NAME = "PythonEvaluator"
+
+ def __init__(self):
+ super().__init__(self.NAME, ContextActions.EVALUATION, 50)
+
+ def matches(self, context: ExecutionContext, return_value: ReturnValue) -> EvaluatorMatchResult:
+ return EvaluatorMatchResult(return_value.status and
+ context.sheerka.isinstance(return_value.value, BuiltinConcepts.PYTHON_CODE))
+
+ def eval(self, context: ExecutionContext,
+ evaluation_context: object,
+ return_value: ReturnValue) -> EvaluatorEvalResult:
+
+ sheerka = context.sheerka
+ fragment = return_value.value.pf
+
+ evaluated = sheerka.evaluate_python(context, fragment)
+ if isinstance(evaluated, ErrorContext):
+ return EvaluatorEvalResult([ReturnValue(self.name, False, evaluated, parents=[return_value])],
+ [])
+ else:
+ return EvaluatorEvalResult([ReturnValue(self.name, True, evaluated, parents=[return_value])],
+ [return_value])
diff --git a/src/evaluators/PythonParser.py b/src/evaluators/PythonParser.py
new file mode 100644
index 0000000..4882946
--- /dev/null
+++ b/src/evaluators/PythonParser.py
@@ -0,0 +1,69 @@
+import ast
+from dataclasses import dataclass
+
+from common.utils import encode_concept
+from core.BuiltinConcepts import BuiltinConcepts
+from core.ExecutionContext import ExecutionContext, ContextActions
+from core.ReturnValue import ReturnValue
+from core.error import ErrorContext, ErrorObj
+from core.python_fragment import PythonFragment
+from evaluators.base_evaluator import EvaluatorEvalResult, EvaluatorMatchResult, OneReturnValueEvaluator
+from parsers.tokenizer import TokenKind
+
+
+@dataclass()
+class PythonErrorNode(ErrorObj):
+ source: str
+ exception: Exception
+
+ def get_error_msg(self) -> str:
+ return repr(self.exception)
+
+ def __eq__(self, other):
+ if not isinstance(other, PythonErrorNode):
+ return False
+
+ return self.source == other.source and self.exception == other.exception
+
+ def __hash__(self):
+ return hash((self.source, self.exception))
+
+
+class PythonParser(OneReturnValueEvaluator):
+ NAME = "PythonParser"
+
+ def __init__(self):
+ super().__init__(self.NAME, ContextActions.PARSING, 80)
+
+ def matches(self, context: ExecutionContext, return_value: ReturnValue) -> EvaluatorMatchResult:
+ return EvaluatorMatchResult(return_value.status and
+ context.sheerka.isinstance(return_value.value, BuiltinConcepts.PARSER_INPUT))
+
+ def eval(self, context: ExecutionContext,
+ evaluation_context: object,
+ return_value: ReturnValue) -> EvaluatorEvalResult:
+ parser_input = return_value.value.body
+
+ tracker = {} # to keep track of concept tokens (c:xxx:)
+ python_switcher = {TokenKind.CONCEPT: lambda t: encode_concept(t.value),
+ TokenKind.RULE: lambda t: encode_concept(t.value, "R")}
+ source_code = parser_input.as_text(python_switcher, tracker).lstrip() # right side spaces must be kept
+
+ try:
+ ast_tree = ast.parse(source_code, f"", 'eval')
+ except:
+ try:
+ ast_tree = ast.parse(source_code, f"", 'exec')
+ except Exception as error:
+ error_context = ErrorContext(self.NAME, context, PythonErrorNode(parser_input.as_text(), error))
+ error_ret_val = ReturnValue(self.NAME, False, error_context, [return_value])
+ return EvaluatorEvalResult([error_ret_val], [])
+
+ # Successfully parsed some python code
+ python_code = context.sheerka.newn(BuiltinConcepts.PYTHON_CODE,
+ pf=PythonFragment(source_code,
+ ast_tree,
+ parser_input.original_text,
+ tracker))
+ new = ReturnValue(self.NAME, True, python_code, parents=[return_value])
+ return EvaluatorEvalResult([new], [return_value])
diff --git a/src/evaluators/base_evaluator.py b/src/evaluators/base_evaluator.py
index e26961a..2e2cfc8 100644
--- a/src/evaluators/base_evaluator.py
+++ b/src/evaluators/base_evaluator.py
@@ -1,6 +1,6 @@
from dataclasses import dataclass
-from core.ExecutionContext import ExecutionContext, ExecutionContextActions
+from core.ExecutionContext import ExecutionContext, ContextActions
from core.ReturnValue import ReturnValue
@@ -21,7 +21,7 @@ class BaseEvaluator:
Base class to evaluate ReturnValues
"""
- def __init__(self, name, step: ExecutionContextActions, priority: int, enabled=True):
+ def __init__(self, name, step: ContextActions, priority: int, enabled=True):
self.name = name
self.step = step
self.priority = priority
diff --git a/src/parsers/ParserInput.py b/src/parsers/ParserInput.py
index 80216e1..e0aae69 100644
--- a/src/parsers/ParserInput.py
+++ b/src/parsers/ParserInput.py
@@ -1,3 +1,4 @@
+from common.utils import get_text_from_tokens
from parsers.tokenizer import Tokenizer
@@ -5,15 +6,24 @@ class ParserInput:
def __init__(self, text, yield_oef=True):
self.original_text = text
self.yield_oef = yield_oef
- self.tokens = None
+ self.all_tokens = None
self.exception = None
def init(self) -> bool:
try:
# the eof if forced, but will not be yield if not set to.
- self.tokens = list(Tokenizer(self.original_text, yield_eof=True))
+ self.all_tokens = list(Tokenizer(self.original_text, yield_eof=True))
return True
except Exception as ex:
- self.tokens = None
+ self.all_tokens = None
self.exception = ex
return False
+
+ def as_text(self, custom_switcher=None, tracker=None):
+ if self.all_tokens is None:
+ raise Exception("You must call init() first !")
+
+ return get_text_from_tokens(self.all_tokens, custom_switcher, tracker)
+
+ def __repr__(self):
+ return f"ParserInput('{self.original_text}', len={len(self.all_tokens)})"
diff --git a/src/parsers/tokenizer.py b/src/parsers/tokenizer.py
index 0f42c18..a170a25 100644
--- a/src/parsers/tokenizer.py
+++ b/src/parsers/tokenizer.py
@@ -2,6 +2,7 @@ from dataclasses import dataclass, field
from enum import Enum
from common.global_symbols import VARIABLE_PREFIX
+from common.utils import str_concept
class TokenKind(Enum):
@@ -11,7 +12,6 @@ class TokenKind(Enum):
IDENTIFIER = "identifier"
CONCEPT = "concept"
RULE = "rule"
- EXPR = "expression"
STRING = "string"
NUMBER = "number"
LPAR = "lpar"
diff --git a/src/server/main.py b/src/server/main.py
index 2feb129..65adfbf 100644
--- a/src/server/main.py
+++ b/src/server/main.py
@@ -8,6 +8,8 @@ from starlette.middleware.cors import CORSMiddleware
from constants import CLIENT_OPERATION_QUIT, EXIT_COMMANDS, SHEERKA_PORT
from core.Sheerka import Sheerka
+from core.concept import Concept
+from core.error import ErrorContext
from server.authentication import ACCESS_TOKEN_EXPIRE_MINUTES, User, authenticate_user, create_access_token, \
fake_users_db, get_current_active_user
@@ -93,10 +95,17 @@ async def command(message: str, current_user: User = Depends(get_current_active_
"response": "Take care.",
"command": CLIENT_OPERATION_QUIT
}
+
res = sheerka.evaluate_user_input(message, current_user)
+ value = res[0].value
+ if isinstance(value, Concept) and value.get_runtime_info().is_evaluated:
+ value = value.body
+ if isinstance(value, ErrorContext):
+ value = value.get_error_msg()
+
return {
"status": res[0].status,
- "response": res[0].value.body,
+ "response": value,
"command": None
}
diff --git a/src/core/services/BaseService.py b/src/services/BaseService.py
similarity index 100%
rename from src/core/services/BaseService.py
rename to src/services/BaseService.py
diff --git a/src/services/SheerkaAdmin.py b/src/services/SheerkaAdmin.py
new file mode 100644
index 0000000..678891e
--- /dev/null
+++ b/src/services/SheerkaAdmin.py
@@ -0,0 +1,27 @@
+from services.BaseService import BaseService
+
+
+class SheerkaAdmin(BaseService):
+ """
+ Service for admin function, when using the CLI
+ """
+ NAME = "Admin"
+
+ def __init__(self, sheerka):
+ super().__init__(sheerka)
+
+ def initialize(self):
+ self.sheerka.bind_service_method(self.NAME, self.extended_isinstance, False)
+
+ def extended_isinstance(self, a, b):
+ """
+ switch between sheerka.isinstance and builtin.isinstance
+ :param a:
+ :param b:
+ :return:
+ """
+
+ if isinstance(b, (type, tuple)):
+ return isinstance(a, b)
+
+ return self.sheerka.isinstance(a, b)
diff --git a/src/services/SheerkaConceptEvaluator.py b/src/services/SheerkaConceptEvaluator.py
new file mode 100644
index 0000000..ca24b3b
--- /dev/null
+++ b/src/services/SheerkaConceptEvaluator.py
@@ -0,0 +1,140 @@
+from dataclasses import dataclass
+
+from caching.FastCache import FastCache
+from core.BuiltinConcepts import BuiltinConcepts
+from core.ExecutionContext import ContextActions, ExecutionContext
+from core.ReturnValue import ReturnValue
+from core.concept import Concept, ConceptDefaultProps, ConceptDefaultPropsAttrs, ConceptMetadata
+from core.error import ErrorObj, SheerkaException
+from core.python_fragment import PythonFragment
+from services.BaseService import BaseService
+from services.SheerkaPython import EvaluationRef
+
+PARSING_STEPS = [
+ ContextActions.BEFORE_PARSING,
+ ContextActions.PARSING,
+]
+
+
+class ConceptCompiled:
+ """
+ Container for all PythonFragment
+ attribute will be accessed by setattr() and getattr()
+ """
+ pass
+
+
+@dataclass
+class ConceptEvaluationHints:
+ force_evaluation: bool = False
+
+
+class ConceptEvaluator(BaseService):
+ """
+ The service is used to evaluate a concept
+ """
+
+ NAME = "ConceptEvaluator"
+
+ def __init__(self, sheerka):
+ super().__init__(sheerka)
+ self.compiled_cache = FastCache()
+
+ def initialize(self):
+ self.sheerka.bind_service_method(self.NAME, self.evaluate_concept, True)
+
+ def evaluate_concept(self, context: ExecutionContext,
+ concept: Concept,
+ hints: ConceptEvaluationHints = None) -> Concept:
+ hints = hints or ConceptEvaluationHints()
+
+ with context.push(self.NAME, ContextActions.EVALUATING_CONCEPT, {"concept": concept}) as sub_context:
+
+ # if the concept is already evaluated, no need to do it again
+ if not hints.force_evaluation and concept.get_runtime_info().is_evaluated:
+ return concept
+
+ if concept.get_definition_digest() not in self.compiled_cache:
+ compiled = self.build(sub_context, concept.get_metadata())
+ self.compiled_cache.put(concept.get_definition_digest(), compiled)
+
+ self.inner_eval_concept(context, concept)
+ return concept
+
+ def build(self, context: ExecutionContext, metadata: ConceptMetadata):
+ sheerka = context.sheerka
+ action_context = {ConceptDefaultProps.WHERE: metadata.where,
+ ConceptDefaultProps.PRE: metadata.pre,
+ ConceptDefaultProps.BODY: metadata.body,
+ ConceptDefaultProps.POST: metadata.post,
+ ConceptDefaultProps.RET: metadata.ret}
+ for k, v in metadata.variables:
+ action_context[k] = v
+
+ compiled = ConceptCompiled()
+ with context.push(self.NAME, ContextActions.BUILD_CONCEPT, {"metadata": action_context}) as sub_context:
+
+ for attr, source_code in action_context.items():
+ if source_code is None or source_code == "":
+ setattr(compiled, attr, None)
+ continue
+
+ with sub_context.push(self.NAME, ContextActions.BUILD_CONCEPT_ATTR, {"attr": attr}) as attr_context:
+ start = ReturnValue(self.NAME,
+ True,
+ sheerka.newn(BuiltinConcepts.USER_INPUT, command=source_code))
+
+ # parse the code to get the python fragment
+ attr_context.add_inputs(start=start)
+ ret = sheerka.execute(attr_context, [start], PARSING_STEPS)
+ attr_context.add_values(return_values=ret)
+
+ # TODO : manage when the parsing fails
+
+ # Add reference to internal variables
+ python_fragment = ret[0].value.pf
+ for k, v in metadata.variables:
+ python_fragment.namespace[k] = EvaluationRef("self", k)
+
+ setattr(compiled, attr, python_fragment)
+
+ return compiled
+
+ def inner_eval_concept(self, context, concept):
+ sheerka = context.sheerka
+ compiled = self.compiled_cache.get(concept.get_definition_digest())
+ compiled_debug = self._get_compiled_debug(compiled)
+
+ attributes = self._get_attributes_to_eval(context, concept)
+
+ with context.push(self.NAME, ContextActions.EVAL_CONCEPT, {"compiled": compiled_debug}) as sub_context:
+ # first evaluate the variables
+ for attr in attributes:
+ with context.push(self.NAME, ContextActions.EVAL_CONCEPT_ATTR, {"attr": attr}) as attr_context:
+ res = sheerka.evaluate_python(sub_context,
+ getattr(compiled, attr),
+ {"self": concept})
+ # TODO : manage errors
+ concept.set_value(attr, res)
+
+ return concept
+
+ @staticmethod
+ def _get_attributes_to_eval(context, concept):
+ res = [v[0] for v in concept.get_metadata().variables]
+ res += ConceptDefaultPropsAttrs
+ return res
+
+ @staticmethod
+ def _get_compiled_debug(compiled):
+ ret = {}
+ for attr, value in vars(compiled).items():
+ if value is None:
+ ret[attr] = None
+ elif isinstance(value, (ErrorObj, SheerkaException)):
+ ret[attr] = value.get_error_msg()
+ elif isinstance(value, PythonFragment):
+ ret[attr] = value.original_source
+ else:
+ ret[attr] = repr(value)
+ return ret
diff --git a/src/core/services/SheerkaConceptManager.py b/src/services/SheerkaConceptManager.py
similarity index 85%
rename from src/core/services/SheerkaConceptManager.py
rename to src/services/SheerkaConceptManager.py
index 76ee7a1..bfa63dc 100644
--- a/src/core/services/SheerkaConceptManager.py
+++ b/src/services/SheerkaConceptManager.py
@@ -6,14 +6,14 @@ from caching.Cache import Cache
from caching.FastCache import FastCache
from caching.ListIfNeededCache import ListIfNeededCache
from common.global_symbols import NotFound, NotInit, VARIABLE_PREFIX
-from common.utils import get_logger_name
+from common.utils import get_logger_name, unstr_concept
from core.BuiltinConcepts import BuiltinConcepts
-from core.ErrorContext import ErrorContext, SheerkaException
from core.ExecutionContext import ExecutionContext
from core.ReturnValue import ReturnValue
-from core.concept import Concept, ConceptMetadata, DefaultProps, DefinitionType
-from core.services.BaseService import BaseService
+from core.concept import Concept, ConceptMetadata, ConceptDefaultPropsAttrs, DefinitionType
+from core.error import ErrorContext, SheerkaException
from parsers.tokenizer import TokenKind, Tokenizer, strip_tokens
+from services.BaseService import BaseService
PROPERTIES_FOR_DIGEST = ("name", "key",
"definition", "definition_type",
@@ -22,15 +22,22 @@ PROPERTIES_FOR_DIGEST = ("name", "key",
"desc", "bound_body", "autouse", "props", "variables", "parameters")
-@dataclass
class ConceptAlreadyDefined(SheerkaException):
- concept: ConceptMetadata
- already_defined_id: str
+ def __init__(self, concept: ConceptMetadata, already_defined_id: str):
+ self.concept = concept
+ self.already_defined_id = already_defined_id
+
+ def get_error_msg(self) -> str:
+ return f"Concept {self.concept.name}, is already defined (id={self.already_defined_id})"
@dataclass
class InvalidBnf(SheerkaException):
- bnf: str
+ def __init__(self, bnf: str):
+ self.bnf = bnf
+
+ def get_error_msg(self) -> str:
+ return f"Invalid bnf '{self.bnf}'"
@dataclass
@@ -65,8 +72,12 @@ class ConceptManager(BaseService):
def initialize(self):
self.init_log.debug(f"Initializing ConceptManager, order={self.order}")
self.sheerka.bind_service_method(self.NAME, self.define_new_concept, True)
+ self.sheerka.bind_service_method(self.NAME, self.new, True)
self.sheerka.bind_service_method(self.NAME, self.newn, True)
self.sheerka.bind_service_method(self.NAME, self.newi, True)
+ self.sheerka.bind_service_method(self.NAME, self.get_by_name, False)
+ self.sheerka.bind_service_method(self.NAME, self.get_by_id, False)
+ self.sheerka.bind_service_method(self.NAME, self.get_by_key, False)
register_concept_cache = self.sheerka.om.register_concept_cache
@@ -93,6 +104,7 @@ class ConceptManager(BaseService):
_(3, BuiltinConcepts.UNKNOWN_CONCEPT, desc="Unknown concept", variables=("requested_name", "requested_id"))
_(4, BuiltinConcepts.USER_INPUT, desc="Any external input", variables=("command",))
_(5, BuiltinConcepts.PARSER_INPUT, desc="tokenized input", variables=("pi",))
+ _(6, BuiltinConcepts.PYTHON_CODE, desc="python code", variables=("pf",)) # pf for PythonFragment
self.init_log.debug('%s builtin concepts created',
len(self.sheerka.om.current_cache_manager().concept_caches))
@@ -215,6 +227,31 @@ class ConceptManager(BaseService):
return self._inner_new(self.get_by_name(BuiltinConcepts.UNKNOWN_CONCEPT), requested_id=concept_id)
return self._inner_new(metadata, **kwargs)
+ def new(self, identifier, **kwargs):
+ """
+ Try to resolve the instantiation of a concept
+ :param identifier:
+ :type identifier:
+ :param kwargs:
+ :type kwargs:
+ :return:
+ :rtype:
+ """
+ if isinstance(identifier, ConceptMetadata):
+ return self._inner_new(identifier, **kwargs)
+
+ if (tmp := unstr_concept(identifier)) != (None, None):
+ # manage c:name#id:
+ identifier = tmp
+
+ if isinstance(identifier, tuple):
+ return self.newi(identifier[1], **kwargs) if identifier[1] else self.newn(identifier[0], **kwargs)
+
+ if isinstance(identifier, str):
+ return self.newn(identifier, **kwargs)
+
+ return self._inner_new(self.get_by_name(BuiltinConcepts.UNKNOWN_CONCEPT), requested_name=identifier)
+
def get_by_name(self, key: str):
"""
Returns a concept metadata, using its name
@@ -245,6 +282,9 @@ class ConceptManager(BaseService):
"""
return self.sheerka.om.get(self.CONCEPTS_BY_KEY_ENTRY, key)
+ def get_all_concepts(self):
+ return list(sorted(self.sheerka.om.list(self.CONCEPTS_BY_ID_ENTRY), key=lambda item: int(item.id)))
+
@staticmethod
def compute_metadata_digest(metadata: ConceptMetadata):
"""
@@ -265,7 +305,7 @@ class ConceptManager(BaseService):
:return:
:rtype:
"""
- all_attrs = DefaultProps.copy()
+ all_attrs = ConceptDefaultPropsAttrs.copy()
if variables:
all_attrs += [k for k, v in variables]
diff --git a/src/core/services/SheerkaEngine.py b/src/services/SheerkaEngine.py
similarity index 91%
rename from src/core/services/SheerkaEngine.py
rename to src/services/SheerkaEngine.py
index 7a745c0..370424d 100644
--- a/src/core/services/SheerkaEngine.py
+++ b/src/services/SheerkaEngine.py
@@ -1,10 +1,11 @@
from dataclasses import dataclass
+from itertools import chain
from common.utils import to_dict
-from core.ExecutionContext import ExecutionContext, ExecutionContextActions
+from core.ExecutionContext import ExecutionContext, ContextActions
from core.ReturnValue import ReturnValue
-from core.services.BaseService import BaseService
from evaluators.base_evaluator import AllReturnValuesEvaluator, BaseEvaluator, OneReturnValueEvaluator
+from services.BaseService import BaseService
@dataclass
@@ -16,8 +17,7 @@ class EvaluationPlan:
class SheerkaEngine(BaseService):
"""
This service is used to process user input
- It is responsible to parse and evaluate the information
- It also holds the rule engine
+ It is responsible for parsing and evaluating the commands
"""
NAME = "Engine"
@@ -33,7 +33,7 @@ class SheerkaEngine(BaseService):
def call_evaluators(self,
context: ExecutionContext,
return_values: list[ReturnValue],
- step: ExecutionContextActions):
+ step: ContextActions):
"""
Calls all evaluators defined for a given step
:param context:
@@ -50,7 +50,7 @@ class SheerkaEngine(BaseService):
iteration = 0
while True:
with context.push(self.NAME,
- ExecutionContextActions.EVALUATING_ITERATION,
+ ContextActions.EVALUATING_ITERATION,
{"step": step, "iteration": iteration},
desc=f"iteration #{iteration}") as iteration_context:
simple_digest = return_values.copy()
@@ -99,18 +99,24 @@ class SheerkaEngine(BaseService):
iteration_context.add_values(return_values=return_values.copy())
- iteration += 1
+ # to avoid infinite loop
+ # plus already evaluated ret_val must not be evaluated a second time
+ already_evaluated = set(chain.from_iterable(r.parents for r in return_values if r.parents))
+ return_values = list(filter(lambda ret_val: ret_val not in already_evaluated, return_values))
+
if simple_digest == return_values:
# I can use a variable like 'has_changed', but I think that this comparison is explicit
# It explains that I stay in the loop if something was modified
break
+ iteration += 1
+
return return_values
def execute(self,
context: ExecutionContext,
return_values: list[ReturnValue],
- steps: list[ExecutionContextActions]):
+ steps: list[ContextActions]):
"""
Runs the processing engine on the return_values
:param context:
@@ -124,7 +130,7 @@ class SheerkaEngine(BaseService):
"""
for step in steps:
copy = return_values.copy()
- with context.push(self.NAME, ExecutionContextActions.EVALUATING_STEP, {"step": step}) as sub_context:
+ with context.push(self.NAME, ContextActions.EVALUATING_STEP, {"step": step}) as sub_context:
sub_context.add_inputs(return_values=copy)
return_values = self.call_evaluators(sub_context, return_values, step)
@@ -134,7 +140,7 @@ class SheerkaEngine(BaseService):
return return_values
- def get_evaluation_plan(self, context: ExecutionContext, step: ExecutionContextActions) -> EvaluationPlan:
+ def get_evaluation_plan(self, context: ExecutionContext, step: ContextActions) -> EvaluationPlan:
if step not in self.execution_plan:
return self.no_evaluation_plan
diff --git a/src/services/SheerkaMemory.py b/src/services/SheerkaMemory.py
new file mode 100644
index 0000000..23fab52
--- /dev/null
+++ b/src/services/SheerkaMemory.py
@@ -0,0 +1,73 @@
+from typing import Any
+
+from caching.FastCache import FastCache
+from common.global_symbols import NotFound
+from core.ExecutionContext import ExecutionContext
+from services.BaseService import BaseService
+
+
+class SheerkaMemory(BaseService):
+ """
+ The purpose of this service is to remember things
+ There are two types of memory
+ * short term memory : that are not persisted
+ * long term memory : that are sent to sdp (through the OntologyManager)
+ Short term memory is also use to store PythonEvaluator results
+ """
+ NAME = "Memory"
+ GLOBAL = "global" # for short term memory with no context (global variable across user inputs)
+
+ OBJECTS_ENTRY = "Memory:Objects"
+
+ def __init__(self, sheerka):
+ super().__init__(sheerka, order=13)
+ self.short_term_objects = FastCache()
+
+ def initialize(self):
+ self.sheerka.bind_service_method(self.NAME, self.get_from_short_term_memory, False, visible=False)
+ self.sheerka.bind_service_method(self.NAME, self.add_to_short_term_memory, True, visible=False)
+ self.sheerka.bind_service_method(self.NAME, self.list_short_term_memory, False, visible=False)
+
+ def get_from_short_term_memory(self, context: ExecutionContext | None, key: str) -> Any:
+ while True:
+ try:
+ id_to_use = context.id if context else self.GLOBAL
+ return self.short_term_objects.cache[id_to_use][key]
+ except KeyError:
+ if context is None:
+ return NotFound
+
+ context = context.get_parent()
+
+ def add_to_short_term_memory(self, context: ExecutionContext | None, key: str, value: Any):
+ if context:
+ context.stm = True
+ id_to_use = context.id
+ else:
+ id_to_use = SheerkaMemory.GLOBAL
+
+ if id_to_use in self.short_term_objects.cache:
+ self.short_term_objects.cache[id_to_use][key] = value
+ else:
+ self.short_term_objects.put(id_to_use, {key: value})
+
+ def list_short_term_memory(self, context: ExecutionContext | None):
+ """
+ list all short term memory data (stm data)
+ :param context:
+ :type context:
+ :return:
+ :rtype:
+ """
+ res = self.short_term_objects.cache[self.GLOBAL].copy() if self.GLOBAL in self.short_term_objects.cache else {}
+ if context is None:
+ return res
+
+ contexts = [context] + list(context.get_parents())
+ for ec in reversed(contexts):
+ try:
+ res.update(self.short_term_objects.cache[ec.id])
+ except KeyError:
+ pass
+
+ return res
diff --git a/src/services/SheerkaPython.py b/src/services/SheerkaPython.py
new file mode 100644
index 0000000..6c85636
--- /dev/null
+++ b/src/services/SheerkaPython.py
@@ -0,0 +1,384 @@
+import ast
+import functools
+import traceback
+from dataclasses import dataclass, field
+
+from common.ast_utils import NamesWithAttributesVisitor, UnreferencedNamesVisitor
+from common.global_symbols import NoFirstToken, NotFound, NotInit, Removed
+from common.utils import dict_product
+from core.BuiltinConcepts import BuiltinConcepts
+from core.ExecutionContext import ContextHint, ExecutionContext
+from core.concept import Concept
+from core.error import ErrorContext, ErrorObj, MethodAccessError
+from core.python_fragment import PythonFragment
+from services.BaseService import BaseService
+
+TO_DISABLED = ["breakpoint", "callable", "compile", "delattr", "eval", "exec", "exit", "input", "locals", "open",
+ "print", "quit", "setattr"]
+
+
+class ReservedNotInitClass:
+ pass
+
+
+ReservedNotInit = ReservedNotInitClass()
+
+sheerka_globals = {
+ "Concept": Concept,
+ "BuiltinConcepts": BuiltinConcepts,
+ "NotInit": NotInit,
+ "NotFound": NotFound,
+ "Removed": Removed,
+ "NoFirstToken": NoFirstToken,
+}
+sheerka_globals.update(dict(__builtins__))
+
+
+class Expando:
+ def __init__(self, name, bag):
+ self.__name = name
+ for k, v in bag.items():
+ setattr(self, k, v)
+
+ def __repr__(self):
+ return f"{vars(self)}"
+
+ def get_name(self):
+ return self.__name
+
+ def __eq__(self, other):
+ if id(other) == id(self):
+ return True
+
+ if not isinstance(other, Expando):
+ return False
+
+ if other.get_name() != self.get_name():
+ return False
+
+ for k, v in vars(self).items():
+ if getattr(other, k) != v:
+ return False
+
+ return True
+
+ def __hash__(self):
+ hash_content = [self.__name] + list(vars(self).keys())
+ return hash(tuple(hash_content))
+
+
+@dataclass
+class PythonEvalError(ErrorObj):
+ error: Exception
+ source: str
+ traceback: str = field(repr=False)
+ concepts: dict | None = field(repr=False)
+
+ def __eq__(self, other):
+ if id(self) == id(other):
+ return True
+
+ if not isinstance(other, PythonEvalError):
+ return False
+
+ return isinstance(self.error, type(other.error)) and \
+ self.source == other.source and \
+ self.traceback == other.traceback and \
+ self.concepts == other.concepts
+
+ def __hash__(self):
+ return hash(self.error)
+
+ def get_error(self):
+ return self.error
+
+ def get_error_msg(self):
+ return ", ".join(self.error.args)
+
+
+@dataclass
+class EvaluationRef:
+ root: str
+ attr: str
+
+ def __eq__(self, other):
+ if not isinstance(other, EvaluationRef):
+ return False
+
+ return self.root == other.root and self.attr == other.attr
+
+ def __hash__(self):
+ return hash((self.root, self.attr))
+
+
+class SheerkaPython(BaseService):
+ """
+ This service manage evaluation of python fragments
+ """
+ NAME = "PythonEvaluator"
+
+ def __init__(self, sheerka):
+ super().__init__(sheerka)
+
+ def initialize(self):
+ self.sheerka.bind_service_method(self.NAME, self.evaluate_python, False, visible=False)
+
+ def evaluate_python(self, context: ExecutionContext, fragment: PythonFragment, global_namespace=None):
+ sheerka = context.sheerka
+ expression_only = False
+ global_namespace = global_namespace or {}
+
+ try:
+ my_globals = self.get_globals(context, fragment, global_namespace, expression_only)
+ except MethodAccessError as ex:
+ if context.in_context(ContextHint.EXPRESSION_ONLY_REQUESTED):
+ # Quick and dirty,
+ # When expression_only, it's normal to have some NameError exceptions
+ error = ErrorContext(self.NAME, context, ex)
+ else:
+ eval_error = PythonEvalError(ex, fragment.source_code, traceback.format_exc(), None)
+ error = ErrorContext(self.NAME, context, eval_error)
+
+ return error
+
+ all_possible_globals = self.get_all_possible_globals(context, my_globals)
+ expect_success = True
+ concepts_entries = None
+ errors = []
+ evaluated = ReservedNotInit
+ my_locals = None
+
+ for globals_ in all_possible_globals:
+ try:
+ # eval
+ tmp_locals = {}
+ evaluated = self.evaluate_ast(fragment, globals_, tmp_locals)
+ my_locals = tmp_locals
+
+ if not expect_success or evaluated:
+ # in this first version, we stop once a success is found
+ # it may not be the best result !
+ break
+
+ except Exception as ex:
+ if concepts_entries is None:
+ # I don't want to init it if no error is raised
+ concepts_entries = self.get_concepts_entries_from_globals(my_globals)
+ eval_error = PythonEvalError(ex,
+ fragment.source_code,
+ traceback.format_exc(),
+ self.get_concepts_values_from_globals(globals_, concepts_entries))
+ errors.append(eval_error)
+
+ # add local namespace to stm
+ if my_locals:
+ for k, v in my_locals.items():
+ sheerka.add_to_short_term_memory(context, k, v)
+
+ return ErrorContext(self.NAME, context, errors) if evaluated == ReservedNotInit else evaluated
+
+ def get_globals(self, context, fragment, global_namespace, expression_only):
+ """
+ Creates the globals variables
+ :param context:
+ :param fragment:
+ :type fragment:
+ :param global_namespace:
+ :type global_namespace:
+ :param expression_only:
+ :return:
+ """
+ unreferenced_names_visitor = UnreferencedNamesVisitor(context)
+ names = unreferenced_names_visitor.get_names(fragment.ast_tree)
+ if "sheerka" in names:
+ sheerka_names = set()
+ visitor = NamesWithAttributesVisitor()
+ for sequence in visitor.get_sequences(fragment.ast_tree, "sheerka"):
+ if len(sequence) > 1:
+ sheerka_names.add(sequence[1])
+ else:
+ sheerka_names = None
+
+ return self.create_namespace(context,
+ names, # names to look for
+ sheerka_names, # sheerka methods
+ fragment.namespace, # objects from python fragment => local namespace
+ global_namespace, # global namespace
+ expression_only)
+
+ def get_sheerka_method(self, context, who, name, expression_only):
+ try:
+ method = context.sheerka.sheerka_methods[name]
+ if expression_only and method.has_side_effect:
+ raise MethodAccessError(name)
+ else:
+ method_to_use = self.inject_context(context)(method.method) \
+ if name in context.sheerka.methods_with_context \
+ else method.method
+
+ return method_to_use
+ except KeyError:
+ return None
+
+ def create_namespace(self, context,
+ names: list,
+ sheerka_objects: dict | None,
+ local_namespace: dict,
+ global_namespace: dict,
+ expression_only: bool):
+ """
+ Create a namespace for the requested names
+ :param context:
+ :param names: requested names
+ :param sheerka_objects: requested sheerka names (ex sheerka.isinstance)
+ :param local_namespace:
+ :type local_namespace:
+ :param global_namespace:
+ :type global_namespace:
+ :param expression_only: if true, discard method that can alter the global state
+ :return:
+ """
+ result = {}
+
+ for name in names:
+ if name == "in_context":
+ result[name] = context.in_context
+ continue
+
+ # need to add it manually to avoid conflict with sheerka.isinstance
+ if name == "isinstance":
+ result["isinstance"] = context.sheerka.extended_isinstance
+ continue
+
+ if not (expression_only and name in TO_DISABLED) and name in sheerka_globals:
+ result[name] = sheerka_globals[name]
+ continue
+
+ # support reference to sheerka
+ if name.lower() == "sheerka":
+ bag = {}
+ for sheerka_name in sheerka_objects:
+ if (method := self.get_sheerka_method(context,
+ context.who,
+ sheerka_name,
+ expression_only)) is not None:
+ bag[sheerka_name] = method
+ result[name] = Expando("sheerka", bag)
+ continue
+
+ # search in short term memory
+ if (obj := context.get_from_short_term_memory(name)) is not NotFound:
+ context.log(f"Resolving '{name}'. Using value found in STM.")
+ result[name] = obj
+ continue
+ #
+ # # search in memory
+ # if (obj := context.sheerka.get_last_from_memory(context, name)) is not NotFound:
+ # context.log(f"Resolving '{name}'. Using value found in Long Term Memory.", who)
+ # result[name] = obj.obj
+ # continue
+
+ # search in sheerka methods
+ if (method := self.get_sheerka_method(context, context.who, name, expression_only)) is not None:
+ result[name] = method
+ continue
+
+ # search in current node (if the name was found during the parsing)
+ # Local namespace references must be evaluated
+ if name in local_namespace:
+ context.log(f"Resolving '{name}'. Using value from local namespace.")
+ result[name] = self.resolve_object(context, name, local_namespace[name], global_namespace)
+ continue
+
+ # global namespace references are returned as is
+ if name in global_namespace:
+ result[name] = global_namespace[name]
+ continue
+
+ # at last, try to instantiate a new concept
+ if (metadata := context.sheerka.get_by_name(name)) != NotFound:
+ context.log(f"Resolving '{name}'. Instantiating new concept.")
+ result[name] = context.sheerka.new(metadata)
+
+ context.log(f"...'{name}' is not found or cannot be instantiated. Skipping.")
+
+ return result
+
+ @staticmethod
+ def resolve_object(context, attr_name, to_resolve, global_namespace):
+ if isinstance(to_resolve, EvaluationRef):
+ return getattr(global_namespace[to_resolve.root], to_resolve.attr)
+
+ raise AttributeError(attr_name)
+
+ @staticmethod
+ def evaluate_ast(fragment, my_globals, my_locals):
+ compiled = fragment.get_compiled()
+
+ if isinstance(compiled, list):
+ exec(compiled[0], my_globals, my_locals)
+ return eval(compiled[1], my_globals, my_locals)
+ elif isinstance(fragment.ast_tree, ast.Expression):
+ return eval(compiled, my_globals, my_locals)
+ else:
+ exec(compiled, my_globals, my_locals)
+
+ @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 concepts or concepts without a 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 not v.get_runtime_info().is_evaluated or v.body is NotInit:
+ 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 = dict_product(res, [{k: v}, {k: context.sheerka.objvalue(v)}])
+
+ return res
+
+ @staticmethod
+ def inject_context(context):
+ """
+ function Decorator used to inject the context in methods that needed
+ TODO : Maybe replace by 'partial' from functool
+ :param context:
+ :return:
+ """
+
+ def wrapped(func):
+ @functools.wraps(func)
+ def inner(*args, **kwargs):
+ return func(context, *args, **kwargs)
+
+ return inner
+
+ return wrapped
+
+ @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}
diff --git a/src/core/services/__init__.py b/src/services/__init__.py
similarity index 100%
rename from src/core/services/__init__.py
rename to src/services/__init__.py
diff --git a/tests/base.py b/tests/base.py
index 9727d6a..e7ddc06 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -8,6 +8,24 @@ from core.Sheerka import Sheerka
from sdp.sheerkaDataProvider import SheerkaDataProvider
+class DummyObj:
+ def __init__(self, a: str = "hello", b: str = "world"):
+ self.a = a
+ self.b = b
+
+ def __eq__(self, other):
+ if not isinstance(other, DummyObj):
+ return False
+
+ return self.a == other.a and self.b == other.b
+
+ def __hash__(self):
+ return hash((self.a, self.b))
+
+ def __repr__(self):
+ return f"Dummy('{self.a}', '{self.b}')"
+
+
class BaseTest:
@pytest.fixture()
def sdp(self) -> SheerkaDataProvider:
diff --git a/tests/caching/test_FastCache.py b/tests/caching/test_FastCache.py
index ad87579..f55706c 100644
--- a/tests/caching/test_FastCache.py
+++ b/tests/caching/test_FastCache.py
@@ -46,7 +46,7 @@ def test_i_can_put_the_same_key_several_times():
assert cache.lru == ["key2", "key1"]
-def test_none_is_returned_when_not_found():
+def test_not_found_is_returned_when_not_found():
cache = FastCache()
assert cache.get("foo") is NotFound
diff --git a/tests/common/test_ast_utils.py b/tests/common/test_ast_utils.py
new file mode 100644
index 0000000..d555935
--- /dev/null
+++ b/tests/common/test_ast_utils.py
@@ -0,0 +1,50 @@
+import ast
+
+import pytest
+
+from common.ast_utils import NamesWithAttributesVisitor, UnreferencedNamesVisitor, UnreferencedVariablesVisitor
+
+
+@pytest.mark.parametrize("source, expected", [
+ ("a,b", {"a", "b"}),
+ ("isinstance(a, int)", {"isinstance", "a", "int"}),
+ ("date.today()", {"date"}),
+ ("test()", {"test"}),
+ ("sheerka.test()", {"sheerka"}),
+ ("for i in range(10): pass", set()),
+ ("func(x=a, y=b)", {"func", "a", "b"}),
+
+])
+def test_i_can_get_unreferenced_names_from_simple_expressions(context, source, expected):
+ ast_ = ast.parse(source)
+ visitor = UnreferencedNamesVisitor(context)
+ visitor.visit(ast_)
+
+ assert visitor.names == expected
+
+
+def test_name_with_attribute():
+ # Looks for all attributes for a given name
+ ast_ = ast.parse("foo.bar.baz", "", mode="exec")
+ assert NamesWithAttributesVisitor().get_sequences(ast_, "foo") == [["foo", "bar", "baz"]]
+
+ # It parses all expressions / statements
+ ast_ = ast.parse("foo.bar.baz; one.two.three; foo.bar", "", mode="exec")
+ assert NamesWithAttributesVisitor().get_sequences(ast_, "foo") == [["foo", "bar", "baz"], ["foo", "bar"]]
+
+
+@pytest.mark.parametrize("source, expected", [
+ ("a,b", {"a", "b"}),
+ ("isinstance(a, int)", {"a", "int"}),
+ ("date.today()", set()),
+ ("test()", set()),
+ ("sheerka.test()", set()),
+ ("for i in range(10): pass", set()),
+ ("func(x=a, y=b)", {"a", "b", "x", "y"}),
+])
+def test_i_can_get_unreferenced_variables_from_simple_expressions(context, source, expected):
+ ast_ = ast.parse(source)
+ visitor = UnreferencedVariablesVisitor(context)
+ visitor.visit(ast_)
+
+ assert visitor.names == expected
diff --git a/tests/common/test_utils.py b/tests/common/test_utils.py
index 74c5a5f..21d0af6 100644
--- a/tests/common/test_utils.py
+++ b/tests/common/test_utils.py
@@ -2,9 +2,9 @@ from dataclasses import dataclass
import pytest
-from common.utils import decode_enum, get_class, to_dict, str_concept, unstr_concept
+from common.utils import decode_enum, dict_product, get_class, get_text_from_tokens, str_concept, to_dict, unstr_concept
from helpers import get_concept
-from parsers.tokenizer import Keywords, Token, TokenKind
+from parsers.tokenizer import Keywords, Token, TokenKind, Tokenizer
@dataclass
@@ -120,3 +120,55 @@ def test_i_can_decode_enum(text, expected):
])
def test_i_can_to_dict(items, expected):
assert to_dict(items, lambda obj: obj.prop1) == expected
+
+
+@pytest.mark.parametrize("text, expected_text", [
+ ("hello world", "hello world"),
+ ("'hello' 'world'", "'hello' 'world'"),
+ ("def concept a from", "def concept a from"),
+ ("()[]{}1=1.5+-/*><&é", "()[]{}1=1.5+-/*><&é"),
+ ("execute(c:concept_name:)", "execute(c:concept_name:)")
+
+])
+def test_i_can_get_text_from_tokens(text, expected_text):
+ tokens = list(Tokenizer(text))
+ assert get_text_from_tokens(tokens) == expected_text
+
+
+@pytest.mark.parametrize("text, custom, expected_text", [
+ ("execute(c:concept_name:)", {TokenKind.CONCEPT: lambda t: f"__C__{t.value[0]}"}, "execute(__C__concept_name)")
+])
+def test_i_can_get_text_from_tokens_with_custom_switcher(text, custom, expected_text):
+ tokens = list(Tokenizer(text))
+ assert get_text_from_tokens(tokens, custom) == expected_text
+
+
+def test_i_can_track_tokens():
+ text = "execute(c:name1: if r:#id: else c:name2:)"
+ switcher = {TokenKind.CONCEPT: lambda t: f"__CONCEPT__{t.value[0]}",
+ TokenKind.RULE: lambda t: f"__RULE__{t.value[1]}"}
+ tracker = {}
+ tokens = list(Tokenizer(text))
+ get_text_from_tokens(tokens, switcher, tracker)
+ assert len(tracker) == 3
+ assert tracker["__CONCEPT__name1"] == tokens[2]
+ assert tracker["__RULE__id"] == tokens[6]
+ assert tracker["__CONCEPT__name2"] == tokens[10]
+
+
+@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 dict_product(a, b) == expected
diff --git a/tests/conftest.py b/tests/conftest.py
index 7094130..9b2523a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,6 +1,7 @@
import pytest
from helpers import GetNextId
+from server.authentication import User
@pytest.fixture(scope="session")
@@ -25,12 +26,12 @@ def on_new_module(sheerka, request):
:rtype:
"""
from core.Event import Event
- from core.ExecutionContext import ExecutionContext, ExecutionContextActions
+ from core.ExecutionContext import ExecutionContext, ContextActions
module_name = request.module.__name__.split(".")[-1]
context = ExecutionContext("test",
Event(message=f"Executing module {module_name}"),
sheerka,
- ExecutionContextActions.TESTING,
+ ContextActions.TESTING,
None)
ontology = sheerka.om.push_ontology(module_name)
@@ -41,12 +42,12 @@ def on_new_module(sheerka, request):
@pytest.fixture(scope="function")
def context(sheerka):
from core.Event import Event
- from core.ExecutionContext import ExecutionContext, ExecutionContextActions
+ from core.ExecutionContext import ExecutionContext, ContextActions
return ExecutionContext("test",
Event(message=""),
sheerka,
- ExecutionContextActions.TESTING,
+ ContextActions.TESTING,
None)
@@ -55,6 +56,11 @@ def next_id():
return GetNextId()
+@pytest.fixture()
+def user():
+ return User(username="johan doe", email="johan.doe@sheerka.com", firstname="johan", lastname="doe")
+
+
class TestUsingFileBasedSheerka:
@pytest.fixture(scope="class")
def sheerka(self):
diff --git a/tests/core/test_concept.py b/tests/core/test_concept.py
index 5977720..9b03938 100644
--- a/tests/core/test_concept.py
+++ b/tests/core/test_concept.py
@@ -10,7 +10,7 @@ def test_i_can_retrieve_concept_properties():
assert foo.id == "1001"
assert foo.str_id == "c:#1001:"
assert foo.all_attrs() == ('#where#', '#pre#', '#post#', '#body#', '#ret#', 'a', 'b')
- assert foo.get_definition_digest() == "3a2cfcda8ffd0d99a7f8c7d2f1ffc4a99fc96162f3be7b9875f30751d3691af6"
+ assert foo.get_definition_digest() == "13b61f45934a802b5486a1bdd60e404b32378a801408769cd584e3b3b7518cc2"
# sanity check to make sure that 'get_concept' works as expected
assert foo.get_metadata().variables == (("a", NotInit), ("b", NotInit))
@@ -20,6 +20,7 @@ def test_i_can_set_and_get_value():
foo = get_concept("foo", variables=["a"])
foo.set_value("a", "some value")
assert foo.get_value("a") == "some value"
+ assert foo.a == "some value"
def test_i_can_set_and_get_value_from_bound_attr():
diff --git a/tests/core/test_execution_context.py b/tests/core/test_execution_context.py
index b399fd0..e6b79c3 100644
--- a/tests/core/test_execution_context.py
+++ b/tests/core/test_execution_context.py
@@ -1,15 +1,15 @@
from core.Event import Event
-from core.ExecutionContext import ExecutionContext, ExecutionContextActions
+from core.ExecutionContext import ExecutionContext, ContextActions
def test_i_can_create_execution_context(sheerka):
event = Event("myEvent", "fake_userid")
- context1 = ExecutionContext("who", event, sheerka, ExecutionContextActions.TESTING, "value1", "my desc")
+ context1 = ExecutionContext("who", event, sheerka, ContextActions.TESTING, "value1", "my desc")
assert context1.who == "who"
assert context1.event == event
assert context1.sheerka == sheerka
- assert context1.action == ExecutionContextActions.TESTING
+ assert context1.action == ContextActions.TESTING
assert context1.action_context == "value1"
assert context1.desc == "my desc"
assert context1.id == 0
@@ -18,12 +18,12 @@ def test_i_can_create_execution_context(sheerka):
def test_i_can_push(sheerka):
event = Event("test")
- context = ExecutionContext("who", event, sheerka, ExecutionContextActions.TESTING, "value")
- with context.push("pusher", ExecutionContextActions.PARSING, "action_context", "my desc") as sub_context:
+ context = ExecutionContext("who", event, sheerka, ContextActions.TESTING, "value")
+ with context.push("pusher", ContextActions.PARSING, "action_context", "my desc") as sub_context:
assert sub_context.who == "pusher"
assert sub_context.event == event
assert sub_context.sheerka == sheerka
- assert sub_context.action == ExecutionContextActions.PARSING
+ assert sub_context.action == ContextActions.PARSING
assert sub_context.action_context == "action_context"
assert sub_context.desc == "my desc"
assert sub_context.id == context.id + 1
@@ -34,11 +34,11 @@ def test_i_can_increment_ids(sheerka):
# If the event is the same, the id is incremented
event = Event("TEST::myEvent", "fake_userid")
- context1 = ExecutionContext("who", event, sheerka, ExecutionContextActions.TESTING, "value")
- context2 = context1.push("who1", ExecutionContextActions.TESTING, "value1")
- context3 = context2.push("who2", ExecutionContextActions.TESTING, "value2")
- context4 = context1.push("who1", ExecutionContextActions.TESTING, "value3")
- context5 = ExecutionContext("who", event, sheerka, ExecutionContextActions.TESTING, "value4")
+ context1 = ExecutionContext("who", event, sheerka, ContextActions.TESTING, "value")
+ context2 = context1.push("who1", ContextActions.TESTING, "value1")
+ context3 = context2.push("who2", ContextActions.TESTING, "value2")
+ context4 = context1.push("who1", ContextActions.TESTING, "value3")
+ context5 = ExecutionContext("who", event, sheerka, ContextActions.TESTING, "value4")
assert context1.id == 0
assert context2.id == 1
@@ -47,15 +47,15 @@ def test_i_can_increment_ids(sheerka):
assert context5.id == 4
event2 = Event("TEST::myEvent2", "fake_userid")
- context6 = ExecutionContext("who", event2, sheerka, ExecutionContextActions.TESTING, "value")
+ context6 = ExecutionContext("who", event2, sheerka, ContextActions.TESTING, "value")
assert context6.id == 0
def test_i_can_manage_global_hints(context):
- context2 = context.push("pusher", ExecutionContextActions.TESTING, None)
- context3 = context2.push("pusher", ExecutionContextActions.TESTING, None)
- context4 = context3.push("pusher", ExecutionContextActions.TESTING, None)
- context5 = context.push("pusher", ExecutionContextActions.TESTING, None)
+ context2 = context.push("pusher", ContextActions.TESTING, None)
+ context3 = context2.push("pusher", ContextActions.TESTING, None)
+ context4 = context3.push("pusher", ContextActions.TESTING, None)
+ context5 = context.push("pusher", ContextActions.TESTING, None)
context.global_hints.add("new_hint")
assert context.global_hints == {"new_hint"}
@@ -75,11 +75,11 @@ def test_i_can_manage_global_hints(context):
def test_i_can_manage_protected_hint(context):
# Note that protected hint only works if the hint is added BEFORE the creation of the child
context.protected_hints.add("new_hint")
- context2 = context.push("pusher", ExecutionContextActions.TESTING, None)
- context3 = context2.push("pusher", ExecutionContextActions.TESTING, None)
+ context2 = context.push("pusher", ContextActions.TESTING, None)
+ context3 = context2.push("pusher", ContextActions.TESTING, None)
context3.protected_hints.add("another_hint")
- context4 = context3.push("pusher", ExecutionContextActions.TESTING, None)
- context5 = context.push("pusher", ExecutionContextActions.TESTING, None)
+ context4 = context3.push("pusher", ContextActions.TESTING, None)
+ context5 = context.push("pusher", ContextActions.TESTING, None)
assert context.protected_hints == {"new_hint"}
assert context2.protected_hints == {"new_hint"}
@@ -90,11 +90,11 @@ def test_i_can_manage_protected_hint(context):
def test_i_can_manage_private_hints(context):
context.private_hints.add("new_hint")
- context2 = context.push("pusher", ExecutionContextActions.TESTING, None)
- context3 = context2.push("pusher", ExecutionContextActions.TESTING, None)
+ context2 = context.push("pusher", ContextActions.TESTING, None)
+ context3 = context2.push("pusher", ContextActions.TESTING, None)
context3.private_hints.add("another_hint")
- context4 = context3.push("pusher", ExecutionContextActions.TESTING, None)
- context5 = context.push("pusher", ExecutionContextActions.TESTING, None)
+ context4 = context3.push("pusher", ContextActions.TESTING, None)
+ context5 = context.push("pusher", ContextActions.TESTING, None)
assert context.private_hints == {"new_hint"}
assert context2.private_hints == set()
@@ -103,10 +103,24 @@ def test_i_can_manage_private_hints(context):
assert context5.private_hints == set()
+def test_i_can_check_if_hints_are_in_context(context):
+ context.private_hints.add("private_hint")
+ context.protected_hints.add("protected_hint")
+ context.global_hints.add("global_hint")
+ assert context.in_context("private_hint")
+ assert context.in_context("protected_hint")
+ assert context.in_context("global_hint")
+
+ context2 = context.push("pusher", ContextActions.TESTING, None)
+ assert not context2.in_context("private_hint")
+ assert context2.in_context("protected_hint")
+ assert context2.in_context("global_hint")
+
+
def test_i_can_keep_track_of_children(context):
- context2 = context.push("pusher", ExecutionContextActions.TESTING, None)
- context3 = context.push("pusher", ExecutionContextActions.TESTING, None)
- context4 = context2.push("pusher2", ExecutionContextActions.TESTING, None)
+ context2 = context.push("pusher", ContextActions.TESTING, None)
+ context3 = context.push("pusher", ContextActions.TESTING, None)
+ context4 = context2.push("pusher2", ContextActions.TESTING, None)
assert len(context._children) == 2
assert len(context2._children) == 1
@@ -115,13 +129,13 @@ def test_i_can_keep_track_of_children(context):
def test_i_can_get_children(context):
- context1 = context.push("child 1", ExecutionContextActions.TESTING, None)
- context2 = context.push("child 2", ExecutionContextActions.TESTING, None)
- context3 = context.push("child 3", ExecutionContextActions.TESTING, None)
- context21 = context2.push("child 21", ExecutionContextActions.TESTING, None)
- context22 = context2.push("child 22", ExecutionContextActions.TESTING, None)
- context211 = context21.push("child 211", ExecutionContextActions.TESTING, None)
- context31 = context3.push("child 31", ExecutionContextActions.TESTING, None)
+ context1 = context.push("child 1", ContextActions.TESTING, None)
+ context2 = context.push("child 2", ContextActions.TESTING, None)
+ context3 = context.push("child 3", ContextActions.TESTING, None)
+ context21 = context2.push("child 21", ContextActions.TESTING, None)
+ context22 = context2.push("child 22", ContextActions.TESTING, None)
+ context211 = context21.push("child 211", ContextActions.TESTING, None)
+ context31 = context3.push("child 31", ContextActions.TESTING, None)
assert list(context1.get_children()) == []
@@ -149,3 +163,15 @@ def test_i_can_get_children(context):
context3,
context31,
]
+
+
+def test_i_can_get_parents(context):
+ context1 = context.push("child 1", ContextActions.TESTING, None)
+ context2 = context1.push("child 2", ContextActions.TESTING, None)
+ context3 = context2.push("child 3", ContextActions.TESTING, None)
+
+ assert list(context3.get_parents()) == [context2, context1, context]
+ assert list(context3.get_parents(level=1)) == [context2]
+ assert list(context3.get_parents(level=2)) == [context2, context1]
+ assert list(context3.get_parents(level=3)) == [context2, context1, context]
+ assert list(context3.get_parents(level=4)) == [context2, context1, context]
diff --git a/tests/evaluators/test_PythonEvaluator.py b/tests/evaluators/test_PythonEvaluator.py
new file mode 100644
index 0000000..fc84bfa
--- /dev/null
+++ b/tests/evaluators/test_PythonEvaluator.py
@@ -0,0 +1,58 @@
+import pytest
+
+from base import BaseTest
+from conftest import NewOntology
+from core.BuiltinConcepts import BuiltinConcepts
+from core.error import ErrorContext
+from evaluators.PythonEvaluator import PythonEvaluator
+from evaluators.PythonParser import PythonParser
+from helpers import _rv, _rvf, define_new_concept, get_concepts, get_metadata
+from parsers.ParserInput import ParserInput
+
+
+def get_parser_input_from(sheerka, context, command):
+ pi = ParserInput(command)
+ pi.init()
+ parser_start = _rv(sheerka.newn(BuiltinConcepts.PARSER_INPUT, pi=pi))
+ ret = PythonParser().eval(context, None, parser_start)
+ return ret.new[0]
+
+
+class TestPythonEvaluator(BaseTest):
+ @pytest.fixture()
+ def evaluator(self, sheerka):
+ return sheerka.evaluators[PythonEvaluator.NAME]
+
+ def test_i_can_match(self, sheerka, context, evaluator):
+ ret_val = _rv(sheerka.newn(BuiltinConcepts.PYTHON_CODE))
+ assert evaluator.matches(context, ret_val).status is True
+
+ ret_val = _rv(sheerka.newn(BuiltinConcepts.UNKNOWN_CONCEPT)) # it responds to USER_INPUT only
+ assert evaluator.matches(context, ret_val).status is False
+
+ ret_val = _rvf(sheerka.newn(BuiltinConcepts.PYTHON_CODE)) # status should be true
+ assert evaluator.matches(context, ret_val).status is False
+
+ @pytest.mark.parametrize("text, expected", [
+ ("1 + 1", 2),
+ ("echo('I have access to Sheerka !')", "I have access to Sheerka !"),
+ ("sheerka.echo('I have access to Sheerka !')", "I have access to Sheerka !"),
+ ("a=10\na", 10),
+ ])
+ def test_i_can_evaluate_simple_expression(self, sheerka, context, evaluator, text, expected):
+ start = get_parser_input_from(sheerka, context, text)
+ ret = evaluator.eval(context, None, start)
+ assert ret.eaten == [start]
+ assert len(ret.new) == 1
+ assert ret.new[0].status is True
+ assert ret.new[0].value == expected
+ assert ret.new[0].parents == [start]
+
+ def test_i_can_detect_evaluation_error(self, sheerka, context, evaluator):
+ start = get_parser_input_from(sheerka, context, "a")
+ ret = evaluator.eval(context, None, start)
+ assert ret.eaten == []
+ assert len(ret.new) == 1
+ assert ret.new[0].status is False
+ assert isinstance(ret.new[0].value, ErrorContext)
+ assert ret.new[0].parents == [start]
diff --git a/tests/evaluators/test_PythonParser.py b/tests/evaluators/test_PythonParser.py
new file mode 100644
index 0000000..7eaf060
--- /dev/null
+++ b/tests/evaluators/test_PythonParser.py
@@ -0,0 +1,75 @@
+import pytest
+
+from base import BaseTest
+from core.BuiltinConcepts import BuiltinConcepts
+from core.error import ErrorContext
+from evaluators.PythonParser import PythonParser
+from helpers import _rv, _rvf
+from parsers.ParserInput import ParserInput
+
+
+class TestPythonParser(BaseTest):
+ @pytest.fixture()
+ def evaluator(self, sheerka):
+ return sheerka.evaluators[PythonParser.NAME]
+
+ def test_i_can_match(self, sheerka, context, evaluator):
+ ret_val = _rv(sheerka.newn(BuiltinConcepts.PARSER_INPUT, pi=ParserInput("a command")))
+ assert evaluator.matches(context, ret_val).status is True
+
+ ret_val = _rv(sheerka.newn(BuiltinConcepts.UNKNOWN_CONCEPT)) # it responds to USER_INPUT only
+ assert evaluator.matches(context, ret_val).status is False
+
+ ret_val = _rvf(sheerka.newn(BuiltinConcepts.PARSER_INPUT, pi=ParserInput("a command"))) # status should be true
+ assert evaluator.matches(context, ret_val).status is False
+
+ @pytest.mark.parametrize("text", [
+ "1 + 1",
+ "a = 20"
+ ])
+ def test_i_can_parse_python(self, sheerka, context, evaluator, text):
+ pi = ParserInput(text)
+ pi.init()
+ start = _rv(sheerka.newn(BuiltinConcepts.PARSER_INPUT, pi=pi))
+
+ res = evaluator.eval(context, None, start)
+
+ assert res.eaten == [start]
+ assert len(res.new) == 1
+ ret_val = res.new[0]
+ assert ret_val.status is True
+ assert sheerka.isinstance(ret_val.value, BuiltinConcepts.PYTHON_CODE)
+ assert ret_val.parents == [start]
+
+ def test_invalid_python_are_rejected(self, sheerka, context, evaluator):
+ text = "1 + "
+ pi = ParserInput(text)
+ pi.init()
+ start = _rv(sheerka.newn(BuiltinConcepts.PARSER_INPUT, pi=pi))
+
+ res = evaluator.eval(context, None, start)
+
+ assert res.eaten == []
+ assert len(res.new) == 1
+ ret_val = res.new[0]
+ assert ret_val.status is False
+ assert isinstance(ret_val.value, ErrorContext)
+ assert ret_val.parents == [start]
+
+ def test_i_can_detect_concepts(self, sheerka, context, evaluator):
+ pi = ParserInput("c:one: + c:two:")
+ pi.init()
+ start = _rv(sheerka.newn(BuiltinConcepts.PARSER_INPUT, pi=pi))
+
+ res = evaluator.eval(context, None, start)
+
+ assert res.eaten == [start]
+ assert len(res.new) == 1
+ ret_val = res.new[0]
+ assert ret_val.status is True
+ assert sheerka.isinstance(ret_val.value, BuiltinConcepts.PYTHON_CODE)
+ assert ret_val.parents == [start]
+ assert len(ret_val.value.pf.namespace) == 2
+ assert ret_val.value.pf.namespace["__C__KEY_one__ID_00None00__C__"].value == ("one", None)
+ assert ret_val.value.pf.namespace["__C__KEY_two__ID_00None00__C__"].value == ("two", None)
+
diff --git a/tests/evaluators/test_error.py b/tests/evaluators/test_error.py
new file mode 100644
index 0000000..7f8ad26
--- /dev/null
+++ b/tests/evaluators/test_error.py
@@ -0,0 +1,22 @@
+import pytest
+
+from core.error import ErrorContext, ErrorObj, MethodAccessError
+
+
+class DummyErrorObj(ErrorObj):
+ def __init__(self, msg):
+ self.msg = msg
+
+ def get_error_msg(self) -> str:
+ return self.msg
+
+
+@pytest.mark.parametrize("error_hint, expected", [
+ ("some value", "some value"),
+ (["value a", "value b"], "value a, value b"),
+ (MethodAccessError("a"), "Cannot access method 'a'"),
+ (DummyErrorObj("error msg"), "error msg")
+])
+def test_i_can_get_error_msg(context, error_hint, expected):
+ error = ErrorContext("Test", context, error_hint)
+ assert error.get_error_msg() == expected
diff --git a/tests/helpers.py b/tests/helpers.py
index 0f846bd..e55f1d2 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -2,7 +2,7 @@ from common.global_symbols import NotInit
from core.ExecutionContext import ExecutionContext
from core.ReturnValue import ReturnValue
from core.concept import Concept, ConceptMetadata, DefinitionType
-from core.services.SheerkaConceptManager import ConceptManager
+from services.SheerkaConceptManager import ConceptManager
class GetNextId:
@@ -294,8 +294,6 @@ def get_metadatas(*args, **kwargs):
def get_concepts(context: ExecutionContext, *concepts, **kwargs) -> list[Concept]:
"""
Simple and quick way to get initialize concepts for a test
- :param sheerka:
- :type sheerka:
:param context:
:type context:
:param concepts:
@@ -322,7 +320,7 @@ def get_concepts(context: ExecutionContext, *concepts, **kwargs) -> list[Concept
return res
-def define_new_concept(context: ExecutionContext, c: str | Concept) -> Concept:
+def define_new_concept(context: ExecutionContext, c: str | Concept | ConceptMetadata) -> Concept:
sheerka = context.sheerka
if isinstance(c, str):
retval = sheerka.define_new_concept(context, c)
diff --git a/tests/non_reg/__init__.py b/tests/non_reg/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/non_reg/test_non_reg1.py b/tests/non_reg/test_non_reg1.py
new file mode 100644
index 0000000..bc8a02e
--- /dev/null
+++ b/tests/non_reg/test_non_reg1.py
@@ -0,0 +1,18 @@
+from base import BaseTest
+
+
+class TestNonReg1(BaseTest):
+
+ def test_i_can_evaluate_python(self, sheerka, user):
+ res = sheerka.evaluate_user_input("1 + 1", user)
+ assert len(res) == 1
+ ret_val = res[0]
+ assert ret_val.status is True
+ assert ret_val.value == 2
+
+ def test_i_can_evaluate_variable_that_is_not_defined(self, sheerka, user):
+ res = sheerka.evaluate_user_input("a", user)
+ assert len(res) == 1
+ ret_val = res[0]
+
+ assert ret_val.status is False
diff --git a/tests/parsers/test_parser_input.py b/tests/parsers/test_parser_input.py
index 142d758..4a9f19a 100644
--- a/tests/parsers/test_parser_input.py
+++ b/tests/parsers/test_parser_input.py
@@ -1,5 +1,7 @@
+import pytest
+
from parsers.ParserInput import ParserInput
-from parsers.tokenizer import LexerError
+from parsers.tokenizer import LexerError, TokenKind
def test_i_can_parser_input():
@@ -12,3 +14,27 @@ def test_i_can_detect_errors():
parser_input = ParserInput('def concept "a')
assert parser_input.init() is False
assert isinstance(parser_input.exception, LexerError)
+
+
+def test_can_as_text_and_track_tokens():
+ parser_input = ParserInput("execute(c:name1: if r:#id: else c:name2:)")
+ parser_input.init()
+
+ switcher = {TokenKind.CONCEPT: lambda t: f"__CONCEPT__{t.value[0]}",
+ TokenKind.RULE: lambda t: f"__RULE__{t.value[1]}"}
+ tracker = {}
+ text = parser_input.as_text(switcher, tracker)
+
+ assert text == "execute(__CONCEPT__name1 if __RULE__id else __CONCEPT__name2)"
+ assert len(tracker) == 3
+ assert tracker["__CONCEPT__name1"] == parser_input.all_tokens[2]
+ assert tracker["__RULE__id"] == parser_input.all_tokens[6]
+ assert tracker["__CONCEPT__name2"] == parser_input.all_tokens[10]
+
+
+def test_i_must_call_init_before_call_as_text():
+ parser_input = ParserInput("execute(c:name1: if r:#id: else c:name2:)")
+ with pytest.raises(Exception) as ex:
+ parser_input.as_text()
+
+ assert ex.value.args[0] == "You must call init() first !"
diff --git a/tests/services/test_ConceptEvaluator.py b/tests/services/test_ConceptEvaluator.py
new file mode 100644
index 0000000..5646763
--- /dev/null
+++ b/tests/services/test_ConceptEvaluator.py
@@ -0,0 +1,101 @@
+import pytest
+
+from base import BaseTest
+from common.global_symbols import NotInit
+from conftest import NewOntology
+from core.concept import ConceptDefaultProps
+from core.python_fragment import PythonFragment
+from helpers import define_new_concept, get_metadata
+from services.SheerkaConceptEvaluator import ConceptEvaluator
+from services.SheerkaPython import EvaluationRef
+
+
+class TestConceptManager(BaseTest):
+
+ @pytest.fixture()
+ def service(self, sheerka) -> ConceptEvaluator:
+ return sheerka.services[ConceptEvaluator.NAME]
+
+ def test_i_can_build_concept(self, context, service):
+ metadata = get_metadata(
+ name="foo",
+ where="isinstance(x, Concept)",
+ pre="in_context(IS_QUESTION)",
+ body="one + a",
+ post="'post parameter'",
+ ret="self",
+ variables=(("a", "1"), ("b", "NotInit"))
+ )
+
+ compiled = service.build(context, metadata)
+ pf = getattr(compiled, ConceptDefaultProps.WHERE)
+ assert isinstance(pf, PythonFragment)
+ assert pf.source_code == metadata.where
+
+ pf = getattr(compiled, ConceptDefaultProps.PRE)
+ assert isinstance(pf, PythonFragment)
+ assert pf.source_code == metadata.pre
+
+ pf = getattr(compiled, ConceptDefaultProps.BODY)
+ assert isinstance(pf, PythonFragment)
+ assert pf.source_code == metadata.body
+
+ pf = getattr(compiled, ConceptDefaultProps.POST)
+ assert isinstance(pf, PythonFragment)
+ assert pf.source_code == metadata.post
+
+ pf = getattr(compiled, ConceptDefaultProps.RET)
+ assert isinstance(pf, PythonFragment)
+ assert pf.source_code == metadata.ret
+
+ pf = getattr(compiled, "a")
+ assert isinstance(pf, PythonFragment)
+ assert pf.source_code == metadata.variables[0][1]
+
+ pf = getattr(compiled, "b")
+ assert isinstance(pf, PythonFragment)
+ assert pf.source_code == metadata.variables[1][1]
+
+ def test_i_can_manage_when_no_source_code(self, context, service):
+ metadata = get_metadata(name="foo")
+
+ compiled = service.build(context, metadata)
+ assert getattr(compiled, ConceptDefaultProps.WHERE) is None
+ assert getattr(compiled, ConceptDefaultProps.PRE) is None
+ assert getattr(compiled, ConceptDefaultProps.BODY) is None
+ assert getattr(compiled, ConceptDefaultProps.POST) is None
+ assert getattr(compiled, ConceptDefaultProps.RET) is None
+
+ def test_i_can_detect_when_requested_names_are_concept_variables(self, context, service):
+ metadata = get_metadata(
+ name="foo",
+ body="one + a",
+ variables=(("a", "1"), ("b", "NotInit")))
+
+ compiled = service.build(context, metadata)
+ pf = getattr(compiled, ConceptDefaultProps.BODY)
+ assert isinstance(pf, PythonFragment)
+ assert pf.namespace == {"a": EvaluationRef("self", "a"),
+ "b": EvaluationRef("self", "b")}
+
+ def test_i_can_eval_concept_attributes(self, context, service):
+ with NewOntology(context, "test_i_can_eval_concept_attributes"):
+ foo_metadata = get_metadata(name="foo",
+ where="isinstance(a, int)",
+ pre="True",
+ body="2 + a",
+ post="'post parameter'",
+ ret="self",
+ variables=(("a", "1"), ("b", "NotInit")))
+ foo = define_new_concept(context, foo_metadata)
+
+ res = service.evaluate_concept(context, foo)
+
+ assert context.sheerka.isinstance(res, foo)
+ assert res.get_value("a") == 1
+ assert res.get_value("b") == NotInit
+ assert res.get_value(ConceptDefaultProps.WHERE) is True
+ assert res.get_value(ConceptDefaultProps.PRE) is True
+ assert res.get_value(ConceptDefaultProps.BODY) == 3
+ assert res.get_value(ConceptDefaultProps.POST) == "post parameter"
+ assert res.get_value(ConceptDefaultProps.RET) == res
diff --git a/tests/services/test_ConceptManager.py b/tests/services/test_ConceptManager.py
index 4724f06..2069055 100644
--- a/tests/services/test_ConceptManager.py
+++ b/tests/services/test_ConceptManager.py
@@ -4,9 +4,9 @@ from base import BaseTest
from common.global_symbols import NotFound, NotInit
from conftest import NewOntology
from core.BuiltinConcepts import BuiltinConcepts
-from core.ErrorContext import ErrorContext
from core.concept import ConceptMetadata
-from core.services.SheerkaConceptManager import ConceptAlreadyDefined, ConceptManager
+from core.error import ErrorContext
+from services.SheerkaConceptManager import ConceptAlreadyDefined, ConceptManager
from helpers import get_metadata
@@ -24,7 +24,7 @@ class TestConceptManager(BaseTest):
"""
metadata = get_metadata("foo", "body")
digest = service.compute_metadata_digest(metadata)
- assert digest == "21a1c2f420da62f4dc60f600c95b19dd9527b19dd28fd38e17f5c0e28963d176"
+ assert digest == "7c0f1708968e0312be622950d3f21d588f718f7ba568054ece64d077052a6476"
another_metadata = get_metadata("foo", "body")
other_digest = service.compute_metadata_digest(another_metadata)
@@ -86,7 +86,7 @@ class TestConceptManager(BaseTest):
assert metadata.name == "name"
assert metadata.key == "name"
assert metadata.body == "body"
- assert metadata.digest == "eb0620bd4a317af8a403c0ae1e185a528f9b58f8b0878d990e62278f89cf10d5"
+ assert metadata.digest == "c75faa4efbc9ef9dbc5174c52786d5b066e2ece41486b81c27336e292917fecb"
assert metadata.all_attrs == ('#where#', '#pre#', '#post#', '#body#', '#ret#')
# is sorted in db
@@ -117,6 +117,11 @@ class TestConceptManager(BaseTest):
res = service.define_new_concept(context, "name", body="body")
assert res.status is True
+ def test_i_cannot_get_by_if_concept_does_not_exist(self, service):
+ assert service.get_by_id("unresolved_id") == NotFound
+ assert service.get_by_name("unresolved name") == NotFound
+ assert service.get_by_key("unresolved_hash") == NotFound
+
def test_i_can_get_a_newly_created_concept(self, context, service):
with NewOntology(context, "test_i_can_get_a_newly_created_concept"):
res = service.define_new_concept(context, "name", body="body")
@@ -141,6 +146,19 @@ class TestConceptManager(BaseTest):
assert foo.var1 == "value1"
assert foo.var2 == "value2"
+ def test_i_can_manage_when_concepts_with_the_same_name(self, context, service):
+ with NewOntology(context, "test_i_can_manage_when_concepts_with_the_same_name"):
+ service.define_new_concept(context, "foo", body="body1")
+ service.define_new_concept(context, "foo", body="body2")
+
+ concepts = service.newn("foo")
+
+ assert len(concepts) == 2
+ assert concepts[0].name == "foo"
+ assert concepts[0].get_metadata().body == "body1"
+ assert concepts[1].name == "foo"
+ assert concepts[1].get_metadata().body == "body2"
+
def test_i_can_instantiate_a_new_concept_by_its_id(self, context, service):
with NewOntology(context, "test_i_can_instantiate_a_new_concept_by_its_id"):
res = service.define_new_concept(context, "foo", variables=[("var1", None), ("var2", None)])
@@ -184,3 +202,64 @@ class TestConceptManager(BaseTest):
context.sheerka.om.pop_ontology(context)
assert service.get_by_id(res.value.metadata.id) is NotFound
+
+ def test_i_can_new(self, context, service):
+ with NewOntology(context, "test_i_can_new"):
+ res = service.define_new_concept(context, "name", body="body", variables=[("my_var", None)])
+ assert res.status
+ metadata = res.value.metadata
+
+ # I can create a new concept
+ res = service.new(metadata, my_var="my_var_value")
+ assert res.id == metadata.id
+ assert res.my_var == "my_var_value"
+
+ res = service.new((metadata.name, None), my_var="my_var_value")
+ assert res.id == metadata.id
+ assert res.my_var == "my_var_value"
+
+ res = service.new((None, metadata.id), my_var="my_var_value")
+ assert res.id == metadata.id
+ assert res.my_var == "my_var_value"
+
+ res = service.new("c:name:", my_var="my_var_value")
+ assert res.id == metadata.id
+ assert res.my_var == "my_var_value"
+
+ res = service.new("c:#1001:", my_var="my_var_value")
+ assert res.id == metadata.id
+ assert res.my_var == "my_var_value"
+
+ res = service.new("c:name#1001:", my_var="my_var_value")
+ assert res.id == metadata.id
+ assert res.my_var == "my_var_value"
+
+ # cannot new using id
+ assert service.new(f"1001").name == BuiltinConcepts.UNKNOWN_CONCEPT
+
+ def test_id_is_used_when_name_and_id_are_provided(self, context, service):
+ with NewOntology(context, "test_id_is_used_when_name_and_id_are_provided"):
+ res = service.define_new_concept(context, "name", body="body1")
+ metadata = res.value.metadata
+ service.define_new_concept(context, "name", body="body2")
+
+ assert service.new((metadata.name, metadata.id)).id == metadata.id
+
+ def test_unknown_concept_is_return_if_the_identifier_is_not_found(self, service):
+ assert service.new("unknown").name == BuiltinConcepts.UNKNOWN_CONCEPT
+
+ def test_can_get_all_concepts(self, context, service):
+ with NewOntology(context, "test_i_can_new"):
+ service.define_new_concept(context, "foo")
+ service.define_new_concept(context, "bar")
+ context.sheerka.om.push_ontology("another ontology")
+ service.define_new_concept(context, "baz")
+ service.define_new_concept(context, "qux")
+
+ all_concepts = service.get_all_concepts()
+ assert [c.name for c in all_concepts if not c.is_builtin] == ["foo", "bar", "baz", "qux"]
+
+ # sanity check. Concepts are discarded when ontology is popped
+ context.sheerka.om.pop_ontology(context)
+ all_concepts = service.get_all_concepts()
+ assert [c.name for c in all_concepts if not c.is_builtin] == ["foo", "bar"]
diff --git a/tests/services/test_SheerkaAdmin.py b/tests/services/test_SheerkaAdmin.py
new file mode 100644
index 0000000..d00d200
--- /dev/null
+++ b/tests/services/test_SheerkaAdmin.py
@@ -0,0 +1,29 @@
+import pytest
+
+from base import BaseTest
+from services.SheerkaAdmin import SheerkaAdmin
+from helpers import get_concepts
+
+
+class TestConceptManager(BaseTest):
+
+ @pytest.fixture()
+ def service(self, sheerka):
+ return sheerka.services[SheerkaAdmin.NAME]
+
+ def test_i_can_test_extended_is_admin(self, context, service):
+ foo, bar = get_concepts(context, "foo", "bar", use_sheerka=True)
+
+ foo1 = context.sheerka.newn("foo")
+
+ assert service.extended_isinstance(1, int)
+ assert service.extended_isinstance(foo, "foo")
+ assert service.extended_isinstance(foo, foo1)
+ assert service.extended_isinstance(foo, foo1.get_metadata())
+ assert service.extended_isinstance(foo, "c:#1001:")
+
+ assert not service.extended_isinstance("1", int)
+ assert not service.extended_isinstance(foo, "bar")
+ assert not service.extended_isinstance(foo, bar)
+ assert not service.extended_isinstance(foo, bar.get_metadata())
+ assert not service.extended_isinstance(foo, "c:#1002:")
diff --git a/tests/services/test_SheerkaEngine.py b/tests/services/test_SheerkaEngine.py
index 7429039..a466406 100644
--- a/tests/services/test_SheerkaEngine.py
+++ b/tests/services/test_SheerkaEngine.py
@@ -4,28 +4,27 @@ import pytest
from base import BaseTest
from core.BuiltinConcepts import BuiltinConcepts
-from core.ExecutionContext import ExecutionContext, ExecutionContextActions
+from core.ExecutionContext import ExecutionContext, ContextActions
from core.ReturnValue import ReturnValue
-from core.services.SheerkaEngine import SheerkaEngine
from evaluators.CreateParserInput import CreateParserInput
from evaluators.base_evaluator import AllReturnValuesEvaluator, BaseEvaluator, EvaluatorEvalResult, \
- EvaluatorMatchResult, \
- OneReturnValueEvaluator
+ EvaluatorMatchResult, OneReturnValueEvaluator
from helpers import _rvc
+from services.SheerkaEngine import SheerkaEngine
ALL_STEPS = [
- ExecutionContextActions.BEFORE_PARSING,
- ExecutionContextActions.PARSING,
- ExecutionContextActions.AFTER_PARSING,
- ExecutionContextActions.BEFORE_EVALUATION,
- ExecutionContextActions.EVALUATION,
- ExecutionContextActions.AFTER_EVALUATION
+ ContextActions.BEFORE_PARSING,
+ ContextActions.PARSING,
+ ContextActions.AFTER_PARSING,
+ ContextActions.BEFORE_EVALUATION,
+ ContextActions.EVALUATION,
+ ContextActions.AFTER_EVALUATION
]
class OneReturnValueEvaluatorForTesting(OneReturnValueEvaluator):
def __init__(self, name,
- step: ExecutionContextActions,
+ step: ContextActions,
priority: int,
enabled=True,
match: bool | Callable = True,
@@ -56,12 +55,12 @@ class OneReturnValueEvaluatorForTesting(OneReturnValueEvaluator):
if ret_val != return_value:
ret_val.parents = [return_value]
- return EvaluatorEvalResult(self.eval_result, self.eval_eaten or [return_value])
+ return EvaluatorEvalResult(self.eval_result, [return_value] if self.eval_eaten is None else self.eval_eaten)
class AllReturnValuesEvaluatorForTesting(AllReturnValuesEvaluator):
def __init__(self, name,
- step: ExecutionContextActions,
+ step: ContextActions,
priority: int,
enabled=True,
match: bool | Callable = True,
@@ -91,31 +90,31 @@ class AllReturnValuesEvaluatorForTesting(AllReturnValuesEvaluator):
for ret_val in self.eval_result:
ret_val.parents = return_values
- return EvaluatorEvalResult(self.eval_result, self.eval_eaten or return_values)
+ return EvaluatorEvalResult(self.eval_result, return_values if self.eval_eaten is None else self.eval_eaten)
class TestSheerkaEngine(BaseTest):
@pytest.fixture()
def service(self, sheerka):
- return SheerkaEngine(sheerka)
+ return SheerkaEngine(sheerka) # I want a new instance to keep Sheerka clean (when a change execution_plan)
def test_i_can_compute_execution_plan(self, service):
assert service.compute_execution_plan([]) == {}
- e1 = BaseEvaluator("eval1", ExecutionContextActions.BEFORE_EVALUATION, 5)
- e2 = BaseEvaluator("eval2", ExecutionContextActions.BEFORE_EVALUATION, 5)
- e3 = BaseEvaluator("eval3", ExecutionContextActions.BEFORE_EVALUATION, 10)
- e4 = BaseEvaluator("eval4", ExecutionContextActions.EVALUATION, 10)
- e5 = BaseEvaluator("eval5", ExecutionContextActions.AFTER_EVALUATION, 10, enabled=False)
+ e1 = BaseEvaluator("eval1", ContextActions.BEFORE_EVALUATION, 5)
+ e2 = BaseEvaluator("eval2", ContextActions.BEFORE_EVALUATION, 5)
+ e3 = BaseEvaluator("eval3", ContextActions.BEFORE_EVALUATION, 10)
+ e4 = BaseEvaluator("eval4", ContextActions.EVALUATION, 10)
+ e5 = BaseEvaluator("eval5", ContextActions.AFTER_EVALUATION, 10, enabled=False)
res = service.compute_execution_plan([e1, e2, e3, e4, e5])
- assert res == {ExecutionContextActions.BEFORE_EVALUATION: {5: [e1, e2], 10: [e3]},
- ExecutionContextActions.EVALUATION: {10: [e4]}}
+ assert res == {ContextActions.BEFORE_EVALUATION: {5: [e1, e2], 10: [e3]},
+ ContextActions.EVALUATION: {10: [e4]}}
def test_i_can_call_execute(self, sheerka, context, service):
- service.execution_plan = {ExecutionContextActions.BEFORE_EVALUATION: {50: [CreateParserInput()]}}
+ service.execution_plan = {ContextActions.BEFORE_EVALUATION: {50: [CreateParserInput()]}}
start = [ReturnValue("TestSheerkaEngine", True, sheerka.newn(BuiltinConcepts.USER_INPUT, command="1 + 1"))]
- ret = service.execute(context, start, [ExecutionContextActions.BEFORE_EVALUATION])
+ ret = service.execute(context, start, [ContextActions.BEFORE_EVALUATION])
assert len(ret) == 1
ret = ret[0]
assert isinstance(ret, ReturnValue)
@@ -127,7 +126,7 @@ class TestSheerkaEngine(BaseTest):
service.execution_plan = {}
start = [_rvc("foo")]
- ret = service.execute(context, start, [ExecutionContextActions.EVALUATION])
+ ret = service.execute(context, start, [ContextActions.EVALUATION])
assert ret == start
@@ -135,12 +134,12 @@ class TestSheerkaEngine(BaseTest):
# properly init the service
_ = OneReturnValueEvaluatorForTesting
evaluators = [
- _("eval1", ExecutionContextActions.AFTER_PARSING, 21, match=False),
- _("eval2", ExecutionContextActions.BEFORE_EVALUATION, 5, match=False),
- _("eval3", ExecutionContextActions.AFTER_EVALUATION, 12, match=False),
- _("eval4", ExecutionContextActions.EVALUATION, 99, match=False),
- _("eval5", ExecutionContextActions.BEFORE_PARSING, 5, match=False),
- _("eval6", ExecutionContextActions.PARSING, 25, match=False),
+ _("eval1", ContextActions.AFTER_PARSING, 21, match=False),
+ _("eval2", ContextActions.BEFORE_EVALUATION, 5, match=False),
+ _("eval3", ContextActions.AFTER_EVALUATION, 12, match=False),
+ _("eval4", ContextActions.EVALUATION, 99, match=False),
+ _("eval5", ContextActions.BEFORE_PARSING, 5, match=False),
+ _("eval6", ContextActions.PARSING, 25, match=False),
]
service.execution_plan = service.compute_execution_plan(evaluators)
@@ -155,15 +154,15 @@ class TestSheerkaEngine(BaseTest):
# properly init the service
_ = OneReturnValueEvaluatorForTesting
evaluators = [
- _("eval1", ExecutionContextActions.EVALUATION, 20, match=False),
- _("eval2", ExecutionContextActions.EVALUATION, 5, match=False),
- _("eval3", ExecutionContextActions.EVALUATION, 20, match=False),
- _("eval4", ExecutionContextActions.EVALUATION, 99, match=False),
+ _("eval1", ContextActions.EVALUATION, 20, match=False),
+ _("eval2", ContextActions.EVALUATION, 5, match=False),
+ _("eval3", ContextActions.EVALUATION, 20, match=False),
+ _("eval4", ContextActions.EVALUATION, 99, match=False),
]
service.execution_plan = service.compute_execution_plan(evaluators)
start = [_rvc("foo")]
- service.execute(context, start, [ExecutionContextActions.EVALUATION])
+ service.execute(context, start, [ContextActions.EVALUATION])
# to check what happened, look at the execution context children
evaluators_executed = [ec.action_context["evaluator"] for ec in context.get_children() if
@@ -176,7 +175,7 @@ class TestSheerkaEngine(BaseTest):
_ = OneReturnValueEvaluatorForTesting
evaluators = [
_("eval1",
- ExecutionContextActions.EVALUATION,
+ ContextActions.EVALUATION,
20,
match=lambda r: context.sheerka.isinstance(r.value, "foo"),
eval_result=[rv_bar])
@@ -184,8 +183,8 @@ class TestSheerkaEngine(BaseTest):
service.execution_plan = service.compute_execution_plan(evaluators)
start = [rv_foo]
- service.execute(context, start, [ExecutionContextActions.EVALUATION])
- children = [ec for ec in context.get_children() if ec.action == ExecutionContextActions.EVALUATING_ITERATION]
+ service.execute(context, start, [ContextActions.EVALUATION])
+ children = [ec for ec in context.get_children() if ec.action == ContextActions.EVALUATING_ITERATION]
assert len(children) == 2
def test_eval_is_not_called_if_match_fails_for_one_return(self, context, service):
@@ -193,7 +192,7 @@ class TestSheerkaEngine(BaseTest):
_ = OneReturnValueEvaluatorForTesting
evaluators = [
_("eval1",
- ExecutionContextActions.EVALUATION,
+ ContextActions.EVALUATION,
20,
match=lambda r: context.sheerka.isinstance(r.value, "foo"),
eval_result=[_rvc("bar")])
@@ -201,7 +200,7 @@ class TestSheerkaEngine(BaseTest):
service.execution_plan = service.compute_execution_plan(evaluators)
start = [_rvc("baz")]
- res = service.execute(context, start, [ExecutionContextActions.EVALUATION])
+ res = service.execute(context, start, [ContextActions.EVALUATION])
assert res == start
# check what happen in details
@@ -214,7 +213,7 @@ class TestSheerkaEngine(BaseTest):
_ = OneReturnValueEvaluatorForTesting
evaluators = [
_("eval1",
- ExecutionContextActions.EVALUATION,
+ ContextActions.EVALUATION,
20,
match=lambda r: context.sheerka.isinstance(r.value, "foo"),
eval_result=[_rvc("bar")])
@@ -222,7 +221,7 @@ class TestSheerkaEngine(BaseTest):
service.execution_plan = service.compute_execution_plan(evaluators)
start = [_rvc("foo")]
- res = service.execute(context, start, [ExecutionContextActions.EVALUATION])
+ res = service.execute(context, start, [ContextActions.EVALUATION])
assert res == [_rvc("bar")]
assert res[0].parents == start
@@ -238,7 +237,7 @@ class TestSheerkaEngine(BaseTest):
_ = OneReturnValueEvaluatorForTesting
evaluators = [
_("eval1",
- ExecutionContextActions.EVALUATION,
+ ContextActions.EVALUATION,
20,
match=lambda r: context.sheerka.isinstance(r.value, "foo"),
eval_result=[rv_qux])
@@ -246,7 +245,7 @@ class TestSheerkaEngine(BaseTest):
service.execution_plan = service.compute_execution_plan(evaluators)
start = [rv_bar, rv_foo, rv_baz]
- res = service.execute(context, start, [ExecutionContextActions.EVALUATION])
+ res = service.execute(context, start, [ContextActions.EVALUATION])
assert res == [rv_bar, rv_qux, rv_baz] # We must keep the order ! rv_qux replaces rv_foo
assert res[0].parents is None
assert res[1].parents == [rv_foo]
@@ -267,12 +266,12 @@ class TestSheerkaEngine(BaseTest):
_ = OneReturnValueEvaluatorForTesting
evaluators = [
_("eval1",
- ExecutionContextActions.EVALUATION,
+ ContextActions.EVALUATION,
20,
match=lambda r: context.sheerka.isinstance(r.value, "foo"),
eval_result=[rv_bar]),
_("eval2",
- ExecutionContextActions.EVALUATION,
+ ContextActions.EVALUATION,
20,
match=lambda r: context.sheerka.isinstance(r.value, "foo"),
eval_result=[rv_baz])
@@ -280,7 +279,7 @@ class TestSheerkaEngine(BaseTest):
service.execution_plan = service.compute_execution_plan(evaluators)
start = [rv_qux, rv_foo, rv_qux]
- res = service.execute(context, start, [ExecutionContextActions.EVALUATION])
+ res = service.execute(context, start, [ContextActions.EVALUATION])
assert res == [rv_qux, rv_bar, rv_baz, rv_qux] # they both eat it !
assert res[1].parents == [rv_foo]
assert res[2].parents == [rv_foo]
@@ -293,12 +292,12 @@ class TestSheerkaEngine(BaseTest):
_ = OneReturnValueEvaluatorForTesting
evaluators = [
_("eval1",
- ExecutionContextActions.EVALUATION,
+ ContextActions.EVALUATION,
20,
match=lambda r: context.sheerka.isinstance(r.value, "foo"),
eval_result=[rv_bar]),
_("eval2",
- ExecutionContextActions.EVALUATION,
+ ContextActions.EVALUATION,
30,
match=lambda r: context.sheerka.isinstance(r.value, "foo"),
eval_result=[rv_baz])
@@ -306,7 +305,7 @@ class TestSheerkaEngine(BaseTest):
service.execution_plan = service.compute_execution_plan(evaluators)
start = [rv_foo]
- res = service.execute(context, start, [ExecutionContextActions.EVALUATION])
+ res = service.execute(context, start, [ContextActions.EVALUATION])
assert res == [rv_baz]
assert res[0].parents == start
@@ -315,7 +314,7 @@ class TestSheerkaEngine(BaseTest):
_ = AllReturnValuesEvaluatorForTesting
evaluators = [
_("eval1",
- ExecutionContextActions.EVALUATION,
+ ContextActions.EVALUATION,
20,
match=lambda r: context.sheerka.isinstance(r[0].value, "foo"),
eval_result=[_rvc("bar")])
@@ -323,11 +322,11 @@ class TestSheerkaEngine(BaseTest):
service.execution_plan = service.compute_execution_plan(evaluators)
start = [_rvc("baz")]
- res = service.execute(context, start, [ExecutionContextActions.EVALUATION])
+ res = service.execute(context, start, [ContextActions.EVALUATION])
assert res == start
start = [_rvc("foo")]
- res = service.execute(context, start, [ExecutionContextActions.EVALUATION])
+ res = service.execute(context, start, [ContextActions.EVALUATION])
assert res == [_rvc("bar")]
assert res[0].parents == start
@@ -338,7 +337,7 @@ class TestSheerkaEngine(BaseTest):
_ = AllReturnValuesEvaluatorForTesting
evaluators = [
_("eval1",
- ExecutionContextActions.EVALUATION,
+ ContextActions.EVALUATION,
20,
match=lambda lst: context.sheerka.isinstance(lst[0].value, "foo"),
eval_result=[rv_bar])
@@ -346,7 +345,7 @@ class TestSheerkaEngine(BaseTest):
service.execution_plan = service.compute_execution_plan(evaluators)
start = [rv_baz, rv_foo] # foo is not the first in the list
- res = service.execute(context, start, [ExecutionContextActions.EVALUATION])
+ res = service.execute(context, start, [ContextActions.EVALUATION])
assert res == start
# check what happen in details
@@ -360,7 +359,7 @@ class TestSheerkaEngine(BaseTest):
_ = AllReturnValuesEvaluatorForTesting
evaluators = [
_("eval1",
- ExecutionContextActions.EVALUATION,
+ ContextActions.EVALUATION,
20,
match=lambda lst: context.sheerka.isinstance(lst[0].value, "foo"),
eval_result=[rv_bar])
@@ -368,7 +367,7 @@ class TestSheerkaEngine(BaseTest):
service.execution_plan = service.compute_execution_plan(evaluators)
start = [rv_foo, rv_baz]
- res = service.execute(context, start, [ExecutionContextActions.EVALUATION])
+ res = service.execute(context, start, [ContextActions.EVALUATION])
assert res == [rv_bar]
assert res[0].parents == start
@@ -377,3 +376,28 @@ class TestSheerkaEngine(BaseTest):
exec_context = next(filter(lambda ec: "evaluator" in ec.action_context, context.get_children()))
evaluation_trace = exec_context.values["evaluation"]
assert evaluation_trace == {"match": True, "new": res, "eaten": start}
+
+ def test_ret_val_not_removed_does_not_cause_infinite_recursion(self, context, service):
+ rv_foo, rv_bar = _rvc("foo"), _rvc("bar") # rv => ReturnValue
+
+ # properly init the service
+ # both evaluator want to eat 'foo'
+ _ = OneReturnValueEvaluatorForTesting
+ evaluators = [
+ _("eval",
+ ContextActions.EVALUATION,
+ 20,
+ match=lambda r: context.sheerka.isinstance(r.value, "foo"),
+ eval_result=[rv_bar], eval_eaten=[]),
+ ]
+ service.execution_plan = service.compute_execution_plan(evaluators)
+
+ # in the test, 'foo' produces 'bar', but is not removed
+ # during the second iteration, 'foo' still exists, so it will produce 'bar' again
+ # and so on...
+ # This test validate that the infinite loop is broken
+ start = [rv_foo]
+ res = service.execute(context, start, [ContextActions.EVALUATION])
+
+ assert res == [rv_bar]
+ assert res[0].parents == [rv_foo]
diff --git a/tests/services/test_SheerkaMemory.py b/tests/services/test_SheerkaMemory.py
new file mode 100644
index 0000000..4ef7b75
--- /dev/null
+++ b/tests/services/test_SheerkaMemory.py
@@ -0,0 +1,75 @@
+import pytest
+
+from base import BaseTest, DummyObj
+from caching.FastCache import FastCache
+from core.ExecutionContext import ContextActions
+from services.SheerkaMemory import SheerkaMemory
+
+
+class TestSheerkaEngine(BaseTest):
+ @pytest.fixture()
+ def service(self, sheerka):
+ return SheerkaMemory(sheerka) # I want a new instance to keep Sheerka clean (when I update stm)
+
+ def test_i_can_add_to_global_short_term_memory(self, service):
+ dummy = DummyObj()
+ service.add_to_short_term_memory(None, "a", dummy)
+
+ assert service.short_term_objects.copy() == {'global': {'a': dummy}}
+
+ def test_i_can_add_and_get_stm_data(self, context, service):
+ sub_context = context.push("TestSheerkaEngine", ContextActions.TESTING, None)
+
+ service.add_to_short_term_memory(None, "a", "global level")
+ service.add_to_short_term_memory(context, "a", "context level")
+ service.add_to_short_term_memory(sub_context, "a", "sub context level")
+
+ assert service.get_from_short_term_memory(sub_context, "a") == "sub context level"
+ assert service.get_from_short_term_memory(context, "a") == "context level"
+ assert service.get_from_short_term_memory(None, "a") == "global level"
+
+ def test_i_can_list_stm_data(self, context, service):
+ sub_context = context.push("TestSheerkaEngine", ContextActions.TESTING, None)
+
+ service.add_to_short_term_memory(None, "a", "global a")
+ service.add_to_short_term_memory(None, "b", "global b")
+ service.add_to_short_term_memory(context, "a", "context a")
+ service.add_to_short_term_memory(context, "c", "context c")
+ service.add_to_short_term_memory(sub_context, "d", "sub context d")
+ service.add_to_short_term_memory(sub_context, "a", "sub context a")
+
+ assert service.list_short_term_memory(sub_context) == {"a": "sub context a",
+ "b": "global b",
+ "c": "context c",
+ "d": "sub context d"}
+
+ assert service.list_short_term_memory(context) == {"a": "context a",
+ "b": "global b",
+ "c": "context c"}
+
+ assert service.list_short_term_memory(None) == {"a": "global a",
+ "b": "global b"}
+
+ def test_i_can_list_stm_data_when_context_have_no_entry(self, context, service):
+ sub_context = context.push("TestSheerkaEngine", ContextActions.TESTING, None)
+
+ service.add_to_short_term_memory(sub_context, "d", "sub context d")
+ service.add_to_short_term_memory(sub_context, "a", "sub context a")
+
+ assert service.list_short_term_memory(sub_context) == {"a": "sub context a", "d": "sub context d"}
+ assert service.list_short_term_memory(context) == {}
+ assert service.list_short_term_memory(None) == {}
+
+ def test_i_value_are_removed_when_cache_is_full(self, context, service):
+ service.short_term_objects = FastCache(3)
+ context1 = context.push("TestSheerkaEngine", ContextActions.TESTING, None)
+ context2 = context.push("TestSheerkaEngine", ContextActions.TESTING, None)
+ context3 = context.push("TestSheerkaEngine", ContextActions.TESTING, None)
+
+ service.add_to_short_term_memory(context, "a", "context")
+ service.add_to_short_term_memory(context1, "b", "context 1")
+ service.add_to_short_term_memory(context2, "c", "context 2")
+ assert context.id in service.short_term_objects
+
+ service.add_to_short_term_memory(context3, "d", "context 3")
+ assert context.id not in service.short_term_objects
diff --git a/tests/services/test_SheerkaPython.py b/tests/services/test_SheerkaPython.py
new file mode 100644
index 0000000..6061baa
--- /dev/null
+++ b/tests/services/test_SheerkaPython.py
@@ -0,0 +1,127 @@
+import pytest
+
+from base import BaseTest, DummyObj
+from common.global_symbols import NoFirstToken, NotFound, NotInit, Removed
+from conftest import NewOntology
+from core.BuiltinConcepts import BuiltinConcepts
+from evaluators.PythonParser import PythonParser
+from helpers import _rv, define_new_concept, get_concepts, get_metadata
+from parsers.ParserInput import ParserInput
+from services.SheerkaPython import EvaluationRef, SheerkaPython
+
+
+def get_python_fragment(sheerka, context, command):
+ pi = ParserInput(command)
+ pi.init()
+ parser_start = _rv(sheerka.newn(BuiltinConcepts.PARSER_INPUT, pi=pi))
+ ret = PythonParser().eval(context, None, parser_start)
+ return ret.new[0].value.pf
+
+
+class TestSheerkaPython(BaseTest):
+ @pytest.fixture()
+ def service(self, sheerka) -> SheerkaPython:
+ return sheerka.services[SheerkaPython.NAME]
+
+ @pytest.mark.parametrize("text, expected", [
+ ("1 + 1", 2),
+ ("echo('I have access to Sheerka !')", "I have access to Sheerka !"),
+ ("sheerka.echo('I have access to Sheerka !')", "I have access to Sheerka !"),
+ ("a=10\na", 10),
+ ("NotInit", NotInit),
+ ("NotFound", NotFound),
+ ("Removed", Removed),
+ ("NoFirstToken", NoFirstToken),
+ ])
+ def test_i_can_evaluate_simple_expression(self, sheerka, context, service, text, expected):
+ python_fragment = get_python_fragment(sheerka, context, text)
+ ret = service.evaluate_python(context, python_fragment)
+ assert ret == expected
+
+ def test_i_can_eval_isinstance_for_type(self, sheerka, context, service):
+ python_fragment = get_python_fragment(sheerka, context, "isinstance('some string', str)")
+ ret = service.evaluate_python(context, python_fragment)
+
+ assert ret is True
+
+ def test_i_can_eval_isinstance_for_concept(self, sheerka, context, service):
+ with NewOntology(context, "test_i_can_eval_isinstance_for_concept"):
+ get_concepts(context, "foo", use_sheerka=True)
+ python_fragment = get_python_fragment(sheerka, context, "isinstance(foo, 'foo')")
+ ret = service.evaluate_python(context, python_fragment)
+ assert ret is True
+
+ # 'foo' is also a Concept
+ python_fragment = get_python_fragment(sheerka, context, "isinstance(foo, Concept)")
+ ret = service.evaluate_python(context, python_fragment)
+ assert ret is True
+
+ def test_i_can_use_value_from_global_namespace(self, sheerka, context, service):
+ python_fragment = get_python_fragment(sheerka, context, "self.a")
+
+ ret = service.evaluate_python(context, python_fragment, {"self": DummyObj("my dummy value")})
+ assert ret == "my dummy value"
+
+ def test_i_can_eval_using_eval_ref(self, sheerka, context, service):
+ python_fragment = get_python_fragment(sheerka, context, "a")
+ python_fragment.namespace = {"a": EvaluationRef("self", "a")}
+
+ ret = service.evaluate_python(context, python_fragment, {"self": DummyObj("my dummy value")})
+ assert ret == "my dummy value"
+
+ @pytest.mark.skip("Concept evaluation is not implemented")
+ def test_i_can_eval_concept_properties(self, sheerka, context, service):
+ with NewOntology(context, "test_i_can_eval_concept_properties"):
+ foo_meta = get_metadata("foo", variables=[("a", "hello world")])
+ define_new_concept(context, foo_meta)
+
+ python_fragment = get_python_fragment(sheerka, context, "foo.a")
+ ret = service.evaluate_python(context, python_fragment)
+ assert ret == "hello world"
+
+ @pytest.mark.skip("Concept evaluation is not implemented")
+ def test_i_can_eval_python_mixed_with_concept(self, sheerka, context, service):
+ with NewOntology(context, "test_i_can_eval_python_mixed_with_concept"):
+ foo_meta = get_metadata("foo", variables=[("a", "1")])
+ bar_meta = get_metadata("bar", body="2")
+ get_concepts(context, foo_meta, bar_meta, use_sheerka=True)
+
+ python_fragment = get_python_fragment(sheerka, context, "bar + foo.a")
+ ret = service.evaluate_python(context, python_fragment)
+ assert ret == "3"
+
+ def test_i_can_remember_previous_results(self, sheerka, context, service):
+ python_fragment = get_python_fragment(sheerka, context, "a=10")
+ ret = service.evaluate_python(context, python_fragment)
+ assert ret is None
+
+ python_fragment = get_python_fragment(sheerka, context, "a")
+ ret = service.evaluate_python(context, python_fragment)
+ assert ret == 10
+
+ def test_i_can_import_module(self, sheerka, context, service):
+ python_fragment = get_python_fragment(sheerka, context, "import math")
+ ret = service.evaluate_python(context, python_fragment)
+ assert ret is None
+
+ python_fragment = get_python_fragment(sheerka, context, "math.sqrt(4)")
+ ret = service.evaluate_python(context, python_fragment)
+ assert ret == 2
+
+ def test_i_can_import_function_from_module(self, sheerka, context, service):
+ python_fragment = get_python_fragment(sheerka, context, "from math import sqrt")
+ ret = service.evaluate_python(context, python_fragment)
+ assert ret is None
+
+ python_fragment = get_python_fragment(sheerka, context, "sqrt(4)")
+ ret = service.evaluate_python(context, python_fragment)
+ assert ret == 2
+
+ def test_i_can_eval_when_context_is_needed(self, sheerka, context, service):
+ with NewOntology(context, "test_i_can_eval_when_context_is_needed"):
+ python_fragment = get_python_fragment(sheerka, context, "define_new_concept('foo')")
+ ret = service.evaluate_python(context, python_fragment)
+ assert sheerka.isinstance(ret.value, BuiltinConcepts.NEW_CONCEPT)
+ # for info, there are two level of value
+ # one for PythonEvaluator return value
+ # one for the ConceptManager return value
diff --git a/tests/sheerkapickle/test_sheerka_handlers.py b/tests/sheerkapickle/test_sheerka_handlers.py
index c24b174..f1d2692 100644
--- a/tests/sheerkapickle/test_sheerka_handlers.py
+++ b/tests/sheerkapickle/test_sheerka_handlers.py
@@ -247,7 +247,7 @@ class TestSheerkaPickleHandler(BaseTest):
def test_i_can_encode_decode_execution_context(self):
sheerka = self.get_sheerka()
c = Concept("foo").def_var("a")
- context = ExecutionContext("who", Event("xxx"), sheerka, BuiltinConcepts.EVALUATE_CONCEPT, c, "my desc")
+ context = ExecutionContext("who", Event("xxx"), sheerka, BuiltinConcepts.EVALUATING_CONCEPT, c, "my desc")
input_list = [ReturnValueConcept("who", True, 10), ReturnValueConcept("who2", False, 20)]
context.inputs = {"a": input_list, "b": set_full_serialization(Concept("foo"))}
context.values = {"c": input_list, "d": set_full_serialization(Concept("bar"))}
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index 070afd1..8be70fe 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -110,7 +110,7 @@ def test_i_can_auto_init():
assert metadata.is_unique is False
assert metadata.is_builtin is False
assert metadata.definition_type is DefinitionType.DEFAULT
- assert metadata.digest == '426d88b1b928a421366c12fb283267b89610cbfb9efb470813ea8b5ba37a2013'
+ assert metadata.digest == '9e058bc1261d1e2c785889147066ce89960fd6844db5bb6f1d1d809a8eb790b7'
def test_sequences_are_incremented_when_multiples_call():