Reputation: 553
I'm trying to use pyparsing to create a very simple langage for a maze bot solver in python.
Because pyparsing seems powerfull but not easy to manipulate, i'm starting by a simple exemple with assignment and if [expression] then [code] elsif [expression] then [code] else [code]
structure
simple_example_assignement = '''
SET x 3 + 2
SET y 2
'''
simple_example_condition = '''
IF x NOT MATCH 3 THEN { SET b 2 }
ELSIF y NOT MATCH 2 THEN {SET d 4}
ELSE { SET c 3}
'''
The code for evaluation of arithmetic expression during assignment
# store variable value for evaluation
vars = {}
class EvalAddOp():
"""Class to evaluate addition and subtraction expressions."""
def __init__(self, tokens):
self.value = tokens[0]
print(self.value)
def eval(self, vars_):
if type(self.value[0]) in [EvalAddOp]:
print("ENTER EVAL ADD OPP")
sum = self.value[0].eval(vars_)
else:
sum = self.checkNum(self.value[0], vars_)
return self.ops(sum, vars_)
def checkNum(self, val, _vars):
print(type(val), " = ", val)
if type(val) in [int, float]:
return val
elif type(val) in [EvalAddOp]:
return val.eval(_vars)
else:
return _vars[val]
def ops(self, sum, vars_):
for op, val in operatorOperands(self.value[1:]):
if op == '+':
sum += self.checkNum(val, vars_)
if op == '-':
sum -= self.checkNum(val, vars_)
return sum
def eval_expression(expr):
if isinstance(expr, str):
if expr[0] in '"\'': # string literal
return expr[1:-1] # remove quotes
else:
return vars.get(expr)
elif isinstance(expr, EvalAddOp):
return expr.eval(vars)
return expr
integer = Word(nums).setParseAction(lambda t: int(t[0]))
variable = Word(alphas, exact=1)
operand = integer | variable
plusop = oneOf('+ -')
signop = oneOf('+ -')
multop = oneOf('* /')
matching = Keyword('MATCH')
arithmeticExpression = infixNotation(operand,
[(signop, 1, opAssoc.RIGHT),
(multop, 2, opAssoc.LEFT),
(plusop, 2, opAssoc.LEFT, EvalAddOp), ]
)
The code to determine parsing of assignement and condition statement :
expression = Forward()
exprOperators = Forward()
code_block = Forward()
literal = quotedString ^ pyparsing_common.number
commonExpression = literal ^ variable ^ arithmeticExpression
matchingExpression = Group(commonExpression + exprOperators + commonExpression)
expression << matchingExpression ^ commonExpression
exprOperators << infixNotation(matching,[("NOT", 1, opAssoc.RIGHT)])
# Assignment rules
set_assignment = Group(Keyword('SET') + variable + commonExpression)
# If/Else rules
simple_if_stmt = Keyword('IF') + expression + Keyword('THEN') + code_block
else_if_stmt = Keyword('ELSIF') + expression + Keyword('THEN') + code_block
else_stmt = Keyword('ELSE') + code_block
simple_if_group = Group(simple_if_stmt + Optional(OneOrMore(else_if_stmt)) + Optional(else_stmt)).setParseAction(IfEval)
# all possible statements in the example prorgam
stmt = set_assignment ^ simple_if_group
# Code to evaluate
code_block << Group(Literal('{').suppress() + OneOrMore(stmt) + Literal('}').suppress()).setName('code block')
program = Dict(OneOrMore(stmt))
I try to attach an Action using setParseAction
on simple_if_group
variable, calling the class IfEval
. Majority of example attach a function as Action, but in the case of a If/Else structure, i'm supposing that a more structured class is better to evaluate condition later... I'm not sure this is the good way, so i take any advice
class IFEval():
def __init__(self):
self.ifStructure = {}
def __len__(self):
return len(self.ifStructure)
def __getitem__(self, item):
return self.ifStructure["item"]
def __setitem__(self, key, value):
self.ifStructure[key] = value
def __delitem__(self, key):
pass
def __copy__(self):
return self.ifStructure[:]
@traceParseAction
def IfEval(s, l, tokens):
if_stmt = IFEval()
if Keyword("IF").parseString(tokens[0][1]):
if_stmt["then_codeblock"] = tokens[0][3]
if Keyword("ELSIF").parseString(tokens[0][4]):
if_stmt["elsif_codeblock"] = tokens[0][6]
if Keyword("ELSE").parseString(tokens[0][8]):
if_stmt["else_codeblock"] = tokens[0][9]
return if_stmt
Assignments using SET
works without problem :
parsed = program.parseString(simple_example_assignement)
for _, name, value in parsed:
vars[name] = eval_expression(value)
print(vars)
[3, '+', 2]
<class 'int'> = 3
<class 'int'> = 2
{'y': 2, 'x': 5}
Now, even before evaluation, i'm trying to parse the second exemple which call the setParseAction
method to IFEval
class :
parsed = program.parseString()
return an str() error ? probably because i don't understand how the parseAction function when you try to use a class and not a method :
>>entering IfEval(line: 'IF x NOT MATCH 3 THEN { SET b 2 } ', 21, ([(['IF', (['x', (['NOT', 'MATCH'], {}), 3], {}), 'THEN', ([(['SET', 'b', 2], {})], {}), 'ELSIF', (['y', (['NOT', 'MATCH'], {}), 2], {}), 'THEN', ([(['SET', 'd', 4], {})], {}), 'ELSE', ([(['SET', 'c', 3], {})], {})], {})], {}))
<<leaving IfEval (exception: 'str' object is not callable)
Traceback (most recent call last):
File "/home/reyman/Projets/cours/exercice/labyrinthe_matplot_python/parsingLanguage.py", line 246, in <module>
parsed = program.parseString(conditional_test)
File "/home/reyman/.pyenv/versions/labyrinthes/lib/python3.5/site-packages/pyparsing.py", line 1666, in parseString
loc, tokens = self._parse( instring, 0 )
File "/home/reyman/.pyenv/versions/labyrinthes/lib/python3.5/site-packages/pyparsing.py", line 1412, in _parseNoCache
loc,tokens = self.parseImpl( instring, preloc, doActions )
File "/home/reyman/.pyenv/versions/labyrinthes/lib/python3.5/site-packages/pyparsing.py", line 3805, in parseImpl
return self.expr._parse( instring, loc, doActions, callPreParse=False )
File "/home/reyman/.pyenv/versions/labyrinthes/lib/python3.5/site-packages/pyparsing.py", line 1412, in _parseNoCache
loc,tokens = self.parseImpl( instring, preloc, doActions )
File "/home/reyman/.pyenv/versions/labyrinthes/lib/python3.5/site-packages/pyparsing.py", line 4033, in parseImpl
loc, tmptokens = self_expr_parse( instring, preloc, doActions )
File "/home/reyman/.pyenv/versions/labyrinthes/lib/python3.5/site-packages/pyparsing.py", line 1412, in _parseNoCache
loc,tokens = self.parseImpl( instring, preloc, doActions )
File "/home/reyman/.pyenv/versions/labyrinthes/lib/python3.5/site-packages/pyparsing.py", line 3555, in parseImpl
return e._parse( instring, loc, doActions )
File "/home/reyman/.pyenv/versions/labyrinthes/lib/python3.5/site-packages/pyparsing.py", line 1445, in _parseNoCache
tokens = fn( instring, tokensStart, retTokens )
File "/home/reyman/.pyenv/versions/labyrinthes/lib/python3.5/site-packages/pyparsing.py", line 1082, in wrapper
ret = func(*args[limit[0]:])
File "/home/reyman/.pyenv/versions/labyrinthes/lib/python3.5/site-packages/pyparsing.py", line 4588, in z
ret = f(*paArgs)
File "/home/reyman/.pyenv/versions/labyrinthes/lib/python3.5/site-packages/pyparsing.py", line 1082, in wrapper
ret = func(*args[limit[0]:])
File "/home/reyman/Projets/cours/exercice/labyrinthe_matplot_python/parsingLanguage.py", line 99, in IfEval
if Keyword("IF").parseString(tokens[0][1]):
File "/home/reyman/.pyenv/versions/labyrinthes/lib/python3.5/site-packages/pyparsing.py", line 1664, in parseString
instring = instring.expandtabs()
TypeError: 'str' object is not callable
Upvotes: 1
Views: 572
Reputation: 63747
(This is a huge! question - you'll get better response on SO if you can strip your problem down to something small.)
Looking at the last 2 lines of your traceback:
File "/home/reyman/Projets/cours/exercice/labyrinthe_matplot_python/parsingLanguage.py", line 99, in IfEval
if Keyword("IF").parseString(tokens[0][1]):
File "/home/reyman/.pyenv/versions/labyrinthes/lib/python3.5/site-packages/pyparsing.py", line 1664, in parseString
instring = instring.expandtabs()
TypeError: 'str' object is not callable
You have passed a ParseResults
to parseString
, not a string. Then when pyparsing tries to call str functions on the input parameter, the ParseResults interprets these as attempts to read attributes, which it default to returning ''. Dissecting this a bit more:
instring.expandtabs()
^^^^^^^^
a ParseResults, not a str
^^^^^^^^^^
an undefined attribute in the ParseResults, so returns ''
^^
trying to "call" the str - but str's aren't callable, so exception!
traceParseAction
is okay for parse actions that take simple tokens, but this one is some complex, I'd recommend that you print(tokens.dump())
as the first line in your parse action, to better visualize just what kind of structure you are getting.
Your method for detecting IF, ELSIF, and ELSE is also error-prone, and you are better off using results names in your grammar. Instead of:
simple_if_stmt = Keyword('IF') + expression + Keyword('THEN') + code_block
else_if_stmt = Keyword('ELSIF') + expression + Keyword('THEN') + code_block
else_stmt = Keyword('ELSE') + code_block
simple_if_group = Group(simple_if_stmt + Optional(OneOrMore(else_if_stmt)) + Optional(else_stmt)).setParseAction(IfEval)
do:
from pyparsing import *
# fake expressions, not intended to replace those in the original problem
ident = pyparsing_common.identifier
integer = pyparsing_common.integer
expression = ident + "MATCH" + (ident | integer)
code_block = originalTextFor(nestedExpr('{', '}'))
simple_if_stmt = Group(Keyword('IF') + expression('condition')
+ Keyword('THEN') + code_block('code'))
else_if_stmt = Group(Keyword('ELSIF') + expression('condition')
+ Keyword('THEN') + code_block('code'))
else_stmt = Group(Keyword('ELSE') + code_block('code'))
simple_if_group = Group(simple_if_stmt('if_')
+ Optional(OneOrMore(else_if_stmt('elsif*')))
+ Optional(else_stmt('else_')))
Now here is a parse action that makes use of those results names:
def IfStatement(s, l, tokens):
# peel of outer grouping layer
tokens = tokens[0]
# dump out inner structure of parsed results
print(tokens.dump())
print('IF:', tokens.if_.condition, '->', tokens.if_.code)
if 'elsif' in tokens:
for elsif in tokens.elsif:
print('ELSIF:', elsif.condition, '->', elsif.code)
if 'else_' in tokens:
print('ELSE:', '->', tokens.else_.code)
print()
simple_if_group.addParseAction(IfStatement)
For this sample (note starting small, then getting more complicated):
sample = """\
IF X MATCH Y THEN { this is some code }
IF X MATCH Y THEN { this is some code }
ELSE { do this instead }
IF X MATCH Y THEN { this is some Y code }
ELSIF X MATCH Z THEN { this is some Z code }
ELSIF X MATCH A THEN { this is some A code }
ELSE { do this instead }
"""
result = OneOrMore(simple_if_group).parseString(sample)
We get:
[['IF', 'X', 'MATCH', 'Y', 'THEN', '{ this is some code }']]
- if_: ['IF', 'X', 'MATCH', 'Y', 'THEN', '{ this is some code }']
- code: '{ this is some code }'
- condition: ['X', 'MATCH', 'Y']
IF: ['X', 'MATCH', 'Y'] -> { this is some code }
[['IF', 'X', 'MATCH', 'Y', 'THEN', '{ this is some code }'], ['ELSE', '{ do this instead }']]
- else_: ['ELSE', '{ do this instead }']
- code: '{ do this instead }'
- if_: ['IF', 'X', 'MATCH', 'Y', 'THEN', '{ this is some code }']
- code: '{ this is some code }'
- condition: ['X', 'MATCH', 'Y']
IF: ['X', 'MATCH', 'Y'] -> { this is some code }
ELSE: -> { do this instead }
[['IF', 'X', 'MATCH', 'Y', 'THEN', '{ this is some Y code }'], ['ELSIF', 'X', 'MATCH', 'Z', 'THEN', '{ this is some Z code }'], ['ELSIF', 'X', 'MATCH', 'A', 'THEN', '{ this is some A code }'], ['ELSE', '{ do this instead }']]
- else_: ['ELSE', '{ do this instead }']
- code: '{ do this instead }'
- elsif: [['ELSIF', 'X', 'MATCH', 'Z', 'THEN', '{ this is some Z code }'], ['ELSIF', 'X', 'MATCH', 'A', 'THEN', '{ this is some A code }']]
[0]:
['ELSIF', 'X', 'MATCH', 'Z', 'THEN', '{ this is some Z code }']
- code: '{ this is some Z code }'
- condition: ['X', 'MATCH', 'Z']
[1]:
['ELSIF', 'X', 'MATCH', 'A', 'THEN', '{ this is some A code }']
- code: '{ this is some A code }'
- condition: ['X', 'MATCH', 'A']
- if_: ['IF', 'X', 'MATCH', 'Y', 'THEN', '{ this is some Y code }']
- code: '{ this is some Y code }'
- condition: ['X', 'MATCH', 'Y']
IF: ['X', 'MATCH', 'Y'] -> { this is some Y code }
ELSIF: ['X', 'MATCH', 'Z'] -> { this is some Z code }
ELSIF: ['X', 'MATCH', 'A'] -> { this is some A code }
ELSE: -> { do this instead }
Upvotes: 2