fix: special operators in compare util (#34145)

This commit is contained in:
Raffael Meyer 2025-09-25 20:45:29 +02:00 committed by GitHub
parent 98d25b31cd
commit 878c089679
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 136 additions and 4 deletions

View file

@ -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):

View file

@ -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)