Merge "Use an actual well defined parser for spec matching"

This commit is contained in:
Jenkins 2016-07-13 11:43:39 +00:00 committed by Gerrit Code Review
commit 9e9f30da57
3 changed files with 54 additions and 71 deletions

View File

@ -15,55 +15,72 @@
import operator import operator
# 1. The following operations are supported: import pyparsing
# =, s==, s!=, s>=, s>, s<=, s<, <in>, <all-in>, <or>, ==, !=, >=, <= from pyparsing import Literal
# 2. Note that <or> is handled in a different way below. from pyparsing import OneOrMore
# 3. If the first word in the extra_specs is not one of the operators, from pyparsing import Regex
# it is ignored.
op_methods = { 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: float(x) >= float(y),
'<in>': lambda x, y: y in x, # More sane ops/methods
'<all-in>': lambda x, y: all(val in x for val in y),
'==': lambda x, y: float(x) == float(y),
'!=': lambda x, y: float(x) != float(y), '!=': lambda x, y: float(x) != float(y),
'>=': 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.ne,
's<': operator.lt, 's<': operator.lt,
's<=': operator.le, 's<=': operator.le,
's==': operator.eq,
's>': operator.gt, 's>': operator.gt,
's>=': operator.ge 's>=': operator.ge,
'<in>': lambda x, y: y in x,
'<or>': lambda x, *y: any(x == a for a in y),
} }
def match(value, req): def make_grammar():
words = req.split() """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 unary_ops = (
if words: # Order matters here (so that '=' doesn't match before '==')
op = words.pop(0) Literal("==") | Literal("=") |
method = op_methods.get(op) Literal("!=") | Literal("<in>") |
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 != '<or>' and not method: or_ = Literal("<or>")
return value == req
if value is None: # An atom is anything not an keyword followed by anything but whitespace
return False atom = ~(unary_ops | or_) + Regex(r"\S+")
if op == '<or>': # Ex: <or> v1 <or> v2 <or> v3 unary = unary_ops + atom
while True: disjunction = OneOrMore(or_ + atom)
if words.pop(0) == value:
return True
if not words:
break
words.pop(0) # remove a keyword <or>
if not words:
break
return False
if words: # Even-numbered tokens will be '<or>', so we drop them
if op == '<all-in>': # requires a list not a string disjunction.setParseAction(lambda _s, _l, t: ["<or>"] + t[1::2])
return method(value, words)
return method(value, words[0]) expr = disjunction | unary | atom
return False 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:])

View File

@ -199,38 +199,3 @@ class SpecsMatcherTestCase(test_base.BaseTestCase):
value='2', value='2',
req='>= 3', req='>= 3',
matches=False) matches=False)
def test_specs_matches_all_with_op_allin(self):
values = ['aes', 'mmx', 'aux']
self._do_specs_matcher_test(
value=str(values),
req='<all-in> 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='<all-in> mmx',
matches=True)
def test_specs_fails_with_op_allin(self):
values = ['aes', 'mmx', 'aux']
self._do_specs_matcher_test(
value=str(values),
req='<all-in> 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='<all-in> 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='<all-in> txt aes',
matches=False)

View File

@ -17,3 +17,4 @@ pytz>=2013.6 # MIT
netaddr!=0.7.16,>=0.7.12 # BSD netaddr!=0.7.16,>=0.7.12 # BSD
netifaces>=0.10.4 # MIT netifaces>=0.10.4 # MIT
debtcollector>=1.2.0 # Apache-2.0 debtcollector>=1.2.0 # Apache-2.0
pyparsing>=2.0.1 # MIT