diff --git a/reflex/state.py b/reflex/state.py index cda36a0a9..64ea960e1 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -74,6 +74,7 @@ from reflex.utils.exceptions import ( EventHandlerShadowsBuiltInStateMethod, ImmutableStateError, LockExpiredError, + SetUndefinedStateVarError, ) from reflex.utils.exec import is_testing_env from reflex.utils.serializers import serializer @@ -1260,6 +1261,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): Args: name: The name of the attribute. value: The value of the attribute. + + Raises: + SetUndefinedStateVarError: If a value of a var is set without first defining it. """ if isinstance(value, MutableProxy): # unwrap proxy objects when assigning back to the state @@ -1277,6 +1281,17 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): self._mark_dirty() return + if ( + name not in self.vars + and name not in self.get_skip_vars() + and not name.startswith("__") + and not name.startswith(f"_{type(self).__name__}__") + ): + raise SetUndefinedStateVarError( + f"The state variable '{name}' has not been defined in '{type(self).__name__}'. " + f"All state variables must be declared before they can be set." + ) + # Set the attribute. super().__setattr__(name, value) diff --git a/reflex/utils/exceptions.py b/reflex/utils/exceptions.py index 7c3532861..9c79a387a 100644 --- a/reflex/utils/exceptions.py +++ b/reflex/utils/exceptions.py @@ -115,3 +115,7 @@ class PrimitiveUnserializableToJSON(ReflexError, ValueError): class InvalidLifespanTaskType(ReflexError, TypeError): """Raised when an invalid task type is registered as a lifespan task.""" + + +class SetUndefinedStateVarError(ReflexError, AttributeError): + """Raised when setting the value of a var without first declaring it.""" diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 205162b9f..5bfac7628 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -41,6 +41,7 @@ from reflex.state import ( ) from reflex.testing import chdir from reflex.utils import format, prerequisites, types +from reflex.utils.exceptions import SetUndefinedStateVarError from reflex.utils.format import json_dumps from reflex.vars.base import ComputedVar, Var from tests.units.states.mutation import MutableSQLAModel, MutableTestState @@ -3262,3 +3263,45 @@ def test_child_mixin_state() -> None: assert "computed" in ChildUsesMixinState.inherited_vars assert "computed" not in ChildUsesMixinState.computed_vars + + +def test_assignment_to_undeclared_vars(): + """Test that an attribute error is thrown when undeclared vars are set.""" + + class State(BaseState): + val: str + _val: str + __val: str # type: ignore + + def handle_supported_regular_vars(self): + self.val = "no underscore" + self._val = "single leading underscore" + self.__val = "double leading undercore" + + def handle_regular_var(self): + self.num = 5 + + def handle_backend_var(self): + self._num = 5 + + def handle_non_var(self): + self.__num = 5 + + class Substate(State): + def handle_var(self): + self.value = 20 + + state = State() # type: ignore + sub_state = Substate() # type: ignore + + with pytest.raises(SetUndefinedStateVarError): + state.handle_regular_var() + + with pytest.raises(SetUndefinedStateVarError): + sub_state.handle_var() + + with pytest.raises(SetUndefinedStateVarError): + state.handle_backend_var() + + state.handle_supported_regular_vars() + state.handle_non_var()