Refactord Concept class to regroup all builtins properties into a ConceptMetadata class

This commit is contained in:
2019-11-30 18:16:20 +01:00
parent 5e539a4b28
commit 75c8793d53
13 changed files with 186 additions and 147 deletions
+18 -16
View File
@@ -58,7 +58,7 @@ class SuccessConcept(Concept):
class ErrorConcept(Concept):
def __init__(self, error=None):
super().__init__(BuiltinConcepts.ERROR, True, False, BuiltinConcepts.ERROR, body=error)
super().__init__(BuiltinConcepts.ERROR, True, False, BuiltinConcepts.ERROR, error)
def __repr__(self):
return f"({self.id}){self.name}: {self.body}"
@@ -71,10 +71,9 @@ class ReturnValueConcept(Concept):
"""
def __init__(self, who=None, status=None, value=None, message=None, parents=None):
super().__init__(BuiltinConcepts.RETURN_VALUE, True, False, BuiltinConcepts.RETURN_VALUE)
super().__init__(BuiltinConcepts.RETURN_VALUE, True, False, BuiltinConcepts.RETURN_VALUE, value)
self.set_prop("who", who)
self.set_prop("status", status)
self.body = value
self.set_prop("message", message)
self.set_prop("parents", parents)
@@ -100,7 +99,7 @@ class ReturnValueConcept(Concept):
@value.setter
def value(self, value):
self.body = value
self.metadata.body = value
@property
def message(self):
@@ -140,9 +139,8 @@ class UnknownPropertyConcept(Concept):
"""
def __init__(self, property_name=None, concept=None):
super().__init__(BuiltinConcepts.UNKNOWN_PROPERTY, True, False, BuiltinConcepts.UNKNOWN_PROPERTY)
super().__init__(BuiltinConcepts.UNKNOWN_PROPERTY, True, False, BuiltinConcepts.UNKNOWN_PROPERTY, property_name)
self.set_prop("concept", concept)
self.body = property_name
def __repr__(self):
return f"UnknownProperty(property={self.property_name}, concept={self.concept})"
@@ -162,11 +160,10 @@ class ParserResultConcept(Concept):
"""
def __init__(self, parser=None, source=None, value=None, try_parsed=None):
super().__init__(BuiltinConcepts.PARSER_RESULT, True, False, BuiltinConcepts.PARSER_RESULT)
super().__init__(BuiltinConcepts.PARSER_RESULT, True, False, BuiltinConcepts.PARSER_RESULT, value)
self.set_prop("parser", parser)
self.set_prop("source", source)
self.set_prop("try_parsed", try_parsed) # in case of error, what was found before the error
self.body = value
def __repr__(self):
return f"ParserResult({self.body})"
@@ -205,9 +202,13 @@ class InvalidReturnValueConcept(Concept):
"""
def __init__(self, return_value=None, evaluator=None):
super().__init__(BuiltinConcepts.INVALID_RETURN_VALUE, True, False, BuiltinConcepts.INVALID_RETURN_VALUE)
super().__init__(
BuiltinConcepts.INVALID_RETURN_VALUE,
True,
False,
BuiltinConcepts.INVALID_RETURN_VALUE,
return_value)
self.set_prop("evaluator", evaluator)
self.body = return_value
class BeforeParsingConcept(Concept):
@@ -227,10 +228,13 @@ class AfterEvaluationConcept(Concept):
class PropertyEvalError(Concept):
def __init__(self, property_name=None, concept=None, error=None):
super().__init__(BuiltinConcepts.PROPERTY_EVAL_ERROR, True, False, BuiltinConcepts.PROPERTY_EVAL_ERROR)
super().__init__(BuiltinConcepts.PROPERTY_EVAL_ERROR,
True,
False,
BuiltinConcepts.PROPERTY_EVAL_ERROR,
property_name)
self.set_prop("concept", concept)
self.set_prop("error", error)
self.body = property_name
def __repr__(self):
return f"PropertyEvalError(property={self.property_name}, concept={self.concept}), error={self.error})"
@@ -250,8 +254,7 @@ class PropertyEvalError(Concept):
class EnumerationConcept(Concept):
def __init__(self, iteration=None):
super().__init__(BuiltinConcepts.ENUMERATION, True, False, BuiltinConcepts.ENUMERATION)
self.body = iteration
super().__init__(BuiltinConcepts.ENUMERATION, True, False, BuiltinConcepts.ENUMERATION, iteration)
def __iter__(self):
return iter(self.body)
@@ -259,8 +262,7 @@ class EnumerationConcept(Concept):
class ListConcept(Concept):
def __init__(self, items=None):
super().__init__(BuiltinConcepts.LIST, True, False, BuiltinConcepts.LIST)
self.body = items or []
super().__init__(BuiltinConcepts.LIST, True, False, BuiltinConcepts.LIST, items or [])
def append(self, obj):
self.body.append(obj)
+3 -3
View File
@@ -49,7 +49,7 @@ def expect_one(context, return_values):
return sheerka.ret(
context.who,
False,
sheerka.new(BuiltinConcepts.IS_EMPTY, obj=return_values),
sheerka.new(BuiltinConcepts.IS_EMPTY, body=return_values),
parents=return_values)
successful_results = [item for item in return_values if item.status]
@@ -77,14 +77,14 @@ def expect_one(context, return_values):
return sheerka.ret(
context.who,
False,
sheerka.new(BuiltinConcepts.TOO_MANY_SUCCESS, obj=successful_results),
sheerka.new(BuiltinConcepts.TOO_MANY_SUCCESS, body=successful_results),
parents=return_values)
# only errors, i cannot help you
return sheerka.ret(
context.who,
False,
sheerka.new(BuiltinConcepts.TOO_MANY_ERRORS, obj=return_values),
sheerka.new(BuiltinConcepts.TOO_MANY_ERRORS, body=return_values),
parents=return_values)
+79 -39
View File
@@ -1,4 +1,5 @@
import hashlib
from dataclasses import dataclass
from enum import Enum
import logging
@@ -6,6 +7,14 @@ from core.tokenizer import Tokenizer, TokenKind
log = logging.getLogger(__name__)
PROPERTIES_FOR_DIGEST = ("name", "key",
"definition", "definition_type",
"is_builtin", "is_unique",
"where", "pre", "post", "body",
"desc")
PROPERTIES_TO_SERIALIZE = PROPERTIES_FOR_DIGEST + tuple(["id"])
VARIABLE_PREFIX = "__var__"
class ConceptParts(Enum):
"""
@@ -17,6 +26,26 @@ class ConceptParts(Enum):
POST = "post"
BODY = "body"
@staticmethod
def get_parts():
return set(item.value for item in ConceptParts)
@dataclass
class ConceptMetadata:
name: str
is_builtin: bool
is_unique: bool
key: str # name od the concept, where prop are replaced. to ease search
body: str # main method, can also be the value of the concept
where: str # condition to recognize variables in name
pre: str # list of pre conditions before calling the main function
post: str # list of post conditions after calling the main function
definition: str # regex used to define the concept
definition_type: str # definition can be done with something else than regex
desc: str # possible description for the concept
id: str # unique identifier for a concept. The id will never be modified (but the key can)
class Concept:
"""
@@ -24,51 +53,49 @@ class Concept:
A concept is a the base object of our universe
Everything is a concept
"""
props_for_digest = ("is_builtin", "is_unique", "key", "name", "where", "pre", "post", "body", "desc")
props_to_serialize = ("id", "is_builtin", "is_unique", "key", "name", "where", "pre", "post", "body", "desc")
concept_parts = set(item.value for item in ConceptParts)
PROPERTY_PREFIX = "__var__"
def __init__(self, name=None,
is_builtin=False,
is_unique=False,
key=None,
body=None,
where=None,
pre=None,
post=None,
body=None,
definition=None,
definition_type=None,
desc=None,
obj=None):
id=None):
self.name = str(name) if name else None
self.is_builtin = is_builtin
self.is_unique = is_unique
self.key = str(key) if key else None # name od the concept, where prop are replaced. to ease search
metadata = ConceptMetadata(
str(name) if name else None,
is_builtin,
is_unique,
str(key) if key else None,
body,
where,
pre,
post,
definition,
definition_type,
desc,
id
)
self.where = where # condition to recognize variables in name
self.pre = pre # list of pre conditions before calling the main function
self.post = post # list of post conditions after calling the main function
self.body = body # main method, can also be the value of the concept
self.desc = desc
self.id = None # unique identifier for a concept. The id will never be modified
self.obj = obj # main of principal property of the concept
self.metadata = metadata
self.props = {} # list of Property for this concept
self.functions = {} # list of helper functions
self.codes = {} # cached ast for the where, pre, post and body parts
self.cached_asts = {} # cached ast for the where, pre, post and body parts
def __repr__(self):
return f"({self.id}){self.name}"
return f"({self.metadata.id}){self.metadata.name}"
def __eq__(self, other):
if not isinstance(other, Concept):
return False
# check the attributes
for prop in self.props_to_serialize:
if getattr(self, prop) != getattr(other, prop):
for prop in PROPERTIES_TO_SERIALIZE:
if getattr(self.metadata, prop) != getattr(other.metadata, prop):
# print(prop) # use full to know which id does not match
return False
@@ -80,10 +107,19 @@ class Concept:
return True
def __hash__(self):
return hash(self.name)
return hash(self.metadata.name)
def get_key(self):
return self.key
@property
def name(self):
return self.metadata.name
@property
def id(self):
return self.metadata.id
@property
def key(self):
return self.metadata.key
def init_key(self, tokens=None):
"""
@@ -94,11 +130,11 @@ class Concept:
:param tokens:
:return:
"""
if self.key is not None:
if self.metadata.key is not None:
return self
if tokens is None:
tokens = iter(Tokenizer(self.name))
tokens = iter(Tokenizer(self.metadata.name))
variables = list(self.props.keys())
@@ -112,14 +148,18 @@ class Concept:
if not first:
key += " "
if variables is not None and token.value in variables:
key += self.PROPERTY_PREFIX + str(variables.index(token.value))
key += VARIABLE_PREFIX + str(variables.index(token.value))
else:
key += token.value[1:-1] if token.type == TokenKind.STRING else token.value
first = False
self.key = key
self.metadata.key = key
return self
@property
def body(self):
return self.metadata.body
def add_codes(self, codes):
"""
Gets the ASTs for 'where', 'pre', 'post' and 'body'
@@ -131,12 +171,12 @@ class Concept:
:param codes:
:return:
"""
possibles_codes = self.concept_parts
possibles_codes = ConceptParts.get_parts()
if codes is None:
return
for key in codes:
if key in possibles_codes:
self.codes[ConceptParts(key)] = codes[key]
self.cached_asts[ConceptParts(key)] = codes[key]
return self
@@ -145,7 +185,7 @@ class Concept:
Returns the digest of the event
:return: hexa form of the sha256
"""
return hashlib.sha256(f"Concept:{self.to_dict(self.props_for_digest)}".encode("utf-8")).hexdigest()
return hashlib.sha256(f"Concept:{self.to_dict(PROPERTIES_FOR_DIGEST)}".encode("utf-8")).hexdigest()
def to_dict(self, props_to_use=None):
"""
@@ -153,9 +193,9 @@ class Concept:
:return:
"""
props_to_use = props_to_use or self.props_to_serialize
props_to_use = props_to_use or PROPERTIES_TO_SERIALIZE
props_as_dict = dict((prop, getattr(self, prop)) for prop in props_to_use)
props_as_dict = dict((prop, getattr(self.metadata, prop)) for prop in props_to_use)
props_as_dict["props"] = [(p, self.props[p].value) for p in self.props]
return props_as_dict
@@ -165,9 +205,9 @@ class Concept:
:param as_dict:
:return:
"""
for prop in self.props_to_serialize:
for prop in PROPERTIES_TO_SERIALIZE:
if prop in as_dict:
setattr(self, prop, as_dict[prop])
setattr(self.metadata, prop, as_dict[prop])
if "props" in as_dict:
for n, v in as_dict["props"]:
self.set_prop(n, v)
+48 -51
View File
@@ -1,6 +1,6 @@
from dataclasses import dataclass
from core.builtin_concepts import BuiltinConcepts, ErrorConcept, ReturnValueConcept
from core.concept import Concept, ConceptParts
from core.concept import Concept, ConceptParts, PROPERTIES_FOR_DIGEST
from evaluators.BaseEvaluator import OneReturnValueEvaluator
from parsers.BaseParser import BaseParser
from sdp.sheerkaDataProvider import SheerkaDataProvider, Event, SheerkaDataProviderDuplicateKeyError
@@ -55,7 +55,7 @@ class Sheerka(Concept):
self.debug = debug
self.skip_builtins_in_db = skip_builtins_in_db
def initialize(self, root_folder=None):
def initialize(self, root_folder: str = None):
"""
Starting Sheerka
Loads the current configuration
@@ -80,18 +80,6 @@ class Sheerka(Concept):
return ReturnValueConcept(self, True, self)
def set_id_if_needed(self, obj, is_builtin):
"""
Set the key for the concept if needed
:param obj:
:param is_builtin:
:return:
"""
if obj.id is not None:
return
obj.id = self.sdp.get_next_key(self.BUILTIN_CONCEPTS_KEYS if is_builtin else self.USER_CONCEPTS_KEYS)
log.debug(f"Setting id '{obj.id}' to concept '{obj.name}'.")
def initialize_builtin_concepts(self):
"""
Initializes the builtin concepts
@@ -107,11 +95,11 @@ class Sheerka(Concept):
else builtins_classes[str(key)]() if str(key) in builtins_classes \
else Concept(key, True, False, key)
if not concept.is_unique and str(key) in builtins_classes:
if not concept.metadata.is_unique and str(key) in builtins_classes:
self.builtin_cache[key] = builtins_classes[str(key)]
if not self.skip_builtins_in_db:
from_db = self.sdp.get_safe(self.CONCEPTS_ENTRY, concept.key)
from_db = self.sdp.get_safe(self.CONCEPTS_ENTRY, concept.metadata.key)
if from_db is None:
log.debug(f"'{concept.name}' concept is not found in db. Adding.")
self.set_id_if_needed(concept, True)
@@ -158,7 +146,7 @@ class Sheerka(Concept):
logging.basicConfig(format=log_format, level=log_level)
def eval(self, text):
def eval(self, text: str):
"""
Note to KSI: If you try to add execution context to this function,
You may end in an infinite loop
@@ -294,7 +282,19 @@ class Sheerka(Concept):
return return_values
def create_new_concept(self, context, concept):
def set_id_if_needed(self, obj: Concept, is_builtin: bool):
"""
Set the key for the concept if needed
:param obj:
:param is_builtin:
:return:
"""
if obj.metadata.id is not None:
return
obj.metadata.id = self.sdp.get_next_key(self.BUILTIN_CONCEPTS_KEYS if is_builtin else self.USER_CONCEPTS_KEYS)
log.debug(f"Setting id '{obj.metadata.id}' to concept '{obj.metadata.name}'.")
def create_new_concept(self, context, concept: Concept):
"""
Adds a new concept to the system
:param context:
@@ -325,7 +325,7 @@ class Sheerka(Concept):
ret = self.ret(self.create_new_concept.__name__, True, self.new(BuiltinConcepts.NEW_CONCEPT, body=concept))
return ret
def add_codes_to_concept(self, context, concept):
def initialize_concept_asts(self, context, concept: Concept):
"""
Updates the codes of the newly created concept
Basically, it runs the parsers on all parts
@@ -334,16 +334,16 @@ class Sheerka(Concept):
:return:
"""
for part_key in ConceptParts:
source = getattr(concept, part_key.value)
source = getattr(concept.metadata, part_key.value)
if source is None or not isinstance(source, str) or source == "":
# the only sources that I am sure to parse are strings
# I refuse empty strings for performance, I don't want to handle useless NOPConcepts
continue
else:
concept.codes[part_key] = self.parse(context, source)
concept.cached_asts[part_key] = self.parse(context, source)
for prop in concept.props:
concept.codes[prop] = self.parse(context, concept.props[prop].value)
concept.cached_asts[prop] = self.parse(context, concept.props[prop].value)
# updates the code of the reference when possible
if concept.key in self.concepts_cache:
@@ -352,11 +352,20 @@ class Sheerka(Concept):
# TODO : manage when there are multiple entries
pass
else:
self.concepts_cache[concept.key].codes = concept.codes
self.concepts_cache[concept.key].cached_asts = concept.cached_asts
def eval_concept(self, context, concept, properties_to_eval=None):
if len(concept.codes) == 0:
self.add_codes_to_concept(context, concept)
def eval_concept(self, context, concept: Concept, properties_to_eval=None):
"""
Evaluation a concept
It means that if the where clause is True, will evaluate the body
Also chc
:param context:
:param concept:
:param properties_to_eval:
:return:
"""
if len(concept.cached_asts) == 0:
self.initialize_concept_asts(context, concept)
if properties_to_eval is None:
properties_to_eval = ["where", "pre", "post", "body", "props"]
@@ -366,13 +375,13 @@ class Sheerka(Concept):
pass
else:
part_key = ConceptParts(prop)
if concept.codes[part_key] is None:
if concept.cached_asts[part_key] is None:
continue
res = self.chain_process(context, concept.codes[part_key], concept_evaluation_steps)
res = self.chain_process(context, concept.cached_asts[part_key], concept_evaluation_steps)
res = core.builtin_helpers.expect_one(context, res)
setattr(concept, prop, res.value)
setattr(concept.metadata, prop, res.value)
def add_in_cache(self, concept):
def add_in_cache(self, concept: Concept):
"""
Adds a concept template in cache.
The cache is used as a proxy before looking at sdp
@@ -416,7 +425,7 @@ class Sheerka(Concept):
unknown_concept = Concept()
template = self.concepts_cache[str(BuiltinConcepts.UNKNOWN_CONCEPT)]
unknown_concept.update_from(template)
unknown_concept.body = concept_key
unknown_concept.metadata.body = concept_key
return unknown_concept
def new(self, concept_key, **kwargs):
@@ -431,7 +440,7 @@ class Sheerka(Concept):
def new_from_template(t, k, **kwargs_):
# manage singleton
if t.is_unique:
if t.metadata.is_unique:
return t
# otherwise, create another instance
@@ -442,6 +451,8 @@ class Sheerka(Concept):
for k, v in kwargs_.items():
if k in concept.props:
concept.set_prop(k, v)
elif k in PROPERTIES_FOR_DIGEST:
setattr(concept.metadata, k, v)
elif hasattr(concept, k):
setattr(concept, k, v)
else:
@@ -462,7 +473,7 @@ class Sheerka(Concept):
concepts = [new_from_template(t, concept_key, **kwargs) for t in template]
return self.new(BuiltinConcepts.ENUMERATION, body=concepts)
def ret(self, who, status, value, message=None, parents=None):
def ret(self, who: str, status: bool, value, message=None, parents=None):
"""
Creates and returns a ReturnValue concept
:param who:
@@ -569,32 +580,18 @@ class Sheerka(Concept):
return sorted(res, key=lambda i: int(i.id))
def test(self):
return f"I have access to Sheerka !"
@staticmethod
def get_builtins_classes_as_dict():
res = {}
for c in core.utils.get_classes("core.builtin_concepts"):
if issubclass(c, Concept) and c != Concept:
res[c().key] = c
res[c().metadata.key] = c
return res
@staticmethod
def get_builtin_parsers():
res = []
# modules = core.utils.get_module("parsers")
# for m in modules:
base_class = core.utils.get_class("parsers.BaseParser.BaseParser")
for c in core.utils.get_classes_recursive("parsers"):
# if issubclass(c, base_class) and c != base_class:
res.append(c)
return res
@staticmethod
def test():
return "I have access to Sheerka !"
@dataclass
class ExecutionContext: