Skip to content

Commit

Permalink
⚗️ add a simple expression parser using lark. #13
Browse files Browse the repository at this point in the history
  • Loading branch information
perillaroc committed Apr 8, 2022
1 parent d43733d commit 04fe114
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 1 deletion.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
packages=find_packages(exclude=['contrib', 'docs', 'tests']),

install_requires=[
# 'click',
'lark',
# 'thrift',
# 'pyparsing'
],
Expand Down
21 changes: 21 additions & 0 deletions takler/core/expression.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Optional, Union

from .expression_parser import parse_trigger
from .expression_ast import AstRoot


class Expression(object):
def __init__(self, expression_str):
self.free = False # type: bool
self.ast = None # type: Optional[AstRoot]
self.expression_str = expression_str

def create_ast(self, parent_node):
self.parse_expression()
self.ast.set_parent_node(parent_node)

def evaluate(self) -> bool:
return self.ast.evaluate()

def parse_expression(self):
self.ast = parse_trigger(self.expression_str)
70 changes: 70 additions & 0 deletions takler/core/expression_ast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from dataclasses import dataclass
from typing import Optional

from .node import Node
from .state import NodeStatus


@dataclass
class AstBase:
def set_parent_node(self, node: Node):
pass

def value(self):
raise NotImplemented()

def evaluate(self) -> bool:
raise NotImplemented()


@dataclass
class AstRoot(AstBase):
left: Optional[AstBase] = None
right: Optional[AstBase] = None

def set_parent_node(self, node: Node):
self.left.set_parent_node(node)
self.right.set_parent_node(node)


@dataclass
class AstOpEq(AstRoot):
def evaluate(self):
return self.left.value() == self.right.value()


@dataclass
class AstNodePath(AstBase):
node_path: str
parent_node: Optional[Node] = None
_reference_node: Optional[Node] = None

def set_parent_node(self, node: Node):
self.parent_node = node

def value(self):
ref_node = self.get_reference_node()
if ref_node is not None:
return ref_node.state.node_status
else:
return NodeStatus.unknown

def get_reference_node(self) -> Optional[Node]:
"""
Find node only once.
"""
if self._reference_node is not None:
return self._reference_node

if self.parent_node is not None:
self._reference_node = self.parent_node.find_node(self.node_path)
return self._reference_node
return None


@dataclass
class AstNodeStatus(AstBase):
node_status: NodeStatus

def value(self):
return self.node_status
53 changes: 53 additions & 0 deletions takler/core/expression_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from lark import Lark, Transformer

from .expression_ast import AstNodePath, AstOpEq, AstNodeStatus, AstRoot
from .state import NodeStatus


class ExpressionTransformer(Transformer):
def node_path(self, items):
return AstNodePath("".join(items))

def node_name(self, s):
(s, ) = s
return s

def st_complete(self, _):
return AstNodeStatus(NodeStatus.complete)

def op_eq(self, _):
return AstOpEq()

def expression(self, s):
s[1].left = s[0]
s[1].right = s[2]
return s[1]


trigger_parser = Lark(r"""
!node_path: "/"node_name("/"node_name)*
op_eq: "==" | "eq"
op_gt: ">"
op_ge: ">="
?operator: op_eq | op_gt | op_ge
st_complete: "complete"
st_aborted: "aborted"
?status: st_complete | st_aborted
node_name: CNAME
expression: node_path operator status
%import common.CNAME
%import common.WORD
%import common.WS
%ignore WS
""", start="expression")


def parse_trigger(trigger_text: str) -> AstRoot:
tree = trigger_parser.parse(trigger_text)
expression_ast = ExpressionTransformer().transform(tree)
return expression_ast

0 comments on commit 04fe114

Please sign in to comment.