diff --git a/reflex/.templates/jinja/web/pages/utils.js.jinja2 b/reflex/.templates/jinja/web/pages/utils.js.jinja2 index a19a51096f..144e228f5a 100644 --- a/reflex/.templates/jinja/web/pages/utils.js.jinja2 +++ b/reflex/.templates/jinja/web/pages/utils.js.jinja2 @@ -6,9 +6,9 @@ {% filter indent(width=indent_width) %} {%- if component is not mapping %} {{- component }} - {%- elif component.iterable %} + {%- elif "iterable" in component %} {{- render_iterable_tag(component) }} - {%- elif component.cond %} + {%- elif "cond" in component %} {{- render_condition_tag(component) }} {%- elif component.children|length %} {{- render_tag(component) }} diff --git a/reflex/components/datadisplay/datatable.py b/reflex/components/datadisplay/datatable.py index 13437a6b4a..0347fca390 100644 --- a/reflex/components/datadisplay/datatable.py +++ b/reflex/components/datadisplay/datatable.py @@ -66,7 +66,11 @@ def create(cls, *children, **props): "Annotation of the computed var assigned to the data field should be provided." ) - if columns and isinstance(columns, ComputedVar) and columns.type_ == Any: + if ( + columns is not None + and isinstance(columns, ComputedVar) + and columns.type_ == Any + ): raise ValueError( "Annotation of the computed var assigned to the column field should be provided." ) @@ -75,7 +79,7 @@ def create(cls, *children, **props): if ( types.is_dataframe(type(data)) or (isinstance(data, Var) and types.is_dataframe(data.type_)) - ) and props.get("columns"): + ) and columns is not None: raise ValueError( "Cannot pass in both a pandas dataframe and columns to the data_table component." ) @@ -84,7 +88,7 @@ def create(cls, *children, **props): if ( (isinstance(data, Var) and types._issubclass(data.type_, List)) or issubclass(type(data), List) - ) and not props.get("columns"): + ) and columns is None: raise ValueError( "column field should be specified when the data field is a list type" ) diff --git a/reflex/components/navigation/link.py b/reflex/components/navigation/link.py index 1032f40287..9c66f712f6 100644 --- a/reflex/components/navigation/link.py +++ b/reflex/components/navigation/link.py @@ -45,7 +45,7 @@ def create(cls, *children, **props) -> Component: Returns: Component: The link component """ - if props.get("href"): + if props.get("href") is not None: if not len(children): raise ValueError("Link without a child will not display") else: diff --git a/reflex/state.py b/reflex/state.py index c749f0cd67..5a623c49cb 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -885,7 +885,7 @@ def _mark_dirty_computed_vars(self) -> None: self.dirty_vars.add(cvar) dirty_vars.add(cvar) actual_var = self.computed_vars.get(cvar) - if actual_var: + if actual_var is not None: actual_var.mark_dirty(instance=self) def _dirty_computed_vars(self, from_vars: set[str] | None = None) -> set[str]: diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 902d9610cf..2d3a2f3281 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -542,7 +542,7 @@ def format_ref(ref: str) -> str: return f"ref_{clean_ref}" -def format_array_ref(refs: str, idx) -> str: +def format_array_ref(refs: str, idx: Var | None) -> str: """Format a ref accessed by array. Args: @@ -553,11 +553,10 @@ def format_array_ref(refs: str, idx) -> str: The formatted ref. """ clean_ref = re.sub(r"[^\w]+", "_", refs) - if idx: + if idx is not None: idx.is_local = True return f"refs_{clean_ref}[{idx}]" - else: - return f"refs_{clean_ref}" + return f"refs_{clean_ref}" def format_dict(prop: ComponentStyle) -> str: diff --git a/reflex/vars.py b/reflex/vars.py index e0a4bf5062..1cc282dc90 100644 --- a/reflex/vars.py +++ b/reflex/vars.py @@ -205,6 +205,27 @@ def __str__(self) -> str: out = format.format_string(out) return out + def __bool__(self) -> bool: + """Raise exception if using Var in a boolean context. + + Raises: + TypeError: when attempting to bool-ify the Var. + """ + raise TypeError( + f"Cannot convert Var {self.full_name!r} to bool for use with `if`, `and`, `or`, and `not`. " + "Instead use `rx.cond` and bitwise operators `&` (and), `|` (or), `~` (invert)." + ) + + def __iter__(self) -> Any: + """Raise exception if using Var in an iterable context. + + Raises: + TypeError: when attempting to iterate over the Var. + """ + raise TypeError( + f"Cannot iterate over Var {self.full_name!r}. Instead use `rx.foreach`." + ) + def __format__(self, format_spec: str) -> str: """Format the var into a Javascript equivalent to an f-string. @@ -1414,7 +1435,7 @@ def get_local_storage(key: Var | str | None = None) -> BaseVar: Raises: TypeError: if the wrong key type is provided. """ - if key: + if key is not None: if not (isinstance(key, Var) and key.type_ == str) and not isinstance(key, str): type_ = type(key) if not isinstance(key, Var) else key.type_ raise TypeError( diff --git a/tests/components/forms/test_debounce.py b/tests/components/forms/test_debounce.py index b235fc50b6..128afb16d0 100644 --- a/tests/components/forms/test_debounce.py +++ b/tests/components/forms/test_debounce.py @@ -50,8 +50,8 @@ def test_render_child_props(): ) )._render() assert tag.props["sx"] == {"foo": "bar", "baz": "quuc"} - assert tag.props["value"] == BaseVar( - name="real", type_=str, is_local=True, is_string=False + assert tag.props["value"].equals( + BaseVar(name="real", type_=str, is_local=True, is_string=False) ) assert len(tag.props["onChange"].events) == 1 assert tag.props["onChange"].events[0].handler == S.on_change @@ -75,6 +75,7 @@ def test_render_child_props_recursive(): on_change=S.on_change, ), value="inner", + debounce_timeout=666, force_notify_on_blur=False, ), debounce_timeout=42, @@ -84,8 +85,8 @@ def test_render_child_props_recursive(): force_notify_by_enter=False, )._render() assert tag.props["sx"] == {"foo": "bar", "baz": "quuc"} - assert tag.props["value"] == BaseVar( - name="real", type_=str, is_local=True, is_string=False + assert tag.props["value"].equals( + BaseVar(name="outer", type_=str, is_local=True, is_string=False) ) assert tag.props["forceNotifyOnBlur"].name == "false" assert tag.props["forceNotifyByEnter"].name == "false" diff --git a/tests/components/layout/test_cond.py b/tests/components/layout/test_cond.py index 2fe1cf9256..7b1b18537b 100644 --- a/tests/components/layout/test_cond.py +++ b/tests/components/layout/test_cond.py @@ -104,7 +104,7 @@ def test_cond_no_else(): assert isinstance(comp, Fragment) comp = comp.children[0] assert isinstance(comp, Cond) - assert comp.cond == True # noqa + assert comp.cond._decode() is True # type: ignore assert comp.comp1 == Fragment.create(Text.create("hello")) assert comp.comp2 == Fragment.create() diff --git a/tests/components/layout/test_foreach.py b/tests/components/layout/test_foreach.py index 065ddacec0..6830985b50 100644 --- a/tests/components/layout/test_foreach.py +++ b/tests/components/layout/test_foreach.py @@ -186,6 +186,6 @@ def test_foreach_render(state_var, render_fn, render_dict): rend = component.render() arg_index = rend["arg_index"] assert rend["iterable_state"] == render_dict["iterable_state"] - assert rend["arg_index"] == render_dict["arg_index"] + assert arg_index.name == render_dict["arg_index"] assert arg_index.type_ == int assert rend["iterable_type"] == render_dict["iterable_type"] diff --git a/tests/components/test_component.py b/tests/components/test_component.py index 8410e7bf47..b351aeba18 100644 --- a/tests/components/test_component.py +++ b/tests/components/test_component.py @@ -329,8 +329,8 @@ def test_valid_props(component1, text: str, number: int): number: A test number. """ c = component1.create(text=text, number=number) - assert c.text == text - assert c.number == number + assert c.text._decode() == text + assert c.number._decode() == number @pytest.mark.parametrize( @@ -357,7 +357,7 @@ def test_var_props(component1, test_state): test_state: A test state. """ c1 = component1.create(text="hello", number=test_state.num) - assert c1.number == test_state.num + assert c1.number.equals(test_state.num) def test_get_controlled_triggers(component1, component2): diff --git a/tests/components/test_tag.py b/tests/components/test_tag.py index 7964838ad4..daf5418eb7 100644 --- a/tests/components/test_tag.py +++ b/tests/components/test_tag.py @@ -52,8 +52,8 @@ def test_is_valid_prop(prop: Var, valid: bool): def test_add_props(): """Test that the props are added.""" tag = Tag().add_props(key="value", key2=42, invalid=None, invalid2={}) - assert tag.props["key"] == Var.create("value") - assert tag.props["key2"] == Var.create(42) + assert tag.props["key"].equals(Var.create("value")) + assert tag.props["key2"].equals(Var.create(42)) assert "invalid" not in tag.props assert "invalid2" not in tag.props @@ -100,7 +100,8 @@ def test_format_tag(tag: Tag, expected: Dict): tag_dict = dict(tag) assert tag_dict["name"] == expected["name"] assert tag_dict["contents"] == expected["contents"] - assert tag_dict["props"] == expected["props"] + for prop, prop_value in tag_dict["props"].items(): + assert prop_value.equals(Var.create_safe(expected["props"][prop])) def test_format_cond_tag(): @@ -116,7 +117,8 @@ def test_format_cond_tag(): tag_dict["true_value"], tag_dict["false_value"], ) - assert cond == "logged_in" + assert cond.name == "logged_in" + assert cond.type_ == bool assert true_value["name"] == "h1" assert true_value["contents"] == "True content" diff --git a/tests/test_app.py b/tests/test_app.py index 1549ab8c13..e190f4e221 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -930,5 +930,6 @@ async def test_process_events(gen_state, mocker): async for _update in process(app, event, "mock_sid", {}, "127.0.0.1"): pass - assert gen_state.value == 5 + + assert app.state_manager.get_state("token").value == 5 assert app.postprocess.call_count == 6 diff --git a/tests/test_event.py b/tests/test_event.py index ac4056d789..debfac54cd 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -55,7 +55,10 @@ def test_fn_with_args(_, arg1, arg2): # Test passing vars as args. assert event_spec.handler == handler - assert event_spec.args == (("arg1", "first"), ("arg2", "second")) + assert event_spec.args[0][0].equals(Var.create_safe("arg1")) + assert event_spec.args[0][1].equals(Var.create_safe("first")) + assert event_spec.args[1][0].equals(Var.create_safe("arg2")) + assert event_spec.args[1][1].equals(Var.create_safe("second")) assert ( format.format_event(event_spec) == 'E("test_fn_with_args", {arg1:first,arg2:second})' @@ -77,10 +80,10 @@ def test_fn_with_args(_, arg1, arg2): ) assert event_spec.handler == handler - assert event_spec.args == ( - ("arg1", format.json_dumps(first)), - ("arg2", format.json_dumps(second)), - ) + assert event_spec.args[0][0].equals(Var.create_safe("arg1")) + assert event_spec.args[0][1].equals(Var.create_safe(first)) + assert event_spec.args[1][0].equals(Var.create_safe("arg2")) + assert event_spec.args[1][1].equals(Var.create_safe(second)) handler = EventHandler(fn=test_fn_with_args) with pytest.raises(TypeError): @@ -121,7 +124,8 @@ def test_event_redirect(): spec = event.redirect("/path") assert isinstance(spec, EventSpec) assert spec.handler.fn.__qualname__ == "_redirect" - assert spec.args == (("path", "/path"),) + assert spec.args[0][0].equals(Var.create_safe("path")) + assert spec.args[0][1].equals(Var.create_safe("/path")) assert format.format_event(spec) == 'E("_redirect", {path:"/path"})' spec = event.redirect(Var.create_safe("path")) assert format.format_event(spec) == 'E("_redirect", {path:path})' @@ -132,7 +136,8 @@ def test_event_console_log(): spec = event.console_log("message") assert isinstance(spec, EventSpec) assert spec.handler.fn.__qualname__ == "_console" - assert spec.args == (("message", "message"),) + assert spec.args[0][0].equals(Var.create_safe("message")) + assert spec.args[0][1].equals(Var.create_safe("message")) assert format.format_event(spec) == 'E("_console", {message:"message"})' spec = event.console_log(Var.create_safe("message")) assert format.format_event(spec) == 'E("_console", {message:message})' @@ -143,7 +148,8 @@ def test_event_window_alert(): spec = event.window_alert("message") assert isinstance(spec, EventSpec) assert spec.handler.fn.__qualname__ == "_alert" - assert spec.args == (("message", "message"),) + assert spec.args[0][0].equals(Var.create_safe("message")) + assert spec.args[0][1].equals(Var.create_safe("message")) assert format.format_event(spec) == 'E("_alert", {message:"message"})' spec = event.window_alert(Var.create_safe("message")) assert format.format_event(spec) == 'E("_alert", {message:message})' @@ -154,7 +160,8 @@ def test_set_focus(): spec = event.set_focus("input1") assert isinstance(spec, EventSpec) assert spec.handler.fn.__qualname__ == "_set_focus" - assert spec.args == (("ref", Var.create_safe("ref_input1")),) + assert spec.args[0][0].equals(Var.create_safe("ref")) + assert spec.args[0][1].equals(Var.create_safe("ref_input1")) assert format.format_event(spec) == 'E("_set_focus", {ref:ref_input1})' spec = event.set_focus("input1") assert format.format_event(spec) == 'E("_set_focus", {ref:ref_input1})' @@ -165,10 +172,10 @@ def test_set_value(): spec = event.set_value("input1", "") assert isinstance(spec, EventSpec) assert spec.handler.fn.__qualname__ == "_set_value" - assert spec.args == ( - ("ref", Var.create_safe("ref_input1")), - ("value", ""), - ) + assert spec.args[0][0].equals(Var.create_safe("ref")) + assert spec.args[0][1].equals(Var.create_safe("ref_input1")) + assert spec.args[1][0].equals(Var.create_safe("value")) + assert spec.args[1][1].equals(Var.create_safe("")) assert format.format_event(spec) == 'E("_set_value", {ref:ref_input1,value:""})' spec = event.set_value("input1", Var.create_safe("message")) assert ( @@ -181,10 +188,10 @@ def test_set_cookie(): spec = event.set_cookie("testkey", "testvalue") assert isinstance(spec, EventSpec) assert spec.handler.fn.__qualname__ == "_set_cookie" - assert spec.args == ( - ("key", "testkey"), - ("value", "testvalue"), - ) + assert spec.args[0][0].equals(Var.create_safe("key")) + assert spec.args[0][1].equals(Var.create_safe("testkey")) + assert spec.args[1][0].equals(Var.create_safe("value")) + assert spec.args[1][1].equals(Var.create_safe("testvalue")) assert ( format.format_event(spec) == 'E("_set_cookie", {key:"testkey",value:"testvalue"})' @@ -196,7 +203,10 @@ def test_remove_cookie(): spec = event.remove_cookie("testkey") assert isinstance(spec, EventSpec) assert spec.handler.fn.__qualname__ == "_remove_cookie" - assert spec.args == (("key", "testkey"), ("options", {})) + assert spec.args[0][0].equals(Var.create_safe("key")) + assert spec.args[0][1].equals(Var.create_safe("testkey")) + assert spec.args[1][0].equals(Var.create_safe("options")) + assert spec.args[1][1].equals(Var.create_safe({})) assert ( format.format_event(spec) == 'E("_remove_cookie", {key:"testkey",options:{}})' ) @@ -213,7 +223,10 @@ def test_remove_cookie_with_options(): spec = event.remove_cookie("testkey", options) assert isinstance(spec, EventSpec) assert spec.handler.fn.__qualname__ == "_remove_cookie" - assert spec.args == (("key", "testkey"), ("options", options)) + assert spec.args[0][0].equals(Var.create_safe("key")) + assert spec.args[0][1].equals(Var.create_safe("testkey")) + assert spec.args[1][0].equals(Var.create_safe("options")) + assert spec.args[1][1].equals(Var.create_safe(options)) assert ( format.format_event(spec) == f'E("_remove_cookie", {{key:"testkey",options:{json.dumps(options)}}})' @@ -225,10 +238,10 @@ def test_set_local_storage(): spec = event.set_local_storage("testkey", "testvalue") assert isinstance(spec, EventSpec) assert spec.handler.fn.__qualname__ == "_set_local_storage" - assert spec.args == ( - ("key", "testkey"), - ("value", "testvalue"), - ) + assert spec.args[0][0].equals(Var.create_safe("key")) + assert spec.args[0][1].equals(Var.create_safe("testkey")) + assert spec.args[1][0].equals(Var.create_safe("value")) + assert spec.args[1][1].equals(Var.create_safe("testvalue")) assert ( format.format_event(spec) == 'E("_set_local_storage", {key:"testkey",value:"testvalue"})' @@ -249,5 +262,6 @@ def test_remove_local_storage(): spec = event.remove_local_storage("testkey") assert isinstance(spec, EventSpec) assert spec.handler.fn.__qualname__ == "_remove_local_storage" - assert spec.args == (("key", "testkey"),) + assert spec.args[0][0].equals(Var.create_safe("key")) + assert spec.args[0][1].equals(Var.create_safe("testkey")) assert format.format_event(spec) == 'E("_remove_local_storage", {key:"testkey"})' diff --git a/tests/test_state.py b/tests/test_state.py index a783e89f8d..e7f51c1170 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -408,18 +408,18 @@ def test_get_class_substate(): def test_get_class_var(): """Test getting the var of a class.""" - assert TestState.get_class_var(("num1",)) == TestState.num1 - assert TestState.get_class_var(("num2",)) == TestState.num2 - assert ChildState.get_class_var(("value",)) == ChildState.value - assert GrandchildState.get_class_var(("value2",)) == GrandchildState.value2 - assert TestState.get_class_var(("child_state", "value")) == ChildState.value - assert ( - TestState.get_class_var(("child_state", "grandchild_state", "value2")) - == GrandchildState.value2 + assert TestState.get_class_var(("num1",)).equals(TestState.num1) + assert TestState.get_class_var(("num2",)).equals(TestState.num2) + assert ChildState.get_class_var(("value",)).equals(ChildState.value) + assert GrandchildState.get_class_var(("value2",)).equals(GrandchildState.value2) + assert TestState.get_class_var(("child_state", "value")).equals(ChildState.value) + assert TestState.get_class_var( + ("child_state", "grandchild_state", "value2") + ).equals( + GrandchildState.value2, ) - assert ( - ChildState.get_class_var(("grandchild_state", "value2")) - == GrandchildState.value2 + assert ChildState.get_class_var(("grandchild_state", "value2")).equals( + GrandchildState.value2, ) with pytest.raises(ValueError): TestState.get_class_var(("invalid_var",)) @@ -837,21 +837,32 @@ def test_get_query_params(test_state): assert test_state.get_query_params() == params -def test_add_var(test_state): - test_state.add_var("dynamic_int", int, 42) - assert test_state.dynamic_int == 42 - - test_state.add_var("dynamic_list", List[int], [5, 10]) - assert test_state.dynamic_list == [5, 10] - assert test_state.dynamic_list == [5, 10] - - # how to test that one? - # test_state.dynamic_list.append(15) - # assert test_state.dynamic_list == [5, 10, 15] +def test_add_var(): + class DynamicState(State): + pass - test_state.add_var("dynamic_dict", Dict[str, int], {"k1": 5, "k2": 10}) - assert test_state.dynamic_dict == {"k1": 5, "k2": 10} - assert test_state.dynamic_dict == {"k1": 5, "k2": 10} + ds1 = DynamicState() + assert "dynamic_int" not in ds1.__dict__ + assert not hasattr(ds1, "dynamic_int") + ds1.add_var("dynamic_int", int, 42) + # Existing instances get the BaseVar + assert ds1.dynamic_int.equals(DynamicState.dynamic_int) # type: ignore + # New instances get an actual value with the default + assert DynamicState().dynamic_int == 42 + + ds1.add_var("dynamic_list", List[int], [5, 10]) + assert ds1.dynamic_list.equals(DynamicState.dynamic_list) # type: ignore + ds2 = DynamicState() + assert ds2.dynamic_list == [5, 10] + ds2.dynamic_list.append(15) + assert ds2.dynamic_list == [5, 10, 15] + assert DynamicState().dynamic_list == [5, 10] + + ds1.add_var("dynamic_dict", Dict[str, int], {"k1": 5, "k2": 10}) + assert ds1.dynamic_dict.equals(DynamicState.dynamic_dict) # type: ignore + assert ds2.dynamic_dict.equals(DynamicState.dynamic_dict) # type: ignore + assert DynamicState().dynamic_dict == {"k1": 5, "k2": 10} + assert DynamicState().dynamic_dict == {"k1": 5, "k2": 10} def test_add_var_default_handlers(test_state):