2929from shiny import Session , reactive , req
3030from shiny .http_staticfiles import StaticFiles
3131from shiny .module import resolve_id
32+ from shiny .reactive ._core import Context , get_current_context
3233from 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 )
214217async 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+
267314def 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()
380435def package_dir (package : str ) -> str :
381436 with tempfile .TemporaryDirectory ():
0 commit comments