Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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/+7489.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Raise descriptive errors when page or compile contexts are accessed outside compilation.
12 changes: 10 additions & 2 deletions packages/reflex-base/src/reflex_base/context/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class BaseContext:

_context_var: ClassVar[ContextVar[Self]]
_attached_context_token: ClassVar[dict[int, Token[Self]]]
_get_usage: ClassVar[str | None] = None

@classmethod
def __init_subclass__(cls, **kwargs):
Expand All @@ -42,9 +43,16 @@ def get(cls) -> Self:
The active context instance.

Raises:
LookupError: If no context has been set for this class.
LookupError: If no context has been set and no usage message is configured.
RuntimeError: If no context has been set and a usage message is configured.
"""
return cls._context_var.get()
try:
return cls._context_var.get()
except LookupError as exc:
if cls._get_usage is None:
raise
msg = f"{cls.__name__}.get() must be called {cls._get_usage}."
raise RuntimeError(msg) from exc
Comment thread
harsh21234i marked this conversation as resolved.
Outdated

@classmethod
def set(cls, context: Self) -> Token[Self]:
Expand Down
6 changes: 5 additions & 1 deletion packages/reflex-base/src/reflex_base/plugins/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import dataclasses
import inspect
from collections.abc import Callable, Sequence
from typing import TYPE_CHECKING, Any, Protocol, TypeAlias, TypeVar, cast
from typing import TYPE_CHECKING, Any, ClassVar, Protocol, TypeAlias, TypeVar, cast

from reflex_base.components.component import BaseComponent, Component
from reflex_base.context.base import BaseContext
Expand Down Expand Up @@ -582,6 +582,8 @@ def _apply_replacement(
class PageContext(BaseContext):
"""Mutable compilation state for a single page."""

_get_usage: ClassVar[str | None] = "during page compilation"

name: str
route: str
root_component: BaseComponent
Expand Down Expand Up @@ -661,6 +663,8 @@ def custom_code_dict(self) -> dict[str, None]:
class CompileContext(BaseContext):
"""Mutable compilation state for an entire compile run."""

_get_usage: ClassVar[str | None] = "during compilation"

app: App | None = None
pages: Sequence[PageDefinition]
hooks: CompilerHooks = dataclasses.field(default_factory=CompilerHooks)
Expand Down
28 changes: 23 additions & 5 deletions tests/units/compiler/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,34 +622,52 @@ def test_context_lifecycle_and_cleanup() -> None:
root_component=Fragment.create(),
)

with pytest.raises(LookupError):
with pytest.raises(RuntimeError, match=r"CompileContext\.get.*compilation"):
CompileContext.get()
with pytest.raises(RuntimeError, match="must be entered"):
compile_ctx.ensure_context_attached()

with compile_ctx:
assert CompileContext.get() is compile_ctx
with pytest.raises(LookupError):
with pytest.raises(RuntimeError, match=r"PageContext\.get.*page compilation"):
PageContext.get()
with page_ctx:
assert CompileContext.get() is compile_ctx
assert PageContext.get() is page_ctx
page_ctx.ensure_context_attached()
with pytest.raises(LookupError):
with pytest.raises(RuntimeError, match=r"PageContext\.get.*page compilation"):
PageContext.get()
assert CompileContext.get() is compile_ctx

with pytest.raises(LookupError):
with pytest.raises(RuntimeError, match=r"CompileContext\.get.*compilation"):
CompileContext.get()

with pytest.raises(ValueError, match="boom"), compile_ctx:
msg = "boom"
raise ValueError(msg)

with pytest.raises(LookupError):
with pytest.raises(RuntimeError, match=r"CompileContext\.get.*compilation"):
CompileContext.get()


@pytest.mark.parametrize(
("context_class", "activity"),
[
(PageContext, "page compilation"),
(CompileContext, "compilation"),
],
)
def test_compilation_context_get_outside_compile_raises_descriptive_error(
context_class: type[PageContext] | type[CompileContext], activity: str
) -> None:
"""Context access outside compilation explains when the context is available."""
with pytest.raises(
RuntimeError,
match=rf"{context_class.__name__}\.get\(\).*{activity}",
):
context_class.get()


def test_page_context_default_factories_are_isolated() -> None:
page_ctx_a = PageContext(
name="a",
Expand Down
Loading