|
1 | 1 | import inspect |
2 | 2 | import logging |
3 | 3 | import weakref |
| 4 | +from contextlib import contextmanager |
4 | 5 |
|
5 | 6 | from .utils import asynchronous, is_dunder, is_private, share |
6 | 7 | from .utils.hot_reload import reload |
|
15 | 16 | TRAME_NON_INIT_VALUE = "__trame__: non_init_value_that_is_not_None" |
16 | 17 |
|
17 | 18 |
|
| 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 | + |
18 | 45 | class StateChangeHandler: |
19 | 46 | def __init__(self, listeners): |
20 | 47 | self._all_listeners = listeners |
@@ -67,38 +94,30 @@ def __init__( |
67 | 94 | self._state_listeners = share( |
68 | 95 | internal, "_state_listeners", StateChangeHandler(self._change_callbacks) |
69 | 96 | ) |
| 97 | + self._status = share(internal, "_status", StateStatus(ready=ready)) |
70 | 98 | self._parent_state = internal |
71 | 99 | self._children_state = [] |
72 | | - self._ready_flag = ready |
73 | 100 | if internal: |
74 | 101 | internal._children_state.append(self) |
75 | 102 |
|
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 | | - |
90 | 103 | @property |
91 | 104 | def is_ready(self) -> bool: |
92 | 105 | """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 |
96 | 107 |
|
97 | 108 | @property |
98 | 109 | def translator(self) -> Translator: |
99 | 110 | """Return the translator instance used to namespace the variable names.""" |
100 | 111 | return self._translator |
101 | 112 |
|
| 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 | + |
102 | 121 | def __getitem__(self, key): |
103 | 122 | key = self._translator.translate_key(key) |
104 | 123 | return self._pending_update.get(key, self._pushed_state.get(key)) |
@@ -267,58 +286,57 @@ def modified_keys(self): |
267 | 286 | # for child server we may need to run the translator on them |
268 | 287 | return self._modified_keys |
269 | 288 |
|
| 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 | + |
270 | 326 | def flush(self): |
271 | 327 | """ |
272 | 328 | Force pushing modified state and execute any @state.change listener |
273 | 329 | if the variable value is different (by value AND reference) from its |
274 | 330 | previous value or if `dirty` has been flagged on the variable and it has |
275 | 331 | not been unflagged since. |
276 | 332 | """ |
277 | | - if not self.is_ready: |
| 333 | + if self._status.skip_flushing: |
278 | 334 | return None |
279 | 335 |
|
280 | 336 | 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() |
322 | 340 |
|
323 | 341 | return keys |
324 | 342 |
|
|
0 commit comments