Browse Source

[ENG-3870] rx.call_script with f-string var produces incorrect code (#4039)

* Add additional test cases for rx.call_script

Include internal vars inside an f-string to be properly rendered on the backend
and frontend.

* [ENG-3870] rx.call_script with f-string var produces incorrect code

Avoid casting javascript code with embedded Var as LiteralStringVar

There are two cases that need to be handled:

1. The javascript code contains Vars with VarData, these can only be evaluated
   in the component context, since they may use hooks. Vars with VarData cannot be
   used from the backend. In this case, we cast the given code as a raw js
   expression and include the extracted VarData.

2. The javascript code has no VarData. In this case, we pass the code as the
   raw js expression and cast to a python str to get a js literal string to eval.

* use VarData.__bool__ instead of `is None`
Masen Furer 7 months ago
parent
commit
a66e0f2e11
2 changed files with 169 additions and 0 deletions
  1. 10 0
      reflex/event.py
  2. 159 0
      tests/integration/test_call_script.py

+ 10 - 0
reflex/event.py

@@ -839,6 +839,16 @@ def call_script(
                 ),
             ),
         }
+    if isinstance(javascript_code, str):
+        # When there is VarData, include it and eval the JS code inline on the client.
+        javascript_code, original_code = (
+            LiteralVar.create(javascript_code),
+            javascript_code,
+        )
+        if not javascript_code._get_all_var_data():
+            # Without VarData, cast to string and eval the code in the event loop.
+            javascript_code = str(Var(_js_expr=original_code))
+
     return server_side(
         "_call_script",
         get_fn_signature(call_script),

+ 159 - 0
tests/integration/test_call_script.py

@@ -46,6 +46,7 @@ def CallScript():
         inline_counter: int = 0
         external_counter: int = 0
         value: str = "Initial"
+        last_result: str = ""
 
         def call_script_callback(self, result):
             self.results.append(result)
@@ -137,6 +138,32 @@ def CallScript():
                 callback=CallScriptState.set_external_counter,  # type: ignore
             )
 
+        def call_with_var_f_string(self):
+            return rx.call_script(
+                f"{rx.Var('inline_counter')} + {rx.Var('external_counter')}",
+                callback=CallScriptState.set_last_result,  # type: ignore
+            )
+
+        def call_with_var_str_cast(self):
+            return rx.call_script(
+                f"{str(rx.Var('inline_counter'))} + {str(rx.Var('external_counter'))}",
+                callback=CallScriptState.set_last_result,  # type: ignore
+            )
+
+        def call_with_var_f_string_wrapped(self):
+            return rx.call_script(
+                rx.Var(f"{rx.Var('inline_counter')} + {rx.Var('external_counter')}"),
+                callback=CallScriptState.set_last_result,  # type: ignore
+            )
+
+        def call_with_var_str_cast_wrapped(self):
+            return rx.call_script(
+                rx.Var(
+                    f"{str(rx.Var('inline_counter'))} + {str(rx.Var('external_counter'))}"
+                ),
+                callback=CallScriptState.set_last_result,  # type: ignore
+            )
+
         def reset_(self):
             yield rx.call_script("inline_counter = 0; external_counter = 0")
             self.reset()
@@ -234,6 +261,68 @@ def CallScript():
                 id="update_value",
             ),
             rx.button("Reset", id="reset", on_click=CallScriptState.reset_),
+            rx.input(
+                value=CallScriptState.last_result,
+                id="last_result",
+                read_only=True,
+                on_click=CallScriptState.set_last_result(""),  # type: ignore
+            ),
+            rx.button(
+                "call_with_var_f_string",
+                on_click=CallScriptState.call_with_var_f_string,
+                id="call_with_var_f_string",
+            ),
+            rx.button(
+                "call_with_var_str_cast",
+                on_click=CallScriptState.call_with_var_str_cast,
+                id="call_with_var_str_cast",
+            ),
+            rx.button(
+                "call_with_var_f_string_wrapped",
+                on_click=CallScriptState.call_with_var_f_string_wrapped,
+                id="call_with_var_f_string_wrapped",
+            ),
+            rx.button(
+                "call_with_var_str_cast_wrapped",
+                on_click=CallScriptState.call_with_var_str_cast_wrapped,
+                id="call_with_var_str_cast_wrapped",
+            ),
+            rx.button(
+                "call_with_var_f_string_inline",
+                on_click=rx.call_script(
+                    f"{rx.Var('inline_counter')} + {CallScriptState.last_result}",
+                    callback=CallScriptState.set_last_result,  # type: ignore
+                ),
+                id="call_with_var_f_string_inline",
+            ),
+            rx.button(
+                "call_with_var_str_cast_inline",
+                on_click=rx.call_script(
+                    f"{str(rx.Var('inline_counter'))} + {str(rx.Var('external_counter'))}",
+                    callback=CallScriptState.set_last_result,  # type: ignore
+                ),
+                id="call_with_var_str_cast_inline",
+            ),
+            rx.button(
+                "call_with_var_f_string_wrapped_inline",
+                on_click=rx.call_script(
+                    rx.Var(
+                        f"{rx.Var('inline_counter')} + {CallScriptState.last_result}"
+                    ),
+                    callback=CallScriptState.set_last_result,  # type: ignore
+                ),
+                id="call_with_var_f_string_wrapped_inline",
+            ),
+            rx.button(
+                "call_with_var_str_cast_wrapped_inline",
+                on_click=rx.call_script(
+                    rx.Var(
+                        f"{str(rx.Var('inline_counter'))} + {str(rx.Var('external_counter'))}"
+                    ),
+                    callback=CallScriptState.set_last_result,  # type: ignore
+                ),
+                id="call_with_var_str_cast_wrapped_inline",
+            ),
         )
 
 
@@ -363,3 +452,73 @@ def test_call_script(
         call_script.poll_for_content(update_value_button, exp_not_equal="Initial")
         == "updated"
     )
+
+
+def test_call_script_w_var(
+    call_script: AppHarness,
+    driver: WebDriver,
+):
+    """Test evaluating javascript expressions containing Vars.
+
+    Args:
+        call_script: harness for CallScript app.
+        driver: WebDriver instance.
+    """
+    assert_token(driver)
+    last_result = driver.find_element(By.ID, "last_result")
+    assert last_result.get_attribute("value") == ""
+
+    inline_return_button = driver.find_element(By.ID, "inline_return")
+
+    call_with_var_f_string_button = driver.find_element(By.ID, "call_with_var_f_string")
+    call_with_var_str_cast_button = driver.find_element(By.ID, "call_with_var_str_cast")
+    call_with_var_f_string_wrapped_button = driver.find_element(
+        By.ID, "call_with_var_f_string_wrapped"
+    )
+    call_with_var_str_cast_wrapped_button = driver.find_element(
+        By.ID, "call_with_var_str_cast_wrapped"
+    )
+    call_with_var_f_string_inline_button = driver.find_element(
+        By.ID, "call_with_var_f_string_inline"
+    )
+    call_with_var_str_cast_inline_button = driver.find_element(
+        By.ID, "call_with_var_str_cast_inline"
+    )
+    call_with_var_f_string_wrapped_inline_button = driver.find_element(
+        By.ID, "call_with_var_f_string_wrapped_inline"
+    )
+    call_with_var_str_cast_wrapped_inline_button = driver.find_element(
+        By.ID, "call_with_var_str_cast_wrapped_inline"
+    )
+
+    inline_return_button.click()
+    call_with_var_f_string_button.click()
+    assert call_script.poll_for_value(last_result, exp_not_equal="") == "1"
+
+    inline_return_button.click()
+    call_with_var_str_cast_button.click()
+    assert call_script.poll_for_value(last_result, exp_not_equal="1") == "2"
+
+    inline_return_button.click()
+    call_with_var_f_string_wrapped_button.click()
+    assert call_script.poll_for_value(last_result, exp_not_equal="2") == "3"
+
+    inline_return_button.click()
+    call_with_var_str_cast_wrapped_button.click()
+    assert call_script.poll_for_value(last_result, exp_not_equal="3") == "4"
+
+    inline_return_button.click()
+    call_with_var_f_string_inline_button.click()
+    assert call_script.poll_for_value(last_result, exp_not_equal="4") == "9"
+
+    inline_return_button.click()
+    call_with_var_str_cast_inline_button.click()
+    assert call_script.poll_for_value(last_result, exp_not_equal="9") == "6"
+
+    inline_return_button.click()
+    call_with_var_f_string_wrapped_inline_button.click()
+    assert call_script.poll_for_value(last_result, exp_not_equal="6") == "13"
+
+    inline_return_button.click()
+    call_with_var_str_cast_wrapped_inline_button.click()
+    assert call_script.poll_for_value(last_result, exp_not_equal="13") == "8"