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)