reacton 1.10.3__tar.gz → 1.10.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {reacton-1.10.3 → reacton-1.10.4}/PKG-INFO +1 -1
- reacton-1.10.4/reacton/_version.py +1 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/core.py +194 -15
- {reacton-1.10.3 → reacton-1.10.4}/reacton/core_test.py +799 -1
- reacton-1.10.4/reacton/fuzz_test.py +182 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/ipyvue.py +1 -0
- reacton-1.10.3/reacton/_version.py +0 -1
- {reacton-1.10.3 → reacton-1.10.4}/.gitignore +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/LICENSE +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/pyproject.toml +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/react_ipywidgets/__init__.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/react_ipywidgets/core.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/__init__.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/bqplot.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/deprecated/__init__.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/deprecated/ipyvuetify.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/find.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/find_test.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/generate.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/generate_test.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/ipycanvas.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/ipyvue_test.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/ipyvuetify.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/ipywidgets.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/logging.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/patch.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/patch_display.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/py.typed +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/rx.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/test.vue +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/test_rx.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/utils.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/utils_test.py +0 -0
- {reacton-1.10.3 → reacton-1.10.4}/reacton/work.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.10.4"
|
|
@@ -7,11 +7,13 @@ ReactJS - ipywidgets relation:
|
|
|
7
7
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
import collections
|
|
10
11
|
import contextlib
|
|
11
12
|
import copy
|
|
12
13
|
import functools
|
|
13
14
|
import inspect
|
|
14
15
|
import logging
|
|
16
|
+
import os
|
|
15
17
|
import sys
|
|
16
18
|
import threading
|
|
17
19
|
import traceback
|
|
@@ -24,6 +26,7 @@ from typing import (
|
|
|
24
26
|
Any,
|
|
25
27
|
Callable,
|
|
26
28
|
ContextManager,
|
|
29
|
+
Deque,
|
|
27
30
|
Dict,
|
|
28
31
|
Generic,
|
|
29
32
|
List,
|
|
@@ -207,6 +210,10 @@ def _event_handler_exception_wrapper(f):
|
|
|
207
210
|
# we add it to exceptions_children, not exception_self
|
|
208
211
|
# this allows a component to catch the exception of a direct child
|
|
209
212
|
context.exceptions_children.append(e)
|
|
213
|
+
# force_update walks the whole tree, but when another thread is rendering it
|
|
214
|
+
# only sets a flag that this render may clear: mark the path to this context
|
|
215
|
+
# so the fast renderer does not skip it on the next render
|
|
216
|
+
_mark_needs_render_ancestors(context)
|
|
210
217
|
rc.force_update()
|
|
211
218
|
|
|
212
219
|
return wrapper
|
|
@@ -231,8 +238,12 @@ def same_component(c1, c2):
|
|
|
231
238
|
|
|
232
239
|
def _mark_needs_render_ancestors(context: "ComponentContext"):
|
|
233
240
|
"""Let the render phase find its way down to a context that needs work, without walking subtrees that do not."""
|
|
241
|
+
# walk up to the root: stopping at the first ancestor that is already marked would
|
|
242
|
+
# assume its ancestors are marked too, which does not hold for a subtree that is
|
|
243
|
+
# being removed (it can carry a flag from before), so the live ancestors above it
|
|
244
|
+
# would stay unmarked and skip e.g. a cleanup exception
|
|
234
245
|
parent = context.parent
|
|
235
|
-
while parent is not None
|
|
246
|
+
while parent is not None:
|
|
236
247
|
parent.needs_render_descendant = True
|
|
237
248
|
parent = parent.parent
|
|
238
249
|
|
|
@@ -255,6 +266,34 @@ def _values_identical(a, b):
|
|
|
255
266
|
return False
|
|
256
267
|
|
|
257
268
|
|
|
269
|
+
_missing = object()
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _widget_holds_values(widget: widgets.Widget, kwargs: Dict[str, Any]) -> bool:
|
|
273
|
+
"""Does the widget already hold these (resolved) kwargs, so that setting them is a no-op?
|
|
274
|
+
|
|
275
|
+
Only for the same element as last render: its event listeners are the same callbacks,
|
|
276
|
+
and no argument was dropped. Compares by identity, where a list and a tuple with the
|
|
277
|
+
same items count as the same (a Tuple trait stores a list as a tuple).
|
|
278
|
+
"""
|
|
279
|
+
for name, value in kwargs.items():
|
|
280
|
+
if name.startswith("on_") and not widget.has_trait(name):
|
|
281
|
+
continue
|
|
282
|
+
if not _value_held(value, getattr(widget, name, _missing)):
|
|
283
|
+
return False
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _value_held(value, held) -> bool:
|
|
288
|
+
if value is held:
|
|
289
|
+
return True
|
|
290
|
+
if isinstance(value, (list, tuple)):
|
|
291
|
+
return type(held) in (list, tuple) and len(value) == len(held) and all(_value_held(x, y) for x, y in zip(value, held))
|
|
292
|
+
if type(value) is dict:
|
|
293
|
+
return type(held) is dict and len(value) == len(held) and all(k in held and _value_held(v, held[k]) for k, v in value.items())
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
|
|
258
297
|
def _with_tracebacks(e, tracebacks):
|
|
259
298
|
# copy it, and we need with_traceback for unknown reasons not to cause
|
|
260
299
|
# an infinite loop
|
|
@@ -1166,6 +1205,11 @@ class ComponentContext:
|
|
|
1166
1205
|
# the render phase skipped this whole subtree (nothing changed), so the
|
|
1167
1206
|
# reconciliation phase can reuse the previous result without walking
|
|
1168
1207
|
clean_subtree: bool = False
|
|
1208
|
+
# Fast renderer only: stale component effect cleanups are run at the same
|
|
1209
|
+
# point where the default renderer removes stale elements, while widget
|
|
1210
|
+
# closing stays deferred to the fast renderer's stale sweep.
|
|
1211
|
+
fast_stale_effect_keys: Optional[List[str]] = None
|
|
1212
|
+
fast_stale_effects_cleaned: Set[str] = field(default_factory=set)
|
|
1169
1213
|
|
|
1170
1214
|
# elements created in this context go there
|
|
1171
1215
|
owns: Set[Element] = field(default_factory=set)
|
|
@@ -1224,6 +1268,8 @@ def _teardown_component_context(context: ComponentContext):
|
|
|
1224
1268
|
context.exceptions_self = []
|
|
1225
1269
|
context.exceptions_children = []
|
|
1226
1270
|
context.context_managers = []
|
|
1271
|
+
context.fast_stale_effect_keys = None
|
|
1272
|
+
context.fast_stale_effects_cleaned = set()
|
|
1227
1273
|
|
|
1228
1274
|
|
|
1229
1275
|
@dataclass
|
|
@@ -1260,6 +1306,30 @@ class Effect:
|
|
|
1260
1306
|
|
|
1261
1307
|
class _RenderContext:
|
|
1262
1308
|
context: Optional[ComponentContext] = None
|
|
1309
|
+
# the element that an element of another type replaces, and its context, while
|
|
1310
|
+
# reconciliation removes it (see _remove_replaced)
|
|
1311
|
+
_replaced: Optional[Tuple[ComponentContext, Element]] = None
|
|
1312
|
+
|
|
1313
|
+
def _remove_replaced(self, el_prev: Element, key: str, parent_key: str):
|
|
1314
|
+
"""Remove the element at key, which an element of another type replaces."""
|
|
1315
|
+
assert self.context is not None
|
|
1316
|
+
replaced = self._replaced
|
|
1317
|
+
self._replaced = (self.context, el_prev)
|
|
1318
|
+
try:
|
|
1319
|
+
self._remove_element(el_prev, key, parent_key=parent_key)
|
|
1320
|
+
finally:
|
|
1321
|
+
self._replaced = replaced
|
|
1322
|
+
|
|
1323
|
+
def _keep_keyed_child(self, context: ComponentContext, el: Element, key: str) -> bool:
|
|
1324
|
+
# An explicit key is the same in any container of a component, so a child with an
|
|
1325
|
+
# explicit key that the new tree still uses (it moved out of the replaced element, or
|
|
1326
|
+
# stays under the new one) is not removed with the replaced element: reconciliation
|
|
1327
|
+
# updates it where the new tree has it. Shared elements keep the old behavior: their
|
|
1328
|
+
# bookkeeping (_shared_elements, _shared_widgets) needs the removal.
|
|
1329
|
+
replaced = self._replaced
|
|
1330
|
+
return (
|
|
1331
|
+
replaced is not None and context is replaced[0] and el is not replaced[1] and el._key is not None and not el.is_shared and key in context.used_keys
|
|
1332
|
+
)
|
|
1263
1333
|
|
|
1264
1334
|
def __init__(self, element: Element, container: widgets.Widget = None, children_trait="children", handle_error: bool = True, initial_state=None):
|
|
1265
1335
|
self.element = element
|
|
@@ -1274,7 +1344,11 @@ class _RenderContext:
|
|
|
1274
1344
|
self.last_root_widget: widgets.Widget = None
|
|
1275
1345
|
self._is_rendering = False
|
|
1276
1346
|
self._rerender_needed = False
|
|
1277
|
-
|
|
1347
|
+
# the reasons are only read for the "too many renders" error message, and a reason
|
|
1348
|
+
# holds the previous and next state value: keeping all of them kept every old state
|
|
1349
|
+
# value alive until close(). REACTON_RERENDER_REASONS keeps more, for debugging.
|
|
1350
|
+
max_reasons = max(1, int(os.environ.get("REACTON_RERENDER_REASONS", "2")))
|
|
1351
|
+
self._rerender_needed_reasons: Deque[RerenderReason] = collections.deque(maxlen=max_reasons)
|
|
1278
1352
|
self.thread_lock = threading.Lock()
|
|
1279
1353
|
self._closing = False
|
|
1280
1354
|
self.tracebacks: List[TracebackType] = []
|
|
@@ -1654,10 +1728,10 @@ class _RenderContext:
|
|
|
1654
1728
|
f += f"Triggered at: {''.join(reason.trigger_stack)}\n"
|
|
1655
1729
|
return f
|
|
1656
1730
|
|
|
1657
|
-
self._rerender_needed_reasons[-1]
|
|
1658
1731
|
msg = f"Too many renders triggered, your render loop does not stop\nLast reason: {format(self._rerender_needed_reasons[-1])}\n"
|
|
1659
1732
|
if len(self._rerender_needed_reasons) >= 2:
|
|
1660
|
-
|
|
1733
|
+
previous = reversed(list(self._rerender_needed_reasons)[:-1])
|
|
1734
|
+
msg += f"Previous reasons: {''.join(format(reason) for reason in previous)}\n"
|
|
1661
1735
|
raise RuntimeError(msg)
|
|
1662
1736
|
logger.info("Entering nested render phase: %r", self._rerender_needed_reasons[-1])
|
|
1663
1737
|
self._rerender_needed = False
|
|
@@ -1760,6 +1834,10 @@ class _RenderContext:
|
|
|
1760
1834
|
finally:
|
|
1761
1835
|
local.rc = prev_rc # type: ignore
|
|
1762
1836
|
self._is_rendering = False
|
|
1837
|
+
# clear before the lock is released: a stale _lock_thread makes the
|
|
1838
|
+
# recursion guard above fire for a thread that merely rendered last,
|
|
1839
|
+
# while a *different* thread holds the lock (false "Recursive render")
|
|
1840
|
+
self._lock_thread = None
|
|
1763
1841
|
assert self.context is self.context_root
|
|
1764
1842
|
|
|
1765
1843
|
exceptions = [*self.context.exceptions_children, *self.context_root.exceptions_self]
|
|
@@ -2040,7 +2118,7 @@ class _RenderContext:
|
|
|
2040
2118
|
try:
|
|
2041
2119
|
if isinstance(el.component, ComponentFunction):
|
|
2042
2120
|
if el_prev and isinstance(el_prev.component, ComponentWidget):
|
|
2043
|
-
self.
|
|
2121
|
+
self._remove_replaced(el_prev, key, parent_key=parent_key)
|
|
2044
2122
|
new_parent_key = join_key(parent_key, key)
|
|
2045
2123
|
try:
|
|
2046
2124
|
# TODO: test suite passes when this block if commented out
|
|
@@ -2101,6 +2179,7 @@ class _RenderContext:
|
|
|
2101
2179
|
try:
|
|
2102
2180
|
effect.cleanup()
|
|
2103
2181
|
except BaseException as e:
|
|
2182
|
+
logger.exception("Effect cleanup %r raised exception %r", effect.callable, e)
|
|
2104
2183
|
context.exceptions_self.append(e)
|
|
2105
2184
|
self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during effect"))
|
|
2106
2185
|
self._rerender_needed = True
|
|
@@ -2112,6 +2191,7 @@ class _RenderContext:
|
|
|
2112
2191
|
continue
|
|
2113
2192
|
effect()
|
|
2114
2193
|
except BaseException as e:
|
|
2194
|
+
logger.exception("Effect %r raised exception %r", effect.callable, e)
|
|
2115
2195
|
context.exceptions_self.append(e)
|
|
2116
2196
|
self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during effect"))
|
|
2117
2197
|
self._rerender_needed = True
|
|
@@ -2123,6 +2203,7 @@ class _RenderContext:
|
|
|
2123
2203
|
continue
|
|
2124
2204
|
effect()
|
|
2125
2205
|
except BaseException as e:
|
|
2206
|
+
logger.exception("Effect %r raised exception %r", effect.callable, e)
|
|
2126
2207
|
context.exceptions_self.append(e)
|
|
2127
2208
|
self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during effect"))
|
|
2128
2209
|
self._rerender_needed = True
|
|
@@ -2214,7 +2295,7 @@ class _RenderContext:
|
|
|
2214
2295
|
else:
|
|
2215
2296
|
assert el_prev is not None, "widget_previous is not None, but el_prev is"
|
|
2216
2297
|
logger.debug("Replacing widget: %r → %r %r", el_prev, el, key)
|
|
2217
|
-
self.
|
|
2298
|
+
self._remove_replaced(el_prev, key, parent_key=parent_key)
|
|
2218
2299
|
kwargs = reconsolidate_children()
|
|
2219
2300
|
widget = None
|
|
2220
2301
|
if not context.exceptions_children:
|
|
@@ -2310,6 +2391,9 @@ class _RenderContext:
|
|
|
2310
2391
|
context = self.context
|
|
2311
2392
|
logger.debug("Remove: (%s, %s) %r", parent_key, key, el)
|
|
2312
2393
|
|
|
2394
|
+
if self._keep_keyed_child(context, el, key):
|
|
2395
|
+
return
|
|
2396
|
+
|
|
2313
2397
|
if el.is_shared:
|
|
2314
2398
|
if el not in self._shared_elements:
|
|
2315
2399
|
return
|
|
@@ -2336,6 +2420,7 @@ class _RenderContext:
|
|
|
2336
2420
|
if not effect._cleaned_up:
|
|
2337
2421
|
effect.cleanup()
|
|
2338
2422
|
except BaseException as e:
|
|
2423
|
+
logger.exception("Effect cleanup %r raised exception %r", effect.callable, e)
|
|
2339
2424
|
child_context.exceptions_self.append(e)
|
|
2340
2425
|
self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during effect"))
|
|
2341
2426
|
self._rerender_needed = True
|
|
@@ -2446,10 +2531,20 @@ class _RenderContextFast(_RenderContext):
|
|
|
2446
2531
|
# without walking either.
|
|
2447
2532
|
# ------------------------------------------------------------------
|
|
2448
2533
|
|
|
2534
|
+
# > 0 while the render phase walks the new children of a widget, or the arguments of a
|
|
2535
|
+
# shared element, that replaces an element of another type (see _render_arguments)
|
|
2536
|
+
_replacing = 0
|
|
2537
|
+
|
|
2449
2538
|
def _set_rerender_needed(self, reason: str):
|
|
2450
2539
|
self._rerender_needed_reasons.append(RerenderReason(reason=reason))
|
|
2451
2540
|
self._rerender_needed = True
|
|
2452
2541
|
|
|
2542
|
+
def _has_effect_work(self, context: "ComponentContext") -> bool:
|
|
2543
|
+
for effect in context.effects:
|
|
2544
|
+
if not effect.executed or effect.next is not None:
|
|
2545
|
+
return True
|
|
2546
|
+
return False
|
|
2547
|
+
|
|
2453
2548
|
def _render(self, element: Element, default_key: str, parent_key: str):
|
|
2454
2549
|
if not isinstance(element, Element):
|
|
2455
2550
|
raise TypeError(f"Expected element, not {element}")
|
|
@@ -2462,6 +2557,8 @@ class _RenderContextFast(_RenderContext):
|
|
|
2462
2557
|
# the root element of a component determines which keys are in use,
|
|
2463
2558
|
# everything else is stale and gets removed during reconciliation
|
|
2464
2559
|
context.used_keys.clear()
|
|
2560
|
+
context.fast_stale_effect_keys = None
|
|
2561
|
+
context.fast_stale_effects_cleaned.clear()
|
|
2465
2562
|
|
|
2466
2563
|
el = element
|
|
2467
2564
|
key = el._key
|
|
@@ -2494,7 +2591,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2494
2591
|
del context.children_next[key]
|
|
2495
2592
|
# the element arguments are part of this component's element tree
|
|
2496
2593
|
if el.kwargs:
|
|
2497
|
-
self.
|
|
2594
|
+
self._render_arguments(el, key, parent_key)
|
|
2498
2595
|
return
|
|
2499
2596
|
|
|
2500
2597
|
assert isinstance(el.component, ComponentFunction)
|
|
@@ -2502,7 +2599,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2502
2599
|
# arguments of a shared element belong to the context it is rendered in;
|
|
2503
2600
|
# for non-shared component elements the component function decides
|
|
2504
2601
|
# what ends up in the tree
|
|
2505
|
-
self.
|
|
2602
|
+
self._render_arguments(el, key, parent_key)
|
|
2506
2603
|
|
|
2507
2604
|
context_previous = context.children_next.get(key)
|
|
2508
2605
|
if context_previous is None:
|
|
@@ -2512,10 +2609,12 @@ class _RenderContextFast(_RenderContext):
|
|
|
2512
2609
|
not self._walk_all
|
|
2513
2610
|
and el is el_prev
|
|
2514
2611
|
and not el.is_shared
|
|
2612
|
+
and not self._replacing
|
|
2515
2613
|
and context_previous is not None
|
|
2516
2614
|
and context.children.get(key) is context_previous
|
|
2517
2615
|
and not context_previous.needs_render
|
|
2518
2616
|
and not context_previous.needs_render_descendant
|
|
2617
|
+
and not self._has_effect_work(context_previous)
|
|
2519
2618
|
and not context_previous.exceptions_self
|
|
2520
2619
|
and not context_previous.exceptions_children
|
|
2521
2620
|
and context_previous.root_element is not None
|
|
@@ -2697,7 +2796,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2697
2796
|
|
|
2698
2797
|
if el_prev and isinstance(el_prev.component, ComponentWidget):
|
|
2699
2798
|
# a widget element was replaced by a component element at this key
|
|
2700
|
-
self.
|
|
2799
|
+
self._remove_replaced(el_prev, key, parent_key=parent_key)
|
|
2701
2800
|
new_parent_key = join_key(parent_key, key)
|
|
2702
2801
|
try:
|
|
2703
2802
|
if el.is_shared and (el.args or el.kwargs):
|
|
@@ -2790,20 +2889,22 @@ class _RenderContextFast(_RenderContext):
|
|
|
2790
2889
|
# update the existing widget in place
|
|
2791
2890
|
kwargs = self._visit_children_values(el.kwargs, key, parent_key, self._reconsolidate)
|
|
2792
2891
|
if not context.exceptions_children:
|
|
2793
|
-
|
|
2892
|
+
# the same element whose kwargs hold no elements: nothing can have changed.
|
|
2893
|
+
# With elements (a container), the kwargs resolve to widgets, so compare
|
|
2894
|
+
# them with what the widget holds: equal means setting them is a no-op
|
|
2895
|
+
# (and a value changed from the frontend is still set back)
|
|
2896
|
+
if el is not el_prev or not (_values_identical(kwargs, el.kwargs) or _widget_holds_values(widget_previous, kwargs)):
|
|
2794
2897
|
try:
|
|
2795
2898
|
el._update_widget(widget_previous, el_prev, kwargs)
|
|
2796
2899
|
except BaseException as e:
|
|
2797
2900
|
context.exceptions_self.append(e)
|
|
2798
2901
|
self._set_rerender_needed("Exception ocurred during reconciliation (updating widget)")
|
|
2799
2902
|
_mark_needs_render_ancestors(context)
|
|
2800
|
-
# else: identical element and all children reconciled to the
|
|
2801
|
-
# same widgets, nothing can have changed
|
|
2802
2903
|
self._store_widget(context, el, key, widget_previous)
|
|
2803
2904
|
else:
|
|
2804
2905
|
assert el_prev is not None, "widget_previous is not None, but el_prev is"
|
|
2805
2906
|
# a different widget type at the same key: replace
|
|
2806
|
-
self.
|
|
2907
|
+
self._remove_replaced(el_prev, key, parent_key=parent_key)
|
|
2807
2908
|
kwargs = self._visit_children_values(el.kwargs, key, parent_key, self._reconsolidate)
|
|
2808
2909
|
widget = None
|
|
2809
2910
|
if not context.exceptions_children:
|
|
@@ -2858,6 +2959,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2858
2959
|
self._shared_elements.add(el)
|
|
2859
2960
|
assert el in self._shared_elements_next
|
|
2860
2961
|
self._shared_elements_next.remove(el)
|
|
2962
|
+
self._cleanup_stale_effects_for_context(context, parent_key)
|
|
2861
2963
|
|
|
2862
2964
|
def _process_effects(self, child_context: "ComponentContext", context: "ComponentContext"):
|
|
2863
2965
|
# NOTE: effect/cleanup exceptions are recorded on the context of the
|
|
@@ -2876,6 +2978,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2876
2978
|
try:
|
|
2877
2979
|
effect.cleanup()
|
|
2878
2980
|
except BaseException as e:
|
|
2981
|
+
logger.exception("Effect cleanup %r raised exception %r", effect.callable, e)
|
|
2879
2982
|
context.exceptions_self.append(e)
|
|
2880
2983
|
self._set_rerender_needed("Exception ocurred during effect")
|
|
2881
2984
|
_mark_needs_render_ancestors(context)
|
|
@@ -2887,6 +2990,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2887
2990
|
try:
|
|
2888
2991
|
effect()
|
|
2889
2992
|
except BaseException as e:
|
|
2993
|
+
logger.exception("Effect %r raised exception %r", effect.callable, e)
|
|
2890
2994
|
context.exceptions_self.append(e)
|
|
2891
2995
|
self._set_rerender_needed("Exception ocurred during effect")
|
|
2892
2996
|
_mark_needs_render_ancestors(context)
|
|
@@ -2898,6 +3002,64 @@ class _RenderContextFast(_RenderContext):
|
|
|
2898
3002
|
else:
|
|
2899
3003
|
context.widgets[key] = widget
|
|
2900
3004
|
|
|
3005
|
+
def _cleanup_stale_effects_for_context(self, context: "ComponentContext", parent_key: str):
|
|
3006
|
+
if context.fast_stale_effect_keys is None:
|
|
3007
|
+
# reversed, so we can pop from the end and still go in sorted order
|
|
3008
|
+
context.fast_stale_effect_keys = sorted(set(context.elements) - context.used_keys, reverse=True)
|
|
3009
|
+
context_prev = self.context
|
|
3010
|
+
try:
|
|
3011
|
+
while context.fast_stale_effect_keys:
|
|
3012
|
+
stale_key = context.fast_stale_effect_keys.pop()
|
|
3013
|
+
if stale_key not in context.elements or stale_key in context.fast_stale_effects_cleaned:
|
|
3014
|
+
continue
|
|
3015
|
+
self.context = context
|
|
3016
|
+
self._cleanup_stale_effects(context.elements[stale_key], stale_key, parent_key)
|
|
3017
|
+
finally:
|
|
3018
|
+
self.context = context_prev
|
|
3019
|
+
|
|
3020
|
+
def _cleanup_stale_effects(self, el: Element, default_key: str, parent_key: str):
|
|
3021
|
+
key = el._key
|
|
3022
|
+
if key is None:
|
|
3023
|
+
key = default_key
|
|
3024
|
+
assert key is not None
|
|
3025
|
+
context = self.context
|
|
3026
|
+
assert context is not None
|
|
3027
|
+
if key in context.fast_stale_effects_cleaned:
|
|
3028
|
+
return
|
|
3029
|
+
if el.is_shared and (el in self._shared_elements_next or el not in self._shared_elements):
|
|
3030
|
+
return
|
|
3031
|
+
context.fast_stale_effects_cleaned.add(key)
|
|
3032
|
+
|
|
3033
|
+
if isinstance(el.component, ComponentFunction):
|
|
3034
|
+
if el.is_shared:
|
|
3035
|
+
self._visit_children(el, key, parent_key, self._cleanup_stale_effects)
|
|
3036
|
+
child_context = context.children.get(key)
|
|
3037
|
+
if child_context is None:
|
|
3038
|
+
return
|
|
3039
|
+
try:
|
|
3040
|
+
self.context = child_context
|
|
3041
|
+
child_context.exceptions_self = []
|
|
3042
|
+
child_context.exceptions_children = []
|
|
3043
|
+
for effect in child_context.effects:
|
|
3044
|
+
try:
|
|
3045
|
+
if not effect._cleaned_up:
|
|
3046
|
+
effect.cleanup()
|
|
3047
|
+
except BaseException as e:
|
|
3048
|
+
effect._cleaned_up = True
|
|
3049
|
+
logger.exception("Effect cleanup %r raised exception %r", effect.callable, e)
|
|
3050
|
+
child_context.exceptions_self.append(e)
|
|
3051
|
+
self._set_rerender_needed("Exception ocurred during effect")
|
|
3052
|
+
_mark_needs_render_ancestors(child_context)
|
|
3053
|
+
assert child_context.root_element is not None
|
|
3054
|
+
self._cleanup_stale_effects(child_context.root_element, "/", parent_key=join_key(parent_key, key))
|
|
3055
|
+
finally:
|
|
3056
|
+
self.context = context
|
|
3057
|
+
if child_context.exceptions_self or child_context.exceptions_children and not child_context.exception_handler:
|
|
3058
|
+
context.exceptions_children.extend(child_context.exceptions_self)
|
|
3059
|
+
context.exceptions_children.extend(child_context.exceptions_children)
|
|
3060
|
+
else:
|
|
3061
|
+
self._visit_children(el, key, parent_key, self._cleanup_stale_effects)
|
|
3062
|
+
|
|
2901
3063
|
def _remove_element(self, el: Element, default_key: str, parent_key):
|
|
2902
3064
|
key = el._key
|
|
2903
3065
|
if key is None:
|
|
@@ -2906,6 +3068,9 @@ class _RenderContextFast(_RenderContext):
|
|
|
2906
3068
|
context = self.context
|
|
2907
3069
|
assert context is not None
|
|
2908
3070
|
|
|
3071
|
+
if self._keep_keyed_child(context, el, key):
|
|
3072
|
+
return
|
|
3073
|
+
|
|
2909
3074
|
if el.is_shared:
|
|
2910
3075
|
if el not in self._shared_elements:
|
|
2911
3076
|
# another use of this element keeps it alive, or it was already removed
|
|
@@ -2930,6 +3095,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2930
3095
|
if not effect._cleaned_up:
|
|
2931
3096
|
effect.cleanup()
|
|
2932
3097
|
except BaseException as e:
|
|
3098
|
+
logger.exception("Effect cleanup %r raised exception %r", effect.callable, e)
|
|
2933
3099
|
child_context.exceptions_self.append(e)
|
|
2934
3100
|
self._set_rerender_needed("Exception ocurred during effect")
|
|
2935
3101
|
_mark_needs_render_ancestors(child_context)
|
|
@@ -3001,6 +3167,21 @@ class _RenderContextFast(_RenderContext):
|
|
|
3001
3167
|
else:
|
|
3002
3168
|
return value
|
|
3003
3169
|
|
|
3170
|
+
def _render_arguments(self, el: Element, key: str, parent_key: str):
|
|
3171
|
+
"""Render the elements in the arguments of a widget element or a shared element."""
|
|
3172
|
+
assert self.context is not None
|
|
3173
|
+
el_reconciled = self.context.elements.get(key)
|
|
3174
|
+
replaced = el_reconciled is not None and el_reconciled.component != el.component
|
|
3175
|
+
if replaced:
|
|
3176
|
+
# a different component at this key: reconciliation removes the old element and
|
|
3177
|
+
# everything below it, so nothing below the new one may be skipped as clean
|
|
3178
|
+
self._replacing += 1
|
|
3179
|
+
try:
|
|
3180
|
+
self._visit_children(el, key, parent_key, self._render)
|
|
3181
|
+
finally:
|
|
3182
|
+
if replaced:
|
|
3183
|
+
self._replacing -= 1
|
|
3184
|
+
|
|
3004
3185
|
def _remove_stale_root_elements(self, parent_key):
|
|
3005
3186
|
# remove stale elements of the root context itself
|
|
3006
3187
|
# (child contexts are swept during their reconciliation)
|
|
@@ -3011,8 +3192,6 @@ class _RenderContextFast(_RenderContext):
|
|
|
3011
3192
|
|
|
3012
3193
|
|
|
3013
3194
|
def _render_context_class():
|
|
3014
|
-
import os
|
|
3015
|
-
|
|
3016
3195
|
return _RenderContextFast if os.environ.get("REACTON_FAST") == "1" else _RenderContext
|
|
3017
3196
|
|
|
3018
3197
|
|