import hashlib from dataclasses import dataclass from enum import Enum from core.sheerka_logger import get_logger import core.utils from core.tokenizer import Tokenizer, TokenKind 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"]) PROPERTIES_FOR_NEW = ("where", "pre", "post", "body", "desc") VARIABLE_PREFIX = "__var__" 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) is_evaluated: bool = False # True is the concept is evaluated by sheerka.eval_concept() 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): 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.metadata = metadata self.props = {} # list of Property for this concept self.cached_asts = {} # cached ast for the where, pre, post and body parts self.bnf = None self.log = get_logger("core." + self.__class__.__name__) self.init_log = get_logger("init.core." + self.__class__.__name__) def __repr__(self): 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 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 # check the props (Concept variables) for var_name, p in self.props.items(): if p != other.props[var_name]: return False return True def __hash__(self): return hash(self.metadata.name) @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: tokens = list(Tokenizer(self.metadata.name)) variables = list(self.props.keys()) 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 += " " if variables is not None and token.value in variables: 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.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' There ASTs are know when the concept is freshly parsed. So the values are kept in cache. For concepts loaded from sdp, these ASTs must be created again TODO : Seems to be a service method. Can be put somewhere else :param codes: :return: """ if codes is None: return for key in codes: self.cached_asts[key] = codes[key] return self def get_digest(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' :return: """ props_to_use = props_to_use or PROPERTIES_TO_SERIALIZE 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 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: setattr(self.metadata, prop, as_dict[prop]) if "props" in as_dict: for n, v in as_dict["props"]: self.set_prop(n, v) return self def update_from(self, other): """ 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: :return: """ if other is None: return self self.from_dict(other.to_dict()) # for prop in self.props_to_serialize: # setattr(self, prop, getattr(other, prop)) return self def set_prop(self, prop_name: str, prop_value=None): self.props[prop_name] = Property(prop_name, prop_value) # Python 3.x order is kept in dictionaries return self def set_prop_by_index(self, index: int, prop_value): prop_name = list(self.props.keys())[index] self.props[prop_name] = Property(prop_name, prop_value) return self def get_prop(self, prop_name: str): return self.props[prop_name].value 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))