diff --git a/oslo_utils/specs_matcher.py b/oslo_utils/specs_matcher.py index f999a9ff..4616424b 100644 --- a/oslo_utils/specs_matcher.py +++ b/oslo_utils/specs_matcher.py @@ -27,6 +27,38 @@ def _all_in(x, *y): return all(val in x for val in y) +def _range_in(x, *y): + x = ast.literal_eval(x) + if len(y) != 4: + raise TypeError(" operator has to be followed by 2 " + "space separated numeric value surrounded by " + "brackets \"range_in [ 10 20 ] \"") + num_x = float(x) + num_y = float(y[1]) + num_z = float(y[2]) + if num_y > num_z: + raise TypeError(" operator's first argument has to be " + "smaller or equal to the second argument EG" + "\"range_in ( 10 20 ] \"") + + if y[0] == '[': + lower = num_x >= num_y + elif y[0] == '(': + lower = num_x > num_y + else: + raise TypeError("The first element should be an opening bracket " + "(\"(\" or \"[\")") + + if y[3] == ']': + upper = num_x <= num_z + elif y[3] == ')': + upper = num_x < num_z + else: + raise TypeError("The last element should be a closing bracket " + "(\")\" or \"]\")") + return lower and upper + + op_methods = { # This one is special/odd, # TODO(harlowja): fix it so that it's not greater than or @@ -51,6 +83,7 @@ op_methods = { '': _all_in, '': lambda x, y: y in x, '': lambda x, *y: any(x == a for a in y), + '': _range_in, } @@ -79,9 +112,12 @@ String operations: * ``s>= :`` Greater than or equal Other operations: - * `` :`` All items 'in' value - * `` :`` Item 'in' value, like a substring in a string. - * `` :`` Logical 'or' + * `` :`` All items 'in' value + * `` :`` Item 'in' value, like a substring in a string. + * `` :`` Logical 'or' + * ``:`` Range tester with customizable boundary conditions, tests + whether value is in the range, boundary condition could be + inclusve \'[\' or exclusive \'(\'. If no operator is specified the default is ``s==`` (string equality comparison) @@ -91,6 +127,9 @@ Example operations: * ``"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 + * ``" [ 10 20 ]"`` float(value) >= 10 and float(value) <= 20 + * ``" ( 10 20 ]"`` float(value) > 10 and float(value) <= 20 + * ``" ( 10 20 )"`` float(value) > 10 and float(value) < 20 :returns: A pyparsing.MatchFirst object. See https://pythonhosted.org/pyparsing/ for details on pyparsing. @@ -113,18 +152,21 @@ Example operations: all_in_nary_op = pyparsing.Literal("") or_ = pyparsing.Literal("") + range_in_binary_op = pyparsing.Literal("") # An atom is anything not an keyword followed by anything but whitespace - atom = ~(unary_ops | all_in_nary_op | or_) + pyparsing.Regex(r"\S+") + atom = ~(unary_ops | all_in_nary_op | or_ | range_in_binary_op) + \ + pyparsing.Regex(r"\S+") unary = unary_ops + atom + range_op = range_in_binary_op + atom + atom + atom + atom nary = all_in_nary_op + pyparsing.OneOrMore(atom) disjunction = pyparsing.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 + expr = disjunction | nary | range_op | unary | atom return expr diff --git a/oslo_utils/tests/test_specs_matcher.py b/oslo_utils/tests/test_specs_matcher.py index 6d65146f..22f3467d 100644 --- a/oslo_utils/tests/test_specs_matcher.py +++ b/oslo_utils/tests/test_specs_matcher.py @@ -435,3 +435,195 @@ class SpecsMatcherTestCase(test_base.BaseTestCase): specs_matcher.match, value="^&*($", req=' aes') + + def test_specs_fails_not_enough_args_with_op_rangein(self): + self.assertRaises( + TypeError, + specs_matcher.match, + value="23", + req=' [ 10 ]') + + def test_specs_fails_no_brackets_with_op_rangein(self): + self.assertRaises( + TypeError, + specs_matcher.match, + value="23", + req=' 10 20') + + def test_specs_fails_no_opening_bracket_with_op_rangein(self): + self.assertRaises( + TypeError, + specs_matcher.match, + value="23", + req=' 10 20 ]') + + def test_specs_fails_no_closing_bracket_with_op_rangein(self): + self.assertRaises( + TypeError, + specs_matcher.match, + value="23", + req=' [ 10 20') + + def test_specs_fails_invalid_brackets_with_op_rangein(self): + self.assertRaises( + TypeError, + specs_matcher.match, + value="23", + req=' { 10 20 }') + + def test_specs_fails_not_opening_brackets_with_op_rangein(self): + self.assertRaises( + TypeError, + specs_matcher.match, + value="23", + req=' ) 10 20 )') + + def test_specs_fails_not_closing_brackets_with_op_rangein(self): + self.assertRaises( + TypeError, + specs_matcher.match, + value="23", + req=' ( 10 20 (') + + def test_specs_fails_reverse_brackets_with_op_rangein(self): + self.assertRaises( + TypeError, + specs_matcher.match, + value="23", + req=' ) 10 20 (') + + def test_specs_fails_too_many_args_with_op_rangein(self): + self.assertRaises( + TypeError, + specs_matcher.match, + value="23", + req=' [ 10 20 30 ]') + + def test_specs_fails_bad_limits_with_op_rangein(self): + self.assertRaises( + TypeError, + specs_matcher.match, + value="23", + req=' [ 20 10 ]') + + def test_specs_fails_match_beyond_scope_with_op_rangein_le(self): + self._do_specs_matcher_test( + matches=False, + value="23", + req=' [ 10 20 ]') + + def test_specs_fails_match_beyond_scope_with_op_rangein_lt(self): + self._do_specs_matcher_test( + matches=False, + value="23", + req=' [ 10 20 )') + + def test_specs_fails_match_under_scope_with_op_rangein_ge(self): + self._do_specs_matcher_test( + matches=False, + value="5", + req=' [ 10 20 ]') + + def test_specs_fails_match_under_scope_with_op_rangein_gt(self): + self._do_specs_matcher_test( + matches=False, + value="5", + req=' ( 10 20 ]') + + def test_specs_fails_match_float_beyond_scope_with_op_rangein_le(self): + self._do_specs_matcher_test( + matches=False, + value="20.3", + req=' [ 10.1 20.2 ]') + + def test_specs_fails_match_float_beyond_scope_with_op_rangein_lt(self): + self._do_specs_matcher_test( + matches=False, + value="20.3", + req=' [ 10.1 20.2 )') + + def test_specs_fails_match_float_under_scope_with_op_rangein_ge(self): + self._do_specs_matcher_test( + matches=False, + value="5.0", + req=' [ 5.1 20.2 ]') + + def test_specs_fails_match_float_under_scope_with_op_rangein_gt(self): + self._do_specs_matcher_test( + matches=False, + value="5.0", + req=' ( 5.1 20.2 ]') + + def test_specs_matches_int_lower_int_range_with_op_rangein_ge(self): + self._do_specs_matcher_test( + matches=True, + value="10", + req=' [ 10 20 ]') + + def test_specs_fails_matchesint_lower_int_range_with_op_rangein_gt(self): + self._do_specs_matcher_test( + matches=False, + value="10", + req=' ( 10 20 ]') + + def test_specs_matches_float_lower_float_range_with_op_rangein_ge(self): + self._do_specs_matcher_test( + matches=True, + value="10.1", + req=' [ 10.1 20 ]') + + def test_specs_fails_matche_float_lower_float_range_with_op_rangein_gt( + self): + self._do_specs_matcher_test( + matches=False, + value="10.1", + req=' ( 10.1 20 ]') + + def test_specs_matches_int_with_int_range_with_op_rangein(self): + self._do_specs_matcher_test( + matches=True, + value="15", + req=' [ 10 20 ]') + + def test_specs_matches_float_with_int_limit_with_op_rangein(self): + self._do_specs_matcher_test( + matches=True, + value="15.5", + req=' [ 10 20 ]') + + def test_specs_matches_int_upper_int_range_with_op_rangein(self): + self._do_specs_matcher_test( + matches=True, + value="20", + req=' [ 10 20 ]') + + def test_specs_fails_matche_int_upper_int_range_with_op_rangein_lt(self): + self._do_specs_matcher_test( + matches=False, + value="20", + req=' [ 10 20 )') + + def test_specs_matches_float_upper_mixed_range_with_op_rangein(self): + self._do_specs_matcher_test( + matches=True, + value="20.5", + req=' [ 10 20.5 ]') + + def test_specs_fails_matche_float_upper_mixed_range_with_op_rangein_lt( + self): + self._do_specs_matcher_test( + matches=False, + value="20.5", + req=' [ 10 20.5 )') + + def test_specs_matches_float_with_float_limit_with_op_rangein(self): + self._do_specs_matcher_test( + matches=True, + value="12.5", + req=' [ 10.1 20.1 ]') + + def test_specs_matches_only_one_with_op_rangein(self): + self._do_specs_matcher_test( + matches=True, + value="10.1", + req=' [ 10.1 10.1 ]') diff --git a/releasenotes/notes/new_spec_dsl_operator-21c80a46f67c56df.yaml b/releasenotes/notes/new_spec_dsl_operator-21c80a46f67c56df.yaml new file mode 100644 index 00000000..f542dd58 --- /dev/null +++ b/releasenotes/notes/new_spec_dsl_operator-21c80a46f67c56df.yaml @@ -0,0 +1,17 @@ +--- +features: + - | + Introducing a new spec DSL operator called ```` that allows users + to match a numeric value against a range of numbers that are delimited with + lower and upper limits. The new operator is a binary operator that accepts + 4 arguments. + + - The first one and the last one are brackets. ``[`` and ``]`` defines + inclusive limits while ``(`` and ``)`` defines exclusive limits. + + - The second one is the lower limit while the third one is the upper + limit. + + Example: " [ 10.4 20 )" will match a value against an range + such as the lower limit of the range is 10.4 and the upper limit is 20. + Note that 10.4 is included while 20 is excluded.