Skip to content

Commit

Permalink
Add Docstring to EvaluateExpression() function
Browse files Browse the repository at this point in the history
Add recursive parsing function to help evaluate ast trees
Fix unexpected behaviour with EvaluateExpression() resulting in successfully parsing constant strings
Improve test completeness for EvaluateExpression and StringFormat methods
  • Loading branch information
chrisinabox committed Nov 28, 2023
1 parent 2eaa9c8 commit 90c30d8
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 21 deletions.
57 changes: 39 additions & 18 deletions src/objdictgen/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
Fore = colorama.Fore
Style = colorama.Style

#Used to match strings such as 'Additional Server SDO %d Parameter[(idx)]'
#The above example matches to two groups ['Additional Server SDO %d Parameter', 'idx']
RE_NAME = re.compile(r'(.*)\[[(](.*)[)]\]')


Expand Down Expand Up @@ -84,35 +86,54 @@ def StringFormat(text, idx, sub): # pylint: disable=unused-argument
args = [a.replace('idx', str(idx)) for a in args]
args = [a.replace('sub', str(sub)) for a in args]

# NOTE: Python2 type evaluations are baked into the maps.py
# and json format OD so cannot be removed currently
if len(args) == 1:
return fmt[0] % (EvaluateExpression(args[0]))
return fmt[0] % (EvaluateExpression(args[0].strip()))
elif len(args) == 2:
return fmt[0] % (EvaluateExpression(args[0]), EvaluateExpression(args[1]))
return fmt[0] % (EvaluateExpression(args[0].strip()), EvaluateExpression(args[1].strip()))

return fmt[0]
except Exception as exc:
log.debug("LITERAL_EVAL FAILED: %s" % (exc, ))
log.debug("PARSING FAILED: %s" % (exc, ))
raise
else:
return text

def EvaluateExpression(expression):
# Try evaluating the literal, if it fails then we do the other operations
try:
return ast.literal_eval(expression)
except:
pass
def EvaluateExpression(expression: str):
"""Parses a string expression and attempts to calculate the result
Supports:
- Addition (i.e. "3+4")
- Subraction (i.e. "7-4")
- Constants (i.e. "5")
This function will handle chained arithmatic i.e. "1+2+3" although operating order is not neccesarily preserved
Parameters:
expression (str): string to parse
"""
tree = ast.parse(expression, mode="eval")

if isinstance(tree.body, ast.BinOp) and isinstance(tree.body.left, ast.Constant) and isinstance(tree.body.right, ast.Constant):
if isinstance(tree.body.op, ast.Add):
return tree.body.left.value + tree.body.right.value
elif isinstance(tree.body.op, ast.Sub):
return tree.body.left.value - tree.body.right.value

log.debug("EvaluateExpression: Unable to evaluate %s" % (expression))
raise SyntaxError("Unable to evaluate %s" % (expression))
return EvaluateNode(tree.body)

def EvaluateNode(node):
'''
Recursively parses ast.Node objects to evaluate arithmatic expressions
'''
if isinstance(node, ast.BinOp):
if isinstance(node.op, ast.Add):
return EvaluateNode(node.left) + EvaluateNode(node.right)
elif isinstance(node.op, ast.Sub):
return EvaluateNode(node.left) - EvaluateNode(node.right)
else:
raise SyntaxError("Unhandled arithmatic operation %s" % type(node.op))
elif isinstance(node, ast.Constant):
if isinstance(node.value, int | float | complex):
return node.value
else:
raise TypeError("Cannot parse str type constant '%s'" % node.value)
else:
raise TypeError("Unhandled ast node class %s" % type(node))



def GetIndexRange(index):
for irange in maps.INDEX_RANGES:
Expand Down
35 changes: 32 additions & 3 deletions tests/test_node.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,44 @@
import pytest

from objdictgen.node import EvaluateExpression
from objdictgen.node import StringFormat, EvaluateExpression

def test_string_format():
assert StringFormat('Additional Server SDO %d Parameter[(idx)]', 5, 0) == 'Additional Server SDO 5 Parameter'
assert StringFormat('Restore Manufacturer Defined Default Parameters %d[(sub - 3)]', 1, 5) == 'Restore Manufacturer Defined Default Parameters 2'
assert StringFormat('This doesn\'t match the regex', 1, 2) == 'This doesn\'t match the regex'

assert StringFormat('%s %.3f[(idx,sub)]', 1, 2) == '1 2.000'
assert StringFormat('%s %.3f[( idx , sub )]', 1, 2) == '1 2.000'

with pytest.raises(TypeError):
StringFormat('What are these %s[("tests")]', 0, 1)

with pytest.raises(TypeError):
StringFormat('There is nothing to format[(idx, sub)]', 1, 2)

with pytest.raises(Exception):
StringFormat('Unhandled arithmatic[(idx*sub)]', 2, 4)


def test_evaluate_expression():

assert EvaluateExpression('4+3') == 7
assert EvaluateExpression('4-3') == 1
assert EvaluateExpression('11') == 11
assert EvaluateExpression('4+3+2') == 9
assert EvaluateExpression('4+3-2') == 5

with pytest.raises(TypeError):
EvaluateExpression('3-"tests"')

with pytest.raises(SyntaxError):
EvaluateExpression('4+3+2')
EvaluateExpression('4+3')
EvaluateExpression('4*3')

with pytest.raises(TypeError):
EvaluateExpression('str')

with pytest.raises(TypeError):
EvaluateExpression('"str"')

with pytest.raises(SyntaxError):
EvaluateExpression('$NODEID+12')

0 comments on commit 90c30d8

Please sign in to comment.