From 693a2cbefcff3a9704c37138ffda0e02dfce432f Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Thu, 8 Oct 2026 15:26:55 +0530 Subject: [PATCH 1/2] Raise helpful errors for missing compiler contexts --- packages/reflex-base/news/+7489.bugfix.md | 1 + .../src/reflex_base/context/base.py | 12 ++++++-- .../src/reflex_base/plugins/compiler.py | 6 +++- tests/units/compiler/test_plugins.py | 28 +++++++++++++++---- 4 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 packages/reflex-base/news/+7489.bugfix.md 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..1ceb8bc9655 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,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 @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..d51047efb7e 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(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", From a9a7cb9846dfe03a54a7e6724ac0deae87e196b0 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Fri, 9 Oct 2026 14:38:20 +0530 Subject: [PATCH 2/2] Preserve LookupError for compiler contexts --- packages/reflex-base/src/reflex_base/context/base.py | 5 ++--- tests/units/compiler/test_plugins.py | 12 ++++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/context/base.py b/packages/reflex-base/src/reflex_base/context/base.py index 1ceb8bc9655..8e0451ea827 100644 --- a/packages/reflex-base/src/reflex_base/context/base.py +++ b/packages/reflex-base/src/reflex_base/context/base.py @@ -43,8 +43,7 @@ def get(cls) -> Self: The active context instance. Raises: - 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. + LookupError: If no context has been set. """ try: return cls._context_var.get() @@ -52,7 +51,7 @@ def get(cls) -> Self: if cls._get_usage is None: raise msg = f"{cls.__name__}.get() must be called {cls._get_usage}." - raise RuntimeError(msg) from exc + raise LookupError(msg) from exc @classmethod def set(cls, context: Self) -> Token[Self]: diff --git a/tests/units/compiler/test_plugins.py b/tests/units/compiler/test_plugins.py index d51047efb7e..00b6d3c986d 100644 --- a/tests/units/compiler/test_plugins.py +++ b/tests/units/compiler/test_plugins.py @@ -622,31 +622,31 @@ def test_context_lifecycle_and_cleanup() -> None: root_component=Fragment.create(), ) - with pytest.raises(RuntimeError, match=r"CompileContext\.get.*compilation"): + 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(RuntimeError, match=r"PageContext\.get.*page compilation"): + 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(RuntimeError, match=r"PageContext\.get.*page compilation"): + with pytest.raises(LookupError, match=r"PageContext\.get.*page compilation"): PageContext.get() assert CompileContext.get() is compile_ctx - with pytest.raises(RuntimeError, match=r"CompileContext\.get.*compilation"): + 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(RuntimeError, match=r"CompileContext\.get.*compilation"): + with pytest.raises(LookupError, match=r"CompileContext\.get.*compilation"): CompileContext.get() @@ -662,7 +662,7 @@ def test_compilation_context_get_outside_compile_raises_descriptive_error( ) -> None: """Context access outside compilation explains when the context is available.""" with pytest.raises( - RuntimeError, + LookupError, match=rf"{context_class.__name__}\.get\(\).*{activity}", ): context_class.get()