diff --git a/reflex/vars.py b/reflex/vars.py index 770ab8598..1000454a5 100644 --- a/reflex/vars.py +++ b/reflex/vars.py @@ -685,6 +685,76 @@ class Var(ABC): """ return self.operation("||", other, type_=bool, flip=True) + def __contains__(self, _: Any) -> Var: + """Override the 'in' operator to alert the user that it is not supported. + + Raises: + TypeError: the operation is not supported + """ + raise TypeError( + "'in' operator not supported for Var types, use Var.contains() instead." + ) + + def contains(self, other: Any) -> Var: + """Check if a var contains the object `other`. + + Args: + other: The object to check. + + Raises: + TypeError: If the var is not a valid type: dict, list, tuple or str. + + Returns: + A var representing the contain check. + """ + if self.type_ is None or not ( + types._issubclass(self.type_, Union[dict, list, tuple, str]) + ): + raise TypeError( + f"Var {self.full_name} of type {self.type_} does not support contains check." + ) + if isinstance(other, str): + other = Var.create(json.dumps(other), is_string=True) + elif not isinstance(other, Var): + other = Var.create(other) + if types._issubclass(self.type_, Dict): + return BaseVar( + name=f"{self.full_name}.has({other.full_name})", + type_=bool, + is_local=self.is_local, + ) + else: # str, list, tuple + # For strings, the left operand must be a string. + if types._issubclass(self.type_, str) and not types._issubclass( + other.type_, str + ): + raise TypeError( + f"'in ' requires string as left operand, not {other.type_}" + ) + return BaseVar( + name=f"{self.full_name}.includes({other.full_name})", + type_=bool, + is_local=self.is_local, + ) + + def reverse(self) -> Var: + """Reverse a list var. + + Raises: + TypeError: If the var is not a list. + + Returns: + A var with the reversed list. + """ + if self.type_ is None or not types._issubclass(self.type_, list): + raise TypeError(f"Cannot reverse non-list var {self.full_name}.") + + return BaseVar( + name=f"[...{self.full_name}].reverse()", + type_=self.type_, + is_local=self.is_local, + ) + def foreach(self, fn: Callable) -> Var: """Return a list of components. after doing a foreach on this var. diff --git a/tests/test_var.py b/tests/test_var.py index f97c928d1..8e49336ce 100644 --- a/tests/test_var.py +++ b/tests/test_var.py @@ -1,3 +1,4 @@ +import json import typing from typing import Dict, List, Set, Tuple @@ -239,7 +240,11 @@ def test_create_type_error(): def v(value) -> Var: - val = Var.create(value, is_local=False) + val = ( + Var.create(json.dumps(value), is_string=True, is_local=False) + if isinstance(value, str) + else Var.create(value, is_local=False) + ) assert val is not None return val @@ -273,6 +278,75 @@ def test_basic_operations(TestObj): assert str(abs(v(1))) == "{Math.abs(1)}" assert str(v([1, 2, 3]).length()) == "{[1, 2, 3].length}" + # Tests for reverse operation + assert str(v([1, 2, 3]).reverse()) == "{[...[1, 2, 3]].reverse()}" + assert str(v(["1", "2", "3"]).reverse()) == '{[...["1", "2", "3"]].reverse()}' + assert ( + str(BaseVar(name="foo", state="state", type_=list).reverse()) + == "{[...state.foo].reverse()}" + ) + assert str(BaseVar(name="foo", type_=list).reverse()) == "{[...foo].reverse()}" + + +@pytest.mark.parametrize( + "var, expected", + [ + (v([1, 2, 3]), "[1, 2, 3]"), + (v(["1", "2", "3"]), '["1", "2", "3"]'), + (BaseVar(name="foo", state="state", type_=list), "state.foo"), + (BaseVar(name="foo", type_=list), "foo"), + (v((1, 2, 3)), "[1, 2, 3]"), + (v(("1", "2", "3")), '["1", "2", "3"]'), + (BaseVar(name="foo", state="state", type_=tuple), "state.foo"), + (BaseVar(name="foo", type_=tuple), "foo"), + ], +) +def test_list_tuple_contains(var, expected): + assert str(var.contains(1)) == f"{{{expected}.includes(1)}}" + assert str(var.contains("1")) == f'{{{expected}.includes("1")}}' + assert str(var.contains(v(1))) == f"{{{expected}.includes(1)}}" + assert str(var.contains(v("1"))) == f'{{{expected}.includes("1")}}' + other_state_var = BaseVar(name="other", state="state", type_=str) + other_var = BaseVar(name="other", type_=str) + assert str(var.contains(other_state_var)) == f"{{{expected}.includes(state.other)}}" + assert str(var.contains(other_var)) == f"{{{expected}.includes(other)}}" + + +@pytest.mark.parametrize( + "var, expected", + [ + (v("123"), json.dumps("123")), + (BaseVar(name="foo", state="state", type_=str), "state.foo"), + (BaseVar(name="foo", type_=str), "foo"), + ], +) +def test_str_contains(var, expected): + assert str(var.contains("1")) == f'{{{expected}.includes("1")}}' + assert str(var.contains(v("1"))) == f'{{{expected}.includes("1")}}' + other_state_var = BaseVar(name="other", state="state", type_=str) + other_var = BaseVar(name="other", type_=str) + assert str(var.contains(other_state_var)) == f"{{{expected}.includes(state.other)}}" + assert str(var.contains(other_var)) == f"{{{expected}.includes(other)}}" + + +@pytest.mark.parametrize( + "var, expected", + [ + (v({"a": 1, "b": 2}), '{"a": 1, "b": 2}'), + (BaseVar(name="foo", state="state", type_=dict), "state.foo"), + (BaseVar(name="foo", type_=dict), "foo"), + ], +) +def test_dict_contains(var, expected): + assert str(var.contains(1)) == f"{{{expected}.has(1)}}" + assert str(var.contains("1")) == f'{{{expected}.has("1")}}' + assert str(var.contains(v(1))) == f"{{{expected}.has(1)}}" + assert str(var.contains(v("1"))) == f'{{{expected}.has("1")}}' + other_state_var = BaseVar(name="other", state="state", type_=str) + other_var = BaseVar(name="other", type_=str) + assert str(var.contains(other_state_var)) == f"{{{expected}.has(state.other)}}" + assert str(var.contains(other_var)) == f"{{{expected}.has(other)}}" + @pytest.mark.parametrize( "var", @@ -632,3 +706,81 @@ def test_get_local_storage_raise_error(key): ) def test_fstrings(out, expected): assert out == expected + + +@pytest.mark.parametrize( + "var", + [ + BaseVar(name="var", type_=int), + BaseVar(name="var", type_=float), + BaseVar(name="var", type_=str), + BaseVar(name="var", type_=bool), + BaseVar(name="var", type_=dict), + BaseVar(name="var", type_=tuple), + BaseVar(name="var", type_=set), + BaseVar(name="var", type_=None), + ], +) +def test_unsupported_types_for_reverse(var): + """Test that unsupported types for reverse throw a type error. + + Args: + var: The base var. + """ + with pytest.raises(TypeError) as err: + var.reverse() + assert err.value.args[0] == f"Cannot reverse non-list var var." + + +@pytest.mark.parametrize( + "var", + [ + BaseVar(name="var", type_=int), + BaseVar(name="var", type_=float), + BaseVar(name="var", type_=bool), + BaseVar(name="var", type_=set), + BaseVar(name="var", type_=None), + ], +) +def test_unsupported_types_for_contains(var): + """Test that unsupported types for contains throw a type error. + + Args: + var: The base var. + """ + with pytest.raises(TypeError) as err: + assert var.contains(1) + assert ( + err.value.args[0] + == f"Var var of type {var.type_} does not support contains check." + ) + + +@pytest.mark.parametrize( + "other", + [ + BaseVar(name="other", type_=int), + BaseVar(name="other", type_=float), + BaseVar(name="other", type_=bool), + BaseVar(name="other", type_=list), + BaseVar(name="other", type_=dict), + BaseVar(name="other", type_=tuple), + BaseVar(name="other", type_=set), + ], +) +def test_unsupported_types_for_string_contains(other): + with pytest.raises(TypeError) as err: + assert BaseVar(name="var", type_=str).contains(other) + assert ( + err.value.args[0] + == f"'in ' requires string as left operand, not {other.type_}" + ) + + +def test_unsupported_default_contains(): + with pytest.raises(TypeError) as err: + assert 1 in BaseVar(name="var", type_=str) + assert ( + err.value.args[0] + == "'in' operator not supported for Var types, use Var.contains() instead." + )