diff --git a/oslo_utils/specs_matcher.py b/oslo_utils/specs_matcher.py index d654db5d..a21798ce 100644 --- a/oslo_utils/specs_matcher.py +++ b/oslo_utils/specs_matcher.py @@ -36,18 +36,21 @@ op_methods = { # 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), @@ -55,7 +58,46 @@ op_methods = { def make_grammar(): - """Creates the grammar to be used by a spec matcher.""" + """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. @@ -90,14 +132,31 @@ def make_grammar(): def match(cmp_value, spec): - """Match a given value to a given spec DSL.""" + """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 - op = op_methods[tree[0]] - return op(cmp_value, *tree[1:]) + # 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:]) diff --git a/oslo_utils/tests/test_specs_matcher.py b/oslo_utils/tests/test_specs_matcher.py index 1d18d451..6d65146f 100644 --- a/oslo_utils/tests/test_specs_matcher.py +++ b/oslo_utils/tests/test_specs_matcher.py @@ -26,6 +26,21 @@ class SpecsMatcherTestCase(test_base.BaseTestCase): req='1', matches=True) + def test_specs_fails_string_vs_int(self): + # With no operator specified it is a string comparison test, therefore + # '1' does not equal '01' + self._do_specs_matcher_test( + value='01', + req='1', + matches=False) + + def test_specs_match_int_leading_zero(self): + # Show that numerical comparison works with leading zero + self._do_specs_matcher_test( + value='01', + req='== 1', + matches=True) + def test_specs_fails_simple(self): self._do_specs_matcher_test( value='',