Refactored sheerka class: splitted to use sub handlers. Refactored unit tests to use classes.
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
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
|
||||
Reference in New Issue
Block a user