# HG changeset patch # User Ted Mielczarek # Date 1307015065 14400 # Node ID 94a293b914af82c93db4d623f799c55368dee596 # Parent c45135ec8c1337520b15de374c649ec34717ed8b add documentation, clean up interface slightly, tweak tests diff -r c45135ec8c13 -r 94a293b914af expr.py --- a/expr.py Wed Jun 01 20:00:51 2011 -0400 +++ b/expr.py Thu Jun 02 07:44:25 2011 -0400 @@ -1,8 +1,30 @@ #!/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 -# ideas taken from http://effbot.org/zone/simple-top-down-parsing.htm # token classes class ident_token: def __init__(self, value): @@ -72,6 +94,9 @@ # lowest left binding power, always ends parsing lbp = 0 +class ParseError(Exception): + pass + class ExpressionParser(object): def __init__(self, text, valuemapping): """ @@ -148,53 +173,78 @@ def parse(self): """ Parse and return the value of the expression in the text - passed to the constructor. + passed to the constructor. Raises a ParseError if the expression + could not be parsed. """ - self.iter = self._tokenize() - self.token = self.iter.next() - return self.expression() + 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 parse(self, text, values): - return ExpressionParser(text, values).parse() - def test_BasicValues(self): - self.assertEqual(1, self.parse("1", {})) - self.assertEqual(100, self.parse("100", {})) - self.assertEqual(True, self.parse("true", {})) - self.assertEqual(False, self.parse("false", {})) - self.assertEqual("", self.parse('""', {})) - self.assertEqual("foo bar", self.parse('"foo bar"', {})) - self.assertEqual(1, self.parse("foo", {'foo':1})) - self.assertEqual(True, self.parse("bar", {'bar':True})) - self.assertEqual("xyz", self.parse("abc123", {'abc123':"xyz"})) + 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(self.parse("true == true", {})) - self.assertTrue(self.parse("false == false", {})) - self.assertTrue(self.parse("false == false", {})) - self.assertTrue(self.parse("1 == 1", {})) - self.assertTrue(self.parse("100 == 100", {})) - self.assertTrue(self.parse('"some text" == "some text"', {})) - self.assertTrue(self.parse("true != false", {})) - self.assertTrue(self.parse("1 != 2", {})) - self.assertTrue(self.parse('"text" != "other text"', {})) - self.assertTrue(self.parse("foo == true", {'foo': True})) - self.assertTrue(self.parse("foo == 1", {'foo': 1})) - self.assertTrue(self.parse('foo == "bar"', {'foo': 'bar'})) - self.assertTrue(self.parse("foo == bar", {'foo': True, 'bar': True})) - self.assertTrue(self.parse("true == foo", {'foo': True})) - self.assertTrue(self.parse("foo != true", {'foo': False})) - self.assertTrue(self.parse("foo != 2", {'foo': 1})) - self.assertTrue(self.parse('foo != "bar"', {'foo': 'abc'})) - self.assertTrue(self.parse("foo != bar", {'foo': True, 'bar': False})) - self.assertTrue(self.parse("true != foo", {'foo': False})) + 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(self.parse("true && true", {})) - self.assertTrue(self.parse("true || false", {})) - self.assertFalse(self.parse("false || false", {})) - self.assertFalse(self.parse("true && false", {})) + 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()