changeset 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
files expr.py
diffstat 1 files changed, 90 insertions(+), 40 deletions(-) [+]
line wrap: on
line diff
--- 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()