Skip to content

Commit 00cbe43

Browse files
committed
When render_widget gets invalidated, invalidate anyone would reads the value
1 parent 2affb72 commit 00cbe43

File tree

1 file changed

+62
-7
lines changed

1 file changed

+62
-7
lines changed

shinywidgets/_shinywidgets.py

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from shiny import Session, reactive, req
3030
from shiny.http_staticfiles import StaticFiles
3131
from shiny.module import resolve_id
32+
from shiny.reactive._core import Context, get_current_context
3233
from shiny.render.transformer import (
3334
TransformerMetadata,
3435
ValueFn,
@@ -209,20 +210,31 @@ def _restore_state():
209210
# Implement @render_widget()
210211
# --------------------------------------------------------------------------------------------
211212

213+
# TODO: pass along IT/OT to get proper typing?
214+
UserValueFn = ValueFn[object | None]
212215

213216
@output_transformer(default_ui=output_widget)
214217
async def WidgetTransformer(
215218
_meta: TransformerMetadata,
216-
_fn: ValueFn[object | None],
219+
_fn: UserValueFn,
217220
) -> dict[str, Any] | None:
218221
value = await resolve_value_fn(_fn)
222+
223+
# Attach value/widget attributes to user func so they can be accessed (in other reactive contexts)
219224
_fn.value = value # type: ignore
220225
_fn.widget = None # type: ignore
226+
227+
# Invalidate any reactive contexts that have read these attributes
228+
invalidate_contexts(_fn)
229+
221230
if value is None:
222231
return None
232+
233+
# Ensure we have a widget & smart layout defaults
223234
widget = as_widget(value)
224235
widget, fill = set_layout_defaults(widget)
225236
_fn.widget = widget # type: ignore
237+
226238
return {"model_id": widget.model_id, "fill": fill} # type: ignore
227239

228240

@@ -243,27 +255,62 @@ def render_widget(
243255
# Make the `res._value_fn.widget` attribute that we set in WidgetTransformer
244256
# accessible via `res.widget`
245257
def get_widget(*_: object) -> Optional[Widget]:
246-
w = res._value_fn.widget # type: ignore
247-
if w is None:
258+
vfn = res._value_fn # pyright: ignore[reportFunctionMemberAccess]
259+
vfn = register_current_context(vfn)
260+
w = vfn.widget # type: ignore
261+
if w is not None:
262+
return w
263+
# If widget is None, we're reading in a reactive context, other than the render context, throw a silent exception
264+
if has_current_context():
248265
req(False)
249-
return None
250-
return w
266+
return None
251267

252268
def set_widget(*_: object):
253269
raise RuntimeError("The widget attribute of a @render_widget function is read only.")
254270

255271
setattr(res.__class__, "widget", property(get_widget, set_widget))
256272

257273
def get_value(*_: object) -> object | None:
258-
return res._value_fn.value # type: ignore
274+
vfn = res._value_fn # pyright: ignore[reportFunctionMemberAccess]
275+
vfn = register_current_context(vfn)
276+
v = vfn.value # type: ignore
277+
if v is not None:
278+
return v
279+
if has_current_context():
280+
req(False)
281+
return None
259282

260283
def set_value(*_: object):
261284
raise RuntimeError("The value attribute of a @render_widget function is read only.")
262285

263286
setattr(res.__class__, "value", property(get_value, set_value))
264287

288+
# Define these attributes directly on the user function so they're defined, even
289+
# if that function hasn't been called yet. (we don't want to raise an exception in that case)
290+
fn.widget = None # type: ignore
291+
fn.value = None # type: ignore
292+
265293
return res
266294

295+
296+
def invalidate_contexts(fn: UserValueFn):
297+
ctxs = getattr(fn, "_shinywidgets_contexts", set[Context]())
298+
for ctx in ctxs:
299+
# TODO: at what point should we be removing contexts?
300+
ctx.invalidate()
301+
302+
303+
# If the widget/value is read in a reactive context, then we'll need to invalidate
304+
# that context when the widget's value changes
305+
def register_current_context(fn: UserValueFn):
306+
if not has_current_context():
307+
return fn
308+
ctxs = getattr(fn, "_shinywidgets_contexts", set[Context]())
309+
ctxs.add(get_current_context())
310+
fn._shinywidgets_contexts = ctxs # type: ignore
311+
return fn
312+
313+
267314
def reactive_read(widget: Widget, names: Union[str, Sequence[str]]) -> Any:
268315
reactive_depend(widget, names)
269316
if isinstance(names, str):
@@ -282,7 +329,7 @@ def reactive_depend(
282329
"""
283330

284331
try:
285-
ctx = reactive.get_current_context() # pyright: ignore[reportPrivateImportUsage]
332+
ctx = get_current_context()
286333
except RuntimeError:
287334
raise RuntimeError("reactive_read() must be called within a reactive context")
288335

@@ -376,6 +423,14 @@ def set_layout_defaults(widget: Widget) -> Tuple[Widget, bool]:
376423

377424
return (widget, fill)
378425

426+
427+
def has_current_context() -> bool:
428+
try:
429+
get_current_context()
430+
return True
431+
except RuntimeError:
432+
return False
433+
379434
# similar to base::system.file()
380435
def package_dir(package: str) -> str:
381436
with tempfile.TemporaryDirectory():

0 commit comments

Comments
 (0)