From 47e3d5c11e2183c06dc74a3d2b75ea9fc2f15141 Mon Sep 17 00:00:00 2001 From: Farhan Ali Raza <62690310+FarhanAliRaza@users.noreply.github.com> Date: Wed, 5 Aug 2026 19:17:32 +0500 Subject: [PATCH 1/3] feat(reflex): compile-time probe for @reflex_xy.figure builders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the data-bound tier validating structure at page evaluation, the figure var is the last place chart-building code defers to hydrate — where a typo'd mark name or a bad kwarg shows up as a blank mount and an err frame, in a browser, after a round trip. XYPlugin.post_compile now walks the state tree and runs each figure builder once against a default state instance. probe="build" (the sync default) runs the body; probe="figure" also compiles the result; probe=False opts out and is the default for async builders, because awaiting a data source at compile is exactly what the "no data ingestion at compile" constraint forbids — an async builder can still opt in. Failures raise FigureProbeError naming the state class, var, and source location, wrapping the original exception. One deliberate softening: a builder whose source reads self.router is session-dependent by declaration, and only a live session can validate it, so its probe failure degrades to a RuntimeWarning rather than failing the compile. The heuristic is source text, which is why it only ever downgrades an error — never invents one. Spec: reflex-integration.md §3.1 (compile probe). --- python/reflex_xy/app.py | 110 ++++++++++++++++++++- python/reflex_xy/tokens.py | 3 + python/reflex_xy/vars.py | 35 +++++-- spec/design/reflex-integration.md | 26 +++++ tests/reflex_adapter/test_figure_probe.py | 113 ++++++++++++++++++++++ 5 files changed, 278 insertions(+), 9 deletions(-) create mode 100644 tests/reflex_adapter/test_figure_probe.py diff --git a/python/reflex_xy/app.py b/python/reflex_xy/app.py index 52ab1d10..3737cf93 100644 --- a/python/reflex_xy/app.py +++ b/python/reflex_xy/app.py @@ -22,20 +22,26 @@ import asyncio import contextlib +import inspect +import warnings from collections.abc import Coroutine -from typing import Any, Optional +from typing import Any, Optional, cast from reflex.plugins import Plugin from .handles import FigureHandle, token_of from .namespace import XYNamespace -from .registry import registry +from .registry import _figure_of, registry from .state_bridge import make_rebuild_hook +from .tokens import BUILDER_ATTR, PROBE_ATTR +from .vars import AsyncFigureVar, FigureVar __all__ = [ + "FigureProbeError", "XYPlugin", "append", "clear_selection", + "probe_figure_builders", "reset_view", "select", "set_view", @@ -137,18 +143,116 @@ async def _xy_lifespan() -> None: await registry.sweep_forever() +class FigureProbeError(Exception): + """A `@reflex_xy.figure` builder failed its compile-time probe.""" + + +def _builder_location(builder: Any) -> str: + try: + filename = inspect.getsourcefile(builder) + _, line = inspect.getsourcelines(builder) + except (OSError, TypeError): + return "" + return f"{filename}:{line}" + + +def _touches_session(builder: Any) -> bool: + """Heuristic escape valve (constraint 2): builders that read the session + (`self.router`) cannot be expected to run against default state — their + probe failures degrade to a warning instead of failing the compile.""" + try: + source = inspect.getsource(builder) + except (OSError, TypeError): + return False + return "self.router" in source + + +def _iter_probed_figure_vars(state_cls: Any, seen: set[Any]): + if state_cls in seen: + return + seen.add(state_cls) + computed = getattr(state_cls, "computed_vars", {}) + for name, var in computed.items(): + if isinstance(var, (FigureVar, AsyncFigureVar)): + yield state_cls, name, var + for subclass in state_cls.get_substates(): + yield from _iter_probed_figure_vars(subclass, seen) + + +def probe_figure_builders(root_cls: Any = None) -> list[str]: + """Run every probe-enabled `@reflex_xy.figure` builder against default + state (reflex-integration.md §3.1): the compile gate of the escape-hatch + tier. Returns the probed var full names; raises :class:`FigureProbeError` + (wrapping the original) on the first failing builder. + + Level ``"build"`` runs the builder body — hallucinated `xy.*` names, + wrong kwargs, and eager chrome errors fail here. ``"figure"`` also + compiles the returned chart. ``False`` (and, by default, async builders) + are skipped. Builders whose source touches ``self.router`` degrade + failures to a `RuntimeWarning`: they are session-dependent by + declaration and only a live session can validate them. + """ + import reflex as rx + + root_cls = root_cls if root_cls is not None else cast("Any", rx.State) + root = root_cls(_reflex_internal_init=True) + probed: list[str] = [] + for state_cls, name, var in _iter_probed_figure_vars(root_cls, set()): + fget = var._fget + level = getattr(fget, PROBE_ATTR, False) + builder = getattr(fget, BUILDER_ATTR, None) + if not level or builder is None: + continue + full_name = f"{state_cls.get_full_name()}.{name}" + try: + substate = root.get_substate(tuple(state_cls.get_full_name().split("."))[1:]) + except (KeyError, ValueError): + continue # not reachable from this root (e.g. mixin scaffolding) + try: + if inspect.iscoroutinefunction(builder): + chart = asyncio.run(builder(substate)) + else: + chart = builder(substate) + if level == "figure" and chart is not None: + _figure_of(chart) + except Exception as exc: + location = _builder_location(builder) + if _touches_session(builder): + warnings.warn( + f"@reflex_xy.figure probe: {full_name} ({location}) reads the " + f"session and failed against default state: {exc!r}. Probes " + "validate what they can; pass probe=False to silence.", + RuntimeWarning, + stacklevel=2, + ) + continue + msg = ( + f"@reflex_xy.figure probe failed for {full_name} ({location}): " + f"{type(exc).__name__}: {exc}. The builder ran against default " + "state at compile so this error would not wait for a browser " + "session; pass @reflex_xy.figure(probe=False) if this builder " + "cannot run outside a live session." + ) + raise FigureProbeError(msg) from exc + probed.append(full_name) + return probed + + class XYPlugin(Plugin): """Reflex plugin: `plugins=[reflex_xy.XYPlugin()]` in rxconfig.py. `post_compile` is the one plugin hook that receives the live App, and it fires at backend worker startup — after the socket server exists, before - any client connects, and never during frontend-only compiles. + any client connects, and never during frontend-only compiles. It wires + the data plane and then runs the figure-builder compile probes (§3.1) so + escape-hatch builders fail `reflex run`, not the browser. """ def post_compile(self, **context: Any) -> None: app = context.get("app") if app is not None: setup(app) + probe_figure_builders() def _token(source: "str | FigureHandle") -> str: diff --git a/python/reflex_xy/tokens.py b/python/reflex_xy/tokens.py index 9aed0308..270a2662 100644 --- a/python/reflex_xy/tokens.py +++ b/python/reflex_xy/tokens.py @@ -65,6 +65,9 @@ #: It lives on the *function* (not the ComputedVar) so it survives reflex's #: `_replace` copies, which re-instantiate the var but thread fget through. BUILDER_ATTR = "__xy_builder__" +#: Attribute stashed beside it carrying the figure var's compile-probe level +#: ("build" | "figure" | False); same placement rationale. +PROBE_ATTR = "__xy_probe__" @dataclass(frozen=True) diff --git a/python/reflex_xy/vars.py b/python/reflex_xy/vars.py index 4898956f..cf52a7b5 100644 --- a/python/reflex_xy/vars.py +++ b/python/reflex_xy/vars.py @@ -40,10 +40,12 @@ from .handles import FigureHandle from .registry import _figure_of, registry -from .tokens import BUILDER_ATTR, build_state_token +from .tokens import BUILDER_ATTR, PROBE_ATTR, build_state_token __all__ = ["AsyncFigureVar", "FigureVar", "figure"] +_PROBE_LEVELS = ("build", "figure", False) + def _builder_target(var: Any, obj: Any) -> Any: """Point dependency tracking at the wrapped method — the figure builder @@ -136,12 +138,15 @@ def figure(builder: Callable[[Any], Any]) -> "FigureVar | AsyncFigureVar": ... @overload def figure( - builder: None = None, **var_kwargs: Any + builder: None = None, *, probe: "str | bool | None" = None, **var_kwargs: Any ) -> Callable[[Callable[[Any], Any]], "FigureVar | AsyncFigureVar"]: ... def figure( - builder: Optional[Callable[[Any], Any]] = None, **var_kwargs: Any + builder: Optional[Callable[[Any], Any]] = None, + *, + probe: "str | bool | None" = None, + **var_kwargs: Any, ) -> "FigureVar | AsyncFigureVar | Callable[[Callable[[Any], Any]], FigureVar | AsyncFigureVar]": """Declare a chart on a Reflex state class. @@ -168,7 +173,20 @@ async def remote(self) -> xy.Chart: Keyword arguments pass through to reflex's computed var (``deps=``, ``auto_deps=``, ``interval=``, ...); dependencies are auto-tracked from the builder's body by default, exactly like a normal ``@rx.var``. + + ``probe=`` sets the compile-time probe level (spec + reflex-integration.md §3.1): at app compile the plugin runs the builder + once against default state, so hallucinated chart APIs and bad kwargs + fail ``reflex run`` instead of a silent blank mount at hydrate. + ``"build"`` (the sync default) runs the body only; ``"figure"`` + additionally compiles the result (full config/shape validation at the + price of one real figure); ``False`` opts out — the default for + ``async def`` builders, whose data sources should not be awaited at + compile. """ + if probe not in (*_PROBE_LEVELS, None): + msg = f"@reflex_xy.figure probe= must be 'build', 'figure', or False, got {probe!r}" + raise ValueError(msg) def _decorate(fn: Callable[[Any], Any]) -> "FigureVar | AsyncFigureVar": if _fn_name(fn).startswith("_"): @@ -180,9 +198,14 @@ def _decorate(fn: Callable[[Any], Any]) -> "FigureVar | AsyncFigureVar": ) raise ValueError(msg) var_kwargs.setdefault("cache", True) - if inspect.iscoroutinefunction(fn): - return AsyncFigureVar(fget=_make_async_fget(fn), return_type=FigureHandle, **var_kwargs) - return FigureVar(fget=_make_fget(fn), return_type=FigureHandle, **var_kwargs) + is_async = inspect.iscoroutinefunction(fn) + if is_async: + var = AsyncFigureVar(fget=_make_async_fget(fn), return_type=FigureHandle, **var_kwargs) + else: + var = FigureVar(fget=_make_fget(fn), return_type=FigureHandle, **var_kwargs) + level = probe if probe is not None else (False if is_async else "build") + setattr(var._fget, PROBE_ATTR, level) + return var if builder is None: return _decorate diff --git a/spec/design/reflex-integration.md b/spec/design/reflex-integration.md index f52a536a..dcc54f96 100644 --- a/spec/design/reflex-integration.md +++ b/spec/design/reflex-integration.md @@ -285,6 +285,32 @@ points at is exactly the recovery contract). This is §27 applied to processes: canonical data is Reflex state; every registered figure is a derived buffer. +**Compile probe.** With the data-bound tier (§3.6) validating everything at +page evaluation, the figure var is the one place chart-building user code +still defers to hydrate — so it gets a compile gate of its own. +`XYPlugin.post_compile` walks the state tree, and for every figure var runs +the builder once against a default state instance +(`@reflex_xy.figure(probe=...)` sets the level): + +- `probe="build"` — the default for sync builders: run the body only. + Hallucinated `xy.*` names, wrong kwargs, and eager chrome errors fail + `reflex run` with the state class, var name, and source location + (`FigureProbeError` wrapping the original), instead of an `err` frame + and a silently blank mount at hydrate. +- `probe="figure"` — additionally compile the result: full config/shape + validation at the price of building one real figure per var at startup. +- `probe=False` — opt out; the default for `async def` builders (compile + is sync, and awaiting a data source at compile is what constraint 2 + forbids). An async builder may opt in explicitly and runs under + `asyncio.run`. + +The escape valve for constraint 2: a builder whose source reads +`self.router` is session-dependent by declaration — its probe failure +degrades to a `RuntimeWarning` instead of failing the compile, because only +a live session can validate it. The probe's cost is the builder's own cost +against *default* state, once per backend worker start — the same order of +work Reflex already accepts evaluating ordinary computed vars at compile. + ### 3.2 Registry miss: rebuild from state `sub` (or `msg`) on an unknown state token parses it, resolves the state diff --git a/tests/reflex_adapter/test_figure_probe.py b/tests/reflex_adapter/test_figure_probe.py new file mode 100644 index 00000000..a14edc65 --- /dev/null +++ b/tests/reflex_adapter/test_figure_probe.py @@ -0,0 +1,113 @@ +"""The @reflex_xy.figure compile probe: escape-hatch builders fail at compile. + +The probed states below raise only while their module flag is armed, so the +process-wide state walk stays healthy for every other test in the session. +""" + +from __future__ import annotations + +import asyncio + +import numpy as np +import pytest +import reflex as rx + +import reflex_xy +import xy +from reflex_xy.app import FigureProbeError, probe_figure_builders + +#: Armed per-test; every probed builder below is a no-op otherwise. +_ARM = {"hallucinated": False, "bad_config": False, "session": False, "async_ran": False} + + +class ProbeDemo(rx.State): + n: int = 8 + + @reflex_xy.figure + def healthy(self) -> xy.Chart: + xs = np.linspace(0.0, 1.0, self.n) + return xy.scatter_chart(xy.scatter(xs, xs)) + + @reflex_xy.figure + def hallucinated(self): + if not _ARM["hallucinated"]: + return None + return xy.polar_scatter([1.0], [1.0]) # no such factory + + @reflex_xy.figure(probe="figure") + def bad_config(self): + if not _ARM["bad_config"]: + return None + return xy.scatter_chart(xy.scatter([1.0], [1.0], colormap="virids")) + + @reflex_xy.figure + def session_bound(self): + if not _ARM["session"]: + return None + # Session-dependent by declaration: reads self.router. Against the + # probe's default state this raises; the probe must downgrade. + raise RuntimeError(f"no session for {self.router.session.client_token!r}") + + @reflex_xy.figure(probe=False) + def opted_out(self): + raise AssertionError("probe=False builders must never run at compile") + + @reflex_xy.figure + async def async_default(self): + raise AssertionError("async builders are not probed by default") + + @reflex_xy.figure(probe="build") + async def async_opted_in(self): + _ARM["async_ran"] = True + await asyncio.sleep(0) + return None + + +@pytest.fixture(autouse=True) +def _disarm(): + yield + for key in _ARM: + _ARM[key] = False + + +def test_probe_runs_sync_builders_and_skips_optouts(): + probed = probe_figure_builders() + names = {name.rsplit(".", 1)[-1] for name in probed if "probe_demo" in name} + assert "healthy" in names + assert "opted_out" not in names + assert "async_default" not in names + assert "async_opted_in" in names # explicit opt-in runs under asyncio.run + assert _ARM["async_ran"] + + +def test_hallucinated_chart_api_fails_the_compile(): + """Problem 2 of the options doc, closed for the escape hatch: the + builder body is no longer dead code until hydrate.""" + _ARM["hallucinated"] = True + with pytest.raises(FigureProbeError, match="hallucinated") as excinfo: + probe_figure_builders() + assert "polar_scatter" in str(excinfo.value) + assert isinstance(excinfo.value.__cause__, AttributeError) + + +def test_probe_figure_level_compiles_the_result(): + _ARM["bad_config"] = True + with pytest.raises(FigureProbeError, match="bad_config") as excinfo: + probe_figure_builders() + assert "colormap" in str(excinfo.value) + + +def test_session_dependent_builder_downgrades_to_warning(): + _ARM["session"] = True + with pytest.warns(RuntimeWarning, match="session_bound.*reads the session"): + probed = probe_figure_builders() + assert not any(name.endswith("session_bound") for name in probed) + + +def test_invalid_probe_level_is_refused_at_decoration(): + with pytest.raises(ValueError, match="probe="): + + class BadProbe(rx.State): # noqa: F841 - definition is the assertion + @reflex_xy.figure(probe="everything") + def chart(self): + return None From 5cb1fd4b349fbf6fc65e70b36eb6dead89bd7d35 Mon Sep 17 00:00:00 2001 From: Farhan Ali Raza Date: Thu, 6 Aug 2026 17:37:34 +0500 Subject: [PATCH 2/3] fix(reflex): identity-strict probe levels; type-check builder returns - probe= validation no longer uses equality membership: 0/0.0 (== False) and 1/True are refused at decoration instead of silently opting the builder out of its compile probe. - The default "build" probe level now also checks the builder's return is a chart (or None): a dict or other non-chart value fails reflex run with the builder's location instead of reaching hydrate. --- python/reflex_xy/app.py | 21 +++++++++++--- python/reflex_xy/vars.py | 16 +++++------ spec/design/reflex-integration.md | 15 ++++++---- tests/reflex_adapter/test_figure_probe.py | 35 ++++++++++++++++++++++- 4 files changed, 69 insertions(+), 18 deletions(-) diff --git a/python/reflex_xy/app.py b/python/reflex_xy/app.py index 3737cf93..2fd276d8 100644 --- a/python/reflex_xy/app.py +++ b/python/reflex_xy/app.py @@ -185,10 +185,11 @@ def probe_figure_builders(root_cls: Any = None) -> list[str]: tier. Returns the probed var full names; raises :class:`FigureProbeError` (wrapping the original) on the first failing builder. - Level ``"build"`` runs the builder body — hallucinated `xy.*` names, - wrong kwargs, and eager chrome errors fail here. ``"figure"`` also - compiles the returned chart. ``False`` (and, by default, async builders) - are skipped. Builders whose source touches ``self.router`` degrade + Level ``"build"`` runs the builder body and type-checks its return — + hallucinated `xy.*` names, wrong kwargs, eager chrome errors, and a + return value that is not a chart (or ``None``) fail here. ``"figure"`` + also compiles the returned chart. ``False`` (and, by default, async + builders) are skipped. Builders whose source touches ``self.router`` degrade failures to a `RuntimeWarning`: they are session-dependent by declaration and only a live session can validate them. """ @@ -213,6 +214,18 @@ def probe_figure_builders(root_cls: Any = None) -> list[str]: chart = asyncio.run(builder(substate)) else: chart = builder(substate) + if chart is not None and not ( + callable(getattr(chart, "figure", None)) + or callable(getattr(chart, "build_payload", None)) + ): + # Every probe level checks the return *type*: a builder that + # returns something no registry publish can accept would + # otherwise reach hydrate before failing. + msg = ( + f"builder returned {type(chart).__name__}; expected an " + "xy Chart (or internal Figure), or None for 'no chart'" + ) + raise TypeError(msg) if level == "figure" and chart is not None: _figure_of(chart) except Exception as exc: diff --git a/python/reflex_xy/vars.py b/python/reflex_xy/vars.py index cf52a7b5..fb2d08fb 100644 --- a/python/reflex_xy/vars.py +++ b/python/reflex_xy/vars.py @@ -44,8 +44,6 @@ __all__ = ["AsyncFigureVar", "FigureVar", "figure"] -_PROBE_LEVELS = ("build", "figure", False) - def _builder_target(var: Any, obj: Any) -> Any: """Point dependency tracking at the wrapped method — the figure builder @@ -178,13 +176,15 @@ async def remote(self) -> xy.Chart: reflex-integration.md §3.1): at app compile the plugin runs the builder once against default state, so hallucinated chart APIs and bad kwargs fail ``reflex run`` instead of a silent blank mount at hydrate. - ``"build"`` (the sync default) runs the body only; ``"figure"`` - additionally compiles the result (full config/shape validation at the - price of one real figure); ``False`` opts out — the default for - ``async def`` builders, whose data sources should not be awaited at - compile. + ``"build"`` (the sync default) runs the body and checks the return is a + chart (or ``None``); ``"figure"`` additionally compiles the result + (full config/shape validation at the price of one real figure); + ``False`` opts out — the default for ``async def`` builders, whose data + sources should not be awaited at compile. """ - if probe not in (*_PROBE_LEVELS, None): + # Identity/equality-strict: 0 and 0.0 compare equal to False but are + # not a probe level — reject them instead of silently skipping probes. + if not (probe is None or probe is False or probe == "build" or probe == "figure"): msg = f"@reflex_xy.figure probe= must be 'build', 'figure', or False, got {probe!r}" raise ValueError(msg) diff --git a/spec/design/reflex-integration.md b/spec/design/reflex-integration.md index dcc54f96..4448cdbc 100644 --- a/spec/design/reflex-integration.md +++ b/spec/design/reflex-integration.md @@ -292,11 +292,12 @@ still defers to hydrate — so it gets a compile gate of its own. the builder once against a default state instance (`@reflex_xy.figure(probe=...)` sets the level): -- `probe="build"` — the default for sync builders: run the body only. - Hallucinated `xy.*` names, wrong kwargs, and eager chrome errors fail - `reflex run` with the state class, var name, and source location - (`FigureProbeError` wrapping the original), instead of an `err` frame - and a silently blank mount at hydrate. +- `probe="build"` — the default for sync builders: run the body and check + the return is a chart (or `None` for "no chart") — hallucinated `xy.*` + names, wrong kwargs, eager chrome errors, and a return value no registry + publish could accept fail `reflex run` with the state class, var name, + and source location (`FigureProbeError` wrapping the original), instead + of an `err` frame and a silently blank mount at hydrate. - `probe="figure"` — additionally compile the result: full config/shape validation at the price of building one real figure per var at startup. - `probe=False` — opt out; the default for `async def` builders (compile @@ -304,6 +305,10 @@ the builder once against a default state instance forbids). An async builder may opt in explicitly and runs under `asyncio.run`. +The three levels are the whole domain and validation is identity-strict: +`probe=0`/`0.0` (which compare equal to `False`) are refused at +decoration, never silently treated as an opt-out. + The escape valve for constraint 2: a builder whose source reads `self.router` is session-dependent by declaration — its probe failure degrades to a `RuntimeWarning` instead of failing the compile, because only diff --git a/tests/reflex_adapter/test_figure_probe.py b/tests/reflex_adapter/test_figure_probe.py index a14edc65..79fb58c9 100644 --- a/tests/reflex_adapter/test_figure_probe.py +++ b/tests/reflex_adapter/test_figure_probe.py @@ -17,7 +17,13 @@ from reflex_xy.app import FigureProbeError, probe_figure_builders #: Armed per-test; every probed builder below is a no-op otherwise. -_ARM = {"hallucinated": False, "bad_config": False, "session": False, "async_ran": False} +_ARM = { + "hallucinated": False, + "bad_config": False, + "bad_return": False, + "session": False, + "async_ran": False, +} class ProbeDemo(rx.State): @@ -48,6 +54,12 @@ def session_bound(self): # probe's default state this raises; the probe must downgrade. raise RuntimeError(f"no session for {self.router.session.client_token!r}") + @reflex_xy.figure + def bad_return(self): + if not _ARM["bad_return"]: + return None + return {"x": [1.0], "y": [2.0]} # not a chart: caught at "build" level + @reflex_xy.figure(probe=False) def opted_out(self): raise AssertionError("probe=False builders must never run at compile") @@ -104,6 +116,15 @@ def test_session_dependent_builder_downgrades_to_warning(): assert not any(name.endswith("session_bound") for name in probed) +def test_non_chart_return_fails_the_default_probe_level(): + """The default "build" level type-checks the return: a value no registry + publish can accept must not survive to hydrate.""" + _ARM["bad_return"] = True + with pytest.raises(FigureProbeError, match="bad_return") as excinfo: + probe_figure_builders() + assert "dict" in str(excinfo.value) + + def test_invalid_probe_level_is_refused_at_decoration(): with pytest.raises(ValueError, match="probe="): @@ -111,3 +132,15 @@ class BadProbe(rx.State): # noqa: F841 - definition is the assertion @reflex_xy.figure(probe="everything") def chart(self): return None + + +@pytest.mark.parametrize("level", [0, 0.0, 1, True]) +def test_probe_levels_are_identity_strict(level): + """0/0.0 compare equal to False (and 1/True to each other) but are not + probe levels; equality-based membership silently accepted them.""" + with pytest.raises(ValueError, match="probe="): + + class BadProbe(rx.State): # noqa: F841 - definition is the assertion + @reflex_xy.figure(probe=level) + def chart(self): + return None From b3ebb05faea320b13d8e7cca6f2861cff0726566 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 6 Aug 2026 22:03:58 +0000 Subject: [PATCH 3/3] test(reflex): scope probe failure tests to their own state The armed-failure tests probed the whole state tree, coupling their outcome to every probe-enabled builder any other test module registers in the session. They now probe ProbeDemo directly (the root_cls parameter exists for exactly this); the one deliberately unscoped test keeps the production-shaped whole-tree walk covered and says so. --- tests/reflex_adapter/test_figure_probe.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/reflex_adapter/test_figure_probe.py b/tests/reflex_adapter/test_figure_probe.py index 79fb58c9..1bf50491 100644 --- a/tests/reflex_adapter/test_figure_probe.py +++ b/tests/reflex_adapter/test_figure_probe.py @@ -83,6 +83,9 @@ def _disarm(): def test_probe_runs_sync_builders_and_skips_optouts(): + # Deliberately unscoped: this one test exercises the production shape — + # the whole-state-tree walk post_compile runs (every other builder in the + # session must probe clean under default state, the contract it enforces). probed = probe_figure_builders() names = {name.rsplit(".", 1)[-1] for name in probed if "probe_demo" in name} assert "healthy" in names @@ -97,7 +100,7 @@ def test_hallucinated_chart_api_fails_the_compile(): builder body is no longer dead code until hydrate.""" _ARM["hallucinated"] = True with pytest.raises(FigureProbeError, match="hallucinated") as excinfo: - probe_figure_builders() + probe_figure_builders(ProbeDemo) assert "polar_scatter" in str(excinfo.value) assert isinstance(excinfo.value.__cause__, AttributeError) @@ -105,14 +108,14 @@ def test_hallucinated_chart_api_fails_the_compile(): def test_probe_figure_level_compiles_the_result(): _ARM["bad_config"] = True with pytest.raises(FigureProbeError, match="bad_config") as excinfo: - probe_figure_builders() + probe_figure_builders(ProbeDemo) assert "colormap" in str(excinfo.value) def test_session_dependent_builder_downgrades_to_warning(): _ARM["session"] = True with pytest.warns(RuntimeWarning, match="session_bound.*reads the session"): - probed = probe_figure_builders() + probed = probe_figure_builders(ProbeDemo) assert not any(name.endswith("session_bound") for name in probed) @@ -121,7 +124,7 @@ def test_non_chart_return_fails_the_default_probe_level(): publish can accept must not survive to hydrate.""" _ARM["bad_return"] = True with pytest.raises(FigureProbeError, match="bad_return") as excinfo: - probe_figure_builders() + probe_figure_builders(ProbeDemo) assert "dict" in str(excinfo.value)