import hashlib from collections import namedtuple from dataclasses import dataclass, field 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", "props") 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) props: list # list properties, with their default values is_evaluated: bool = False # True is the concept is evaluated by sheerka.eval_concept() simplec = namedtuple("concept", "name body") # for simple concept (tests purposes only) 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): 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 [] ) self.metadata = metadata self.compiled = {} # cached ast for the where, pre, post and body parts self.values = {} # values of metadata once resolved self.props = {} # resolved properties of this concept 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 isinstance(other, simplec): return self.name == other.name and self.body == other.body if id(self) == id(other): return True 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 metadata in self.values: if self.get_metadata_value(metadata) != other.get_metadata_value(metadata): return False if len(self.props) != len(other.props): return False for prop in self.props: if self.get_prop(prop) != other.get_prop(prop): 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 'props' in vars(self) and item in self.props: return self.props[item].value name = self.name if 'metadata' in vars(self) else 'Concept' raise AttributeError(f"'{name}' concept has no attribute '{item}'") def def_prop(self, prop_name: str, default_value=None): """ Adds a property to the metadata :param prop_name: :param default_value: :return: """ assert default_value is None or isinstance(default_value, str) # default properties will have to be evaluated self.metadata.props.append((prop_name, default_value)) self.props[prop_name] = Property(prop_name, None) # do not set the default value # why not setting props to the default values ? # Because it may not be the real values, as metadata.props need to be evaluated return self def def_prop_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 prop = self.metadata.props[index] self.metadata.props[index] = (prop[0], 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: tokens = list(Tokenizer(self.metadata.name)) variables = [p[0] for p in self.metadata.props] 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: 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.values[ConceptParts.BODY] if ConceptParts.BODY in self.values else None 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.compiled[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) 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 == "props": for name, value in as_dict[prop]: self.def_prop(name, value) else: setattr(self.metadata, prop, as_dict[prop]) 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 if id(other) == id(self): return self # update metadata self.from_dict(other.to_dict()) # update values for k, v in other.values.items(): self.values[k] = v # update properties for k, v in other.props.items(): self.set_prop(k, v.value) return self def set_prop(self, prop_name: str, prop_value): """Directly sets a value to a property""" self.props[prop_name] = Property(prop_name, prop_value) return self def get_prop(self, prop_name: str): return self.props[prop_name].value def set_metadata_value(self, metadata: ConceptParts, value): """ Set the resolved value of a metadata (not the metadata itself) :param metadata: :param value: :return: """ self.values[metadata] = value def get_metadata_value(self, metadata: ConceptParts): """ Gets the resolved value of a metadata :param metadata: :return: """ return self.values[metadata] 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.values[metadata] = value for prop, value in self.metadata.props: self.set_prop(prop, value) self.metadata.is_evaluated = True return self 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