diff --git a/features/comparisons.feature b/features/comparisons.feature index 5f7df45..fb2ad1d 100644 --- a/features/comparisons.feature +++ b/features/comparisons.feature @@ -15,25 +15,21 @@ Scenario: eq_literal/not_eq_int When CEL expression '-1 == 1' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: eq_literal/eq_int_uint When CEL expression 'dyn(1) == 1u' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: eq_literal/not_eq_int_uint When CEL expression 'dyn(2) == 1u' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: eq_literal/eq_int_double When CEL expression 'dyn(1) == 1.0' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: eq_literal/not_eq_int_double When CEL expression 'dyn(2) == 1.0' is evaluated @@ -49,25 +45,21 @@ Scenario: eq_literal/not_eq_uint When CEL expression '1u == 2u' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: eq_literal/eq_uint_int When CEL expression 'dyn(1u) == 1' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: eq_literal/not_eq_uint_int When CEL expression 'dyn(2u) == 1' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: eq_literal/eq_uint_double When CEL expression 'dyn(1u) == 1.0' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: eq_literal/not_eq_uint_double When CEL expression 'dyn(2u) == 1.0' is evaluated @@ -89,37 +81,31 @@ Scenario: eq_literal/not_eq_double_nan When CEL expression '0.0/0.0 == 0.0/0.0' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: eq_literal/not_eq_int_double_nan When CEL expression 'dyn(1) == 0.0/0.0' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: eq_literal/not_eq_uint_double_nan When CEL expression 'dyn(1u) == 0.0/0.0' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: eq_literal/eq_double_int When CEL expression 'dyn(1.0) == 1' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: eq_literal/not_eq_double_int When CEL expression 'dyn(2.0) == 1' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: eq_literal/eq_double_uint When CEL expression 'dyn(1.0) == 1u' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: eq_literal/not_eq_double_uint When CEL expression 'dyn(2.0) == 1u' is evaluated @@ -207,13 +193,11 @@ Scenario: eq_literal/eq_list_numbers When CEL expression '[1, 2, 3] == [1, 2, 3]' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: eq_literal/eq_list_mixed_type_numbers When CEL expression '[1.0, 2.0, 3] == [1u, 2, 3u]' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: eq_literal/not_eq_list_mixed_type_numbers When CEL expression '[1.0, 2.1] == [1u, 2]' is evaluated @@ -266,7 +250,6 @@ Scenario: eq_literal/eq_map_double_value When CEL expression "{'k':1.0} == {'k':1e+0}" is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: eq_literal/eq_map_mixed_type_numbers When CEL expression '{1: 1.0, 2u: 3u} == {1u: 1, 2: 3.0}' is evaluated @@ -297,14 +280,12 @@ Scenario: eq_literal/not_eq_map_false_vs_types When CEL expression "{'k1': 1, 'k2': 'dos', 'k3': 3} == {'k1': 1, 'k2': 2, 'k3': 4}" is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: eq_literal/eq_mixed_types Given disable_check parameter is True When CEL expression '1.0 == 1' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: eq_literal/eq_list_elem_mixed_types Given disable_check parameter is True @@ -394,73 +375,61 @@ Scenario: eq_literal/not_eq_map_value_null When CEL expression "{1:'hello', 2:'world'} == {1:'goodbye', 2:null}" is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: eq_literal/eq_dyn_int_uint When CEL expression 'dyn(1) == 1u' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: eq_literal/eq_dyn_int_double When CEL expression 'dyn(1) == 1.0' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: eq_literal/eq_dyn_uint_int When CEL expression 'dyn(1u) == 1' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: eq_literal/eq_dyn_uint_double When CEL expression 'dyn(1u) == 1.0' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: eq_literal/eq_dyn_double_int When CEL expression 'dyn(1.0) == 1' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: eq_literal/eq_dyn_double_uint When CEL expression 'dyn(1.0) == 1u' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: eq_literal/not_eq_dyn_int_uint When CEL expression 'dyn(1) == 2u' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: eq_literal/not_eq_dyn_int_double When CEL expression 'dyn(1) == 2.0' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: eq_literal/not_eq_dyn_uint_int When CEL expression 'dyn(1u) == 2' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: eq_literal/not_eq_dyn_uint_double When CEL expression 'dyn(1u) == 120' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: eq_literal/not_eq_dyn_double_int When CEL expression 'dyn(1.0) == 2' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: eq_literal/not_eq_dyn_double_uint When CEL expression 'dyn(1.0) == 2u' is evaluated @@ -875,25 +844,21 @@ Scenario: ne_literal/not_ne_int When CEL expression '1 != 1' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: ne_literal/ne_int_double When CEL expression 'dyn(24) != 24.1' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: ne_literal/not_ne_int_double When CEL expression 'dyn(1) != 1.0' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: ne_literal/ne_int_uint When CEL expression 'dyn(24) != 42u' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: ne_literal/not_ne_int_uint When CEL expression 'dyn(1) != 1u' is evaluated @@ -909,13 +874,11 @@ Scenario: ne_literal/not_ne_uint When CEL expression '99u != 99u' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: ne_literal/ne_uint_double When CEL expression 'dyn(1u) != 2.0' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: ne_literal/not_ne_uint_double When CEL expression 'dyn(99u) != 99.0' is evaluated @@ -932,13 +895,11 @@ Scenario: ne_literal/not_ne_double_nan When CEL expression '0.0/0.0 != 0.0/0.0' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: ne_literal/not_ne_int_double_nan When CEL expression 'dyn(1) != 0.0/0.0' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: ne_literal/not_ne_uint_double_nan When CEL expression 'dyn(1u) != 0.0/0.0' is evaluated @@ -949,25 +910,21 @@ Scenario: ne_literal/not_ne_double When CEL expression '1.0 != 1e+0' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: ne_literal/ne_double_int When CEL expression 'dyn(9000) != 9001.0' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: ne_literal/not_ne_double_int When CEL expression 'dyn(1) != 1e+0' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: ne_literal/ne_double_uint When CEL expression 'dyn(9000u) != 9001.0' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: ne_literal/not_ne_double_uint When CEL expression 'dyn(1u) != 1e+0' is evaluated @@ -1070,7 +1027,6 @@ Scenario: ne_literal/not_ne_map_key_order When CEL expression "{'a':'b','c':'d'} != {'c':'d','a':'b'}" is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: ne_literal/ne_mixed_types Given disable_check parameter is True @@ -1288,13 +1244,11 @@ Scenario: lt_literal/lt_mixed_types_error When CEL expression "'foo' < 1024" is evaluated Then eval_error is 'no such overload' -@wip Scenario: lt_literal/lt_dyn_int_uint When CEL expression 'dyn(1) < 2u' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: lt_literal/lt_dyn_int_double When CEL expression 'dyn(1) < 2.0' is evaluated @@ -1320,13 +1274,11 @@ Scenario: lt_literal/lt_dyn_double_uint When CEL expression 'dyn(1.0) < 2u' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: lt_literal/not_lt_dyn_int_uint When CEL expression 'dyn(1) < 1u' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: lt_literal/not_lt_dyn_int_double When CEL expression 'dyn(1) < 1.0' is evaluated @@ -1352,13 +1304,11 @@ Scenario: lt_literal/not_lt_dyn_double_uint When CEL expression 'dyn(1.0) < 1u' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: lt_literal/lt_dyn_int_big_uint When CEL expression 'dyn(1) < 9223372036854775808u' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: lt_literal/lt_dyn_small_int_uint When CEL expression 'dyn(-1) < 0u' is evaluated @@ -1370,19 +1320,16 @@ Scenario: lt_literal/not_lt_dyn_int_big_lossy_double When CEL expression 'dyn(9223372036854775807) < 9223372036854775808.0' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: lt_literal/lt_dyn_int_big_lossy_double When CEL expression 'dyn(9223372036854775807) < 9223372036854777857.0' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: lt_literal/not_lt_dyn_int_small_double When CEL expression 'dyn(9223372036854775807) < -9223372036854777857.0' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: lt_literal/not_lt_dyn_int_small_lossy_double When CEL expression 'dyn(-9223372036854775808) < -9223372036854775809.0' is evaluated @@ -1531,13 +1478,11 @@ Scenario: gt_literal/gt_mixed_types_error When CEL expression "'foo' > 1024" is evaluated Then eval_error is 'no such overload' -@wip Scenario: gt_literal/gt_dyn_int_uint When CEL expression 'dyn(2) > 1u' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: gt_literal/gt_dyn_int_double When CEL expression 'dyn(2) > 1.0' is evaluated @@ -1563,13 +1508,11 @@ Scenario: gt_literal/gt_dyn_double_uint When CEL expression 'dyn(2.0) > 1u' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: gt_literal/not_gt_dyn_int_uint When CEL expression 'dyn(1) > 1u' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: gt_literal/not_gt_dyn_int_double When CEL expression 'dyn(1) > 1.0' is evaluated @@ -1595,25 +1538,21 @@ Scenario: gt_literal/not_gt_dyn_double_uint When CEL expression 'dyn(1.0) > 1u' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: gt_literal/not_gt_dyn_int_big_uint When CEL expression 'dyn(1) > 9223372036854775808u' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: gt_literal/not_gt_dyn_small_int_uint When CEL expression 'dyn(-1) > 0u' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: gt_literal/not_gt_dyn_int_big_double When CEL expression 'dyn(9223372036854775807) > 9223372036854775808.0' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: gt_literal/not_gt_dyn_int_small_lossy_double The conversion of the int to double is lossy and the numbers end up being equal @@ -1621,7 +1560,6 @@ Scenario: gt_literal/not_gt_dyn_int_small_lossy_double When CEL expression 'dyn(-9223372036854775808) > -9223372036854775809.0' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: gt_literal/gt_dyn_int_small_lossy_double_greater When CEL expression 'dyn(-9223372036854775808) > -9223372036854777857.0' is evaluated @@ -1796,13 +1734,11 @@ Scenario: lte_literal/lte_mixed_types_error When CEL expression "'foo' <= 1024" is evaluated Then eval_error is 'no such overload' -@wip Scenario: lte_literal/lte_dyn_int_uint When CEL expression 'dyn(1) <= 2u' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: lte_literal/lte_dyn_int_double When CEL expression 'dyn(1) <= 2.0' is evaluated @@ -1828,13 +1764,11 @@ Scenario: lte_literal/lte_dyn_double_uint When CEL expression 'dyn(1.0) <= 2u' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: lte_literal/not_lte_dyn_int_uint When CEL expression 'dyn(2) <= 1u' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: lte_literal/not_lte_dyn_int_double When CEL expression 'dyn(2) <= 1.0' is evaluated @@ -1860,25 +1794,21 @@ Scenario: lte_literal/not_lte_dyn_double_uint When CEL expression 'dyn(2.0) <= 1u' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: lte_literal/lte_dyn_int_big_uint When CEL expression 'dyn(1) <= 9223372036854775808u' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: lte_literal/lte_dyn_small_int_uint When CEL expression 'dyn(-1) <= 0u' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: lte_literal/lte_dyn_int_big_double When CEL expression 'dyn(9223372036854775807) <= 9223372036854775808.0' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: lte_literal/lte_dyn_int_small_lossy_double The conversion of the int to double is lossy and the numbers end up being equal @@ -1886,7 +1816,6 @@ Scenario: lte_literal/lte_dyn_int_small_lossy_double When CEL expression 'dyn(-9223372036854775808) <= -9223372036854775809.0' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: lte_literal/not_lte_dyn_int_small_lossy_double_less When CEL expression 'dyn(-9223372036854775808) <= -9223372036854777857.0' is evaluated @@ -2071,13 +2000,11 @@ Scenario: gte_literal/gte_mixed_types_error When CEL expression "'foo' >= 1.0" is evaluated Then eval_error is 'no such overload' -@wip Scenario: gte_literal/gte_dyn_int_uint When CEL expression 'dyn(2) >= 1u' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: gte_literal/gte_dyn_int_double When CEL expression 'dyn(2) >= 1.0' is evaluated @@ -2103,13 +2030,11 @@ Scenario: gte_literal/gte_dyn_double_uint When CEL expression 'dyn(2.0) >= 1u' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: gte_literal/not_gte_dyn_int_uint When CEL expression 'dyn(0) >= 1u' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: gte_literal/not_gte_dyn_int_double When CEL expression 'dyn(0) >= 1.0' is evaluated @@ -2135,13 +2060,11 @@ Scenario: gte_literal/not_gte_dyn_double_uint When CEL expression 'dyn(0.0) >= 1u' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: gte_literal/not_gte_dyn_int_big_uint When CEL expression 'dyn(1) >= 9223372036854775808u' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: gte_literal/not_gte_dyn_small_int_uint When CEL expression 'dyn(-1) >= 0u' is evaluated @@ -2153,13 +2076,11 @@ Scenario: gte_literal/gte_dyn_int_big_lossy_double When CEL expression 'dyn(9223372036854775807) >= 9223372036854775808.0' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: gte_literal/not_gte_dyn_int_big_double When CEL expression 'dyn(9223372036854775807) >= 9223372036854777857.0' is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: gte_literal/gte_dyn_int_small_lossy_double_equal The conversion of the int to double is lossy and the numbers end up being equal @@ -2167,7 +2088,6 @@ Scenario: gte_literal/gte_dyn_int_small_lossy_double_equal When CEL expression 'dyn(-9223372036854775808) >= -9223372036854775809.0' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: gte_literal/gte_dyn_int_small_lossy_double_greater When CEL expression 'dyn(-9223372036854775808) >= -9223372036854777857.0' is evaluated diff --git a/features/fields.feature b/features/fields.feature index aa41675..8b38aea 100644 --- a/features/fields.feature +++ b/features/fields.feature @@ -41,13 +41,11 @@ Scenario: map_fields/map_key_mixed_numbers_lossy_double_key When CEL expression '{1u: 1.0, 2: 2.0, 3u: 3.0}[3.1]' is evaluated Then eval_error is 'no such key' -@wip Scenario: map_fields/map_key_mixed_numbers_uint_key When CEL expression '{1u: 1.0, 2: 2.0, 3u: 3.0}[2u]' is evaluated Then value is celpy.celtypes.DoubleType(source=2.0) -@wip Scenario: map_fields/map_key_mixed_numbers_int_key When CEL expression '{1u: 1.0, 2: 2.0, 3u: 3.0}[1]' is evaluated @@ -357,13 +355,11 @@ Scenario: in/absent When CEL expression "'spider' in {'ant': 6, 'fly': 6, 'centipede': 100}" is evaluated Then value is celpy.celtypes.BoolType(source=False) -@wip Scenario: in/mixed_numbers_and_keys_present When CEL expression '3.0 in {1: 1, 2: 2, 3u: 3} && 2u in {1u: 1, 2: 2} && 1 in {1u: 1, 2: 2}' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: in/mixed_numbers_and_keys_absent When CEL expression '3.1 in {1: 1, 2: 2, 3u: 3}' is evaluated diff --git a/features/json_query.feature b/features/json_query.feature index ff9b1d0..1b08755 100644 --- a/features/json_query.feature +++ b/features/json_query.feature @@ -258,13 +258,14 @@ And exit status is 0 Scenario: JQ Conditionals and Comparisons: == This requires a syntax change. - The semantics of == are different. CEL does not do the required type coercions. + The semantics of == are different. CEL does not coerce strings to numbers, however + it does compare int, uint, and double heterogeneously, per the CEL spec. Given JSON document '1' And JSON document '1.0' And JSON document '"1"' And JSON document '"banana"' When echo document | celpy --json-document=_ '_ == 1' is run -Then stdout is 'true\nnull\nnull\nnull\n' +Then stdout is 'true\ntrue\nnull\nnull\n' And stderr is '' And exit status is 0 @@ -288,6 +289,24 @@ And stderr is '' And exit status is 0 +Scenario: heterogeneous numeric comparison, double on the left + https://github.com/cloud-custodian/cel-python/issues/114 +When celpy -n '4.0 < 10' is run +Then stdout is 'true\n' +And stderr is '' +And exit status is 0 + + +Scenario: heterogeneous numeric comparison, int on the left + https://github.com/cloud-custodian/cel-python/issues/114 + Previously raised "found no matching overload" -- the type coercion was + only applied when the double was the left-hand operand. +When celpy -n '10 < 4.0' is run +Then stdout is 'false\n' +And stderr is '' +And exit status is 0 + + Scenario: JQ Conditionals and Comparisons: and/or/not, example 1 This requires a syntax change. The semantics of logical operators are different. CEL does not do the same type coercions. diff --git a/features/lists.feature b/features/lists.feature index 83fadf6..8585aad 100644 --- a/features/lists.feature +++ b/features/lists.feature @@ -152,37 +152,31 @@ Scenario: in/last When CEL expression '20u in [4u, 6u, 8u, 12u, 20u]' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: in/double_in_ints When CEL expression 'dyn(3.0) in [5, 4, 3, 2, 1]' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: in/uint_in_ints When CEL expression 'dyn(3u) in [5, 4, 3, 2, 1]' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: in/int_in_doubles When CEL expression 'dyn(3) in [5.0, 4.0, 3.0, 2.0, 1.0]' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: in/uint_in_doubles When CEL expression 'dyn(3u) in [5.0, 4.0, 3.0, 2.0, 1.0]' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: in/int_in_uints When CEL expression 'dyn(3) in [5u, 4u, 3u, 2u, 1u]' is evaluated Then value is celpy.celtypes.BoolType(source=True) -@wip Scenario: in/double_in_uints When CEL expression 'dyn(3.0) in [5u, 4u, 3u, 2u, 1u]' is evaluated diff --git a/features/proto2.feature b/features/proto2.feature index e27316e..351ba22 100644 --- a/features/proto2.feature +++ b/features/proto2.feature @@ -208,7 +208,6 @@ Scenario: literal_wellknown/timestamp When CEL expression "TestAllTypes{single_timestamp: timestamp('2009-02-13T23:31:30Z')}" is evaluated Then value is TestAllTypes(single_timestamp=celpy.celtypes.TimestampType(datetime.datetime(2009, 2, 13, 23, 31, 30, tzinfo=datetime.timezone.utc))) -@wip Scenario: literal_wellknown/struct Given container is 'cel.expr.conformance.proto2' @@ -302,7 +301,6 @@ Scenario: empty_field/scalar_with_default When CEL expression 'TestAllTypes{}.single_int32' is evaluated Then value is celpy.celtypes.IntType(source=-32) -@wip Scenario: empty_field/scalar_no_default Given container is 'cel.expr.conformance.proto2' diff --git a/features/proto3.feature b/features/proto3.feature index 471a404..cce277f 100644 --- a/features/proto3.feature +++ b/features/proto3.feature @@ -124,7 +124,6 @@ Scenario: literal_wellknown/timestamp When CEL expression "TestAllTypes{single_timestamp: timestamp('2009-02-13T23:31:30Z')}" is evaluated Then value is TestAllTypes(single_timestamp=celpy.celtypes.TimestampType(datetime.datetime(2009, 2, 13, 23, 31, 30, tzinfo=datetime.timezone.utc))) -@wip Scenario: literal_wellknown/struct Given container is 'cel.expr.conformance.proto3' @@ -211,7 +210,6 @@ Scenario: singular_bind/int64 # empty_field -- Tests on empty fields. -@wip Scenario: empty_field/scalar Given container is 'cel.expr.conformance.proto3' diff --git a/src/celpy/celtypes.py b/src/celpy/celtypes.py index 2f71118..cab43b8 100644 --- a/src/celpy/celtypes.py +++ b/src/celpy/celtypes.py @@ -188,6 +188,7 @@ import datetime import logging +import operator import re from functools import reduce, wraps from math import fsum, trunc @@ -250,18 +251,48 @@ ] +# Dispatch table for the heterogeneous numeric comparisons in type_matched(). +_COMPARISON_OPERATORS: Dict[str, Callable[[Any, Any], Any]] = { + "__eq__": operator.eq, + "__ne__": operator.ne, + "__lt__": operator.lt, + "__le__": operator.le, + "__gt__": operator.gt, + "__ge__": operator.ge, +} + + def type_matched(method: Callable[[Any, Any], Any]) -> Callable[[Any, Any], Any]: - """Decorates a method to assure the "other" value has the same type.""" + """ + Decorates a method to assure the "other" value has the same type. + + IntType, UintType, and DoubleType are excepted: the CEL spec requires the + six comparison operators (but not arithmetic) to work across them in + any operand order. See the "Numeric Values" section of + https://github.com/google/cel-spec/blob/master/doc/langdef.md, and Issue + #114. BoolType subclasses int but is a distinct CEL type, so it is not + part of the exception. + """ @wraps(method) def type_matching_method(self: Any, other: Any) -> Any: - if not ( - issubclass(type(other), type(self)) or issubclass(type(self), type(other)) + if issubclass(type(other), type(self)) or issubclass(type(self), type(other)): + return method(self, other) + if isinstance(self, (IntType, UintType, DoubleType)) and isinstance( + other, (IntType, UintType, DoubleType) ): - raise TypeError( - f"no such overload: {self!r} {type(self)} != {other!r} {type(other)}" + # Unwrap to the natural Python type, never int -> float: the native + # mixed comparison is exact where a cast loses precision past 2**53. + self_value: Union[int, float] = ( + float(self) if isinstance(self, DoubleType) else int(self) ) - return method(self, other) + other_value: Union[int, float] = ( + float(other) if isinstance(other, DoubleType) else int(other) + ) + return _COMPARISON_OPERATORS[method.__name__](self_value, other_value) + raise TypeError( + f"no such overload: {self!r} {type(self)} != {other!r} {type(other)}" + ) return type_matching_method diff --git a/tests/test_celtypes.py b/tests/test_celtypes.py index a5f0cc9..2522305 100644 --- a/tests/test_celtypes.py +++ b/tests/test_celtypes.py @@ -211,6 +211,134 @@ def test_uint_type(): assert UintType(MessageType({"value": UintType(42)})) == UintType(42) +def test_heterogeneous_numeric_comparison_issue_114(): + """https://github.com/cloud-custodian/cel-python/issues/114""" + assert DoubleType(4.0) < IntType(10) + assert IntType(10) > DoubleType(4.0) + assert not (IntType(10) < DoubleType(4.0)) + assert not (DoubleType(4.0) > IntType(10)) + + +def test_heterogeneous_numeric_comparison_int_uint(): + assert IntType(1) < UintType(2) + assert UintType(2) > IntType(1) + assert IntType(2) == UintType(2) + assert UintType(2) == IntType(2) + assert IntType(1) != UintType(2) + assert UintType(2) != IntType(1) + assert IntType(1) <= UintType(1) + assert UintType(1) <= IntType(1) + assert IntType(2) >= UintType(2) + assert UintType(2) >= IntType(2) + # A negative int is always less than any uint. + assert IntType(-1) < UintType(0) + assert UintType(0) > IntType(-1) + # A uint outside the int64 range is always greater than any int. + big_uint = UintType(2**64 - 1) + assert IntType(5) < big_uint + assert big_uint > IntType(5) + + +def test_heterogeneous_numeric_comparison_int_double(): + assert IntType(1) < DoubleType(1.5) + assert DoubleType(1.5) > IntType(1) + assert IntType(2) == DoubleType(2.0) + assert DoubleType(2.0) == IntType(2) + assert IntType(1) != DoubleType(1.5) + assert DoubleType(1.5) != IntType(1) + assert IntType(1) <= DoubleType(1.0) + assert DoubleType(1.0) <= IntType(1) + assert IntType(2) >= DoubleType(2.0) + assert DoubleType(2.0) >= IntType(2) + + +def test_heterogeneous_numeric_comparison_uint_double(): + assert UintType(1) < DoubleType(1.5) + assert DoubleType(1.5) > UintType(1) + assert UintType(2) == DoubleType(2.0) + assert DoubleType(2.0) == UintType(2) + assert UintType(1) != DoubleType(1.5) + assert DoubleType(1.5) != UintType(1) + assert UintType(1) <= DoubleType(1.0) + assert DoubleType(1.0) <= UintType(1) + assert UintType(2) >= DoubleType(2.0) + assert DoubleType(2.0) >= UintType(2) + + +def test_heterogeneous_numeric_comparison_exact_beyond_2_53(): + """A double cannot represent every integer above 2**53, so converting the + int side would make these (wrongly) equal.""" + big_int = IntType(2**53 + 1) + boundary_double = DoubleType(float(2**53)) + assert big_int != boundary_double + assert not (big_int == boundary_double) + assert big_int > boundary_double + assert not (big_int < boundary_double) + assert boundary_double < big_int + assert boundary_double != big_int + # Same check against uint. + big_uint = UintType(2**53 + 1) + assert big_uint > boundary_double + assert boundary_double < big_uint + assert big_uint != boundary_double + + +def test_heterogeneous_numeric_comparison_nan(): + nan = DoubleType(float("nan")) + for value in (IntType(5), UintType(5), DoubleType(5.0)): + assert not (nan == value) + assert not (value == nan) + assert nan != value + assert value != nan + assert not (nan < value) + assert not (value < nan) + assert not (nan <= value) + assert not (value <= nan) + assert not (nan > value) + assert not (value > nan) + assert not (nan >= value) + assert not (value >= nan) + + +def test_heterogeneous_numeric_comparison_infinity(): + inf = DoubleType(float("inf")) + neg_inf = DoubleType(float("-inf")) + for value in (IntType(5), UintType(5)): + assert inf > value + assert value < inf + assert neg_inf < value + assert value > neg_inf + + +def test_heterogeneous_numeric_comparison_negative_zero(): + assert DoubleType(-0.0) == IntType(0) + assert IntType(0) == DoubleType(-0.0) + assert DoubleType(-0.0) == UintType(0) + assert UintType(0) == DoubleType(-0.0) + assert not (DoubleType(-0.0) < IntType(0)) + assert not (IntType(0) < DoubleType(-0.0)) + + +def test_heterogeneous_numeric_comparison_rejects_non_numeric_types(): + """BoolType subclasses int, but is a distinct CEL type and stays rejected.""" + with pytest.raises(TypeError): + IntType(1) == BoolType(True) + with pytest.raises(TypeError): + UintType(1) == BoolType(True) + with pytest.raises(TypeError): + DoubleType(1.0) == StringType("1.0") + with pytest.raises(TypeError): + IntType(1) < StringType("1") + + +def test_heterogeneous_numeric_arithmetic_still_rejected(): + """CEL relaxes comparison across numeric types, but not arithmetic.""" + with pytest.raises(TypeError): + IntType(1) + DoubleType(2.0) + with pytest.raises(TypeError): + UintType(1) + DoubleType(2.0) + + def test_list_type(): l_1 = ListType([IntType(42), IntType(6), IntType(7)]) l_2 = ListType( diff --git a/tools/tags.toml b/tools/tags.toml index f5018f2..7fb2333 100644 --- a/tools/tags.toml +++ b/tools/tags.toml @@ -52,45 +52,14 @@ ternary = "@wip" timestamp = "@wip" [comparisons.eq_literal] -eq_double_int = "@wip" -eq_double_uint = "@wip" -eq_dyn_double_int = "@wip" -eq_dyn_double_uint = "@wip" -eq_dyn_int_double = "@wip" -eq_dyn_int_uint = "@wip" eq_dyn_json_null = "@wip" -eq_dyn_uint_double = "@wip" -eq_dyn_uint_int = "@wip" -eq_int_double = "@wip" -eq_int_uint = "@wip" -eq_list_elem_mixed_types = "@wip" -eq_list_mixed_type_numbers = "@wip" -eq_map_mixed_type_numbers = "@wip" eq_map_value_mixed_types = "@wip" -eq_mixed_types = "@wip" -eq_uint_double = "@wip" -eq_uint_int = "@wip" -not_eq_double_int = "@wip" not_eq_double_nan = "@wip" -not_eq_double_uint = "@wip" -not_eq_dyn_double_int = "@wip" not_eq_dyn_double_null = "@wip" -not_eq_dyn_double_uint = "@wip" -not_eq_dyn_int_double = "@wip" not_eq_dyn_int_null = "@wip" -not_eq_dyn_int_uint = "@wip" not_eq_dyn_timestamp_null = "@wip" -not_eq_dyn_uint_double = "@wip" -not_eq_dyn_uint_int = "@wip" -not_eq_int_double = "@wip" -not_eq_int_double_nan = "@wip" -not_eq_int_uint = "@wip" not_eq_list_elem_null = "@wip" -not_eq_list_mixed_type_numbers = "@wip" not_eq_map_null = "@wip" -not_eq_uint_double = "@wip" -not_eq_uint_double_nan = "@wip" -not_eq_uint_int = "@wip" [comparisons.eq_wrapper] eq_bool = "@wip" @@ -147,30 +116,12 @@ eq_uint64_proto2_null = "@wip" eq_uint64_proto3_null = "@wip" [comparisons.gt_literal] -gt_dyn_int_double = "@wip" -gt_dyn_int_small_lossy_double_greater = "@wip" -gt_dyn_int_uint = "@wip" not_gt_dyn_big_double_int = "@wip" -not_gt_dyn_int_big_double = "@wip" -not_gt_dyn_int_big_uint = "@wip" -not_gt_dyn_int_double = "@wip" -not_gt_dyn_int_small_lossy_double = "@wip" -not_gt_dyn_int_uint = "@wip" not_gt_dyn_small_double_int = "@wip" -not_gt_dyn_small_int_uint = "@wip" [comparisons.gte_literal] gte_dyn_int_big_lossy_double = "@wip" -gte_dyn_int_double = "@wip" -gte_dyn_int_small_lossy_double_equal = "@wip" -gte_dyn_int_small_lossy_double_greater = "@wip" -gte_dyn_int_uint = "@wip" gte_dyn_small_double_int = "@wip" -not_gte_dyn_int_big_double = "@wip" -not_gte_dyn_int_big_uint = "@wip" -not_gte_dyn_int_double = "@wip" -not_gte_dyn_int_uint = "@wip" -not_gte_dyn_small_int_uint = "@wip" [comparisons.in_list_literal] elem_in_mixed_type_list_cross_type = "@wip" @@ -179,52 +130,21 @@ elem_in_mixed_type_list_cross_type = "@wip" key_in_mixed_key_type_map_cross_type = "@wip" [comparisons.lt_literal] -lt_dyn_int_big_lossy_double = "@wip" -lt_dyn_int_big_uint = "@wip" -lt_dyn_int_double = "@wip" -lt_dyn_int_uint = "@wip" -lt_dyn_small_int_uint = "@wip" not_lt_dyn_int_big_lossy_double = "@wip" -not_lt_dyn_int_double = "@wip" -not_lt_dyn_int_small_double = "@wip" -not_lt_dyn_int_small_lossy_double = "@wip" -not_lt_dyn_int_uint = "@wip" not_lt_dyn_small_double_int = "@wip" [comparisons.lte_literal] lte_dyn_big_double_int = "@wip" -lte_dyn_int_big_double = "@wip" -lte_dyn_int_big_uint = "@wip" -lte_dyn_int_double = "@wip" -lte_dyn_int_small_lossy_double = "@wip" -lte_dyn_int_uint = "@wip" lte_dyn_small_double_int = "@wip" -lte_dyn_small_int_uint = "@wip" -not_lte_dyn_int_double = "@wip" -not_lte_dyn_int_small_lossy_double_less = "@wip" -not_lte_dyn_int_uint = "@wip" [comparisons.ne_literal] -ne_double_int = "@wip" ne_double_nan = "@wip" -ne_double_uint = "@wip" -ne_int_double = "@wip" -ne_int_uint = "@wip" -ne_mixed_types = "@wip" ne_proto2_any_unpack = "@wip" ne_proto2_any_unpack_bytewise_fallback = "@wip" ne_proto3_any_unpack = "@wip" ne_proto3_any_unpack_bytewise_fallback = "@wip" ne_proto_different_types = "@wip" -ne_uint_double = "@wip" -not_ne_double_int = "@wip" not_ne_double_nan = "@wip" -not_ne_double_uint = "@wip" -not_ne_int_double = "@wip" -not_ne_int_double_nan = "@wip" -not_ne_int_uint = "@wip" -not_ne_uint_double = "@wip" -not_ne_uint_double_nan = "@wip" [conversions.identity] timestamp = "@wip" @@ -389,13 +309,9 @@ comparison_true = "@wip" convert_symbol_to_int = "@wip" [fields.in] -mixed_numbers_and_keys_absent = "@wip" -mixed_numbers_and_keys_present = "@wip" [fields.map_fields] map_key_mixed_numbers_double_key = "@wip" -map_key_mixed_numbers_int_key = "@wip" -map_key_mixed_numbers_uint_key = "@wip" [fields.quoted_map_fields] field_access_dash = "@wip" @@ -406,12 +322,6 @@ has_field_dot = "@wip" has_field_slash = "@wip" [lists.in] -double_in_ints = "@wip" -double_in_uints = "@wip" -int_in_doubles = "@wip" -int_in_uints = "@wip" -uint_in_doubles = "@wip" -uint_in_ints = "@wip" [lists.index] zero_based_double = "@wip" @@ -1013,7 +923,6 @@ nested_message_subfield = "@wip" repeated_enum = "@wip" repeated_nested = "@wip" repeated_scalar = "@wip" -scalar_no_default = "@wip" scalar_with_default = "@wip" wkt = "@wip" @@ -1075,7 +984,6 @@ uint32_eq_double = "@wip" uint32_eq_int = "@wip" [proto2.literal_wellknown] -struct = "@wip" [proto2.quoted_fields] get_field_with_quoted_name = "@wip" @@ -1125,7 +1033,6 @@ nested_message_subfield = "@wip" repeated_enum = "@wip" repeated_nested = "@wip" repeated_scalar = "@wip" -scalar = "@wip" wkt = "@wip" [proto3.has] @@ -1151,7 +1058,6 @@ float = "@wip" int64_nocontainer = "@wip" [proto3.literal_wellknown] -struct = "@wip" [proto3.quoted_fields] get_field = "@wip"