# 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(" 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 # Numerical 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), # String methods 's!=': operator.ne, 's<': operator.lt, 's<=': operator.le, 's==': operator.eq, 's>': operator.gt, 's>=': operator.ge, # Other '': _all_in, '': lambda x, y: y in x, '': lambda x, *y: any(x == a for a in y), } def make_grammar(): """Creates the grammar to be used by a spec matcher. The grammar created supports the following operations. Numerical values: * ``= :`` equal to or greater than. This is equivalent to ``>=`` and is supported for `legacy reasons `_ * ``!= :`` Float/integer value not equal * ``<= :`` Float/integer value less than or equal * ``< :`` Float/integer value less than * ``== :`` Float/integer value equal * ``>= :`` Float/integer value greater than or equal * ``> :`` Float/integer value greater String operations: * ``s!= :`` Not equal * ``s< :`` Less than * ``s<= :`` Less than or equal * ``s== :`` Equal * ``s> :`` Greater than * ``s>= :`` Greater than or equal Other operations: * `` :`` All items 'in' value * `` :`` Item 'in' value, like a substring in a string. * `` :`` Logical 'or' If no operator is specified the default is ``s==`` (string equality comparison) Example operations: * ``">= 60"`` Is the numerical value greater than or equal to 60 * ``" spam eggs"`` Does the value contain ``spam`` or ``eggs`` * ``"s== 2.1.0"`` Is the string value equal to ``2.1.0`` * ``" gcc"`` Is the string ``gcc`` contained in the value string * ``" aes mmx"`` Are both ``aes`` and ``mmx`` in the value :returns: A pyparsing.MatchFirst object. See https://pythonhosted.org/pyparsing/ for details on pyparsing. """ # 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("") | 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("") or_ = Literal("") # 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 '', so we drop them disjunction.setParseAction(lambda _s, _l, t: [""] + t[1::2]) expr = disjunction | nary | unary | atom return expr def match(cmp_value, spec): """Match a given value to a given spec DSL. This uses the grammar defined by make_grammar() :param cmp_value: Value to be checked for match. :param spec: The comparison specification string, for example ``">= 70"`` or ``"s== string_value"``. See ``make_grammar()`` for examples of a specification string. :returns: True if cmp_value is a match for spec. False otherwise. """ expr = make_grammar() try: # As of 2018-01-29 documentation on parseString() # https://pythonhosted.org/pyparsing/pyparsing.ParserElement-class.html#parseString # # parseString() will take our specification string, for example "< 6" # and convert it into a list of ['<', "6"] tree = expr.parseString(spec) except pyparsing.ParseException: # If an exception then we will just check if the value matches the spec tree = [spec] if len(tree) == 1: return tree[0] == cmp_value # tree[0] will contain a string representation of a comparison operation # such as '>=', we then convert that string to a comparison function compare_func = op_methods[tree[0]] return compare_func(cmp_value, *tree[1:])