diff --git a/news/+decorated-pages-shim.deprecation.md b/news/+decorated-pages-shim.deprecation.md new file mode 100644 index 00000000000..4954214b9fb --- /dev/null +++ b/news/+decorated-pages-shim.deprecation.md @@ -0,0 +1 @@ +`reflex.page.DECORATED_PAGES` is deprecated (removal in 1.0) but keeps working: reading it emits a deprecation warning and resolves to a mapping of the app name to the active `RegistrationContext`'s `decorated_pages` list of `(render_fn, kwargs)` entries. Use `RegistrationContext.ensure_context().decorated_pages` instead. diff --git a/news/6382.breaking.md b/news/6382.breaking.md new file mode 100644 index 00000000000..0cd811a6429 --- /dev/null +++ b/news/6382.breaking.md @@ -0,0 +1 @@ +A `RegistrationContext` can only be associated with a single `App` instance, so creating a second bare `rx.App()` in one process now raises `ReflexRuntimeError` (0.9.8 allowed it); use a fresh `RegistrationContext` (e.g. `RegistrationContext.fork()`) to create multiple apps. The module-level `reflex.page.DECORATED_PAGES` registry has moved onto the active `RegistrationContext` as `decorated_pages`; reading the old name still works as a deprecated shim that is removed in 1.0. diff --git a/news/6593.bugfix.1.md b/news/6593.bugfix.1.md new file mode 100644 index 00000000000..ddcce2ad62f --- /dev/null +++ b/news/6593.bugfix.1.md @@ -0,0 +1 @@ +Cancelling stale `on_load` work on navigation also applies to `on_load` handlers that are background tasks (`@rx.event(background=True)`): navigating away now cancels such a task mid-flight, where 0.9.8 let it run to completion. Background tasks started from regular (non-superseding) events are unaffected. diff --git a/packages/reflex-base/news/+get-config-reload-shim.deprecation.md b/packages/reflex-base/news/+get-config-reload-shim.deprecation.md new file mode 100644 index 00000000000..f361e0b19c9 --- /dev/null +++ b/packages/reflex-base/news/+get-config-reload-shim.deprecation.md @@ -0,0 +1 @@ +`get_config(reload=True)` is deprecated (removal in 1.0) but keeps working: passing `reload=True` emits a deprecation warning and delegates to `reload_config()`, which forces a fresh load of the config into the current `RegistrationContext`. diff --git a/packages/reflex-base/src/reflex_base/config.py b/packages/reflex-base/src/reflex_base/config.py index a17bdbbe40b..f11fd4fb01d 100644 --- a/packages/reflex-base/src/reflex_base/config.py +++ b/packages/reflex-base/src/reflex_base/config.py @@ -898,16 +898,29 @@ def _load_config() -> Config: sys.path.extend(orig_sys_path) -def get_config() -> Config: +def get_config(reload: bool = False) -> Config: """Get the app config from the current RegistrationContext. The config is loaded from rxconfig.py once per RegistrationContext and cached on the context thereafter. If no context is currently attached, one is created and attached automatically. + Args: + reload: Deprecated; force a fresh load of the config. Use + reload_config() instead. + Returns: The app config. """ + if reload: + console.deprecate( + feature_name="get_config(reload=True)", + reason="Use reload_config() to force a fresh load of the config", + deprecation_version="0.9.9", + removal_version="1.0", + ) + with _load_config_lock: + return reload_config() ctx = RegistrationContext.ensure_context() if ctx._config is None: # Serialize check/load/set so threads sharing a context load once. diff --git a/reflex/page.py b/reflex/page.py index 5b636bc0598..8f234753c64 100644 --- a/reflex/page.py +++ b/reflex/page.py @@ -3,9 +3,12 @@ from __future__ import annotations import sys +from collections import defaultdict from typing import TYPE_CHECKING +from reflex_base.config import get_config from reflex_base.registry import RegistrationContext +from reflex_base.utils import console if TYPE_CHECKING: from collections.abc import Callable @@ -13,6 +16,8 @@ from reflex_base.event import EventType + DECORATED_PAGES: defaultdict[str, list[tuple[Callable, dict[str, Any]]]] + def page( route: str | None = None, @@ -69,7 +74,48 @@ def decorator(render_fn: Callable): return decorator -class PageNamespace: +class PageNamespaceMeta(type): + """Metaclass serving deprecated module-level globals on the page namespace.""" + + def __getattr__(cls, name: str) -> Any: + """Provide the module-level globals that moved onto `RegistrationContext`. + + Kept so 0.9.8-era code doing `from reflex.page import DECORATED_PAGES` + keeps working (the namespace class replaces this module in `sys.modules`, + so a plain module `__getattr__` would never be consulted). + + Args: + name: The name of the attribute to look up. + + Returns: + The relocated value, resolved against the active `RegistrationContext`. + + Raises: + AttributeError: If the attribute is not a relocated global. + """ + if name == "DECORATED_PAGES": + console.deprecate( + feature_name="reflex.page.DECORATED_PAGES", + reason=( + "Decorated pages now live on the active RegistrationContext. " + "Use RegistrationContext.ensure_context().decorated_pages to " + "read the list of (render_fn, kwargs) entries" + ), + deprecation_version="0.9.9", + removal_version="1.0", + ) + pages: defaultdict[str, list[tuple[Callable, dict[str, Any]]]] = ( + defaultdict(list) + ) + pages[get_config().app_name] = ( + RegistrationContext.ensure_context().decorated_pages + ) + return pages + msg = f"module {cls.__module__!r} has no attribute {name!r}" + raise AttributeError(msg) + + +class PageNamespace(metaclass=PageNamespaceMeta): """A namespace for page names.""" def __new__( diff --git a/tests/units/test_config.py b/tests/units/test_config.py index 361a52dd537..3fddf1d8090 100644 --- a/tests/units/test_config.py +++ b/tests/units/test_config.py @@ -890,3 +890,27 @@ def worker(i: int) -> None: assert load_count == 1 assert all(config is results[0] for config in results) + + +def test_get_config_reload_deprecated(mocker: MockerFixture): + """get_config(reload=True) reloads the config and warns about deprecation. + + Args: + mocker: The pytest-mock fixture. + """ + from reflex_base.registry import RegistrationContext + + deprecate = mocker.patch("reflex_base.utils.console.deprecate") + first = rx.Config(app_name="first") + second = rx.Config(app_name="second") + mocker.patch.object(reflex_base.config, "_load_config", side_effect=[first, second]) + + with RegistrationContext(): + assert reflex_base.config.get_config() is first + deprecate.assert_not_called() + assert reflex_base.config.get_config(reload=True) is second + deprecate.assert_called_once() + assert deprecate.call_args.kwargs["feature_name"] == "get_config(reload=True)" + # The freshly loaded config stays cached on the context afterwards. + assert reflex_base.config.get_config() is second + deprecate.assert_called_once() diff --git a/tests/units/test_page.py b/tests/units/test_page.py index 61ba5c886e0..09172b89005 100644 --- a/tests/units/test_page.py +++ b/tests/units/test_page.py @@ -1,3 +1,8 @@ +import importlib + +import pytest +from pytest_mock import MockerFixture +from reflex_base.config import get_config from reflex_base.registry import RegistrationContext from reflex import text @@ -59,3 +64,54 @@ def load_foo(): "script_tags": ["foo-script"], "title": "Foo", } + + +def test_decorated_pages_shim_from_import( + clean_registration_context: RegistrationContext, mocker: MockerFixture +): + """The deprecated `from reflex.page import DECORATED_PAGES` still works. + + Args: + clean_registration_context: A fresh registration context. + mocker: The pytest-mock fixture. + """ + deprecate = mocker.patch("reflex_base.utils.console.deprecate") + + def foo_(): + return text("foo") + + page(route="foo")(foo_) + + from reflex.page import DECORATED_PAGES + + assert ( + next(iter(DECORATED_PAGES.values())) + is clean_registration_context.decorated_pages + ) + deprecate.assert_called_once() + assert deprecate.call_args.kwargs["feature_name"] == "reflex.page.DECORATED_PAGES" + + +def test_decorated_pages_shim_module_attribute( + clean_registration_context: RegistrationContext, mocker: MockerFixture +): + """`reflex.page.DECORATED_PAGES` maps the app name to the context's pages. + + Args: + clean_registration_context: A fresh registration context. + mocker: The pytest-mock fixture. + """ + mocker.patch("reflex_base.utils.console.deprecate") + page_module = importlib.import_module("reflex.page") + + pages = page_module.DECORATED_PAGES + assert pages[get_config().app_name] is clean_registration_context.decorated_pages + # 0.9.8 exposed a defaultdict(list), so unknown keys resolve to empty lists. + assert pages["some-other-app"] == [] + + +def test_page_namespace_unknown_attribute_raises(): + """Unknown attributes on the page namespace raise AttributeError.""" + page_module = importlib.import_module("reflex.page") + with pytest.raises(AttributeError, match=r"reflex\.page"): + _ = page_module.definitely_not_an_attribute