586 lines
18 KiB
Python
586 lines
18 KiB
Python
import hashlib
|
|
from collections import namedtuple
|
|
from copy import deepcopy
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from typing import Union
|
|
|
|
import core.utils
|
|
from core.sheerka_logger import get_logger
|
|
from core.tokenizer import Tokenizer, TokenKind
|
|
|
|
PROPERTIES_FOR_DIGEST = ("name", "key",
|
|
"definition", "definition_type",
|
|
"is_builtin", "is_unique",
|
|
"where", "pre", "post", "body",
|
|
"desc", "props", "variables")
|
|
PROPERTIES_TO_SERIALIZE = PROPERTIES_FOR_DIGEST + tuple(["id"])
|
|
PROPERTIES_FOR_NEW = ("where", "pre", "post", "body", "desc")
|
|
VARIABLE_PREFIX = "__var__"
|
|
ORIGIN = "##origin##" # same as Serializer.ORIGIN but I don't want to include the reference
|
|
DEFINITION_TYPE_BNF = "bnf"
|
|
DEFINITION_TYPE_DEF = "def"
|
|
|
|
|
|
class ConceptParts(Enum):
|
|
"""
|
|
Lists metadata that can contains some code
|
|
"""
|
|
WHERE = "where"
|
|
PRE = "pre"
|
|
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)
|
|
props: dict # hashmap of properties, values
|
|
variables: list # list of concept variables, with their default values
|
|
is_evaluated: bool = False # True is the concept is evaluated by sheerka.eval_concept()
|
|
need_validation = False # True if the properties of the concept need to be validated
|
|
full_serialization: bool = False # If True, the full object will be serialized, rather than just the diff
|
|
|
|
|
|
class Concept:
|
|
"""
|
|
Default concept object
|
|
A concept is a the base object of our universe
|
|
Everything is a concept
|
|
"""
|
|
|
|
def __init__(self, name=None,
|
|
is_builtin=False,
|
|
is_unique=False,
|
|
key=None,
|
|
body=None,
|
|
where=None,
|
|
pre=None,
|
|
post=None,
|
|
definition=None,
|
|
definition_type=None,
|
|
desc=None,
|
|
id=None,
|
|
props=None,
|
|
variables=None):
|
|
|
|
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,
|
|
props or {},
|
|
variables or []
|
|
)
|
|
|
|
self.metadata = metadata
|
|
self.compiled = {} # cached ast for the where, pre, post and body parts and variables
|
|
self.values = {} # resolved values. As compiled, it's used both for metadata and variables
|
|
self.bnf = None # parsing expression
|
|
self.log = get_logger("core." + self.__class__.__name__)
|
|
self.init_log = get_logger("init.core." + self.__class__.__name__)
|
|
self.original_definition_hash = None # concept hash before any alteration of the metadata
|
|
|
|
def __repr__(self):
|
|
return f"({self.metadata.id}){self.metadata.name}"
|
|
|
|
def __eq__(self, other):
|
|
|
|
if id(self) == id(other):
|
|
return True
|
|
|
|
if isinstance(other, simplec):
|
|
return self.name == other.name and self.body == other.body
|
|
|
|
if isinstance(other, CC):
|
|
return other == self
|
|
|
|
if isinstance(other, CB):
|
|
return other == self
|
|
|
|
if not isinstance(other, Concept):
|
|
return False
|
|
|
|
# check the metadata
|
|
for prop in PROPERTIES_TO_SERIALIZE:
|
|
# print(prop) # use full to know which id does not match
|
|
my_value = getattr(self.metadata, prop)
|
|
other_value = getattr(other.metadata, prop)
|
|
if isinstance(my_value, Concept) and isinstance(other_value, Concept):
|
|
# need to check if circular references
|
|
if id(self) == id(other):
|
|
continue
|
|
|
|
sub_value = getattr(other_value.metadata, prop)
|
|
while isinstance(sub_value, Concept):
|
|
if id(self) == id(sub_value):
|
|
return False # circular reference
|
|
sub_value = getattr(sub_value.metadata, prop)
|
|
|
|
if my_value != other_value:
|
|
return False
|
|
|
|
else:
|
|
if my_value != other_value:
|
|
return False
|
|
|
|
# checks the values
|
|
if len(self.values) != len(other.values):
|
|
return False
|
|
|
|
for name in self.values:
|
|
if self.get_value(name) != other.get_value(name):
|
|
return False
|
|
|
|
return True
|
|
|
|
def __hash__(self):
|
|
return hash(self.metadata.name)
|
|
|
|
def __getattr__(self, item):
|
|
# I have this complicated implementation because of the usage of Pickle
|
|
|
|
if 'values' in vars(self) and item in self.values:
|
|
return self.get_value(item)
|
|
|
|
name = self.name if 'metadata' in vars(self) else 'Concept'
|
|
raise AttributeError(f"'{name}' concept has no attribute '{item}'")
|
|
|
|
def def_var(self, var_name, default_value=None):
|
|
"""
|
|
Adds a property to the metadata
|
|
:param var_name: name or concept
|
|
:param default_value:
|
|
:return:
|
|
"""
|
|
|
|
# this assert in not a functional requirement
|
|
# It's just to control what I put in the default value of properties
|
|
# You can allow more type if it's REALLY needed.
|
|
# - str are for standard definition
|
|
# - list of concepts is used by ISA
|
|
assert default_value is None or isinstance(default_value, str)
|
|
|
|
self.metadata.variables.append((var_name, default_value))
|
|
|
|
self.set_value(var_name, None) # do not set the default value
|
|
# why not setting variables to the default values ?
|
|
# Because it may not be the real values, as metadata.variables need to be evaluated
|
|
|
|
return self
|
|
|
|
def def_var_by_index(self, index: int, value):
|
|
"""
|
|
Re-assign a value to a property (mainly used by ExactConceptParser)
|
|
:param index:
|
|
:param value:
|
|
:return:
|
|
"""
|
|
assert value is None or isinstance(value, str) # default properties will have to be evaluated
|
|
var_name = self.metadata.variables[index]
|
|
self.metadata.variables[index] = (var_name[0], value) # change the default value
|
|
return self
|
|
|
|
@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):
|
|
"""
|
|
Create the key for this concept.
|
|
Must be called only when the concept if fully initialized
|
|
|
|
The method is not called 'set_key' to make sure that no other class set the key by mistake
|
|
:param tokens:
|
|
:return:
|
|
"""
|
|
if self.metadata.key is not None:
|
|
return self
|
|
|
|
if tokens is None:
|
|
if self.metadata.definition_type == DEFINITION_TYPE_DEF:
|
|
tokens = list(Tokenizer(self.metadata.definition))
|
|
else:
|
|
tokens = list(Tokenizer(self.metadata.name))
|
|
|
|
variables = [p[0] for p in self.metadata.variables] if len(core.utils.strip_tokens(tokens, True)) > 1 else []
|
|
|
|
key = ""
|
|
first = True
|
|
for token in tokens:
|
|
if token.type == TokenKind.EOF:
|
|
break
|
|
if token.type == TokenKind.WHITESPACE:
|
|
continue
|
|
if not first:
|
|
key += " " # spaces are normalized
|
|
if token.value in variables:
|
|
key += VARIABLE_PREFIX + str(variables.index(token.value))
|
|
else:
|
|
#value = token.value[1:-1] if token.type == TokenKind.STRING else token.value
|
|
key += token.value
|
|
first = False
|
|
|
|
self.metadata.key = key
|
|
return self
|
|
|
|
@property
|
|
def body(self):
|
|
return self.get_value(ConceptParts.BODY)
|
|
|
|
def get_origin(self):
|
|
"""
|
|
Return the digest used to save the concept if it exists
|
|
:return:
|
|
"""
|
|
if hasattr(self, ORIGIN):
|
|
return getattr(self, ORIGIN)
|
|
return None
|
|
|
|
def set_origin(self, origin):
|
|
setattr(self, ORIGIN, origin)
|
|
|
|
def get_definition_hash(self):
|
|
"""
|
|
Returns the digest of the event
|
|
:return: hexa form of the sha256
|
|
"""
|
|
return hashlib.sha256(f"Concept:{self.to_dict(PROPERTIES_FOR_DIGEST)}".encode("utf-8")).hexdigest()
|
|
|
|
def to_dict(self, props_to_use=None):
|
|
"""
|
|
Returns a dict representing 'self'
|
|
to_dict() is used for serializing the definition of the concept
|
|
You will not that it does not dump the actual values of the properties, nor the body
|
|
|
|
If you need a dictionary version of the Concept, use to_bag()
|
|
:return:
|
|
"""
|
|
|
|
props_to_use = props_to_use or PROPERTIES_TO_SERIALIZE
|
|
props_as_dict = {}
|
|
for prop in props_to_use:
|
|
if prop == "props": # no need to copy variables as the ref won't be used in from_dict
|
|
props_as_dict[prop] = deepcopy(getattr(self.metadata, prop))
|
|
else:
|
|
props_as_dict[prop] = getattr(self.metadata, prop)
|
|
return props_as_dict
|
|
|
|
def from_dict(self, as_dict):
|
|
"""
|
|
Initializes 'self' from a dict
|
|
:param as_dict:
|
|
:return:
|
|
"""
|
|
for prop in PROPERTIES_TO_SERIALIZE:
|
|
if prop in as_dict:
|
|
if prop == "variables":
|
|
for name, value in as_dict[prop]:
|
|
self.def_var(name, value)
|
|
else:
|
|
setattr(self.metadata, prop, as_dict[prop])
|
|
return self
|
|
|
|
def update_from(self, other, update_value=True):
|
|
"""
|
|
Update self using the properties of another concept
|
|
This method is to mimic the class to instance pattern
|
|
'other' is the class, the template, and 'self' is a new instance
|
|
:param other:
|
|
:param update_value:
|
|
:return:
|
|
"""
|
|
if other is None:
|
|
return self
|
|
|
|
if id(other) == id(self):
|
|
return self
|
|
|
|
# update metadata
|
|
self.from_dict(other.to_dict())
|
|
|
|
# update values
|
|
if update_value:
|
|
for k in other.values:
|
|
self.set_value(k, other.get_value(k))
|
|
|
|
# origin
|
|
from sdp.sheerkaSerializer import Serializer
|
|
if hasattr(other, Serializer.ORIGIN):
|
|
setattr(self, Serializer.ORIGIN, getattr(other, Serializer.ORIGIN))
|
|
|
|
return self
|
|
|
|
def add_prop(self, concept_key, value):
|
|
"""
|
|
Set or add a behaviour to a concept
|
|
A behaviour is a value from another concept (ex BuiltinConcepts.ISA
|
|
:param concept_key: Concept key
|
|
:param value:
|
|
:return:
|
|
"""
|
|
if concept_key in self.metadata.props:
|
|
self.metadata.props[concept_key].add(value)
|
|
else:
|
|
self.metadata.props[concept_key] = {value} # a set
|
|
return self
|
|
|
|
def get_prop(self, concept_key):
|
|
"""
|
|
Gets a behaviour of a concept
|
|
:param concept_key: name of the behaviour
|
|
:return:
|
|
"""
|
|
return self.metadata.props[concept_key] if concept_key in self.metadata.props else None
|
|
|
|
def set_value(self, name, value):
|
|
"""
|
|
Set the resolved value of a metadata or a variable (not the metadata itself)
|
|
:param name:
|
|
:param value:
|
|
:return:
|
|
"""
|
|
if name in self.values:
|
|
self.values[name].value = value
|
|
else:
|
|
self.values[name] = Property(name, value)
|
|
return self
|
|
|
|
def get_value(self, prop_name):
|
|
"""
|
|
Gets the resolved value of a metadata
|
|
:param prop_name:
|
|
:return:
|
|
"""
|
|
if prop_name not in self.values:
|
|
return None
|
|
return self.values[prop_name].value
|
|
|
|
def variables(self):
|
|
return dict([(k, v) for k, v in self.values.items() if isinstance(k, str)])
|
|
|
|
def auto_init(self):
|
|
"""
|
|
Sometimes (for tests purposes)
|
|
You don't need the full process of evaluation to to get the values of the concept
|
|
Directly use the values of the metadata
|
|
:return:
|
|
"""
|
|
|
|
if self.metadata.is_evaluated:
|
|
return self
|
|
|
|
for metadata in ConceptParts:
|
|
value = getattr(self.metadata, metadata.value)
|
|
if value is not None:
|
|
self.set_value(metadata, value)
|
|
|
|
for var, value in self.metadata.variables:
|
|
self.set_value(var, value)
|
|
|
|
self.metadata.is_evaluated = True
|
|
return self
|
|
|
|
def freeze_definition_hash(self):
|
|
self.original_definition_hash = self.get_definition_hash()
|
|
|
|
def get_original_definition_hash(self):
|
|
return self.original_definition_hash
|
|
|
|
def to_bag(self):
|
|
"""
|
|
Creates a dictionary with the useful properties of the concept
|
|
It quicker to implement than creating the actual property mechanism with @property
|
|
And it removes the visibility from the other attributes/methods
|
|
"""
|
|
bag = {}
|
|
for var in self.values:
|
|
if isinstance(var, str):
|
|
bag[var] = self.get_value(var)
|
|
bag["var." + var] = self.get_value(var)
|
|
for prop in ("id", "name", "key", "body"):
|
|
bag[prop] = getattr(self, prop)
|
|
return bag
|
|
|
|
|
|
class Property:
|
|
"""
|
|
Defines the variables of a concept
|
|
It as its specific class, because from experience,
|
|
property management is more complex than a key/value pair
|
|
"""
|
|
|
|
def __init__(self, name, value):
|
|
self.name = name
|
|
self.value = value
|
|
|
|
def __repr__(self):
|
|
return f"{self.name}={self.value}"
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, Property):
|
|
return False
|
|
|
|
return self.name == other.name and self.value == other.value
|
|
|
|
def __hash__(self):
|
|
return hash((self.name, self.value))
|
|
|
|
|
|
@dataclass()
|
|
class DoNotResolve:
|
|
"""
|
|
This class is used to that the metadata (or the prop) of the concept must not be evaluated
|
|
thru sheerka.execute
|
|
|
|
For example, if you want to set a value to the BODY that will not change when
|
|
when the concept will be evaluated,
|
|
set concept.compiled[BODY] to DoNotResolve(value)
|
|
"""
|
|
value: object
|
|
|
|
|
|
@dataclass()
|
|
class InfiniteRecursionResolved:
|
|
"""This class is used to when we managed to break an infinite recursion concept definition"""
|
|
value: object
|
|
|
|
def get_obj_value(self):
|
|
return self.value
|
|
|
|
|
|
# ################################
|
|
#
|
|
# Class created for tests purpose
|
|
#
|
|
# ################################
|
|
|
|
|
|
class CC:
|
|
"""
|
|
Concept class for test purpose
|
|
CC means concept for compiled (or concept with compiled)
|
|
It matches a concept if the compiles are equals
|
|
"""
|
|
|
|
# The only properties that are testes are concept_key and compiled
|
|
# The other properties (concept, source, start and end)
|
|
# are used in tests/parsers/parsers_utils.py to help creating helper objects
|
|
|
|
def __init__(self, concept, source=None, exclude_body=False, **kwargs):
|
|
self.concept_key = concept.key if isinstance(concept, Concept) else concept
|
|
self.compiled = kwargs
|
|
self.concept = concept if isinstance(concept, Concept) else None
|
|
self.source = source # to use when the key is different from the sub str to search when filling start and stop
|
|
self.start = None # for debug purpose, indicate where the concept starts
|
|
self.end = None # for debug purpose, indicate where the concept ends
|
|
self.exclude_body = exclude_body
|
|
|
|
def __eq__(self, other):
|
|
if id(self) == id(other):
|
|
return True
|
|
|
|
if isinstance(other, Concept):
|
|
if other.key != self.concept_key:
|
|
return False
|
|
if self.exclude_body:
|
|
to_compare = {k: v for k, v in other.compiled.items() if k != ConceptParts.BODY}
|
|
else:
|
|
to_compare = other.compiled
|
|
return self.compiled == to_compare
|
|
|
|
if not isinstance(other, CC):
|
|
return False
|
|
|
|
if self.concept_key != other.concept_key:
|
|
return False
|
|
|
|
return self.compiled == other.compiled
|
|
|
|
def __hash__(self):
|
|
if self.concept:
|
|
return hash(self.concept)
|
|
return hash(self.concept_key)
|
|
|
|
def __repr__(self):
|
|
if self.concept:
|
|
txt = f"CC(concept='{self.concept}'"
|
|
else:
|
|
txt = f"CC(concept_key='{self.concept_key}'"
|
|
|
|
for k, v in self.compiled.items():
|
|
txt += f", {k}='{v}'"
|
|
return txt + ")"
|
|
|
|
def fix_pos(self, node):
|
|
start = node.start if hasattr(node, "start") else \
|
|
node[0] if isinstance(node, tuple) else None
|
|
end = node.end if hasattr(node, "end") else \
|
|
node[1] if isinstance(node, tuple) else None
|
|
|
|
if start is not None:
|
|
if self.start is None or start < self.start:
|
|
self.start = start
|
|
|
|
if end is not None:
|
|
if self.end is None or end > self.end:
|
|
self.end = end
|
|
return self
|
|
|
|
|
|
@dataclass()
|
|
class CB:
|
|
"""
|
|
Concept with body only
|
|
Test class that test only the body of the concept
|
|
"""
|
|
concept: Union[str, Concept]
|
|
body: object
|
|
|
|
def __eq__(self, other):
|
|
if isinstance(other, Concept):
|
|
key = self.concept if isinstance(self.concept, str) else self.concept.key
|
|
return key == other.key and self.body == other.body
|
|
|
|
if not isinstance(other, CB):
|
|
return False
|
|
|
|
return self.concept == other.concept and self.body == other.body
|
|
|
|
def __hash__(self):
|
|
return hash((self.concept, self.body))
|
|
|
|
|
|
simplec = namedtuple("concept", "name body") # for simple concept (tests purposes only)
|