Package: openexpressions
OpenExpressionsThe Most Simple And Expandable Python Expression LibraryOpenExpressions is a lightweight, open-source, and easily integrated expression library.FeaturesParse and Evaluate Expressions With Built-In and Self-Implemented FunctionalityInspect Abstract Syntax Tree generated by the ParserQuickly Create New Operators and Operands by Inheriting from Predefined Abstract ClassesDefault Modes Available for Math or Boolean ExpressionsEmpty Mode Available for Clean Slate Start for New Expression ParserOpenExpressions generates a context-free grammar from the given operators and operands and then
creates an LR(1) Automata and Parse Table to quickly generate and Abstract Syntax Tree which represents
the expression provided. The expression can then be evaluated from the leaf nodes up and the generated
structure of the nodes determines the order of operations.The goal of this project is to provide a better alternative
to the native eval function that python provides for evaluating
expressions. Although it can provide the functionality, the
security risks can quickly become an issue if not used carefully.
Additionally, using custom operations in eval must be done with
function calls which can quickly become verbose in longer expressions
OpenExpressions' options to create new Unary and Binary operators
allows for simpler expression syntax and more readable expressions.
It also resolves the security risks associated with eval as all
the expression is not executed as code but instead interpreted as
the available operators and operands.InstallationOpenExpressions is available on PyPi and easily installable on Python 3.3+ using pippipinstallopenexpressionsSource code is available at thisGithub RepositoryModule info is available atPyPi PageDocumentationBasic UsageThe basic usage of this library consists of using the preset of the Parser.
Currently, the 2 presets are:math(default) andbooleanfromopenexpressions.ParserimportParserif__name__=="__main__":# create the parsermath_parser=Parser()# or explicitly with math_parser = Parser(mode="math")# using the parser, store the parsed expressionexpression=math_parser.parse("1 + (2 ** a ** b - 4 / -c)")# expression is now a Tree of ExpressionNodes, each one is an Operator or Operand# evaluate the expression by passing in a context we would like for variablesvalue_0=expression.eval({'a':1,'b':3,'c':-5})# we can reuse the same expression to evaluate with a different contextvalue_1=expression.eval({'a':0.1,'b':9,'c':18.2})# you can also reuse the same parser for multiple expression stringsother_expression=math_parser.parse("i * like * math")value_2=other_expression.eval({'i':5,'like':32,'math':64})Here's an equivalent usage for thebooleanmodefromopenexpressions.ParserimportParserif__name__=="__main__":# create the parserbool_parser=Parser(mode="boolean")# using the parser, store the parsed expressionexpression=bool_parser.parse("1 & a | 0 | (b ^ c)")# expression is now a Tree of ExpressionNodes, each one is an Operator or Operand# evaluate the expression by passing in a context we would like for variablesvalue_0=expression.eval({'a':1,'b':0,'c':1})# we can reuse the same expression to evaluate with a different contextvalue_1=expression.eval({'a':0,'b':1,'c':0})# you can also reuse the same parser for multiple expression stringsother_expression=bool_parser.parse("i | like | boolean")value_2=other_expression.eval({'i':1,'like':1,'boolean':1})With the preset modes, you can use the following default Operators and OperandsBuilt-InsSource code for built-in operators and operands foundhereThis provides an overview of each of the default values, to better understand the significance
of each field, taking a look at theExpression Node DeclarationsMath Mode Built-InsOperandsInt -An Integer Literal-identifier regex:\d+(?!\.)Float -An Floating Point Literal-identifier regex:\d*\.\d+Var -A Variable Usage-identifier regex:[a-zA-Z]\w*Unary Operators (UnOp)Neg -Arithmetic Negation-identifier regex:-order value: 70000Binary Operators (BinOp)Add -Arithmetic Addition-identifier regex:\+order value: 100000Sub -Arithmetic Subtraction-identifier regex:-order value: 100000Mult -Arithmetic Multiplication-identifier regex:\*order value: 90000Div - _Arithmetic Division -identifier regex:/order value: 90000IntDiv -Integer Division-identifier regex://order value: 90000Mod -Arithmetic Modulo-identifier regex:%order value: 90000Pow -Arithmetic Exponentiation-identifier regex:\*\*order value: 80000Poly Operators (PolyOp)Sum -Summation Operator-identifier regex:SUMnumber of sub-expressions: 4first sub-expression:counter variablesecond sub-expression:lower bound countthird sub-expression:upper bound countfourth sub-expression:evaluated expressionProd -Product Notation Operator-identifier regex:PRODnumber of sub-expressions: 4first sub-expression:counter variablesecond sub-expression:lower bound countthird sub-expression:upper bound countfourth sub-expression:evaluated expressionWrap Operators (WrapOp)Paren -Parenthetical Expression-left identifier regex:\( -right identifier regex:\)Abs -Absolute Value Expression-left identifier regex:\| -right identifier regex:\|Boolean Mode Built-InsOperandsBoolVal -A Boolean Literal-identifier regex:(0 | 1)BoolVar -A Boolean Variable-identifier regex:[a-zA-Z]\w*Unary Operators (UnOp)Not -Arithmetic Negation-identifier regex:~order value: 70000Binary Operators (BinOp)BitAnd -Logical And-identifier regex:&order value: 90000BitOr -Logical Or-identifier regex:|order value: 100000BitXOr -Logical XOr-identifier regex:\^order value: 80000Wrap Operators (WrapOp)Paren -Parenthetical Expression-left identifier regex:\( -right identifier regex:\)Advanced UsageAdvanced usage of this library consists of creating custom operators and operands, here we'll go over templates and an example usage of this feature. The current options are to create newOperands,UnOps,BinOps,PolyOps, andWrapOpsby inheriting from their respective abstract classes.Custom OperandA custom operand can be made by using the following templatefromopenexpressions.ExpressionNodesimportOperandclassNEW_OPERAND(Operand):#REQUIRED - must be a python re library compatible regexidentifier=r"0"#OPTIONAL - used by some operators to determine behavior.negation_inclusive=False# used by Pow to determine if result should be negativedef__init__(self,image):# will be passed the raw string image of the operand matchsuper().__init__(image)# super will store passed value in self.val#REQUIRED - must accept a context objectdefeval(context=None):#RETURN WHAT THIS OPERAND SHOULD EVALUATE TOreturn(self.val)The parser will scan for a regex match of the given identifier and the matched string will be passed to the constructor of this class as "image". Since operands are the leaves of the expression tree, there will be no children of this ExpressionNode.Custom Unary OperatorA custom unary operator can be made by using the following templatefromopenexpressions.ExpressionNodesimportUnOpclassNEW_UNOP(UnOp):#REQUIRED - must be a python re library compatible regex#Ensure that operators match only 1 non-zero length substring to prevent issues#In other words, only use characters and not any other regex functionalityidentifier=r"UNOP"#OPTIONAL - used by some operators to determine behavior.negation_inclusive=False# used by Pow to determine if result should be negativedef__init__(self,expression)->None:super().__init__(expression)# super will store passed expression in self.exprdefeval(self,context=None):#PERFORM OPERATION ON self.expr EVALUATION AND RETURN NEW VALUEreturn(do_something(self.expr.eval(context)))The parser will scan for a regex match of the given identifier and the appropriate following expression will be passed into the constructor as "expression."Custom Binary OperatorA custom binary operator can be made by using the following templatefromopenexpressions.ExpressionNodesimportBinOpclassNEW_BINOP(BinOp):#REQUIRED - must be a python re library compatible regex#Ensure that operators match only 1 non-zero length substring to prevent issues#In other words, only use characters and not any other regex functionalityidentifier=r"BINOP"#OPTIONAL - used by some operators to determine behavior.negation_inclusive=False# used by Pow to determine if result should be negativedef__init__(self,left_expression,right_expression)->None:# store expressions in self.left and self.rightsuper().__init__(left_expression,right_expression)defeval(self,context=None):#PERFORM OPERATION ON self.left and self.right, EVALUATE AND RETURN NEW VALUEreturn(do_something(self.left.eval(context),self.right.eval(context)))The parser will scan for a regex match of the given identifier and the appropriate preceding and following expressions will be passed into the constructor as "left_expression" and "right_expression" respectively.Custom Poly OperatorA custom poly operator can be made by using the following templatefromopenexpressions.ExpressionNodesimportPolyOpclassNEW_POLYOP(PolyOp):#REQUIRED - must be a python re library compatible regex#Ensure that operators match only 1 non-zero length substring to prevent issues#In other words, only use characters and not any other regex functionalityidentifier=r"POLYOP"#REQUIRED - must be an integer which represents the number of required fieldsnum_fields=3#OPTIONAL - used by some operators to determine behavior.negation_inclusive=False# used by Pow to determine if result should be negativedef__init__(self,op_1,op_2,op_3)->None:# super will store passed parameters in self.ops with the leftmost being index 0# and each subsequence paramter being at the index after the previous onesuper().__init__(op_1,op_2,op_3)# op_1 stored in self.op[0]# op_2 stored in self.op[1]# op_3 stored in self.op[2]defeval(self,context=None):#PERFORM OPERATION ON all OPERANDS EVALUATION AND RETURN NEW VALUEreturn(do_something(self.op[0].eval(context),self.op[1].eval(context),self.op[2].eval(context)))The parser will scan for a regex match of the given identifier and followed by a parenthetical expression with "num_field" sub-expressions which are delimited by commas.Custom Wrap OperatorA custom wrap operator can be made by using the following templatefromopenexpressions.ExpressionNodesimportWrapOpclassNEW_WRAPOP(WrapOp):#REQUIRED - must be a python re library compatible regex#Ensure that operators match only 1 non-zero length substring to prevent issues#In other words, only use characters and not any other regex functionalityleft_identifier=r"LEFT_WRAPOP_BOUND"right_identifier=r"RIGHT_WRAPOP_BOUND"#OPTIONAL - used by some operators to determine behavior.negation_inclusive=False# used by Pow to determine if result should be negativedef__init__(self,expression)->None:super().__init__(expression)# super will store passed expression in self.exprdefeval(self,context=None):#PERFORM OPERATION ON self.expr EVALUATION AND RETURN NEW VALUEreturn(do_something(self.expr.eval(context)))The parser will scan for a regex match of the given left_identifier followed by the right_identifier and the expression between them will be passed into the constructor as "expression."Create a Parser With Custom Operands and OperatorsAfter creating your operands, you need to create a parser that uses them by passing them
in the constructor of the parser. This can be done as shown in the following example.Note: The parser creates an expression tree that evaluates the given expression
from lowest to highestorder valueoperators.fromopenexpressions.ParserimportParserfromopenexpressions.ExpressionNodesimportUnOp,BinOp,PolyOp,WrapOp# CREATE DECLARATIONS OF CUSTOM OPERANDS AND OPERATORS HEREif__name__=="__main__":# when a mode is selected, custom operators and operands are appended# on top of the provided built-in ones for that mode. If you want a empty# start, use mode = "empty".# UnOps must be passed in as a tuple of 2 elements# index 0: Class# index 1: order value# BinOps must be passed in as a tuple of 3 elements# index 0: Class# index 1: order value# index 2: reversed boolean (order of evaluation for equal priority operations)# when reversed = False -> left to right# when reverse = True -> right to left# PolyOps and WrapOps can simply be passed in as a Class# Operands can simply be passed in as a Classparser=Parser(mode="math"custom_operators=[(UnOp1,100),(UnOp2,99)...,(BinOp1,50,True),(BinOp2,49,False)...PolyOp1,PolyOp2...,WrapOp1,WrapOp2...]custom_operands=[Operand1,Operand2...])Warnings About Custom Operators and OperandsSince this library relies on LR(1) parsing, there are restrictions on the types of context-free
grammars that can be parsed using an LR(1) parser. To ensure that the parser works as expected,
make sure to follow the following rules.Priority ValuesMustbe Non-NegativeEach Priority ValueMustcontainexclusivelyUnOpsORBinOpsEach Priority ValueMustcontainexclusivelyReversedORNon-Reversed BinOpsEach Priority ValueMustcontainexclusivelyOperators with unique identifiersFull-Scale ExampleTo get a better idea of the capabilities of the customization for this library, lets take a look
at an example setup for 3 dimensional vector expressions.fromopenexpressions.ParserimportParserfromopenexpressions.ExpressionNodesimportUnOp,BinOp,PolyOp,WrapOp,Operand,Var,ParenimportmathimportreclassVector(Operand):identifier=r"<-?(\d+.\d+|\d+), -?(\d+.\d+|\d+), -?(\d+.\d+|\d+)>"def__init__(self,image)->None:super().__init__(tuple(float(i)foriinre.findall(r"-?(\d+.\d+|\d+)",image)))defeval(self,context=None):return(self.val)classNegate(UnOp):identifier=r"-"def__init__(self,expression)->None:super().__init__(expression)defeval(self,context=None):return(tuple(-iforiinself.expr.eval(context)))classCrossProduct(BinOp):identifier=r"X"def__init__(self,left_expression,right_expression)->None:super().__init__(left_expression,right_expression)defeval(self,context=None):lv=self.left.eval(context)rv=self.right.eval(context)return((lv[1]*rv[2]-lv[2]*rv[1],lv[2]*rv[0]-lv[0]*rv[2],lv[0]*rv[1]-lv[1]*rv[0]))classDotProduct(BinOp):identifier=r"\."def__init__(self,left_expression,right_expression)->None:super().__init__(left_expression,right_expression)defeval(self,context=None):lv=self.left.eval(context)rv=self.right.eval(context)s=0foriinrange(3):s+=lv[i]*rv[i]return(s)classMagnitude(WrapOp):left_identifier=r"\|"right_identifier=r"\|"def__init__(self,expression)->None:super().__init__(expression)defeval(self,context=None):v=self.expr.eval(context)s=0foriinrange(3):s+=abs(v[i])**2return(math.sqrt(s))classSelect(PolyOp):identifier=r"SELECT"num_fields=3def__init__(self,one,two,three)->None:super().__init__(one,two,three)defeval(self,context=None):one=self.ops[0].eval(context)two=self.ops[1].eval(context)three=self.ops[2].eval(context)return((one[0],two[1],three[2]))if__name__=="__main__":vector_parser=Parser(mode="empty",custom_operands=(Vector,Var),custom_operators=((Negate,10),(CrossProduct,20,False),(DotProduct,20,False),Magnitude,Select,Paren))print(vector_parser.parse("-(SELECT(<0, -991, -992>, <-990, 1, -992>, <-990, -991, 2>) X <3, 4, 5>) . a").eval({'a':(-6,7,-8)}))Using this example, we can see the power of this library. It's capabilities extend beyond just
numbers and it's generic nature can quickly be used to create entire new expression systems to
help simplify representing them in your code.Note: Notice that Var and Paren we imported fromopenexpressions.ExpressionNodesand
then passed as an extra operand and operator respectively. Since we were using the "empty"
mode, we can reuse these operators as they fulfilled the purpose we wanted.LicenseThis library operates under theMIT LicenseContactIf discover some issue within this library or would like to collaborate on it's development
or some other project, please reach out to me ataryamanbhute@gmail.com. I am always
happy to chat!