Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/6662.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Avoid re-entering config loading when a `State` subclass is defined in `rxconfig.py`.
1 change: 1 addition & 0 deletions packages/reflex-base/news/6662.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Avoid re-entering config loading when a `State` subclass is defined in `rxconfig.py`.
27 changes: 27 additions & 0 deletions packages/reflex-base/src/reflex_base/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,11 @@ def _post_init(self, **kwargs):
self._non_default_attributes = set(kwargs.keys())
self._replace_defaults(**kwargs)

# Publish for State-class creation so it never re-enters get_config()
# (which AttributeErrors if a State is defined while rxconfig.py is mid-import).
global _state_auto_setters
_state_auto_setters = self.state_auto_setters

if (
self.state_manager_mode == constants.StateManagerMode.REDIS
and not self.redis_url
Expand Down Expand Up @@ -739,6 +744,28 @@ def _get_config() -> Config:
# Protect sys.path from concurrent modification
_config_lock = threading.RLock()

# Cached state_auto_setters so State-class creation never re-enters get_config().
_state_auto_setters: bool | None = None


def get_state_auto_setters() -> bool:
"""Return whether state auto-setters are enabled, without importing rxconfig.

Reads the value cached when the Config was built. Before any Config exists
(e.g. a State defined inside rxconfig.py during its import), falls back to the
REFLEX_STATE_AUTO_SETTERS env var, then the default (False). This never calls
get_config() or imports rxconfig, so it cannot re-enter config loading.

Returns:
Whether state auto-setters are enabled.
"""
if _state_auto_setters is not None:
return _state_auto_setters
env_val = os.environ.get(Config._prefixes[0] + "STATE_AUTO_SETTERS")
if env_val and env_val.strip():
return interpret_env_var_value(env_val, bool, "state_auto_setters")
return False


def get_config(reload: bool = False) -> Config:
"""Get the app config.
Expand Down
4 changes: 2 additions & 2 deletions reflex/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -1139,7 +1139,7 @@ def _init_var(cls, name: str, prop: Var):
Raises:
VarTypeError: if the variable has an incorrect type
"""
from reflex_base.config import get_config
from reflex_base.config import get_state_auto_setters
from reflex_base.utils.exceptions import VarTypeError

if not types.is_valid_var_type(prop._var_type):
Expand All @@ -1151,7 +1151,7 @@ def _init_var(cls, name: str, prop: Var):
)
raise VarTypeError(msg)
cls._set_var(name, prop)
if cls.is_user_defined() and get_config().state_auto_setters is True:
if cls.is_user_defined() and get_state_auto_setters() is True:
cls._create_setter(name, prop)
cls._set_default_value(name, prop)

Expand Down
123 changes: 123 additions & 0 deletions tests/units/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -4005,6 +4005,129 @@ class TestState(State):
assert "setvar" in TestState.event_handlers


def test_state_defined_in_rxconfig_does_not_crash(tmp_path):
"""A State subclass defined in rxconfig.py must not crash config loading.

Regression: _init_var read get_config().state_auto_setters at class-creation
time, which re-entered config loading while rxconfig was still importing and
raised AttributeError because the rxconfig module had no `config` attribute
yet.
"""
proj_root = tmp_path / "project1"
proj_root.mkdir()

config_string = """
import reflex as rx


class RxconfigDefinedState(rx.State):
n: int = 0


config = rx.Config(
app_name="project1",
)
"""

(proj_root / "rxconfig.py").write_text(dedent(config_string))

with chdir(proj_root):
# Must not raise (previously raised AttributeError mid-import).
reflex_base.config.get_config(reload=True)
del sys.modules[constants.Config.MODULE]


def test_state_in_rxconfig_honors_env_auto_setters(tmp_path, monkeypatch):
"""A State defined in rxconfig.py (pre-config) honors REFLEX_STATE_AUTO_SETTERS.

During rxconfig import the Config does not exist yet, so the cached value is
unset and get_state_auto_setters falls back to the env var.
"""
# Simulate a fresh process where no Config has been built yet.
monkeypatch.setattr(reflex_base.config, "_state_auto_setters", None)
monkeypatch.setenv("REFLEX_STATE_AUTO_SETTERS", "true")

proj_root = tmp_path / "project1"
proj_root.mkdir()
config_string = """
import reflex as rx


class RxconfigEnvSetterState(rx.State):
n: int = 0


config = rx.Config(app_name="project1")
"""
(proj_root / "rxconfig.py").write_text(dedent(config_string))

with chdir(proj_root):
reflex_base.config.get_config(reload=True)
state_cls = sys.modules[constants.Config.MODULE].RxconfigEnvSetterState
assert "set_n" in state_cls.event_handlers
del sys.modules[constants.Config.MODULE]


def test_state_in_rxconfig_defaults_to_no_auto_setters(tmp_path, monkeypatch):
"""A State defined in rxconfig.py gets no auto-setters by default (pre-config)."""
monkeypatch.setattr(reflex_base.config, "_state_auto_setters", None)
monkeypatch.delenv("REFLEX_STATE_AUTO_SETTERS", raising=False)

proj_root = tmp_path / "project1"
proj_root.mkdir()
config_string = """
import reflex as rx


class RxconfigNoSetterState(rx.State):
n: int = 0


config = rx.Config(app_name="project1")
"""
(proj_root / "rxconfig.py").write_text(dedent(config_string))

with chdir(proj_root):
reflex_base.config.get_config(reload=True)
state_cls = sys.modules[constants.Config.MODULE].RxconfigNoSetterState
assert list(state_cls.event_handlers) == ["setvar"]
del sys.modules[constants.Config.MODULE]


def test_state_auto_setters_cache_tracks_reload(tmp_path):
"""The cached state_auto_setters value follows config reloads (no stale flag)."""
proj_root = tmp_path / "project1"
proj_root.mkdir()
rxconfig_path = proj_root / "rxconfig.py"
off_config = """
import reflex as rx
config = rx.Config(app_name="project1", state_auto_setters=False)
"""
on_config = """
import reflex as rx
config = rx.Config(app_name="project1", state_auto_setters=True)
"""

with chdir(proj_root):
rxconfig_path.write_text(dedent(off_config))
reflex_base.config.get_config(reload=True)
from reflex.state import State

class ReloadOffState(State):
num: int = 0

assert list(ReloadOffState.event_handlers) == ["setvar"]

rxconfig_path.write_text(dedent(on_config))
reflex_base.config.get_config(reload=True)

class ReloadOnState(State):
num: int = 0

assert "set_num" in ReloadOnState.event_handlers
del sys.modules[constants.Config.MODULE]


class MixinState(State, mixin=True):
"""A mixin state for testing."""

Expand Down
Loading