Ver código fonte

raise error when passing a str(var) (#4769)

* raise error when passing a str(var)

* make it faster

* fix typo

* fix tests

* mocker consistency

Co-authored-by: Thomas Brandého <thomas.brandeho@gmail.com>

* ditto

Co-authored-by: Thomas Brandého <thomas.brandeho@gmail.com>

---------

Co-authored-by: Thomas Brandého <thomas.brandeho@gmail.com>
Khaleel Al-Adhami 3 meses atrás
pai
commit
372bd22475

+ 40 - 0
reflex/components/base/bare.py

@@ -7,9 +7,44 @@ from typing import Any, Iterator
 from reflex.components.component import Component, LiteralComponentVar
 from reflex.components.tags import Tag
 from reflex.components.tags.tagless import Tagless
+from reflex.config import PerformanceMode, environment
+from reflex.utils import console
+from reflex.utils.decorator import once
 from reflex.utils.imports import ParsedImportDict
 from reflex.vars import BooleanVar, ObjectVar, Var
 from reflex.vars.base import VarData
+from reflex.vars.sequence import LiteralStringVar
+
+
+@once
+def get_performance_mode():
+    """Get the performance mode.
+
+    Returns:
+        The performance mode.
+    """
+    return environment.REFLEX_PERF_MODE.get()
+
+
+def validate_str(value: str):
+    """Validate a string value.
+
+    Args:
+        value: The value to validate.
+
+    Raises:
+        ValueError: If the value is a Var and the performance mode is set to raise.
+    """
+    perf_mode = get_performance_mode()
+    if perf_mode != PerformanceMode.OFF and value.startswith("reflex___state"):
+        if perf_mode == PerformanceMode.WARN:
+            console.warn(
+                f"Output includes {value!s} which will be displayed as a string. If you are calling `str` on a Var, consider using .to_string() instead."
+            )
+        elif perf_mode == PerformanceMode.RAISE:
+            raise ValueError(
+                f"Output includes {value!s} which will be displayed as a string. If you are calling `str` on a Var, consider using .to_string() instead."
+            )
 
 
 class Bare(Component):
@@ -28,9 +63,14 @@ class Bare(Component):
             The component.
         """
         if isinstance(contents, Var):
+            if isinstance(contents, LiteralStringVar):
+                validate_str(contents._var_value)
             return cls(contents=contents)
         else:
+            if isinstance(contents, str):
+                validate_str(contents)
             contents = str(contents) if contents is not None else ""
+
         return cls(contents=contents)
 
     def _get_all_hooks_internal(self) -> dict[str, VarData | None]:

+ 1 - 1
reflex/config.py

@@ -577,7 +577,7 @@ class EnvironmentVariables:
     REFLEX_CHECK_LATEST_VERSION: EnvVar[bool] = env_var(True)
 
     # In which performance mode to run the app.
-    REFLEX_PERF_MODE: EnvVar[Optional[PerformanceMode]] = env_var(PerformanceMode.WARN)
+    REFLEX_PERF_MODE: EnvVar[PerformanceMode] = env_var(PerformanceMode.WARN)
 
     # The maximum size of the reflex state in kilobytes.
     REFLEX_STATE_SIZE_LIMIT: EnvVar[int] = env_var(1000)

+ 25 - 0
reflex/utils/decorator.py

@@ -0,0 +1,25 @@
+"""Decorator utilities."""
+
+from typing import Callable, TypeVar
+
+T = TypeVar("T")
+
+
+def once(f: Callable[[], T]) -> Callable[[], T]:
+    """A decorator that calls the function once and caches the result.
+
+    Args:
+        f: The function to call.
+
+    Returns:
+        A function that calls the function once and caches the result.
+    """
+    unset = object()
+    value: object | T = unset
+
+    def wrapper() -> T:
+        nonlocal value
+        value = f() if value is unset else value
+        return value  # pyright: ignore[reportReturnType]
+
+    return wrapper

+ 25 - 0
tests/units/test_var.py

@@ -8,6 +8,7 @@ from pandas import DataFrame
 
 import reflex as rx
 from reflex.base import Base
+from reflex.config import PerformanceMode
 from reflex.constants.base import REFLEX_VAR_CLOSING_TAG, REFLEX_VAR_OPENING_TAG
 from reflex.state import BaseState
 from reflex.utils.exceptions import (
@@ -1893,3 +1894,27 @@ def test_var_data_hooks():
 def test_var_data_with_hooks_value():
     var_data = VarData(hooks={"what": VarData(hooks={"whot": VarData(hooks="whott")})})
     assert var_data == VarData(hooks=["what", "whot", "whott"])
+
+
+def test_str_var_in_components(mocker):
+    class StateWithVar(rx.State):
+        field: int = 1
+
+    mocker.patch(
+        "reflex.components.base.bare.get_performance_mode",
+        return_value=PerformanceMode.RAISE,
+    )
+
+    with pytest.raises(ValueError):
+        rx.vstack(
+            str(StateWithVar.field),
+        )
+
+    mocker.patch(
+        "reflex.components.base.bare.get_performance_mode",
+        return_value=PerformanceMode.OFF,
+    )
+
+    rx.vstack(
+        str(StateWithVar.field),
+    )