Files
Sheerka-Old/src/core/concept.py
T

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)