Skip to content
Open
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

* State-kind reactive accessors (e.g., `map.bounds()`, `plot.camera()`, `grid.view_config()`) now attach to the widget at render time instead of on first read. An interaction made before the accessor's first read -- for example, a pan or selection made before a chat handler or LLM tool first reads the accessor -- is now reflected in that first read instead of being lost. Event-kind accessors (e.g., plotly's `selected()`/`clicked()`) still attach on first read; if their first reader runs late, read them once at startup in a `@reactive.effect` to attach immediately.
* shinywidgets now requires ipywidgets 8.1 or newer, and the bundled JavaScript widget manager (`@jupyter-widgets/html-manager`) was upgraded from 0.20 to 1.0. Together these fix a wide range of long-standing issues with ipywidgets 8 (see the bug fixes below). (#237, #259)
* shinywidgets now requires plotly >= 5.12 (the first release whose `FigureWidget` frontend supports ipywidgets 8). Constructing a plotly widget with an older plotly raises an error explaining how to upgrade, instead of rendering a blank output with no error. (#332)
* shinywidgets now requires shiny 1.9.0 or newer, which sends widget updates made outside the reactive graph (e.g., from a background thread) to the browser right away. (#377)
Expand Down
13 changes: 12 additions & 1 deletion shinywidgets/_accessors.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ class AccessorSpec:
eager
Attach during render rather than on first read, for widgets whose
events are lost unless a handler exists before the view is created.
The default is True for ``kind="state"`` (attaching seeds the current
value, so a change made before the first read -- e.g. a lasso drawn
before a chat handler's first read -- is not lost) and False for
``kind="event"``. An event-kind accessor stays lazy unless it opts
in, so an app whose first reader is gated (a chat handler, an LLM
tool) should warm it up with a ``@reactive.effect`` that reads the
accessor once, unconditionally, at startup.
not_ready
True for raw values that mean "the widget hasn't reported yet"; reads
then cancel (`req(False)`) instead of returning a value.
Expand All @@ -72,10 +79,14 @@ class AccessorSpec:
priority: Literal["input", "event"] = "input"
rate_policy: Optional[RatePolicy] = None
rate_delay: float = 0.25
eager: bool = False
# None resolves to kind == "state" in __post_init__ (a frozen dataclass
# can't default one field from another)
eager: Optional[bool] = None
not_ready: Callable[[Any], bool] = never_ready_check

def __post_init__(self) -> None:
if self.eager is None:
object.__setattr__(self, "eager", self.kind == "state")
if self.name == "value":
raise ValueError(
"An accessor can't be named `value`: renderers already use it for the rendered object."
Expand Down
33 changes: 33 additions & 0 deletions tests/unit/test_accessors.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,39 @@ def test_eager_attach_in_on_render() -> None:
assert cap.attached == 1


def test_eager_defaults_to_kind() -> None:
# State accessors attach at render (a change before the first read -- a
# lasso drawn before a chat handler's first read -- is then not lost);
# event accessors stay lazy
state = acc.AccessorSpec(name="zoom", kind="state", attach=noop_attach)
event = acc.AccessorSpec(name="clicked", kind="event", attach=noop_attach)
assert state.eager is True and event.eager is False


def test_state_attach_is_eager_by_default() -> None:
cap = Capture()
spec = acc.AccessorSpec(name="zoom", kind="state", attach=cap.attach)
a = acc.Accessors([spec], schedule=FakeSchedule())
a.on_render(object())
assert cap.attached == 1 # attached at render, with no read


def test_event_attach_stays_lazy_by_default() -> None:
a, cap, _ = make() # make() builds a kind="event" spec
a.on_render(object())
assert cap.attached == 0


def test_eager_false_keeps_a_state_accessor_lazy() -> None:
cap = Capture()
spec = acc.AccessorSpec(name="zoom", kind="state", attach=cap.attach, eager=False)
a = acc.Accessors([spec], schedule=FakeSchedule())
a.on_render(object())
assert cap.attached == 0
a.read("zoom")
assert cap.attached == 1


def test_event_publishes_and_resets_on_render() -> None:
a, cap, _ = make()
a.on_render(object())
Expand Down
Loading