Use an actual well defined parser for spec matching
Instead of a custom build parser and evaluator for specs use an actual formal parser and evaluation of that parse result instead, making that custom parser now be one that follows a more normal way of creating and evaluating a DSL. Also removes <all-in> operator, since it is buggy. (It will be added later when the issue has been resolved.) Co-authored-by: Alexis Lee <lxsli@hpe.com> Change-Id: I10f7dff8c83e5b6983515677e80cefa55881a92c
This commit is contained in:
parent
e97f08bb07
commit
7699788d59
@ -15,55 +15,72 @@
|
||||
|
||||
import operator
|
||||
|
||||
# 1. The following operations are supported:
|
||||
# =, s==, s!=, s>=, s>, s<=, s<, <in>, <all-in>, <or>, ==, !=, >=, <=
|
||||
# 2. Note that <or> 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),
|
||||
'<in>': lambda x, y: y in x,
|
||||
'<all-in>': 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,
|
||||
'<in>': lambda x, y: y in x,
|
||||
'<or>': 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("<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:
|
||||
return value == req
|
||||
or_ = Literal("<or>")
|
||||
|
||||
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 == '<or>': # Ex: <or> v1 <or> v2 <or> v3
|
||||
while True:
|
||||
if words.pop(0) == value:
|
||||
return True
|
||||
if not words:
|
||||
break
|
||||
words.pop(0) # remove a keyword <or>
|
||||
if not words:
|
||||
break
|
||||
return False
|
||||
unary = unary_ops + atom
|
||||
disjunction = OneOrMore(or_ + atom)
|
||||
|
||||
if words:
|
||||
if op == '<all-in>': # requires a list not a string
|
||||
return method(value, words)
|
||||
return method(value, words[0])
|
||||
return False
|
||||
# Even-numbered tokens will be '<or>', so we drop them
|
||||
disjunction.setParseAction(lambda _s, _l, t: ["<or>"] + 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:])
|
||||
|
@ -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='<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)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user