Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENG-3476]Setting State Vars that are not defined should raise an error #4007

Merged
merged 12 commits into from
Oct 3, 2024
15 changes: 15 additions & 0 deletions reflex/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
EventHandlerShadowsBuiltInStateMethod,
ImmutableStateError,
LockExpiredError,
SetUndefinedStateVarError,
)
from reflex.utils.exec import is_testing_env
from reflex.utils.serializers import serializer
Expand Down Expand Up @@ -1260,6 +1261,9 @@ def __setattr__(self, name: str, value: Any):
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
Expand All @@ -1277,6 +1281,17 @@ def __setattr__(self, name: str, value: Any):
self._mark_dirty()
return

if (
not name.startswith("__")
and not name.startswith(f"_{type(self).__name__}__")
and name not in self.vars
ElijahAhianyo marked this conversation as resolved.
Show resolved Hide resolved
and name not in self.get_skip_vars()
):
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)

Expand Down
4 changes: 4 additions & 0 deletions reflex/utils/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
43 changes: 43 additions & 0 deletions tests/units/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
)
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
Expand Down Expand Up @@ -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()
Loading