# -*- coding: utf-8 -*-
from pyparsing import CaselessLiteral, Word, delimitedList, Optional, \
Combine, Group, alphas, nums, alphanums, ParseException, Forward, oneOf, \
quotedString, ZeroOrMore, Keyword, ParseResults, removeQuotes, Suppress
class SubstituteVal(object):
"""
Represents a token that will later be replaced by a context value.
"""
def __init__(self, t):
self._path = t[0]
def get_val(self, context):
if not context:
raise MissingVariableException(
'context missing or empty'
)
val = context
try:
for part in self._path.split(pathDelimiter):
val = getattr(val, part) if hasattr(val, part) else val[part]
except KeyError:
raise MissingVariableException(
'no value supplied for {}'.format(self._path)
)
return val
def __repr__(self):
return 'SubstituteVal(%s)' % self._path
# Grammar definition
pathDelimiter = '.'
identifier = Word(alphas, alphanums + "_")
propertyPath = delimitedList(identifier, pathDelimiter, combine=True)
and_ = Keyword("and", caseless=True)
or_ = Keyword("or", caseless=True)
in_ = Keyword("in", caseless=True)
lparen = Suppress('(')
rparen = Suppress(')')
binaryOp = oneOf(
"= == != < > >= <= eq ne lt le gt ge in notin is isnot "
"≠ ≤ ≥ ∈ ∉ ⊆ ⊇ ∩", caseless=True
)('operator')
E = CaselessLiteral("E")
numberSign = Word("+-", exact=1)
realNumber = Combine(
Optional(numberSign) + (
Word(nums) + "." + Optional(Word(nums))
| ("." + Word(nums))
) + Optional(E + Optional(numberSign) + Word(nums))
)
integer = Combine(
Optional(numberSign) + Word(nums) + Optional(
E + Optional("+") + Word(nums)
)
)
str_ = quotedString.addParseAction(removeQuotes)
bool_ = oneOf('true false', caseless=True)
none_ = CaselessLiteral('none')
simpleVals = (
realNumber.setParseAction(lambda toks: float(toks[0]))
| integer.setParseAction(lambda toks: int(toks[0]))
| str_
| bool_.setParseAction(lambda toks: toks[0] == 'true')
| none_.setParseAction(lambda toks: [None]) # see pyparsing bug 63
| propertyPath.setParseAction(lambda toks: SubstituteVal(toks))
) # need to add support for alg expressions
propertyVal = (
simpleVals
| (lparen + Group(delimitedList(simpleVals)) + rparen)
)
boolExpression = Forward()
boolCondition = Group(
(Group(propertyVal)('lval') + binaryOp + Group(propertyVal)('rval'))
| (lparen + boolExpression + rparen)
)
boolExpression << boolCondition + ZeroOrMore((and_ | or_) + boolExpression)
[docs]class BoolRule(object):
"""
Represents a boolean expression and provides a `test` method to evaluate
the expression and determine its truthiness.
:param query: A string containing the query to be evaluated
:param lazy: If ``True``, parse the query the first time it's tested rather
than immediately. This can help with performance if you
instantiate a lot of rules and only end up evaluating a
small handful.
"""
_compiled = False
_tokens = None
_query = None
def __init__(self, query, lazy=False):
self._query = query
if not lazy:
self._compile()
[docs] def test(self, context=None):
"""
Test the expression against the given context and return the result.
:param context: A dict context to evaluate the expression against.
:return: True if the expression succesfully evaluated against the
context, or False otherwise.
"""
if self._is_match_all():
return True
self._compile()
return self._test_tokens(self._tokens, context)
def _is_match_all(self):
return True if self._query == '*' else False
def _compile(self):
if not self._compiled:
# special case match-all query
if self._is_match_all():
return
self._tokens = boolExpression.parseString(self._query, True)
self._compiled = True
def _expand_val(self, val, context):
if type(val) == list:
val = [self._expand_val(v, context) for v in val]
if isinstance(val, SubstituteVal):
ret = val.get_val(context)
return ret
if isinstance(val, ParseResults):
return [self._expand_val(x, context) for x in val.asList()]
return val
def _test_tokens(self, tokens, context):
passed = False
for token in tokens:
if not isinstance(token, ParseResults):
if token == 'or' and passed:
return True
elif token == 'and' and not passed:
return False
continue
if not token.getName():
return self._test_tokens(token, context)
items = token.asDict()
operator = items['operator']
lval = self._expand_val(items['lval'][0], context)
rval = self._expand_val(items['rval'][0], context)
if operator in ('=', '==', 'eq'):
passed = lval == rval
elif operator in ('!=', 'ne', '≠'):
passed = lval != rval
elif operator in ('>', 'gt'):
passed = lval > rval
elif operator in ('>=', 'ge', '≥'):
passed = lval >= rval
elif operator in ('<', 'lt'):
passed = lval < rval
elif operator in ('<=', 'le', '≤'):
passed = lval <= rval
elif operator in ('in', '∈'):
passed = lval in rval
elif operator in ('notin', '∉'):
passed = lval not in rval
elif operator == 'is':
passed = lval is rval
elif operator == 'isnot':
passed = lval is not rval
elif operator == '⊆':
passed = all((False for x in lval if x not in rval))
elif operator == '⊇':
passed = all((False for x in rval if x not in lval))
elif operator == '∩':
passed = any((True for x in lval if x in rval))
else:
raise UnknownOperatorException(
"Unknown operator '{}'".format(operator)
)
return passed
[docs]class MissingVariableException(Exception):
"""
Raised when an expression contains a property path that's not supplied in
the context.
"""
pass
[docs]class UnknownOperatorException(Exception):
"""
Raised when an expression uses an unknown operator.
This should never be thrown since the operator won't be correctly parsed as
a token by pyparsing, but it's useful to have this hanging around for when
additional operators are being added.
"""
pass