Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 0 additions & 80 deletions features/comparisons.feature

Large diffs are not rendered by default.

4 changes: 0 additions & 4 deletions features/fields.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
23 changes: 21 additions & 2 deletions features/json_query.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down
6 changes: 0 additions & 6 deletions features/lists.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions features/proto2.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down
2 changes: 0 additions & 2 deletions features/proto3.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down
43 changes: 37 additions & 6 deletions src/celpy/celtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@

import datetime
import logging
import operator
import re
from functools import reduce, wraps
from math import fsum, trunc
Expand Down Expand Up @@ -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

Expand Down
128 changes: 128 additions & 0 deletions tests/test_celtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading