diff --git a/packages/reflex-base/news/+7489.bugfix.md b/packages/reflex-base/news/+7489.bugfix.md new file mode 100644 index 00000000000..30950adc0f6 --- /dev/null +++ b/packages/reflex-base/news/+7489.bugfix.md @@ -0,0 +1 @@ +Raise descriptive errors when page or compile contexts are accessed outside compilation. diff --git a/packages/reflex-base/src/reflex_base/context/base.py b/packages/reflex-base/src/reflex_base/context/base.py index d08557b06d5..8e0451ea827 100644 --- a/packages/reflex-base/src/reflex_base/context/base.py +++ b/packages/reflex-base/src/reflex_base/context/base.py @@ -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): @@ -42,9 +43,15 @@ 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. """ - 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 LookupError(msg) from exc @classmethod def set(cls, context: Self) -> Token[Self]: diff --git a/packages/reflex-base/src/reflex_base/plugins/compiler.py b/packages/reflex-base/src/reflex_base/plugins/compiler.py index cd50d44ac43..aef1cb7dd66 100644 --- a/packages/reflex-base/src/reflex_base/plugins/compiler.py +++ b/packages/reflex-base/src/reflex_base/plugins/compiler.py @@ -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 @@ -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 @@ -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) diff --git a/tests/units/compiler/test_plugins.py b/tests/units/compiler/test_plugins.py index 483b2ca3b05..00b6d3c986d 100644 --- a/tests/units/compiler/test_plugins.py +++ b/tests/units/compiler/test_plugins.py @@ -622,34 +622,52 @@ def test_context_lifecycle_and_cleanup() -> None: root_component=Fragment.create(), ) - with pytest.raises(LookupError): + with pytest.raises(LookupError, 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(LookupError, 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(LookupError, match=r"PageContext\.get.*page compilation"): PageContext.get() assert CompileContext.get() is compile_ctx - with pytest.raises(LookupError): + with pytest.raises(LookupError, 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(LookupError, 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( + LookupError, + 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",