From 3d93996ad16b4777096635786194e17bbc032a31 Mon Sep 17 00:00:00 2001 From: vjymisal0 Date: Tue, 27 Oct 2026 18:48:16 +0530 Subject: [PATCH 1/3] fix(config): stop rx.asset in rxconfig.py from recursing rx.asset builds its URL with get_config(). Called at the top level of rxconfig.py, that started a second config load, which imported rxconfig.py again and recursed until RecursionError. While rxconfig.py is importing, _get_config() now returns the env and default config instead of importing it again, and get_config() does not cache that stand-in on the context. Signed-off-by: vjymisal0 --- packages/reflex-base/news/7669.bugfix.md | 1 + .../reflex-base/src/reflex_base/config.py | 17 +++++++-- tests/units/test_config.py | 35 +++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 packages/reflex-base/news/7669.bugfix.md diff --git a/packages/reflex-base/news/7669.bugfix.md b/packages/reflex-base/news/7669.bugfix.md new file mode 100644 index 00000000000..284ab8a6203 --- /dev/null +++ b/packages/reflex-base/news/7669.bugfix.md @@ -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. diff --git a/packages/reflex-base/src/reflex_base/config.py b/packages/reflex-base/src/reflex_base/config.py index c3861dc4c3d..36500dde099 100644 --- a/packages/reflex-base/src/reflex_base/config.py +++ b/packages/reflex-base/src/reflex_base/config.py @@ -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 @@ -989,10 +992,14 @@ 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 project_root = (project_root or Path.cwd()).resolve() with _load_config_lock: + if _loading_config: + # rxconfig.py read the config while importing (e.g. via rx.asset); + # importing it again would recurse, so use the env and defaults. + return Config(app_name="", _skip_plugins_checks=True) # 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. @@ -1017,9 +1024,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 @@ -1087,7 +1096,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 diff --git a/tests/units/test_config.py b/tests/units/test_config.py index 4ec4bc3d68c..49283044e76 100644 --- a/tests/units/test_config.py +++ b/tests/units/test_config.py @@ -1127,6 +1127,41 @@ 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_accepts_explicit_project_root( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, clean_config_modules: None ): From fb5136ced3d6354f658b8af6dddef814bd9f9a7d Mon Sep 17 00:00:00 2001 From: vjymisal0 Date: Wed, 28 Oct 2026 16:10:42 +0530 Subject: [PATCH 2/3] fix(config): keep state_auto_setters when building the mid-import stand-in Constructing the stand-in Config published its default state_auto_setters, replacing the value a Config created earlier in rxconfig.py had set. State classes defined later lost their generated setters. Restore the published value after building the stand-in. Signed-off-by: vjymisal0 --- .../reflex-base/src/reflex_base/config.py | 9 ++++-- tests/units/test_config.py | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/config.py b/packages/reflex-base/src/reflex_base/config.py index 36500dde099..0f7a269db2c 100644 --- a/packages/reflex-base/src/reflex_base/config.py +++ b/packages/reflex-base/src/reflex_base/config.py @@ -992,14 +992,19 @@ def _get_config(project_root: Path | None = None) -> Config: Returns: The app config. """ - global _config_module_deps_root, _loading_config + 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: # rxconfig.py read the config while importing (e.g. via rx.asset); # importing it again would recurse, so use the env and defaults. - return Config(app_name="", _skip_plugins_checks=True) + # Building it must not replace the state_auto_setters value a + # Config already created in rxconfig.py published. + state_auto_setters = _state_auto_setters + config = Config(app_name="", _skip_plugins_checks=True) + _state_auto_setters = state_auto_setters + return config # 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. diff --git a/tests/units/test_config.py b/tests/units/test_config.py index 49283044e76..703090e73f5 100644 --- a/tests/units/test_config.py +++ b/tests/units/test_config.py @@ -1162,6 +1162,38 @@ def test_get_config_inside_rxconfig_does_not_reload_it( 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_accepts_explicit_project_root( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, clean_config_modules: None ): From 00472708a4bb2267f234e8d755400f03ed24983c Mon Sep 17 00:00:00 2001 From: vjymisal0 Date: Sat, 31 Oct 2026 08:36:57 +0530 Subject: [PATCH 3/3] fix(config): restore state_auto_setters even when the stand-in config fails The mid-import stand-in Config publishes its state_auto_setters before validation, so a ConfigError (e.g. redis mode without a redis URL) left that value in place. Restore the saved value in a finally block. Signed-off-by: vjymisal0 --- .../reflex-base/src/reflex_base/config.py | 7 ++++--- tests/units/test_config.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/config.py b/packages/reflex-base/src/reflex_base/config.py index 0f7a269db2c..193c6e099f3 100644 --- a/packages/reflex-base/src/reflex_base/config.py +++ b/packages/reflex-base/src/reflex_base/config.py @@ -1002,9 +1002,10 @@ def _get_config(project_root: Path | None = None) -> Config: # Building it must not replace the state_auto_setters value a # Config already created in rxconfig.py published. state_auto_setters = _state_auto_setters - config = Config(app_name="", _skip_plugins_checks=True) - _state_auto_setters = state_auto_setters - return config + 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. diff --git a/tests/units/test_config.py b/tests/units/test_config.py index 703090e73f5..e45206e640a 100644 --- a/tests/units/test_config.py +++ b/tests/units/test_config.py @@ -1194,6 +1194,25 @@ def test_get_config_inside_rxconfig_keeps_state_auto_setters( 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 ):