소스 검색

Add contains, reverse operations for Var (#1679)

Martin Xu 1 년 전
부모
커밋
2e1aea9713
2개의 변경된 파일223개의 추가작업 그리고 1개의 파일을 삭제
  1. 70 0
      reflex/vars.py
  2. 153 1
      tests/test_var.py

+ 70 - 0
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 <string>' 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.
 

+ 153 - 1
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 <string>' 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."
+    )