104 lines
3.4 KiB
Python
104 lines
3.4 KiB
Python
# Copyright (c) 2011 OpenStack Foundation
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import ast
|
|
import operator
|
|
|
|
import pyparsing
|
|
from pyparsing import Literal
|
|
from pyparsing import OneOrMore
|
|
from pyparsing import Regex
|
|
|
|
|
|
def _all_in(x, *y):
|
|
x = ast.literal_eval(x)
|
|
if not isinstance(x, list):
|
|
raise TypeError("<all-in> must compare with a list literal"
|
|
" string, EG \"%s\"" % (['aes', 'mmx'],))
|
|
return all(val in x for val in y)
|
|
|
|
|
|
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),
|
|
# 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),
|
|
'==': lambda x, y: float(x) == float(y),
|
|
'>=': 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,
|
|
'<all-in>': _all_in,
|
|
'<in>': lambda x, y: y in x,
|
|
'<or>': lambda x, *y: any(x == a for a in y),
|
|
}
|
|
|
|
|
|
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.
|
|
|
|
unary_ops = (
|
|
# Order matters here (so that '=' doesn't match before '==')
|
|
Literal("==") | Literal("=") |
|
|
Literal("!=") | Literal("<in>") |
|
|
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>"))
|
|
|
|
all_in_nary_op = Literal("<all-in>")
|
|
or_ = Literal("<or>")
|
|
|
|
# An atom is anything not an keyword followed by anything but whitespace
|
|
atom = ~(unary_ops | all_in_nary_op | or_) + Regex(r"\S+")
|
|
|
|
unary = unary_ops + atom
|
|
nary = all_in_nary_op + OneOrMore(atom)
|
|
disjunction = OneOrMore(or_ + atom)
|
|
|
|
# Even-numbered tokens will be '<or>', so we drop them
|
|
disjunction.setParseAction(lambda _s, _l, t: ["<or>"] + t[1::2])
|
|
|
|
expr = disjunction | nary | unary | atom
|
|
return expr
|
|
|
|
|
|
def match(cmp_value, spec):
|
|
"""Match a given value to a given spec DSL."""
|
|
expr = make_grammar()
|
|
try:
|
|
tree = expr.parseString(spec)
|
|
except pyparsing.ParseException:
|
|
tree = [spec]
|
|
if len(tree) == 1:
|
|
return tree[0] == cmp_value
|
|
|
|
op = op_methods[tree[0]]
|
|
return op(cmp_value, *tree[1:])
|