Skip to content

Commit a9bb9a5

Browse files
Merge pull request #64 from Kitware/avoid-nested-flushes
Avoid nested flushes
2 parents 2f96ca9 + a19168b commit a9bb9a5

File tree

2 files changed

+109
-76
lines changed

2 files changed

+109
-76
lines changed

examples/child-server/multi_server_router.py

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,16 @@
99

1010

1111
class FirstApp(TrameApp):
12-
def __init__(self, server: trame_server.Server | str | None = None) -> None:
12+
def __init__(
13+
self,
14+
server: trame_server.Server | str | None = None,
15+
template_name: str = "main",
16+
) -> None:
1317
super().__init__(server)
1418

1519
self.state.test = "first"
1620

17-
self._build_ui()
21+
self._build_ui(template_name)
1822

1923
@trigger("test")
2024
def test_trigger(self) -> None:
@@ -24,21 +28,25 @@ def test_trigger(self) -> None:
2428
def test_controller(self) -> None:
2529
print(self.state.test)
2630

27-
def _build_ui(self) -> None:
28-
with VAppLayout(self.server, full_height=True), v3.VContainer(), v3.VCard(
29-
title="This is the first app"
30-
):
31+
def _build_ui(self, template_name) -> None:
32+
with VAppLayout(
33+
self.server, template_name=template_name, full_height=True
34+
), v3.VContainer(), v3.VCard(title="This is the first app"):
3135
v3.VBtn("Test Trigger", click="console.log(test); trame.trigger('test');")
3236
v3.VBtn("Test Controller", click=self.ctrl.test_controller)
3337

3438

3539
class SecondApp(TrameApp):
36-
def __init__(self, server: trame_server.Server | str | None = None) -> None:
40+
def __init__(
41+
self,
42+
server: trame_server.Server | str | None = None,
43+
template_name: str = "main",
44+
) -> None:
3745
super().__init__(server)
3846

3947
self.state.test = "second"
4048

41-
self._build_ui()
49+
self._build_ui(template_name)
4250

4351
@trigger("test")
4452
def trigger_test(self) -> None:
@@ -48,10 +56,10 @@ def trigger_test(self) -> None:
4856
def test_controller(self) -> None:
4957
print(self.state.test)
5058

51-
def _build_ui(self) -> None:
52-
with VAppLayout(self.server, full_height=True), v3.VContainer(), v3.VCard(
53-
title="This is the second app"
54-
):
59+
def _build_ui(self, template_name) -> None:
60+
with VAppLayout(
61+
self.server, template_name=template_name, full_height=True
62+
), v3.VContainer(), v3.VCard(title="This is the second app"):
5563
v3.VBtn("Test Trigger", click="console.log(test); trame.trigger('test');")
5664
v3.VBtn("Test Controller", click=self.ctrl.test_controller)
5765

@@ -70,10 +78,17 @@ def test_trigger(self) -> None:
7078

7179
def _build_ui(self) -> None:
7280
# Register routes
73-
with RouterViewLayout(self.server, "/"):
74-
FirstApp(self.server.create_child_server(prefix="first_route_"))
75-
with RouterViewLayout(self.server, "/second"):
76-
SecondApp(self.server.create_child_server(prefix="second_route_"))
81+
first_layout = RouterViewLayout(self.server, "/")
82+
second_layout = RouterViewLayout(self.server, "/second")
83+
84+
FirstApp(
85+
self.server.create_child_server(prefix="first_route_"),
86+
template_name=first_layout.template_name,
87+
)
88+
SecondApp(
89+
self.server.create_child_server(prefix="second_route_"),
90+
template_name=second_layout.template_name,
91+
)
7792

7893
with SinglePageLayout(self.server, full_height=True) as layout:
7994
with layout.toolbar:

trame_server/state.py

Lines changed: 78 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import inspect
22
import logging
33
import weakref
4+
from contextlib import contextmanager
45

56
from .utils import asynchronous, is_dunder, is_private, share
67
from .utils.hot_reload import reload
@@ -15,6 +16,32 @@
1516
TRAME_NON_INIT_VALUE = "__trame__: non_init_value_that_is_not_None"
1617

1718

19+
class StateStatus:
20+
"""
21+
Tracks status flags for a State.
22+
"""
23+
24+
def __init__(self, flushing: bool = False, ready: bool = False):
25+
self.flushing = flushing
26+
self.ready = ready
27+
28+
def mark_ready(self):
29+
self.ready = True
30+
31+
@property
32+
def skip_flushing(self) -> bool:
33+
return self.flushing or not self.ready
34+
35+
@contextmanager
36+
def flushing_context(self):
37+
"""Context manager for flushing state safely."""
38+
self.flushing = True
39+
try:
40+
yield
41+
finally:
42+
self.flushing = False
43+
44+
1845
class StateChangeHandler:
1946
def __init__(self, listeners):
2047
self._all_listeners = listeners
@@ -67,38 +94,30 @@ def __init__(
6794
self._state_listeners = share(
6895
internal, "_state_listeners", StateChangeHandler(self._change_callbacks)
6996
)
97+
self._status = share(internal, "_status", StateStatus(ready=ready))
7098
self._parent_state = internal
7199
self._children_state = []
72-
self._ready_flag = ready
73100
if internal:
74101
internal._children_state.append(self)
75102

76-
def ready(self) -> None:
77-
"""Mark the state as ready for synchronization."""
78-
if self._ready_flag:
79-
return
80-
81-
self._ready_flag = True
82-
self.flush()
83-
84-
if self._parent_state:
85-
self._parent_state.ready()
86-
87-
for child in self._children_state:
88-
child.ready()
89-
90103
@property
91104
def is_ready(self) -> bool:
92105
"""Return True is the instance is ready for synchronization, False otherwise."""
93-
if self._parent_state:
94-
return self._parent_state.is_ready
95-
return self._ready_flag
106+
return self._status.ready
96107

97108
@property
98109
def translator(self) -> Translator:
99110
"""Return the translator instance used to namespace the variable names."""
100111
return self._translator
101112

113+
def ready(self) -> None:
114+
"""Mark the state as ready for synchronization."""
115+
if self.is_ready:
116+
return
117+
118+
self._status.mark_ready()
119+
self.flush()
120+
102121
def __getitem__(self, key):
103122
key = self._translator.translate_key(key)
104123
return self._pending_update.get(key, self._pushed_state.get(key))
@@ -267,58 +286,57 @@ def modified_keys(self):
267286
# for child server we may need to run the translator on them
268287
return self._modified_keys
269288

289+
def _flush_pending_keys(self) -> set[str]:
290+
_keys = set(self._pending_update.keys())
291+
292+
# update modified keys for current update batch
293+
self._modified_keys.clear()
294+
self._modified_keys |= _keys
295+
296+
# Do the flush
297+
if self._push_state_fn:
298+
self._push_state_fn(self._pending_update)
299+
self._pushed_state.update(self._pending_update)
300+
self._pending_update.clear()
301+
302+
# Execute state listeners
303+
self._state_listeners.add_all(_keys)
304+
for fn, translator in self._state_listeners:
305+
if isinstance(fn, weakref.WeakMethod):
306+
callback = fn()
307+
if callback is None:
308+
continue
309+
else:
310+
callback = fn
311+
312+
if self._hot_reload:
313+
if not inspect.iscoroutinefunction(callback):
314+
callback = reload(callback)
315+
316+
reverse_translated_state = translator.reverse_translate_dict(
317+
self._pushed_state
318+
)
319+
coroutine = callback(**reverse_translated_state)
320+
if inspect.isawaitable(coroutine):
321+
asynchronous.create_task(coroutine)
322+
323+
self._state_listeners.clear()
324+
return _keys
325+
270326
def flush(self):
271327
"""
272328
Force pushing modified state and execute any @state.change listener
273329
if the variable value is different (by value AND reference) from its
274330
previous value or if `dirty` has been flagged on the variable and it has
275331
not been unflagged since.
276332
"""
277-
if not self.is_ready:
333+
if self._status.skip_flushing:
278334
return None
279335

280336
keys = set()
281-
if len(self._pending_update):
282-
_keys = set(self._pending_update.keys())
283-
284-
while len(_keys):
285-
keys |= _keys
286-
287-
# update modified keys for current update batch
288-
self._modified_keys.clear()
289-
self._modified_keys |= _keys
290-
291-
# Do the flush
292-
if self._push_state_fn:
293-
self._push_state_fn(self._pending_update)
294-
self._pushed_state.update(self._pending_update)
295-
self._pending_update.clear()
296-
297-
# Execute state listeners
298-
self._state_listeners.add_all(_keys)
299-
for fn, translator in self._state_listeners:
300-
if isinstance(fn, weakref.WeakMethod):
301-
callback = fn()
302-
if callback is None:
303-
continue
304-
else:
305-
callback = fn
306-
307-
if self._hot_reload:
308-
if not inspect.iscoroutinefunction(callback):
309-
callback = reload(callback)
310-
311-
reverse_translated_state = translator.reverse_translate_dict(
312-
self._pushed_state
313-
)
314-
coroutine = callback(**reverse_translated_state)
315-
if inspect.isawaitable(coroutine):
316-
asynchronous.create_task(coroutine)
317-
318-
self._state_listeners.clear()
319-
320-
# Check if state change from state listeners
321-
_keys = set(self._pending_update.keys())
337+
with self._status.flushing_context():
338+
while bool(self._pending_update):
339+
keys |= self._flush_pending_keys()
322340

323341
return keys
324342

0 commit comments

Comments
 (0)