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 packages/reflex-base/news/7669.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Calling `rx.asset` at the top level of `rxconfig.py` no longer raises `RecursionError`. Config reads made while `rxconfig.py` is importing get the environment and default config instead of importing `rxconfig.py` again.
23 changes: 21 additions & 2 deletions packages/reflex-base/src/reflex_base/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,9 @@ def _record_imports() -> Iterator[_ImportRecorder]:
# Protect sys.path from concurrent modification during config loading.
_load_config_lock = threading.RLock()

# Set while rxconfig.py is importing, guarded by _load_config_lock.
_loading_config = False

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

Expand Down Expand Up @@ -989,10 +992,20 @@ def _get_config(project_root: Path | None = None) -> Config:
Returns:
The app config.
"""
global _config_module_deps_root
global _config_module_deps_root, _loading_config, _state_auto_setters

project_root = (project_root or Path.cwd()).resolve()
with _load_config_lock:
if _loading_config:
Comment thread
vjymisal0 marked this conversation as resolved.
Resolved
# rxconfig.py read the config while importing (e.g. via rx.asset);
# importing it again would recurse, so use the env and defaults.
# Building it must not replace the state_auto_setters value a
# Config already created in rxconfig.py published.
state_auto_setters = _state_auto_setters
try:
return Config(app_name="", _skip_plugins_checks=True)
finally:
_state_auto_setters = state_auto_setters
# A fresh str object, so the exact inserted entry can be removed by
# identity: rxconfig.py may itself add or remove equal cwd entries,
# which removal by value could confuse with caller-owned ones.
Expand All @@ -1017,9 +1030,11 @@ def _get_config(project_root: Path | None = None) -> Config:
if PathFinder.find_spec(constants.Config.MODULE, [cwd]) is None:
return Config(app_name="", _skip_plugins_checks=True)
with _record_imports() as recorder:
_loading_config = True
try:
rxconfig = importlib.import_module(constants.Config.MODULE)
finally:
_loading_config = False
# Record even on failure so a later load from another root
# evicts what this one imported. Nothing is evicted here:
# Python already drops a module whose execution failed, and
Expand Down Expand Up @@ -1087,7 +1102,11 @@ def get_config(reload: bool = False) -> Config:
# Serialize check/load/set so threads sharing a context load once.
with _load_config_lock:
if ctx._config is None:
ctx._set_config(_get_config())
config = _get_config()
if _loading_config:
# Don't cache the stand-in returned while rxconfig.py imports.
return config
ctx._set_config(config)
return ctx.config


Expand Down
86 changes: 86 additions & 0 deletions tests/units/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,92 @@ def test_get_config_keeps_caller_owned_cwd_entry(
assert sys.path.count(cwd) == caller_owned


def test_get_config_inside_rxconfig_does_not_reload_it(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, clean_config_modules: None
):
"""rxconfig.py can call rx.asset, which reads the config, without recursing.

The stand-in config rx.asset sees mid-import must not be cached on the context.

Args:
tmp_path: The pytest tmp_path fixture.
monkeypatch: The pytest monkeypatch fixture.
clean_config_modules: Cleanup for modules left behind by the load.
"""
from reflex_base.registry import RegistrationContext

(tmp_path / "assets").mkdir()
(tmp_path / "assets" / "favicon.ico").write_bytes(b"")
(tmp_path / "rxconfig.py").write_text(
textwrap.dedent(
"""
import reflex as rx

ASSET = rx.asset("favicon.ico")
config = rx.Config(app_name="asset_in_config")
"""
)
)
monkeypatch.chdir(tmp_path)

with RegistrationContext() as ctx:
assert reflex_base.config._get_config().app_name == "asset_in_config"
assert ctx._config is None
assert reflex_base.config.get_config().app_name == "asset_in_config"
assert sys.modules["rxconfig"].ASSET.startswith("/favicon.ico")


def test_get_config_inside_rxconfig_keeps_state_auto_setters(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, clean_config_modules: None
):
"""The stand-in config built mid-import does not reset state_auto_setters.

Args:
tmp_path: The pytest tmp_path fixture.
monkeypatch: The pytest monkeypatch fixture.
clean_config_modules: Cleanup for modules left behind by the load.
"""
from reflex_base.registry import RegistrationContext

(tmp_path / "assets").mkdir()
(tmp_path / "assets" / "favicon.ico").write_bytes(b"")
(tmp_path / "rxconfig.py").write_text(
textwrap.dedent(
"""
import reflex as rx

config = rx.Config(app_name="asset_in_config", state_auto_setters=True)
ASSET = rx.asset("favicon.ico")
"""
)
)
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(reflex_base.config, "_state_auto_setters", None)

with RegistrationContext():
reflex_base.config._get_config()
assert reflex_base.config.get_state_auto_setters() is True


def test_get_config_stand_in_failure_keeps_state_auto_setters(
monkeypatch: pytest.MonkeyPatch,
):
"""A stand-in config that fails validation does not leave its value published.

Args:
monkeypatch: The pytest monkeypatch fixture.
"""
monkeypatch.setenv("REFLEX_STATE_MANAGER_MODE", "redis")
monkeypatch.delenv("REFLEX_REDIS_URL", raising=False)
monkeypatch.delenv("REDIS_URL", raising=False)
monkeypatch.setattr(reflex_base.config, "_loading_config", True)
monkeypatch.setattr(reflex_base.config, "_state_auto_setters", True)

with pytest.raises(ConfigError):
reflex_base.config._get_config()
assert reflex_base.config._state_auto_setters is True


def test_get_config_accepts_explicit_project_root(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, clean_config_modules: None
):
Expand Down
Loading