From f2b0915aff62c9ee6fda2cae4f5d41e244b966a4 Mon Sep 17 00:00:00 2001 From: Elijah Ahianyo Date: Tue, 12 Sep 2023 22:57:40 +0000 Subject: [PATCH] Type Validation for Var Operations and Enhanced Compatibility (#1674) --- integration/test_var_operations.py | 701 +++++++++++++++++++++++++++ reflex/.templates/web/utils/state.js | 16 + reflex/compiler/compiler.py | 1 + reflex/vars.py | 251 +++++++++- tests/test_var.py | 418 +++++++++++++++- 5 files changed, 1359 insertions(+), 28 deletions(-) create mode 100644 integration/test_var_operations.py diff --git a/integration/test_var_operations.py b/integration/test_var_operations.py new file mode 100644 index 000000000..8f79927b8 --- /dev/null +++ b/integration/test_var_operations.py @@ -0,0 +1,701 @@ +"""Integration tests for var operations.""" +from typing import Generator + +import pytest +from selenium.webdriver.common.by import By + +from reflex.testing import AppHarness + +# pyright: reportOptionalMemberAccess=false, reportGeneralTypeIssues=false, reportUnknownMemberType=false + + +def VarOperations(): + """App with var operations.""" + import reflex as rx + + class VarOperationState(rx.State): + int_var1: int = 10 + int_var2: int = 5 + int_var3: int = 7 + float_var1: float = 10.5 + float_var2: float = 5.5 + list1: list = [1, 2] + list2: list = [3, 4] + str_var1: str = "first" + str_var2: str = "second" + dict1: dict = {1: 2} + dict2: dict = {3: 4} + + app = rx.App(state=VarOperationState) + + @app.add_page + def index(): + return rx.vstack( + # INT INT + rx.text( + VarOperationState.int_var1 + VarOperationState.int_var2, + id="int_add_int", + ), + rx.text( + VarOperationState.int_var1 * VarOperationState.int_var2, + id="int_mult_int", + ), + rx.text( + VarOperationState.int_var1 - VarOperationState.int_var2, + id="int_sub_int", + ), + rx.text( + VarOperationState.int_var1**VarOperationState.int_var2, + id="int_exp_int", + ), + rx.text( + VarOperationState.int_var1 / VarOperationState.int_var2, + id="int_div_int", + ), + rx.text( + VarOperationState.int_var1 // VarOperationState.int_var3, + id="int_floor_int", + ), + rx.text( + VarOperationState.int_var1 % VarOperationState.int_var2, + id="int_mod_int", + ), + rx.text( + VarOperationState.int_var1 | VarOperationState.int_var2, + id="int_or_int", + ), + rx.text( + (VarOperationState.int_var1 > VarOperationState.int_var2).to_string(), + id="int_gt_int", + ), + rx.text( + (VarOperationState.int_var1 < VarOperationState.int_var2).to_string(), + id="int_lt_int", + ), + rx.text( + (VarOperationState.int_var1 >= VarOperationState.int_var2).to_string(), + id="int_gte_int", + ), + rx.text( + (VarOperationState.int_var1 <= VarOperationState.int_var2).to_string(), + id="int_lte_int", + ), + rx.text( + VarOperationState.int_var1 & VarOperationState.int_var2, + id="int_and_int", + ), + rx.text( + (VarOperationState.int_var1 | VarOperationState.int_var2).to_string(), + id="int_or_int", + ), + rx.text( + (VarOperationState.int_var1 == VarOperationState.int_var2).to_string(), + id="int_eq_int", + ), + rx.text( + (VarOperationState.int_var1 != VarOperationState.int_var2).to_string(), + id="int_neq_int", + ), + # INT FLOAT OR FLOAT INT + rx.text( + VarOperationState.float_var1 + VarOperationState.int_var2, + id="float_add_int", + ), + rx.text( + VarOperationState.float_var1 * VarOperationState.int_var2, + id="float_mult_int", + ), + rx.text( + VarOperationState.float_var1 - VarOperationState.int_var2, + id="float_sub_int", + ), + rx.text( + VarOperationState.float_var1**VarOperationState.int_var2, + id="float_exp_int", + ), + rx.text( + VarOperationState.float_var1 / VarOperationState.int_var2, + id="float_div_int", + ), + rx.text( + VarOperationState.float_var1 // VarOperationState.int_var3, + id="float_floor_int", + ), + rx.text( + VarOperationState.float_var1 % VarOperationState.int_var2, + id="float_mod_int", + ), + rx.text( + (VarOperationState.float_var1 > VarOperationState.int_var2).to_string(), + id="float_gt_int", + ), + rx.text( + (VarOperationState.float_var1 < VarOperationState.int_var2).to_string(), + id="float_lt_int", + ), + rx.text( + ( + VarOperationState.float_var1 >= VarOperationState.int_var2 + ).to_string(), + id="float_gte_int", + ), + rx.text( + ( + VarOperationState.float_var1 <= VarOperationState.int_var2 + ).to_string(), + id="float_lte_int", + ), + rx.text( + ( + VarOperationState.float_var1 == VarOperationState.int_var2 + ).to_string(), + id="float_eq_int", + ), + rx.text( + ( + VarOperationState.float_var1 != VarOperationState.int_var2 + ).to_string(), + id="float_neq_int", + ), + rx.text( + (VarOperationState.float_var1 & VarOperationState.int_var2).to_string(), + id="float_and_int", + ), + rx.text( + (VarOperationState.float_var1 | VarOperationState.int_var2).to_string(), + id="float_or_int", + ), + # INT, DICT + rx.text( + (VarOperationState.int_var1 | VarOperationState.dict1).to_string(), + id="int_or_dict", + ), + rx.text( + (VarOperationState.int_var1 & VarOperationState.dict1).to_string(), + id="int_and_dict", + ), + rx.text( + (VarOperationState.int_var1 == VarOperationState.dict1).to_string(), + id="int_eq_dict", + ), + rx.text( + (VarOperationState.int_var1 != VarOperationState.dict1).to_string(), + id="int_neq_dict", + ), + # FLOAT FLOAT + rx.text( + VarOperationState.float_var1 + VarOperationState.float_var2, + id="float_add_float", + ), + rx.text( + VarOperationState.float_var1 * VarOperationState.float_var2, + id="float_mult_float", + ), + rx.text( + VarOperationState.float_var1 - VarOperationState.float_var2, + id="float_sub_float", + ), + rx.text( + VarOperationState.float_var1**VarOperationState.float_var2, + id="float_exp_float", + ), + rx.text( + VarOperationState.float_var1 / VarOperationState.float_var2, + id="float_div_float", + ), + rx.text( + VarOperationState.float_var1 // VarOperationState.float_var2, + id="float_floor_float", + ), + rx.text( + VarOperationState.float_var1 % VarOperationState.float_var2, + id="float_mod_float", + ), + rx.text( + ( + VarOperationState.float_var1 > VarOperationState.float_var2 + ).to_string(), + id="float_gt_float", + ), + rx.text( + ( + VarOperationState.float_var1 < VarOperationState.float_var2 + ).to_string(), + id="float_lt_float", + ), + rx.text( + ( + VarOperationState.float_var1 >= VarOperationState.float_var2 + ).to_string(), + id="float_gte_float", + ), + rx.text( + ( + VarOperationState.float_var1 <= VarOperationState.float_var2 + ).to_string(), + id="float_lte_float", + ), + rx.text( + ( + VarOperationState.float_var1 == VarOperationState.float_var2 + ).to_string(), + id="float_eq_float", + ), + rx.text( + ( + VarOperationState.float_var1 != VarOperationState.float_var2 + ).to_string(), + id="float_neq_float", + ), + rx.text( + ( + VarOperationState.float_var1 & VarOperationState.float_var2 + ).to_string(), + id="float_and_float", + ), + rx.text( + ( + VarOperationState.float_var1 | VarOperationState.float_var2 + ).to_string(), + id="float_or_float", + ), + # FLOAT STR + rx.text( + VarOperationState.float_var1 | VarOperationState.str_var1, + id="float_or_str", + ), + rx.text( + VarOperationState.float_var1 & VarOperationState.str_var1, + id="float_and_str", + ), + rx.text( + ( + VarOperationState.float_var1 == VarOperationState.str_var1 + ).to_string(), + id="float_eq_str", + ), + rx.text( + ( + VarOperationState.float_var1 != VarOperationState.str_var1 + ).to_string(), + id="float_neq_str", + ), + # FLOAT LIST + rx.text( + (VarOperationState.float_var1 | VarOperationState.list1).to_string(), + id="float_or_list", + ), + rx.text( + (VarOperationState.float_var1 & VarOperationState.list1).to_string(), + id="float_and_list", + ), + rx.text( + (VarOperationState.float_var1 == VarOperationState.list1).to_string(), + id="float_eq_list", + ), + rx.text( + (VarOperationState.float_var1 != VarOperationState.list1).to_string(), + id="float_neq_list", + ), + # FLOAT DICT + rx.text( + (VarOperationState.float_var1 | VarOperationState.dict1).to_string(), + id="float_or_dict", + ), + rx.text( + (VarOperationState.float_var1 & VarOperationState.dict1).to_string(), + id="float_and_dict", + ), + rx.text( + (VarOperationState.float_var1 == VarOperationState.dict1).to_string(), + id="float_eq_dict", + ), + rx.text( + (VarOperationState.float_var1 != VarOperationState.dict1).to_string(), + id="float_neq_dict", + ), + # STR STR + rx.text( + VarOperationState.str_var1 + VarOperationState.str_var2, + id="str_add_str", + ), + rx.text( + (VarOperationState.str_var1 > VarOperationState.str_var2).to_string(), + id="str_gt_str", + ), + rx.text( + (VarOperationState.str_var1 < VarOperationState.str_var2).to_string(), + id="str_lt_str", + ), + rx.text( + (VarOperationState.str_var1 >= VarOperationState.str_var2).to_string(), + id="str_gte_str", + ), + rx.text( + (VarOperationState.str_var1 <= VarOperationState.str_var2).to_string(), + id="str_lte_str", + ), + rx.text( + ( + VarOperationState.float_var1 == VarOperationState.float_var2 + ).to_string(), + id="str_eq_str", + ), + rx.text( + ( + VarOperationState.float_var1 != VarOperationState.float_var2 + ).to_string(), + id="str_neq_str", + ), + rx.text( + VarOperationState.str_var1.contains("fir").to_string(), + id="str_contains", + ), + rx.text( + VarOperationState.str_var1 | VarOperationState.str_var1, id="str_or_str" + ), + rx.text( + VarOperationState.str_var1 & VarOperationState.str_var2, + id="str_and_str", + ), + # STR, INT + rx.text( + VarOperationState.str_var1 * VarOperationState.int_var2, + id="str_mult_int", + ), + rx.text( + VarOperationState.str_var1 & VarOperationState.int_var2, + id="str_and_int", + ), + rx.text( + VarOperationState.str_var1 | VarOperationState.int_var2, id="str_or_int" + ), + rx.text( + (VarOperationState.str_var1 == VarOperationState.int_var1).to_string(), + id="str_eq_int", + ), + rx.text( + (VarOperationState.str_var1 != VarOperationState.int_var1).to_string(), + id="str_neq_int", + ), + # STR, LIST + rx.text( + VarOperationState.str_var1 | VarOperationState.list1, id="str_or_list" + ), + rx.text( + (VarOperationState.str_var1 & VarOperationState.list1).to_string(), + id="str_and_list", + ), + rx.text( + (VarOperationState.str_var1 == VarOperationState.list1).to_string(), + id="str_eq_list", + ), + rx.text( + (VarOperationState.str_var1 != VarOperationState.list1).to_string(), + id="str_neq_list", + ), + # STR, DICT + rx.text( + VarOperationState.str_var1 | VarOperationState.dict1, id="str_or_dict" + ), + rx.text( + (VarOperationState.str_var1 & VarOperationState.dict1).to_string(), + id="str_and_dict", + ), + rx.text( + (VarOperationState.str_var1 == VarOperationState.dict1).to_string(), + id="str_eq_dict", + ), + rx.text( + (VarOperationState.str_var1 != VarOperationState.dict1).to_string(), + id="str_neq_dict", + ), + # LIST, LIST + rx.text( + (VarOperationState.list1 + VarOperationState.list2).to_string(), + id="list_add_list", + ), + rx.text( + (VarOperationState.list1 & VarOperationState.list2).to_string(), + id="list_and_list", + ), + rx.text( + (VarOperationState.list1 | VarOperationState.list2).to_string(), + id="list_or_list", + ), + rx.text( + (VarOperationState.list1 > VarOperationState.list2).to_string(), + id="list_gt_list", + ), + rx.text( + (VarOperationState.list1 < VarOperationState.list2).to_string(), + id="list_lt_list", + ), + rx.text( + (VarOperationState.list1 >= VarOperationState.list2).to_string(), + id="list_gte_list", + ), + rx.text( + (VarOperationState.list1 <= VarOperationState.list2).to_string(), + id="list_lte_list", + ), + rx.text( + (VarOperationState.list1 == VarOperationState.list2).to_string(), + id="list_eq_list", + ), + rx.text( + (VarOperationState.list1 != VarOperationState.list2).to_string(), + id="list_neq_list", + ), + rx.text( + VarOperationState.list1.contains(1).to_string(), id="list_contains" + ), + rx.text(VarOperationState.list1.reverse().to_string(), id="list_reverse"), + # LIST, INT + rx.text( + (VarOperationState.list1 * VarOperationState.int_var2).to_string(), + id="list_mult_int", + ), + rx.text( + (VarOperationState.list1 | VarOperationState.int_var1).to_string(), + id="list_or_int", + ), + rx.text( + (VarOperationState.list1 & VarOperationState.int_var1).to_string(), + id="list_and_int", + ), + rx.text( + (VarOperationState.list1 == VarOperationState.int_var1).to_string(), + id="list_eq_int", + ), + rx.text( + (VarOperationState.list1 != VarOperationState.int_var1).to_string(), + id="list_neq_int", + ), + # LIST, DICT + rx.text( + (VarOperationState.list1 | VarOperationState.dict1).to_string(), + id="list_or_dict", + ), + rx.text( + (VarOperationState.list1 & VarOperationState.dict1).to_string(), + id="list_and_dict", + ), + rx.text( + (VarOperationState.list1 == VarOperationState.dict1).to_string(), + id="list_eq_dict", + ), + rx.text( + (VarOperationState.list1 != VarOperationState.dict1).to_string(), + id="list_neq_dict", + ), + # DICT, DICT + rx.text( + (VarOperationState.dict1 | VarOperationState.dict2).to_string(), + id="dict_or_dict", + ), + rx.text( + (VarOperationState.dict1 & VarOperationState.dict2).to_string(), + id="dict_and_dict", + ), + rx.text( + (VarOperationState.dict1 == VarOperationState.dict2).to_string(), + id="dict_eq_dict", + ), + rx.text( + (VarOperationState.dict1 != VarOperationState.dict2).to_string(), + id="dict_neq_dict", + ), + rx.text( + VarOperationState.dict1.contains(1).to_string(), id="dict_contains" + ), + ) + + app.compile() + + +@pytest.fixture(scope="session") +def var_operations(tmp_path_factory) -> Generator[AppHarness, None, None]: + """Start VarOperations app at tmp_path via AppHarness. + + Args: + tmp_path_factory: pytest tmp_path_factory fixture + + Yields: + running AppHarness instance + """ + with AppHarness.create( + root=tmp_path_factory.mktemp("var_operations"), + app_source=VarOperations, # type: ignore + ) as harness: + assert harness.app_instance is not None, "app is not running" + yield harness + + +@pytest.fixture +def driver(var_operations: AppHarness): + """Get an instance of the browser open to the var operations app. + + Args: + var_operations: harness for VarOperations app + + Yields: + WebDriver instance. + """ + driver = var_operations.frontend() + try: + assert var_operations.poll_for_clients() + yield driver + finally: + driver.quit() + + +def test_var_operations(driver, var_operations: AppHarness): + """Test that the var operations produce the right results. + + Args: + driver: selenium WebDriver open to the app + var_operations: AppHarness for the var operations app + """ + assert var_operations.app_instance is not None, "app is not running" + # INT INT + assert driver.find_element(By.ID, "int_add_int").text == "15" + assert driver.find_element(By.ID, "int_mult_int").text == "50" + assert driver.find_element(By.ID, "int_sub_int").text == "5" + assert driver.find_element(By.ID, "int_exp_int").text == "100000" + assert driver.find_element(By.ID, "int_div_int").text == "2" + assert driver.find_element(By.ID, "int_floor_int").text == "1" + assert driver.find_element(By.ID, "int_mod_int").text == "0" + assert driver.find_element(By.ID, "int_gt_int").text == "true" + assert driver.find_element(By.ID, "int_lt_int").text == "false" + assert driver.find_element(By.ID, "int_gte_int").text == "true" + assert driver.find_element(By.ID, "int_lte_int").text == "false" + assert driver.find_element(By.ID, "int_and_int").text == "5" + assert driver.find_element(By.ID, "int_or_int").text == "10" + assert driver.find_element(By.ID, "int_eq_int").text == "false" + assert driver.find_element(By.ID, "int_neq_int").text == "true" + + # INT FLOAT OR FLOAT INT + assert driver.find_element(By.ID, "float_add_int").text == "15.5" + assert driver.find_element(By.ID, "float_mult_int").text == "52.5" + assert driver.find_element(By.ID, "float_sub_int").text == "5.5" + assert driver.find_element(By.ID, "float_exp_int").text == "127628.15625" + assert driver.find_element(By.ID, "float_div_int").text == "2.1" + assert driver.find_element(By.ID, "float_floor_int").text == "1" + assert driver.find_element(By.ID, "float_mod_int").text == "0.5" + assert driver.find_element(By.ID, "float_gt_int").text == "true" + assert driver.find_element(By.ID, "float_lt_int").text == "false" + assert driver.find_element(By.ID, "float_gte_int").text == "true" + assert driver.find_element(By.ID, "float_lte_int").text == "false" + assert driver.find_element(By.ID, "float_eq_int").text == "false" + assert driver.find_element(By.ID, "float_neq_int").text == "true" + assert driver.find_element(By.ID, "float_and_int").text == "5" + assert driver.find_element(By.ID, "float_or_int").text == "10.5" + + # INT, DICT + assert driver.find_element(By.ID, "int_or_dict").text == "10" + assert driver.find_element(By.ID, "int_and_dict").text == '{"1":2}' + assert driver.find_element(By.ID, "int_eq_dict").text == "false" + assert driver.find_element(By.ID, "int_neq_dict").text == "true" + + # FLOAT FLOAT + assert driver.find_element(By.ID, "float_add_float").text == "16" + assert driver.find_element(By.ID, "float_mult_float").text == "57.75" + assert driver.find_element(By.ID, "float_sub_float").text == "5" + assert driver.find_element(By.ID, "float_exp_float").text == "413562.49323606625" + assert driver.find_element(By.ID, "float_div_float").text == "1.9090909090909092" + assert driver.find_element(By.ID, "float_floor_float").text == "1" + assert driver.find_element(By.ID, "float_mod_float").text == "5" + assert driver.find_element(By.ID, "float_gt_float").text == "true" + assert driver.find_element(By.ID, "float_lt_float").text == "false" + assert driver.find_element(By.ID, "float_gte_float").text == "true" + assert driver.find_element(By.ID, "float_lte_float").text == "false" + assert driver.find_element(By.ID, "float_eq_float").text == "false" + assert driver.find_element(By.ID, "float_neq_float").text == "true" + assert driver.find_element(By.ID, "float_and_float").text == "5.5" + assert driver.find_element(By.ID, "float_or_float").text == "10.5" + + # FLOAT STR + assert driver.find_element(By.ID, "float_or_str").text == "10.5" + assert driver.find_element(By.ID, "float_and_str").text == "first" + assert driver.find_element(By.ID, "float_eq_str").text == "false" + assert driver.find_element(By.ID, "float_neq_str").text == "true" + + # FLOAT,LIST + assert driver.find_element(By.ID, "float_or_list").text == "10.5" + assert driver.find_element(By.ID, "float_and_list").text == "[1,2]" + assert driver.find_element(By.ID, "float_eq_list").text == "false" + assert driver.find_element(By.ID, "float_neq_list").text == "true" + + # FLOAT, DICT + assert driver.find_element(By.ID, "float_or_dict").text == "10.5" + assert driver.find_element(By.ID, "float_and_dict").text == '{"1":2}' + assert driver.find_element(By.ID, "float_eq_dict").text == "false" + assert driver.find_element(By.ID, "float_neq_dict").text == "true" + + # STR STR + assert driver.find_element(By.ID, "str_add_str").text == "firstsecond" + assert driver.find_element(By.ID, "str_gt_str").text == "false" + assert driver.find_element(By.ID, "str_lt_str").text == "true" + assert driver.find_element(By.ID, "str_gte_str").text == "false" + assert driver.find_element(By.ID, "str_lte_str").text == "true" + assert driver.find_element(By.ID, "str_eq_str").text == "false" + assert driver.find_element(By.ID, "str_neq_str").text == "true" + assert driver.find_element(By.ID, "str_and_str").text == "second" + assert driver.find_element(By.ID, "str_or_str").text == "first" + assert driver.find_element(By.ID, "str_contains").text == "true" + + # STR INT + assert ( + driver.find_element(By.ID, "str_mult_int").text == "firstfirstfirstfirstfirst" + ) + assert driver.find_element(By.ID, "str_and_int").text == "5" + assert driver.find_element(By.ID, "str_or_int").text == "first" + assert driver.find_element(By.ID, "str_eq_int").text == "false" + assert driver.find_element(By.ID, "str_neq_int").text == "true" + + # STR, LIST + assert driver.find_element(By.ID, "str_and_list").text == "[1,2]" + assert driver.find_element(By.ID, "str_or_list").text == "first" + assert driver.find_element(By.ID, "str_eq_list").text == "false" + assert driver.find_element(By.ID, "str_neq_list").text == "true" + + # STR, DICT + + assert driver.find_element(By.ID, "str_or_dict").text == "first" + assert driver.find_element(By.ID, "str_and_dict").text == '{"1":2}' + assert driver.find_element(By.ID, "str_eq_dict").text == "false" + assert driver.find_element(By.ID, "str_neq_dict").text == "true" + + # LIST,LIST + assert driver.find_element(By.ID, "list_add_list").text == "[1,2,3,4]" + assert driver.find_element(By.ID, "list_gt_list").text == "false" + assert driver.find_element(By.ID, "list_lt_list").text == "true" + assert driver.find_element(By.ID, "list_gte_list").text == "false" + assert driver.find_element(By.ID, "list_lte_list").text == "true" + assert driver.find_element(By.ID, "list_eq_list").text == "false" + assert driver.find_element(By.ID, "list_neq_list").text == "true" + assert driver.find_element(By.ID, "list_and_list").text == "[3,4]" + assert driver.find_element(By.ID, "list_or_list").text == "[1,2]" + assert driver.find_element(By.ID, "list_contains").text == "true" + assert driver.find_element(By.ID, "list_reverse").text == "[2,1]" + + # LIST INT + assert driver.find_element(By.ID, "list_mult_int").text == "[1,2,1,2,1,2,1,2,1,2]" + assert driver.find_element(By.ID, "list_or_int").text == "[1,2]" + assert driver.find_element(By.ID, "list_and_int").text == "10" + assert driver.find_element(By.ID, "list_eq_int").text == "false" + assert driver.find_element(By.ID, "list_neq_int").text == "true" + + # LIST DICT + assert driver.find_element(By.ID, "list_and_dict").text == '{"1":2}' + assert driver.find_element(By.ID, "list_or_dict").text == "[1,2]" + assert driver.find_element(By.ID, "list_eq_dict").text == "false" + assert driver.find_element(By.ID, "list_neq_dict").text == "true" + + # DICT, DICT + assert driver.find_element(By.ID, "dict_or_dict").text == '{"1":2}' + assert driver.find_element(By.ID, "dict_and_dict").text == '{"3":4}' + assert driver.find_element(By.ID, "dict_eq_dict").text == "false" + assert driver.find_element(By.ID, "dict_neq_dict").text == "true" + assert driver.find_element(By.ID, "dict_contains").text == "true" diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index d45509146..018da581c 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -530,3 +530,19 @@ export const getRefValues = (refs) => { // getAttribute is used by RangeSlider because it doesn't assign value return refs.map((ref) => ref.current.value || ref.current.getAttribute("aria-valuenow")); } + +/** +* Spread two arrays or two objects. +* @param first The first array or object. +* @param second The second array or object. +* @returns The final merged array or object. +*/ +export const spreadArraysOrObjects = (first, second) => { + if (Array.isArray(first) && Array.isArray(second)) { + return [...first, ...second]; + } else if (typeof first === 'object' && typeof second === 'object') { + return { ...first, ...second }; + } else { + throw new Error('Both parameters must be either arrays or objects.'); + } +} diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index 53c8623fd..5484d08bd 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -24,6 +24,7 @@ DEFAULT_IMPORTS: imports.ImportDict = { ImportVar(tag="uploadFiles"), ImportVar(tag="E"), ImportVar(tag="isTrue"), + ImportVar(tag="spreadArraysOrObjects"), ImportVar(tag="preventDefault"), ImportVar(tag="refs"), ImportVar(tag="getRefValue"), diff --git a/reflex/vars.py b/reflex/vars.py index 1cc282dc9..b6ed08e1d 100644 --- a/reflex/vars.py +++ b/reflex/vars.py @@ -38,6 +38,35 @@ if TYPE_CHECKING: # Set of unique variable names. USED_VARIABLES = set() +# Supported operators for all types. +ALL_OPS = ["==", "!=", "!==", "===", "&&", "||"] +# Delimiters used between function args or operands. +DELIMITERS = [","] +# Mapping of valid operations for different type combinations. +OPERATION_MAPPING = { + (int, int): { + "+", + "-", + "/", + "//", + "*", + "%", + "**", + ">", + "<", + "<=", + ">=", + "|", + "&", + }, + (int, str): {"*"}, + (int, list): {"*"}, + (str, str): {"+", ">", "<", "<=", ">="}, + (float, float): {"+", "-", "/", "//", "*", "%", "**", ">", "<", "<=", ">="}, + (float, int): {"+", "-", "/", "//", "*", "%", "**", ">", "<", "<=", ">="}, + (list, list): {"+", ">", "<", "<=", ">="}, +} + def get_unique_variable_name() -> str: """Get a unique variable name. @@ -383,6 +412,7 @@ class Var(ABC): type_: Type | None = None, flip: bool = False, fn: str | None = None, + invoke_fn: bool = False, ) -> Var: """Perform an operation on a var. @@ -392,32 +422,100 @@ class Var(ABC): type_: The type of the operation result. flip: Whether to flip the order of the operation. fn: A function to apply to the operation. + invoke_fn: Whether to invoke the function. Returns: The operation result. + + Raises: + TypeError: If the operation between two operands is invalid. + ValueError: If flip is set to true and value of operand is not provided """ - # Wrap strings in quotes. if isinstance(other, str): other = Var.create(json.dumps(other)) else: other = Var.create(other) - if type_ is None: - type_ = self.type_ - if other is None: - name = f"{op}{self.full_name}" + + type_ = type_ or self.type_ + + if other is None and flip: + raise ValueError( + "flip_operands cannot be set to True if the value of 'other' operand is not provided" + ) + + left_operand, right_operand = (other, self) if flip else (self, other) + + if other is not None: + # check if the operation between operands is valid. + if op and not self.is_valid_operation( + types.get_base_class(left_operand.type_), # type: ignore + types.get_base_class(right_operand.type_), # type: ignore + op, + ): + raise TypeError( + f"Unsupported Operand type(s) for {op}: `{left_operand.full_name}` of type {left_operand.type_.__name__} and `{right_operand.full_name}` of type {right_operand.type_.__name__}" # type: ignore + ) + + # apply function to operands + if fn is not None: + if invoke_fn: + # invoke the function on left operand. + operation_name = f"{left_operand.full_name}.{fn}({right_operand.full_name})" # type: ignore + else: + # pass the operands as arguments to the function. + operation_name = f"{left_operand.full_name} {op} {right_operand.full_name}" # type: ignore + operation_name = f"{fn}({operation_name})" + else: + # apply operator to operands (left operand right_operand) + operation_name = f"{left_operand.full_name} {op} {right_operand.full_name}" # type: ignore + operation_name = format.wrap(operation_name, "(") else: - props = (other, self) if flip else (self, other) - name = f"{props[0].full_name} {op} {props[1].full_name}" - if fn is None: - name = format.wrap(name, "(") - if fn is not None: - name = f"{fn}({name})" + # apply operator to left operand ( left_operand) + operation_name = f"{op}{self.full_name}" + # apply function to operands + if fn is not None: + operation_name = ( + f"{fn}({operation_name})" + if not invoke_fn + else f"{self.full_name}.{fn}()" + ) + return BaseVar( - name=name, + name=operation_name, type_=type_, is_local=self.is_local, ) + @staticmethod + def is_valid_operation( + operand1_type: Type, operand2_type: Type, operator: str + ) -> bool: + """Check if an operation between two operands is valid. + + Args: + operand1_type: Type of the operand + operand2_type: Type of the second operand + operator: The operator. + + Returns: + Whether operation is valid or not + + """ + if operator in ALL_OPS or operator in DELIMITERS: + return True + + # bools are subclasses of ints + pair = tuple( + sorted( + [ + int if operand1_type == bool else operand1_type, + int if operand2_type == bool else operand2_type, + ], + key=lambda x: x.__name__, + ) + ) + return pair in OPERATION_MAPPING and operator in OPERATION_MAPPING[pair] + def compare(self, op: str, other: Var) -> Var: """Compare two vars with inequalities. @@ -537,16 +635,26 @@ class Var(ABC): """ return self.compare("<=", other) - def __add__(self, other: Var) -> Var: + def __add__(self, other: Var, flip=False) -> Var: """Add two vars. Args: other: The other var to add. + flip: Whether to flip operands. Returns: A var representing the sum. """ - return self.operation("+", other) + other_type = other.type_ if isinstance(other, Var) else type(other) + # For list-list addition, javascript concatenates the content of the lists instead of + # merging the list, and for that reason we use the spread operator available through spreadArraysOrObjects + # utility function + if ( + types.get_base_class(self.type_) == list + and types.get_base_class(other_type) == list + ): + return self.operation(",", other, fn="spreadArraysOrObjects", flip=flip) + return self.operation("+", other, flip=flip) def __radd__(self, other: Var) -> Var: """Add two vars. @@ -557,7 +665,7 @@ class Var(ABC): Returns: A var representing the sum. """ - return self.operation("+", other, flip=True) + return self.__add__(other=other, flip=True) def __sub__(self, other: Var) -> Var: """Subtract two vars. @@ -581,15 +689,39 @@ class Var(ABC): """ return self.operation("-", other, flip=True) - def __mul__(self, other: Var) -> Var: + def __mul__(self, other: Var, flip=True) -> Var: """Multiply two vars. Args: other: The other var to multiply. + flip: Whether to flip operands Returns: A var representing the product. """ + other_type = other.type_ if isinstance(other, Var) else type(other) + # For str-int multiplication, we use the repeat function. + # i.e "hello" * 2 is equivalent to "hello".repeat(2) in js. + if (types.get_base_class(self.type_), types.get_base_class(other_type)) in [ + (int, str), + (str, int), + ]: + return self.operation(other=other, fn="repeat", invoke_fn=True) + + # For list-int multiplication, we use the Array function. + # i.e ["hello"] * 2 is equivalent to Array(2).fill().map(() => ["hello"]).flat() in js. + if (types.get_base_class(self.type_), types.get_base_class(other_type)) in [ + (int, list), + (list, int), + ]: + other_name = other.full_name if isinstance(other, Var) else other + name = f"Array({other_name}).fill().map(() => {self.full_name}).flat()" + return BaseVar( + name=name, + type_=str, + is_local=self.is_local, + ) + return self.operation("*", other) def __rmul__(self, other: Var) -> Var: @@ -601,7 +733,7 @@ class Var(ABC): Returns: A var representing the product. """ - return self.operation("*", other, flip=True) + return self.__mul__(other=other, flip=True) def __pow__(self, other: Var) -> Var: """Raise a var to a power. @@ -684,10 +816,29 @@ class Var(ABC): """Perform a logical and. Args: - other: The other var to perform the logical and with. + other: The other var to perform the logical AND with. Returns: - A var representing the logical and. + A var representing the logical AND. + + Note: + This method provides behavior specific to JavaScript, where it returns the JavaScript + equivalent code (using the '&&' operator) of a logical AND operation. + In JavaScript, the + logical OR operator '&&' is used for Boolean logic, and this method emulates that behavior + by returning the equivalent code as a Var instance. + + In Python, logical AND 'and' operates differently, evaluating expressions immediately, making + it challenging to override the behavior entirely. + Therefore, this method leverages the + bitwise AND '__and__' operator for custom JavaScript-like behavior. + + Example: + >>> var1 = Var.create(True) + >>> var2 = Var.create(False) + >>> js_code = var1 & var2 + >>> print(js_code.full_name) + '(true && false)' """ return self.operation("&&", other, type_=bool) @@ -695,10 +846,29 @@ class Var(ABC): """Perform a logical and. Args: - other: The other var to perform the logical and with. + other: The other var to perform the logical AND with. Returns: - A var representing the logical and. + A var representing the logical AND. + + Note: + This method provides behavior specific to JavaScript, where it returns the JavaScript + equivalent code (using the '&&' operator) of a logical AND operation. + In JavaScript, the + logical OR operator '&&' is used for Boolean logic, and this method emulates that behavior + by returning the equivalent code as a Var instance. + + In Python, logical AND 'and' operates differently, evaluating expressions immediately, making + it challenging to override the behavior entirely. + Therefore, this method leverages the + bitwise AND '__rand__' operator for custom JavaScript-like behavior. + + Example: + >>> var1 = Var.create(True) + >>> var2 = Var.create(False) + >>> js_code = var1 & var2 + >>> print(js_code.full_name) + '(false && true)' """ return self.operation("&&", other, type_=bool, flip=True) @@ -710,6 +880,23 @@ class Var(ABC): Returns: A var representing the logical or. + + Note: + This method provides behavior specific to JavaScript, where it returns the JavaScript + equivalent code (using the '||' operator) of a logical OR operation. In JavaScript, the + logical OR operator '||' is used for Boolean logic, and this method emulates that behavior + by returning the equivalent code as a Var instance. + + In Python, logical OR 'or' operates differently, evaluating expressions immediately, making + it challenging to override the behavior entirely. Therefore, this method leverages the + bitwise OR '__or__' operator for custom JavaScript-like behavior. + + Example: + >>> var1 = Var.create(True) + >>> var2 = Var.create(False) + >>> js_code = var1 | var2 + >>> print(js_code.full_name) + '(true || false)' """ return self.operation("||", other, type_=bool) @@ -721,6 +908,23 @@ class Var(ABC): Returns: A var representing the logical or. + + Note: + This method provides behavior specific to JavaScript, where it returns the JavaScript + equivalent code (using the '||' operator) of a logical OR operation. In JavaScript, the + logical OR operator '||' is used for Boolean logic, and this method emulates that behavior + by returning the equivalent code as a Var instance. + + In Python, logical OR 'or' operates differently, evaluating expressions immediately, making + it challenging to override the behavior entirely. Therefore, this method leverages the + bitwise OR '__or__' operator for custom JavaScript-like behavior. + + Example: + >>> var1 = Var.create(True) + >>> var2 = Var.create(False) + >>> js_code = var1 | var2 + >>> print(js_code) + 'false || true' """ return self.operation("||", other, type_=bool, flip=True) @@ -752,13 +956,16 @@ class Var(ABC): raise TypeError( f"Var {self.full_name} of type {self.type_} does not support contains check." ) + method = ( + "hasOwnProperty" if types.get_base_class(self.type_) == dict else "includes" + ) 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})", + name=f"{self.full_name}.{method}({other.full_name})", type_=bool, is_local=self.is_local, ) diff --git a/tests/test_var.py b/tests/test_var.py index 8e49336ce..f654a4d9b 100644 --- a/tests/test_var.py +++ b/tests/test_var.py @@ -277,6 +277,7 @@ def test_basic_operations(TestObj): ) assert str(abs(v(1))) == "{Math.abs(1)}" assert str(v([1, 2, 3]).length()) == "{[1, 2, 3].length}" + assert str(v([1, 2]) + v([3, 4])) == "{spreadArraysOrObjects([1, 2] , [3, 4])}" # Tests for reverse operation assert str(v([1, 2, 3]).reverse()) == "{[...[1, 2, 3]].reverse()}" @@ -338,14 +339,17 @@ def test_str_contains(var, expected): ], ) 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")}}' + assert str(var.contains(1)) == f"{{{expected}.hasOwnProperty(1)}}" + assert str(var.contains("1")) == f'{{{expected}.hasOwnProperty("1")}}' + assert str(var.contains(v(1))) == f"{{{expected}.hasOwnProperty(1)}}" + assert str(var.contains(v("1"))) == f'{{{expected}.hasOwnProperty("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)}}" + assert ( + str(var.contains(other_state_var)) + == f"{{{expected}.hasOwnProperty(state.other)}}" + ) + assert str(var.contains(other_var)) == f"{{{expected}.hasOwnProperty(other)}}" @pytest.mark.parametrize( @@ -784,3 +788,405 @@ def test_unsupported_default_contains(): err.value.args[0] == "'in' operator not supported for Var types, use Var.contains() instead." ) + + +@pytest.mark.parametrize( + "operand1_var,operand2_var,operators", + [ + ( + Var.create(10), + Var.create(5), + [ + "+", + "-", + "/", + "//", + "*", + "%", + "**", + ">", + "<", + "<=", + ">=", + "|", + "&", + ], + ), + ( + Var.create(10.5), + Var.create(5), + ["+", "-", "/", "//", "*", "%", "**", ">", "<", "<=", ">="], + ), + ( + Var.create(5), + Var.create(True), + [ + "+", + "-", + "/", + "//", + "*", + "%", + "**", + ">", + "<", + "<=", + ">=", + "|", + "&", + ], + ), + ( + Var.create(10.5), + Var.create(5.5), + ["+", "-", "/", "//", "*", "%", "**", ">", "<", "<=", ">="], + ), + ( + Var.create(10.5), + Var.create(True), + ["+", "-", "/", "//", "*", "%", "**", ">", "<", "<=", ">="], + ), + (Var.create("10"), Var.create("5"), ["+", ">", "<", "<=", ">="]), + (Var.create([10, 20]), Var.create([5, 6]), ["+", ">", "<", "<=", ">="]), + (Var.create([10, 20]), Var.create(5), ["*"]), + (Var.create([10, 20]), Var.create(True), ["*"]), + ( + Var.create(True), + Var.create(True), + [ + "+", + "-", + "/", + "//", + "*", + "%", + "**", + ">", + "<", + "<=", + ">=", + "|", + "&", + ], + ), + ], +) +def test_valid_var_operations(operand1_var: Var, operand2_var, operators: List[str]): + """Test that operations do not raise a TypeError. + + Args: + operand1_var: left operand. + operand2_var: right operand. + operators: list of supported operators. + """ + for operator in operators: + operand1_var.operation(op=operator, other=operand2_var) + operand1_var.operation(op=operator, other=operand2_var, flip=True) + + +@pytest.mark.parametrize( + "operand1_var,operand2_var,operators", + [ + ( + Var.create(10), + Var.create(5), + [ + "^", + "<<", + ">>", + ], + ), + ( + Var.create(10.5), + Var.create(5), + [ + "|", + "^", + "<<", + ">>", + "&", + ], + ), + ( + Var.create(10.5), + Var.create(True), + [ + "|", + "^", + "<<", + ">>", + "&", + ], + ), + ( + Var.create(10.5), + Var.create(5.5), + [ + "|", + "^", + "<<", + ">>", + "&", + ], + ), + ( + Var.create("10"), + Var.create("5"), + [ + "-", + "/", + "//", + "*", + "%", + "**", + "|", + "^", + "<<", + ">>", + "&", + ], + ), + ( + Var.create([10, 20]), + Var.create([5, 6]), + [ + "-", + "/", + "//", + "*", + "%", + "**", + "|", + "^", + "<<", + ">>", + "&", + ], + ), + ( + Var.create([10, 20]), + Var.create(5), + [ + "+", + "-", + "/", + "//", + "%", + "**", + ">", + "<", + "<=", + ">=", + "|", + "^", + "<<", + ">>", + "&", + ], + ), + ( + Var.create([10, 20]), + Var.create(True), + [ + "+", + "-", + "/", + "//", + "%", + "**", + ">", + "<", + "<=", + ">=", + "|", + "^", + "<<", + ">>", + "&", + ], + ), + ( + Var.create([10, 20]), + Var.create("5"), + [ + "+", + "-", + "/", + "//", + "*", + "%", + "**", + ">", + "<", + "<=", + ">=", + "|", + "^", + "<<", + ">>", + "&", + ], + ), + ( + Var.create([10, 20]), + Var.create({"key": "value"}), + [ + "+", + "-", + "/", + "//", + "*", + "%", + "**", + ">", + "<", + "<=", + ">=", + "|", + "^", + "<<", + ">>", + "&", + ], + ), + ( + Var.create([10, 20]), + Var.create(5.5), + [ + "+", + "-", + "/", + "//", + "*", + "%", + "**", + ">", + "<", + "<=", + ">=", + "|", + "^", + "<<", + ">>", + "&", + ], + ), + ( + Var.create({"key": "value"}), + Var.create({"another_key": "another_value"}), + [ + "+", + "-", + "/", + "//", + "*", + "%", + "**", + ">", + "<", + "<=", + ">=", + "|", + "^", + "<<", + ">>", + "&", + ], + ), + ( + Var.create({"key": "value"}), + Var.create(5), + [ + "+", + "-", + "/", + "//", + "*", + "%", + "**", + ">", + "<", + "<=", + ">=", + "|", + "^", + "<<", + ">>", + "&", + ], + ), + ( + Var.create({"key": "value"}), + Var.create(True), + [ + "+", + "-", + "/", + "//", + "*", + "%", + "**", + ">", + "<", + "<=", + ">=", + "|", + "^", + "<<", + ">>", + "&", + ], + ), + ( + Var.create({"key": "value"}), + Var.create(5.5), + [ + "+", + "-", + "/", + "//", + "*", + "%", + "**", + ">", + "<", + "<=", + ">=", + "|", + "^", + "<<", + ">>", + "&", + ], + ), + ( + Var.create({"key": "value"}), + Var.create("5"), + [ + "+", + "-", + "/", + "//", + "*", + "%", + "**", + ">", + "<", + "<=", + ">=", + "|", + "^", + "<<", + ">>", + "&", + ], + ), + ], +) +def test_invalid_var_operations(operand1_var: Var, operand2_var, operators: List[str]): + for operator in operators: + with pytest.raises(TypeError): + operand1_var.operation(op=operator, other=operand2_var) + + with pytest.raises(TypeError): + operand1_var.operation(op=operator, other=operand2_var, flip=True)