diff --git a/oslo_utils/specs_matcher.py b/oslo_utils/specs_matcher.py index 01486f2c..a384a6c1 100644 --- a/oslo_utils/specs_matcher.py +++ b/oslo_utils/specs_matcher.py @@ -15,55 +15,72 @@ import operator -# 1. The following operations are supported: -# =, s==, s!=, s>=, s>, s<=, s<, , , , ==, !=, >=, <= -# 2. Note that is handled in a different way below. -# 3. If the first word in the extra_specs is not one of the operators, -# it is ignored. +import pyparsing +from pyparsing import Literal +from pyparsing import OneOrMore +from pyparsing import Regex + op_methods = { + # This one is special/odd, + # TODO(harlowja): fix it so that it's not greater than or + # equal, see here for the original @ https://review.openstack.org/#/c/8089/ '=': lambda x, y: float(x) >= float(y), - '': lambda x, y: y in x, - '': lambda x, y: all(val in x for val in y), - '==': lambda x, y: float(x) == float(y), + # More sane ops/methods '!=': lambda x, y: float(x) != float(y), - '>=': lambda x, y: float(x) >= float(y), '<=': lambda x, y: float(x) <= float(y), - 's==': operator.eq, + '==': lambda x, y: float(x) == float(y), + '>=': lambda x, y: float(x) >= float(y), 's!=': operator.ne, 's<': operator.lt, 's<=': operator.le, + 's==': operator.eq, 's>': operator.gt, - 's>=': operator.ge + 's>=': operator.ge, + '': lambda x, y: y in x, + '': lambda x, *y: any(x == a for a in y), } -def match(value, req): - words = req.split() +def make_grammar(): + """Creates the grammar to be used by a spec matcher.""" + # This is apparently how pyparsing recommends to be used, + # as http://pyparsing.wikispaces.com/share/view/644825 states that + # it is not thread-safe to use a parser across threads. - op = method = None - if words: - op = words.pop(0) - method = op_methods.get(op) + unary_ops = ( + # Order matters here (so that '=' doesn't match before '==') + Literal("==") | Literal("=") | + Literal("!=") | Literal("") | + Literal(">=") | Literal("<=") | + Literal("s==") | Literal("s!=") | + # Order matters here (so that '<' doesn't match before '<=') + Literal("s<=") | Literal("s<") | + # Order matters here (so that '>' doesn't match before '>=') + Literal("s>=") | Literal("s>")) - if op != '' and not method: - return value == req + or_ = Literal("") - if value is None: - return False + # An atom is anything not an keyword followed by anything but whitespace + atom = ~(unary_ops | or_) + Regex(r"\S+") - if op == '': # Ex: v1 v2 v3 - while True: - if words.pop(0) == value: - return True - if not words: - break - words.pop(0) # remove a keyword - if not words: - break - return False + unary = unary_ops + atom + disjunction = OneOrMore(or_ + atom) - if words: - if op == '': # requires a list not a string - return method(value, words) - return method(value, words[0]) - return False + # Even-numbered tokens will be '', so we drop them + disjunction.setParseAction(lambda _s, _l, t: [""] + t[1::2]) + + expr = disjunction | unary | atom + return expr + + +def match(cmp_value, spec): + """Match a given value to a given spec DSL.""" + expr = make_grammar() + try: + ast = expr.parseString(spec) + except pyparsing.ParseException: + ast = [spec] + if len(ast) == 1: + return ast[0] == cmp_value + op = op_methods[ast[0]] + return op(cmp_value, *ast[1:]) diff --git a/oslo_utils/tests/test_specs_matcher.py b/oslo_utils/tests/test_specs_matcher.py index 7f99f0ac..00819c33 100644 --- a/oslo_utils/tests/test_specs_matcher.py +++ b/oslo_utils/tests/test_specs_matcher.py @@ -199,38 +199,3 @@ class SpecsMatcherTestCase(test_base.BaseTestCase): value='2', req='>= 3', matches=False) - - def test_specs_matches_all_with_op_allin(self): - values = ['aes', 'mmx', 'aux'] - self._do_specs_matcher_test( - value=str(values), - req=' aes mmx', - matches=True) - - def test_specs_matches_one_with_op_allin(self): - values = ['aes', 'mmx', 'aux'] - self._do_specs_matcher_test( - value=str(values), - req=' mmx', - matches=True) - - def test_specs_fails_with_op_allin(self): - values = ['aes', 'mmx', 'aux'] - self._do_specs_matcher_test( - value=str(values), - req=' txt', - matches=False) - - def test_specs_fails_all_with_op_allin(self): - values = ['aes', 'mmx', 'aux'] - self._do_specs_matcher_test( - value=str(values), - req=' txt 3dnow', - matches=False) - - def test_specs_fails_match_one_with_op_allin(self): - values = ['aes', 'mmx', 'aux'] - self._do_specs_matcher_test( - value=str(values), - req=' txt aes', - matches=False) diff --git a/requirements.txt b/requirements.txt index 07f0e55a..90356707 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,4 @@ pytz>=2013.6 # MIT netaddr!=0.7.16,>=0.7.12 # BSD netifaces>=0.10.4 # MIT debtcollector>=1.2.0 # Apache-2.0 +pyparsing>=2.0.1 # MIT