BooleanExpression.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import re
  2. class BooleanExpression:
  3. # A simple evaluator of boolean expressions.
  4. #
  5. # Grammar:
  6. # expr :: or_expr
  7. # or_expr :: and_expr ('||' and_expr)*
  8. # and_expr :: not_expr ('&&' not_expr)*
  9. # not_expr :: '!' not_expr
  10. # '(' or_expr ')'
  11. # identifier
  12. # identifier :: [-+=._a-zA-Z0-9]+
  13. # Evaluates `string` as a boolean expression.
  14. # Returns True or False. Throws a ValueError on syntax error.
  15. #
  16. # Variables in `variables` are true.
  17. # Substrings of `triple` are true.
  18. # 'true' is true.
  19. # All other identifiers are false.
  20. @staticmethod
  21. def evaluate(string, variables, triple=""):
  22. try:
  23. parser = BooleanExpression(string, set(variables), triple)
  24. return parser.parseAll()
  25. except ValueError as e:
  26. raise ValueError(str(e) + ('\nin expression: %r' % string))
  27. #####
  28. def __init__(self, string, variables, triple=""):
  29. self.tokens = BooleanExpression.tokenize(string)
  30. self.variables = variables
  31. self.variables.add('true')
  32. self.triple = triple
  33. self.value = None
  34. self.token = None
  35. # Singleton end-of-expression marker.
  36. END = object()
  37. # Tokenization pattern.
  38. Pattern = re.compile(r'\A\s*([()]|[-+=._a-zA-Z0-9]+|&&|\|\||!)\s*(.*)\Z')
  39. @staticmethod
  40. def tokenize(string):
  41. while True:
  42. m = re.match(BooleanExpression.Pattern, string)
  43. if m is None:
  44. if string == "":
  45. yield BooleanExpression.END;
  46. return
  47. else:
  48. raise ValueError("couldn't parse text: %r" % string)
  49. token = m.group(1)
  50. string = m.group(2)
  51. yield token
  52. def quote(self, token):
  53. if token is BooleanExpression.END:
  54. return '<end of expression>'
  55. else:
  56. return repr(token)
  57. def accept(self, t):
  58. if self.token == t:
  59. self.token = next(self.tokens)
  60. return True
  61. else:
  62. return False
  63. def expect(self, t):
  64. if self.token == t:
  65. if self.token != BooleanExpression.END:
  66. self.token = next(self.tokens)
  67. else:
  68. raise ValueError("expected: %s\nhave: %s" %
  69. (self.quote(t), self.quote(self.token)))
  70. def isIdentifier(self, t):
  71. if (t is BooleanExpression.END or t == '&&' or t == '||' or
  72. t == '!' or t == '(' or t == ')'):
  73. return False
  74. return True
  75. def parseNOT(self):
  76. if self.accept('!'):
  77. self.parseNOT()
  78. self.value = not self.value
  79. elif self.accept('('):
  80. self.parseOR()
  81. self.expect(')')
  82. elif not self.isIdentifier(self.token):
  83. raise ValueError("expected: '!' or '(' or identifier\nhave: %s" %
  84. self.quote(self.token))
  85. else:
  86. self.value = (self.token in self.variables or
  87. self.token in self.triple)
  88. self.token = next(self.tokens)
  89. def parseAND(self):
  90. self.parseNOT()
  91. while self.accept('&&'):
  92. left = self.value
  93. self.parseNOT()
  94. right = self.value
  95. # this is technically the wrong associativity, but it
  96. # doesn't matter for this limited expression grammar
  97. self.value = left and right
  98. def parseOR(self):
  99. self.parseAND()
  100. while self.accept('||'):
  101. left = self.value
  102. self.parseAND()
  103. right = self.value
  104. # this is technically the wrong associativity, but it
  105. # doesn't matter for this limited expression grammar
  106. self.value = left or right
  107. def parseAll(self):
  108. self.token = next(self.tokens)
  109. self.parseOR()
  110. self.expect(BooleanExpression.END)
  111. return self.value
  112. #######
  113. # Tests
  114. import unittest
  115. class TestBooleanExpression(unittest.TestCase):
  116. def test_variables(self):
  117. variables = {'its-true', 'false-lol-true', 'under_score',
  118. 'e=quals', 'd1g1ts'}
  119. self.assertTrue(BooleanExpression.evaluate('true', variables))
  120. self.assertTrue(BooleanExpression.evaluate('its-true', variables))
  121. self.assertTrue(BooleanExpression.evaluate('false-lol-true', variables))
  122. self.assertTrue(BooleanExpression.evaluate('under_score', variables))
  123. self.assertTrue(BooleanExpression.evaluate('e=quals', variables))
  124. self.assertTrue(BooleanExpression.evaluate('d1g1ts', variables))
  125. self.assertFalse(BooleanExpression.evaluate('false', variables))
  126. self.assertFalse(BooleanExpression.evaluate('True', variables))
  127. self.assertFalse(BooleanExpression.evaluate('true-ish', variables))
  128. self.assertFalse(BooleanExpression.evaluate('not_true', variables))
  129. self.assertFalse(BooleanExpression.evaluate('tru', variables))
  130. def test_triple(self):
  131. triple = 'arch-vendor-os'
  132. self.assertTrue(BooleanExpression.evaluate('arch-', {}, triple))
  133. self.assertTrue(BooleanExpression.evaluate('ar', {}, triple))
  134. self.assertTrue(BooleanExpression.evaluate('ch-vend', {}, triple))
  135. self.assertTrue(BooleanExpression.evaluate('-vendor-', {}, triple))
  136. self.assertTrue(BooleanExpression.evaluate('-os', {}, triple))
  137. self.assertFalse(BooleanExpression.evaluate('arch-os', {}, triple))
  138. def test_operators(self):
  139. self.assertTrue(BooleanExpression.evaluate('true || true', {}))
  140. self.assertTrue(BooleanExpression.evaluate('true || false', {}))
  141. self.assertTrue(BooleanExpression.evaluate('false || true', {}))
  142. self.assertFalse(BooleanExpression.evaluate('false || false', {}))
  143. self.assertTrue(BooleanExpression.evaluate('true && true', {}))
  144. self.assertFalse(BooleanExpression.evaluate('true && false', {}))
  145. self.assertFalse(BooleanExpression.evaluate('false && true', {}))
  146. self.assertFalse(BooleanExpression.evaluate('false && false', {}))
  147. self.assertFalse(BooleanExpression.evaluate('!true', {}))
  148. self.assertTrue(BooleanExpression.evaluate('!false', {}))
  149. self.assertTrue(BooleanExpression.evaluate(' ((!((false) )) ) ', {}))
  150. self.assertTrue(BooleanExpression.evaluate('true && (true && (true))', {}))
  151. self.assertTrue(BooleanExpression.evaluate('!false && !false && !! !false', {}))
  152. self.assertTrue(BooleanExpression.evaluate('false && false || true', {}))
  153. self.assertTrue(BooleanExpression.evaluate('(false && false) || true', {}))
  154. self.assertFalse(BooleanExpression.evaluate('false && (false || true)', {}))
  155. # Evaluate boolean expression `expr`.
  156. # Fail if it does not throw a ValueError containing the text `error`.
  157. def checkException(self, expr, error):
  158. try:
  159. BooleanExpression.evaluate(expr, {})
  160. self.fail("expression %r didn't cause an exception" % expr)
  161. except ValueError as e:
  162. if -1 == str(e).find(error):
  163. self.fail(("expression %r caused the wrong ValueError\n" +
  164. "actual error was:\n%s\n" +
  165. "expected error was:\n%s\n") % (expr, e, error))
  166. except BaseException as e:
  167. self.fail(("expression %r caused the wrong exception; actual " +
  168. "exception was: \n%r") % (expr, e))
  169. def test_errors(self):
  170. self.checkException("ba#d",
  171. "couldn't parse text: '#d'\n" +
  172. "in expression: 'ba#d'")
  173. self.checkException("true and true",
  174. "expected: <end of expression>\n" +
  175. "have: 'and'\n" +
  176. "in expression: 'true and true'")
  177. self.checkException("|| true",
  178. "expected: '!' or '(' or identifier\n" +
  179. "have: '||'\n" +
  180. "in expression: '|| true'")
  181. self.checkException("true &&",
  182. "expected: '!' or '(' or identifier\n" +
  183. "have: <end of expression>\n" +
  184. "in expression: 'true &&'")
  185. self.checkException("",
  186. "expected: '!' or '(' or identifier\n" +
  187. "have: <end of expression>\n" +
  188. "in expression: ''")
  189. self.checkException("*",
  190. "couldn't parse text: '*'\n" +
  191. "in expression: '*'")
  192. self.checkException("no wait stop",
  193. "expected: <end of expression>\n" +
  194. "have: 'wait'\n" +
  195. "in expression: 'no wait stop'")
  196. self.checkException("no-$-please",
  197. "couldn't parse text: '$-please'\n" +
  198. "in expression: 'no-$-please'")
  199. self.checkException("(((true && true) || true)",
  200. "expected: ')'\n" +
  201. "have: <end of expression>\n" +
  202. "in expression: '(((true && true) || true)'")
  203. self.checkException("true (true)",
  204. "expected: <end of expression>\n" +
  205. "have: '('\n" +
  206. "in expression: 'true (true)'")
  207. self.checkException("( )",
  208. "expected: '!' or '(' or identifier\n" +
  209. "have: ')'\n" +
  210. "in expression: '( )'")
  211. if __name__ == '__main__':
  212. unittest.main()