Browse Source

Track backend-only vars that are declared without a default value (#4016)

* Track backend-only vars that are declared without a default value

Without this provision, declared backend vars can be accidentally shared among
all states if a mutable value is assigned to the class attribute.

* add test case for no default backend var
Masen Furer 7 months ago
parent
commit
9ca5d4a095
2 changed files with 17 additions and 1 deletions
  1. 10 0
      reflex/state.py
  2. 7 1
      tests/units/test_state.py

+ 10 - 0
reflex/state.py

@@ -30,6 +30,7 @@ from typing import (
     Type,
     Type,
     Union,
     Union,
     cast,
     cast,
+    get_type_hints,
 )
 )
 
 
 import dill
 import dill
@@ -573,6 +574,15 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
             for name, value in cls.__dict__.items()
             for name, value in cls.__dict__.items()
             if types.is_backend_base_variable(name, cls)
             if types.is_backend_base_variable(name, cls)
         }
         }
+        # Add annotated backend vars that do not have a default value.
+        new_backend_vars.update(
+            {
+                name: Var("", _var_type=annotation_value).get_default_value()
+                for name, annotation_value in get_type_hints(cls).items()
+                if name not in new_backend_vars
+                and types.is_backend_base_variable(name, cls)
+            }
+        )
 
 
         cls.backend_vars = {
         cls.backend_vars = {
             **cls.inherited_backend_vars,
             **cls.inherited_backend_vars,

+ 7 - 1
tests/units/test_state.py

@@ -3216,6 +3216,7 @@ class MixinState(State, mixin=True):
 
 
     num: int = 0
     num: int = 0
     _backend: int = 0
     _backend: int = 0
+    _backend_no_default: dict
 
 
     @rx.var(cache=True)
     @rx.var(cache=True)
     def computed(self) -> str:
     def computed(self) -> str:
@@ -3243,11 +3244,16 @@ def test_mixin_state() -> None:
     """Test that a mixin state works correctly."""
     """Test that a mixin state works correctly."""
     assert "num" in UsesMixinState.base_vars
     assert "num" in UsesMixinState.base_vars
     assert "num" in UsesMixinState.vars
     assert "num" in UsesMixinState.vars
-    assert UsesMixinState.backend_vars == {"_backend": 0}
+    assert UsesMixinState.backend_vars == {"_backend": 0, "_backend_no_default": {}}
 
 
     assert "computed" in UsesMixinState.computed_vars
     assert "computed" in UsesMixinState.computed_vars
     assert "computed" in UsesMixinState.vars
     assert "computed" in UsesMixinState.vars
 
 
+    assert (
+        UsesMixinState(_reflex_internal_init=True)._backend_no_default  # type: ignore
+        is not UsesMixinState.backend_vars["_backend_no_default"]
+    )
+
 
 
 def test_child_mixin_state() -> None:
 def test_child_mixin_state() -> None:
     """Test that mixin vars are only applied to the highest state in the hierarchy."""
     """Test that mixin vars are only applied to the highest state in the hierarchy."""