fix: special operators in compare util (#34145)
This commit is contained in:
parent
98d25b31cd
commit
878c089679
2 changed files with 136 additions and 4 deletions
|
|
@ -59,6 +59,7 @@ from frappe.utils.data import (
|
|||
cint,
|
||||
comma_and,
|
||||
comma_or,
|
||||
compare,
|
||||
cstr,
|
||||
duration_to_seconds,
|
||||
evaluate_filters,
|
||||
|
|
@ -236,6 +237,111 @@ class TestFilters(IntegrationTestCase):
|
|||
}
|
||||
self.assertFalse(evaluate_filters(doc, [("last_password_reset_date", "Timespan", "today")]))
|
||||
|
||||
def test_is_operator(self):
|
||||
"""Test 'is' operator for checking if values are set or not set."""
|
||||
# Test "is set" with different fieldtypes and values
|
||||
self.assertTrue(compare("1", "is", "set", "Int"))
|
||||
self.assertTrue(compare(1, "is", "set", "Int"))
|
||||
self.assertTrue(compare(0, "is", "set", "Int")) # 0 is considered "set"
|
||||
self.assertTrue(compare("hello", "is", "set", "Data"))
|
||||
self.assertTrue(compare(0.0, "is", "set", "Float"))
|
||||
|
||||
# Test "is set" with unset values - None should always be "not set" regardless of fieldtype
|
||||
self.assertFalse(compare(None, "is", "set", "Int"))
|
||||
self.assertFalse(compare(None, "is", "set", "Float"))
|
||||
self.assertFalse(compare(None, "is", "set", "Check"))
|
||||
self.assertFalse(compare(None, "is", "set", "Data"))
|
||||
self.assertFalse(compare("", "is", "set"))
|
||||
self.assertFalse(compare("", "is", "set", "Data"))
|
||||
self.assertFalse(compare(None, "is", "set"))
|
||||
|
||||
# Test "is not set" with set values
|
||||
self.assertFalse(compare("1", "is", "not set", "Int"))
|
||||
self.assertFalse(compare(1, "is", "not set", "Int"))
|
||||
self.assertFalse(compare(0, "is", "not set", "Int"))
|
||||
self.assertFalse(compare("hello", "is", "not set", "Data"))
|
||||
self.assertFalse(compare(0.0, "is", "not set", "Float"))
|
||||
|
||||
# Test "is not set" with unset values - None should always be "not set" regardless of fieldtype
|
||||
self.assertTrue(compare(None, "is", "not set", "Int"))
|
||||
self.assertTrue(compare(None, "is", "not set", "Float"))
|
||||
self.assertTrue(compare(None, "is", "not set", "Check"))
|
||||
self.assertTrue(compare(None, "is", "not set", "Data"))
|
||||
self.assertTrue(compare("", "is", "not set"))
|
||||
self.assertTrue(compare("", "is", "not set", "Data"))
|
||||
self.assertTrue(compare(None, "is", "not set"))
|
||||
|
||||
def test_in_operators(self):
|
||||
"""Test 'in' and 'not in' operators with and without fieldtype casting."""
|
||||
test_list = ["a", "b", "c"]
|
||||
|
||||
# Test "in" operator without fieldtype
|
||||
self.assertTrue(compare("a", "in", test_list))
|
||||
self.assertFalse(compare("", "in", test_list))
|
||||
self.assertFalse(compare("d", "in", test_list))
|
||||
self.assertFalse(compare(None, "in", test_list))
|
||||
|
||||
# Test "not in" operator without fieldtype
|
||||
self.assertFalse(compare("a", "not in", test_list))
|
||||
self.assertTrue(compare("", "not in", test_list))
|
||||
self.assertTrue(compare("d", "not in", test_list))
|
||||
self.assertTrue(compare(None, "not in", test_list))
|
||||
|
||||
# Test "in" operator with fieldtype casting - only first value should be cast
|
||||
string_list = ["1", "2", "3"]
|
||||
self.assertTrue(compare(1, "in", string_list, "Data"))
|
||||
self.assertTrue(compare("2", "in", string_list, "Data"))
|
||||
self.assertFalse(compare(4, "in", string_list, "Data"))
|
||||
|
||||
# Test type mismatch: Int fieldtype with string list (val2 is NOT cast)
|
||||
mixed_list = ["1", "2", "3"]
|
||||
self.assertFalse(compare("1", "in", mixed_list, "Int"))
|
||||
self.assertFalse(compare(1, "in", mixed_list, "Int"))
|
||||
|
||||
# Test with matching types: Int fieldtype with int list
|
||||
int_list = [1, 2, 3]
|
||||
self.assertTrue(compare("1", "in", int_list, "Int"))
|
||||
self.assertTrue(compare(2, "in", int_list, "Int"))
|
||||
self.assertFalse(compare("4", "in", int_list, "Int"))
|
||||
|
||||
# Test "not in" operator with fieldtype casting
|
||||
self.assertFalse(compare(1, "not in", string_list, "Data"))
|
||||
self.assertFalse(compare("2", "not in", string_list, "Data"))
|
||||
self.assertTrue(compare(4, "not in", string_list, "Data"))
|
||||
|
||||
# Test "not in" with type mismatch
|
||||
self.assertTrue(compare("1", "not in", mixed_list, "Int"))
|
||||
self.assertFalse(compare("1", "not in", int_list, "Int"))
|
||||
|
||||
# Test with Float fieldtype
|
||||
float_list = [1.5, 2.5, 3.5]
|
||||
self.assertTrue(compare("1.5", "in", float_list, "Float"))
|
||||
self.assertFalse(compare("4.5", "in", float_list, "Float"))
|
||||
|
||||
# Test None with "in"/"not in" operators - None should not be cast
|
||||
self.assertFalse(compare(None, "in", [""], "Data"))
|
||||
self.assertFalse(compare(None, "in", [0], "Int"))
|
||||
self.assertFalse(compare(None, "in", [0.0], "Float"))
|
||||
self.assertFalse(compare(None, "in", ["", "test"], "Data"))
|
||||
self.assertTrue(compare(None, "in", [None, "test"], "Data"))
|
||||
|
||||
# Test "not in" with None
|
||||
self.assertTrue(compare(None, "not in", [""], "Data"))
|
||||
self.assertTrue(compare(None, "not in", [0], "Int"))
|
||||
self.assertTrue(compare(None, "not in", [0.0], "Float"))
|
||||
self.assertTrue(compare(None, "not in", ["", "test"], "Data"))
|
||||
self.assertFalse(compare(None, "not in", [None, "test"], "Data"))
|
||||
|
||||
def test_is_operator_case_insensitive(self):
|
||||
"""Test that 'is' operator patterns are case insensitive."""
|
||||
self.assertTrue(compare("value", "is", "SET"))
|
||||
self.assertTrue(compare("value", "is", "Set"))
|
||||
self.assertTrue(compare("value", "is", "set"))
|
||||
|
||||
self.assertTrue(compare(None, "is", "NOT SET"))
|
||||
self.assertTrue(compare(None, "is", "Not Set"))
|
||||
self.assertTrue(compare(None, "is", "not set"))
|
||||
|
||||
|
||||
class TestMoney(IntegrationTestCase):
|
||||
def test_money_in_words(self):
|
||||
|
|
|
|||
|
|
@ -2021,7 +2021,7 @@ def sql_like(value: str, pattern: str) -> bool:
|
|||
return pattern in value
|
||||
|
||||
|
||||
def filter_operator_is(value: str, pattern: str) -> bool:
|
||||
def filter_operator_is(value: str | None, pattern: str) -> bool:
|
||||
"""Operator `is` can have two values: 'set' or 'not set'."""
|
||||
pattern = pattern.lower()
|
||||
|
||||
|
|
@ -2082,11 +2082,37 @@ def evaluate_filters(doc: "Mapping", filters: FilterSignature):
|
|||
return True
|
||||
|
||||
|
||||
def compare(val1: Any, condition: str, val2: Any, fieldtype: str | None = None):
|
||||
def compare(val1: Any, condition: str, val2: Any, fieldtype: str | None = None) -> bool:
|
||||
"""Compare two values using the specified operator with optional fieldtype casting.
|
||||
|
||||
Args:
|
||||
val1: The left operand value to compare
|
||||
condition: The comparison operator (e.g., "=", ">", "is", "in", "like")
|
||||
val2: The right operand value to compare against
|
||||
fieldtype: Optional fieldtype for casting val1 (and val2 for most operators)
|
||||
|
||||
Returns:
|
||||
bool: True if the comparison evaluates to True, False otherwise
|
||||
|
||||
Note:
|
||||
- For "is" operator: No casting is performed to preserve None values
|
||||
- For "in"/"not in" operators: Only val1 is cast (if not None), val2 remains unchanged
|
||||
- For "Timespan" operator: No casting is performed
|
||||
- For other operators: Both val1 and val2 are cast to the specified fieldtype
|
||||
"""
|
||||
if fieldtype:
|
||||
val1 = cast(fieldtype, val1)
|
||||
if condition != "Timespan":
|
||||
if condition in {"is", "Timespan"}:
|
||||
# No casting to preserve original values
|
||||
pass
|
||||
elif condition in {"in", "not in"}:
|
||||
# Cast only val1 (if not None), preserve val2 container
|
||||
if val1 is not None:
|
||||
val1 = cast(fieldtype, val1)
|
||||
else:
|
||||
# Cast both values for comparison operators (=, !=, >, <, >=, <=, like, etc.)
|
||||
val1 = cast(fieldtype, val1)
|
||||
val2 = cast(fieldtype, val2)
|
||||
|
||||
if condition in operator_map:
|
||||
return operator_map[condition](val1, val2)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue