reyman64
reyman64

Reputation: 553

Error when passing class to Pyparsing setParseAction() class for "if/else" structure

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

Answers (1)

PaulMcG
PaulMcG

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

Related Questions