Skip to content
Merged
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/+decorated-pages-shim.deprecation.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions news/6382.breaking.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions news/6593.bugfix.1.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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`.
15 changes: 14 additions & 1 deletion packages/reflex-base/src/reflex_base/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
48 changes: 47 additions & 1 deletion reflex/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@
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
from typing import Any

from reflex_base.event import EventType

DECORATED_PAGES: defaultdict[str, list[tuple[Callable, dict[str, Any]]]]


def page(
route: str | None = None,
Expand Down Expand Up @@ -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",
Comment thread
masenf marked this conversation as resolved.
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__(
Expand Down
24 changes: 24 additions & 0 deletions tests/units/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
56 changes: 56 additions & 0 deletions tests/units/test_page.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Loading