view expr.py @ 2:94a293b914af

add documentation, clean up interface slightly, tweak tests
author Ted Mielczarek <ted.mielczarek@gmail.com>
date Thu, 02 Jun 2011 07:44:25 -0400
parents c45135ec8c13
children 5ac8eed85684
line wrap: on
line source

#!/usr/bin/env python

# Implements a top-down parser/evaluator for simple boolean expressions.
# ideas taken from http://effbot.org/zone/simple-top-down-parsing.htm
#
# Rough grammar:
# expr := literal
#       | '(' expr ')'
#       | expr '&&' expr
#       | expr '||' expr
#       | expr '==' expr
#       | expr '!=' expr
# literal := BOOL
#          | INT
#          | STRING
#          | IDENT
# BOOL   := true|false
# INT    := [0-9]+
# STRING := "[^"]*"
# IDENT  := [A-Za-z_]\w*

# Identifiers take their values from a mapping dictionary passed as the second
# argument.

__all__ = ['parse', 'ParseError']
import re, unittest

# token classes
class ident_token:
    def __init__(self, value):
        self.value = value
    def nud(self, parser):
        # identifiers take their value from the value mappings passed
        # to the parser
        return parser.value(self.value)

class int_token:
    def __init__(self, value):
        self.value = int(value)
    def nud(self, parser):
        return self.value

class bool_token:
    def __init__(self, value):
        self.value = {'true':True, 'false':False}[value]
    def nud(self, parser):
        return self.value

class eq_op_token:
    "=="
    lbp = 20
    def led(self, parser, left):
        return left == parser.expression(self.lbp)
    
class neq_op_token:
    "!="
    lbp = 20
    def led(self, parser, left):
        return left != parser.expression(self.lbp)

class and_op_token:
    "&&"
    lbp = 11
    def led(self, parser, left):
        right = parser.expression(self.lbp)
        return left and right
    
class or_op_token:
    "||"
    lbp = 10
    def led(self, parser, left):
        right = parser.expression(self.lbp)
        return left or right

class lparen_token:
    "("
    lbp = 50
    def nud(self, parser):
        expr = parser.expression()
        parser.advance(rparen_token)
        return expr

class rparen_token:
    ")"
    lbp = 0

class string_token:
    def __init__(self, value):
        self.value = value
    def nud(self, parser):
        return self.value

class end_token:
    # lowest left binding power, always ends parsing
    lbp = 0

class ParseError(Exception):
    pass

class ExpressionParser(object):
    def __init__(self, text, valuemapping):
        """
        Initialize the parser with input |text|, and |valuemapping| as
        a dict mapping identifier names to values.
        """
        self.text = text
        self.valuemapping = valuemapping

    def _tokenize(self):
        """
        Lex the input text into tokens and yield them in sequence.
        """
        # scanner callbacks
        def bool_(scanner, t): return bool_token(t)
        def identifier(scanner, t): return ident_token(t)
        def integer(scanner, t): return int_token(t)
        def eq(scanner, t): return eq_op_token()
        def neq(scanner, t): return neq_op_token()
        def or_(scanner, t): return or_op_token()
        def and_(scanner, t): return and_op_token()
        def lparen(scanner, t): return lparen_token()
        def rparen(scanner, t): return rparen_token()
        def string_(scanner, t): return string_token(t[1:-1])

        scanner = re.Scanner([
            (r"true|false", bool_),
            (r"[a-zA-Z_]\w*", identifier),
            (r"[0-9]+", integer),
            (r'"[^"]*"', string_),
            (r"==", eq),
            (r"!=", neq),
            (r"\|\|", or_),
            (r"&&", and_),
            (r"\(", lparen),
            (r"\)", rparen),
            (r"\s+", None), # skip whitespace
            ])
        tokens, remainder = scanner.scan(self.text)
        for t in tokens:
            yield t
        yield end_token()

    def value(self, ident):
        """
        Look up the value of |ident| in the value mapping passed in the
        constructor.
        """
        return self.valuemapping[ident]

    def advance(self, expected):
        """
        Assert that the next token is an instance of |expected|, and advance
        to the next token.
        """
        if not isinstance(self.token, expected):
            raise Exception, "Unexpected token!"
        self.token = self.iter.next()
        
    def expression(self, rbp=0):
        """
        Parse and return the value of an expression until a token with
        right binding power greater than rbp is encountered.
        """
        t = self.token
        self.token = self.iter.next()
        left = t.nud(self)
        while rbp < self.token.lbp:
            t = self.token
            self.token = self.iter.next()
            left = t.led(self, left)
        return left

    def parse(self):
        """
        Parse and return the value of the expression in the text
        passed to the constructor. Raises a ParseError if the expression
        could not be parsed.
        """
        try:
            self.iter = self._tokenize()
            self.token = self.iter.next()
            return self.expression()
        except:
            raise ParseError

def parse(text, values):
    """
    Parse and evaluate a boolean expression in |text|. Use |values| to look
    up the value of identifiers referenced in the expression. Returns the final
    value of the expression. A ParseError will be raised if parsing fails.
    """
    return ExpressionParser(text, values).parse()

class ExpressionParserUnittest(unittest.TestCase):
    def test_BasicValues(self):
        self.assertEqual(1, parse("1", {}))
        self.assertEqual(100, parse("100", {}))
        self.assertEqual(True, parse("true", {}))
        self.assertEqual(False, parse("false", {}))
        self.assertEqual("", parse('""', {}))
        self.assertEqual("foo bar", parse('"foo bar"', {}))
        self.assertEqual(1, parse("foo", {'foo':1}))
        self.assertEqual(True, parse("bar", {'bar':True}))
        self.assertEqual("xyz", parse("abc123", {'abc123':"xyz"}))

    def test_Equality(self):
        self.assertTrue(parse("true == true", {}))
        self.assertTrue(parse("false == false", {}))
        self.assertTrue(parse("false == false", {}))
        self.assertTrue(parse("1 == 1", {}))
        self.assertTrue(parse("100 == 100", {}))
        self.assertTrue(parse('"some text" == "some text"', {}))
        self.assertTrue(parse("true != false", {}))
        self.assertTrue(parse("1 != 2", {}))
        self.assertTrue(parse('"text" != "other text"', {}))
        self.assertTrue(parse("foo == true", {'foo': True}))
        self.assertTrue(parse("foo == 1", {'foo': 1}))
        self.assertTrue(parse('foo == "bar"', {'foo': 'bar'}))
        self.assertTrue(parse("foo == bar", {'foo': True, 'bar': True}))
        self.assertTrue(parse("true == foo", {'foo': True}))
        self.assertTrue(parse("foo != true", {'foo': False}))
        self.assertTrue(parse("foo != 2", {'foo': 1}))
        self.assertTrue(parse('foo != "bar"', {'foo': 'abc'}))
        self.assertTrue(parse("foo != bar", {'foo': True, 'bar': False}))
        self.assertTrue(parse("true != foo", {'foo': False}))

    def test_Conjunctions(self):
        self.assertTrue(parse("true && true", {}))
        self.assertTrue(parse("true || false", {}))
        self.assertFalse(parse("false || false", {}))
        self.assertFalse(parse("true && false", {}))
        self.assertTrue(parse("true || false && false", {}))

    def test_Parens(self):
        self.assertTrue(parse("(true)", {}))
        self.assertEquals(10, parse("(10)", {}))
        self.assertEquals('foo', parse('("foo")', {}))
        self.assertEquals(1, parse("(foo)", {'foo':1}))
        self.assertTrue(parse("(true == true)", {}))
        self.assertTrue(parse("(true != false)", {}))
        self.assertTrue(parse("(true && true)", {}))
        self.assertTrue(parse("(true || false)", {}))
        self.assertTrue(parse("(true && true || false)", {}))
        self.assertFalse(parse("(true || false) && false", {}))
        self.assertTrue(parse("(true || false) && true", {}))
        self.assertTrue(parse("true && (true || false)", {}))
        self.assertTrue(parse("true && (true || false)", {}))
        
if __name__ == '__main__':
    unittest.main()