Skip to content

Commit 7e6faf8

Browse files
committed
Close #190: Only cleanup widgets that get initialized in an output context
1 parent c46111f commit 7e6faf8

File tree

2 files changed

+34
-15
lines changed

2 files changed

+34
-15
lines changed

shinywidgets/_render_widget_base.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
get_current_context, # pyright: ignore[reportPrivateImportUsage]
1212
)
1313
from shiny.render.renderer import Jsonifiable, Renderer, ValueFn
14+
from shiny.session import require_active_session
1415

1516
from ._as_widget import as_widget
1617
from ._dependencies import widget_pkg
@@ -69,7 +70,8 @@ def __init__(
6970
self._contexts: set[Context] = set()
7071

7172
async def render(self) -> Jsonifiable | None:
72-
value = await self.fn()
73+
with CurrentSessionOutput(self.output_id):
74+
value = await self.fn()
7375

7476
# Attach value/widget attributes to user func so they can be accessed (in other reactive contexts)
7577
self._value = value
@@ -213,3 +215,29 @@ def set_layout_defaults(widget: Widget) -> Tuple[Widget, bool]:
213215
widget.chart = chart
214216

215217
return (widget, fill)
218+
219+
220+
# --------------------------------------------------------------------------------------------
221+
# Context manager to set the current output id
222+
# (this is needed since, in order to clean up widgets properly, we need to
223+
# know if they were initialized in a output context or not)
224+
# --------------------------------------------------------------------------------------------
225+
class CurrentSessionOutput:
226+
def __init__(self, output_id):
227+
self.session = require_active_session(None)
228+
self.output_id = output_id
229+
self._old_id = None
230+
231+
def __enter__(self):
232+
self._old_id = self.session.__dict__.get("__shinywidget_current_output_id")
233+
self.session.__dict__["__shinywidget_current_output_id"] = self.output_id
234+
return self
235+
236+
def __exit__(self, exc_type, exc_value, traceback):
237+
self.session.__dict__["__shinywidget_current_output_id"] = self._old_id
238+
return False
239+
240+
@staticmethod
241+
def has_current_output(session):
242+
id = session.__dict__.get("__shinywidget_current_output_id")
243+
return id is not None

shinywidgets/_shinywidgets.py

+5-14
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from ._cdn import SHINYWIDGETS_CDN_ONLY, SHINYWIDGETS_EXTENSION_WARNING
3030
from ._comm import BufferType, OrphanedShinyComm, ShinyComm, ShinyCommManager
3131
from ._dependencies import require_dependency
32-
from ._render_widget_base import has_current_context
32+
from ._render_widget_base import CurrentSessionOutput, has_current_context
3333
from ._utils import package_dir
3434

3535
__all__ = (
@@ -60,6 +60,7 @@ def init_shiny_widget(w: Widget):
6060
return
6161
# Break out of any module-specific session. Otherwise, input.shinywidgets_comm_send
6262
# will be some module-specific copy.
63+
# TODO: session = session.root_scope()
6364
while hasattr(session, "_parent"):
6465
session = cast(Session, session._parent) # pyright: ignore
6566

@@ -148,12 +149,8 @@ def _open_shiny_comm():
148149

149150
_open_shiny_comm.destroy()
150151

151-
# If we're in a reactive context, close this widget when the context is invalidated
152-
# TODO: this should probably only be done in an output context, but I'm pretty sure
153-
# we don't have a decent way to determine that at the moment. In theory, doing this
154-
# in _any_ reactive context be problematic if you have an effect() that adds one
155-
# widget to another (i.e., a marker to a map) and want that marker to persist through
156-
# the next invalidation. The example provided in #174 is one such example.
152+
# If the widget initialized in a reactive _output_ context, then cleanup the widget
153+
# when the context gets invalidated.
157154
if has_current_context():
158155
ctx = get_current_context()
159156

@@ -170,13 +167,7 @@ def on_close():
170167
if id in WIDGET_INSTANCE_MAP:
171168
del WIDGET_INSTANCE_MAP[id]
172169

173-
# This could be running in a shiny.reactive.ExtendedTask, in which case,
174-
# the context is a DenialContext. As a result, on_invalidate() will throw
175-
# (since reading/invalidating reactive sources isn't allowed in this context).
176-
# For now, we just don't clean up the widget in this case.
177-
# TODO: this line can likely be removed once we start closing iff we're in a
178-
# output context (see TODO comment above)
179-
if "DenialContext" != ctx.__class__.__name__:
170+
if CurrentSessionOutput.has_current_output(session):
180171
ctx.on_invalidate(on_close)
181172

182173
# Keep track of what session this widget belongs to (so we can close it when the

0 commit comments

Comments
 (0)