Skip to content

Commit 4ff39c5

Browse files
fix: restore kernel context on reactive event handler
If a reactive variable was set from a different (or no) kernel context, the event handler would not restore the kernel context that was active when the event handler was registered. Fixes #961
1 parent c219e3a commit 4ff39c5

2 files changed

Lines changed: 95 additions & 12 deletions

File tree

solara/toestand.py

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import contextlib
21
import dataclasses
32
import inspect
43
import logging
@@ -33,6 +32,7 @@
3332
import solara
3433
import solara.settings
3534
from solara import _using_solara_server
35+
from solara.util import nullcontext
3636

3737
T = TypeVar("T")
3838
TS = TypeVar("TS")
@@ -176,18 +176,18 @@ def fire(self, new: T, old: T):
176176
scope_id = self._get_scope_key()
177177
scopes = set()
178178
for listener, scope in self.listeners[scope_id].copy():
179-
if scope is not None:
180-
scopes.add(scope)
179+
scopes.add(scope)
181180
for listener2, scope in self.listeners2[scope_id].copy():
182-
if scope is not None:
183-
scopes.add(scope)
184-
with contextlib.ExitStack() as stack:
181+
scopes.add(scope)
182+
if scopes:
185183
for scope in scopes:
186-
stack.enter_context(scope)
187-
for listener, scope in self.listeners[scope_id].copy():
188-
listener(new)
189-
for listener2, scope in self.listeners2[scope_id].copy():
190-
listener2(new, old)
184+
with scope or nullcontext():
185+
for listener, scope_listener in self.listeners[scope_id].copy():
186+
if scope == scope_listener:
187+
listener(new)
188+
for listener2, scope_listener in self.listeners2[scope_id].copy():
189+
if scope == scope_listener:
190+
listener2(new, old)
191191

192192
def update(self, _f=None, **kwargs):
193193
if _f is not None:
@@ -885,6 +885,35 @@ def __exit__(self, exc_type, exc_val, exc_tb):
885885
thread_local.reactive_used = self.reactive_used_before
886886

887887

888+
class Context:
889+
def __init__(self, render_context, kernel_context):
890+
# combine the render context *and* the kernel context into one context
891+
self.render_context = render_context
892+
self.kernel_context = kernel_context
893+
894+
def __enter__(self):
895+
self.render_context.__enter__()
896+
self.kernel_context.__enter__()
897+
898+
def __exit__(self, exc_type, exc_val, exc_tb):
899+
# this will trigger a render
900+
res1 = self.render_context.__exit__(exc_type, exc_val, exc_tb)
901+
# pop the current context from the stack
902+
res2 = self.kernel_context.__exit__(exc_type, exc_val, exc_tb)
903+
return res1 or res2
904+
905+
def __eq__(self, value: object) -> bool:
906+
if not isinstance(value, Context):
907+
return False
908+
return self.render_context == value.render_context and self.kernel_context == value.kernel_context
909+
910+
def __hash__(self) -> int:
911+
return hash(id(self.render_context)) ^ hash(id(self.kernel_context))
912+
913+
def __repr__(self) -> str:
914+
return f"Context(render_context={self.render_context}, kernel_context={self.kernel_context})"
915+
916+
888917
class AutoSubscribeContextManagerReacton(AutoSubscribeContextManagerBase):
889918
def __init__(self, element: solara.Element):
890919
self.element = element
@@ -900,7 +929,9 @@ def force_update(new_value, old_value):
900929
super().__enter__()
901930

902931
def update_subscribers():
903-
self.update_subscribers(force_update, scope=reacton.core.get_render_context(required=True))
932+
rc = reacton.core.get_render_context(required=True)
933+
kernel = solara.server.kernel_context.get_current_context() if solara.server.kernel_context.has_current_context() else nullcontext()
934+
self.update_subscribers(force_update, scope=Context(rc, kernel))
904935

905936
solara.use_effect(update_subscribers, None)
906937

tests/unit/toestand_test.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import solara as sol
1616
import solara.lab
1717
import solara.toestand as toestand
18+
import solara.server.kernel_context
1819
from solara.server import kernel, kernel_context
1920
from solara.toestand import Reactive, Ref, State, use_sync_external_store
2021
import solara.settings
@@ -180,25 +181,30 @@ def test_scopes(no_kernel_context):
180181
@solara.component
181182
def Page():
182183
reactive = solara.use_reactive(0)
184+
assert solara.server.kernel_context.get_current_context() is expected_context
183185
if reactives[index] is None:
184186
reactives[index] = reactive # type: ignore
185187
solara.Info(str(reactive.value))
186188

187189
with context1:
190+
expected_context = context1
188191
_, rc1 = react.render(Page(), handle_error=False)
189192
assert rc1.find(v.Alert).widget.children[0] == "0"
190193
assert reactives[0] is not None
191194

192195
with context2:
193196
index = 1
197+
expected_context = context2
194198
_, rc2 = react.render(Page(), handle_error=False)
195199
assert rc2.find(v.Alert).widget.children[0] == "0"
196200
assert reactives[1] is not None
197201

202+
expected_context = context1
198203
reactives[0].value = 1
199204
assert rc1.find(v.Alert).widget.children[0] == "1"
200205
assert rc2.find(v.Alert).widget.children[0] == "0"
201206

207+
expected_context = context2
202208
reactives[1].value = 2
203209
assert rc1.find(v.Alert).widget.children[0] == "1"
204210
assert rc2.find(v.Alert).widget.children[0] == "2"
@@ -215,6 +221,52 @@ def Page():
215221
rc2.close()
216222

217223

224+
def test_scopes_restore(no_kernel_context):
225+
kernel1 = kernel.Kernel()
226+
kernel2 = kernel.Kernel()
227+
assert kernel_context.current_context[kernel_context.get_current_thread_key()] is None
228+
229+
context1 = kernel_context.VirtualKernelContext(id="toestand-1", kernel=kernel1, session_id="session-1")
230+
context2 = kernel_context.VirtualKernelContext(id="toestand-2", kernel=kernel2, session_id="session-2")
231+
232+
from solara._stores import SharedStore
233+
234+
reactive: solara.Reactive[int] = solara.Reactive(SharedStore(10))
235+
236+
@solara.component
237+
def Page1():
238+
assert solara.server.kernel_context.get_current_context() is context1
239+
solara.Info(str(reactive.value))
240+
Page1b()
241+
242+
@solara.component
243+
def Page1b():
244+
assert solara.server.kernel_context.get_current_context() is context1
245+
solara.Error(str(reactive.value))
246+
247+
@solara.component
248+
def Page2():
249+
assert solara.server.kernel_context.get_current_context() is context2
250+
solara.Info(str(reactive.value))
251+
252+
with context2:
253+
_, rc2 = react.render(Page2(), handle_error=False)
254+
assert rc2.find(v.Alert).widget.children[0] == "10"
255+
256+
with context1:
257+
_, rc1 = react.render(Page1(), handle_error=False)
258+
assert rc1.find(v.Alert)[0].widget.children[0] == "10"
259+
260+
reactive.value = 42
261+
assert rc1.find(v.Alert)[0].widget.children[0] == "42"
262+
assert rc2.find(v.Alert).widget.children[0] == "42"
263+
264+
with context1:
265+
rc1.close()
266+
with context2:
267+
rc2.close()
268+
269+
218270
def test_nested_update():
219271
# this effectively test the RLock vs Lock
220272
bear_store = BearReactive(bears)

0 commit comments

Comments
 (0)