Merge "Use an actual well defined parser for spec matching"
This commit is contained in:
commit
9e9f30da57
@ -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:])
|
||||||
|
@ -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)
|
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user