reacton 1.10.3__tar.gz → 1.10.5__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.5}/PKG-INFO +1 -1
- reacton-1.10.5/reacton/_version.py +1 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/core.py +209 -19
- {reacton-1.10.3 → reacton-1.10.5}/reacton/core_test.py +858 -1
- reacton-1.10.5/reacton/fuzz_test.py +213 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/ipyvue.py +1 -0
- reacton-1.10.3/reacton/_version.py +0 -1
- {reacton-1.10.3 → reacton-1.10.5}/.gitignore +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/LICENSE +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/pyproject.toml +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/react_ipywidgets/__init__.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/react_ipywidgets/core.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/__init__.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/bqplot.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/deprecated/__init__.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/deprecated/ipyvuetify.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/find.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/find_test.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/generate.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/generate_test.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/ipycanvas.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/ipyvue_test.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/ipyvuetify.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/ipywidgets.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/logging.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/patch.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/patch_display.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/py.typed +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/rx.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/test.vue +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/test_rx.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/utils.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/utils_test.py +0 -0
- {reacton-1.10.3 → reacton-1.10.5}/reacton/work.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.10.5"
|
|
@@ -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,31 @@ class Effect:
|
|
|
1260
1306
|
|
|
1261
1307
|
class _RenderContext:
|
|
1262
1308
|
context: Optional[ComponentContext] = None
|
|
1309
|
+
# the element that reconciliation removes because the new tree replaced it by an element
|
|
1310
|
+
# of another type, or no longer has it, and its context (see _remove_outgoing)
|
|
1311
|
+
_outgoing: Optional[Tuple[ComponentContext, Element]] = None
|
|
1312
|
+
|
|
1313
|
+
def _remove_outgoing(self, el: Element, key: str, parent_key: str):
|
|
1314
|
+
"""Remove the element at key, which the new tree replaced or no longer has."""
|
|
1315
|
+
assert self.context is not None
|
|
1316
|
+
outgoing = self._outgoing
|
|
1317
|
+
self._outgoing = (self.context, el)
|
|
1318
|
+
try:
|
|
1319
|
+
self._remove_element(el, key, parent_key=parent_key)
|
|
1320
|
+
finally:
|
|
1321
|
+
self._outgoing = outgoing
|
|
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 outgoing element, for
|
|
1326
|
+
# instance a wrapper that went away, or stays under the element that replaces it) is
|
|
1327
|
+
# not removed with the outgoing element: reconciliation updates it where the new tree
|
|
1328
|
+
# has it. Shared elements keep the old behavior: their bookkeeping (_shared_elements,
|
|
1329
|
+
# _shared_widgets) needs the removal.
|
|
1330
|
+
outgoing = self._outgoing
|
|
1331
|
+
return (
|
|
1332
|
+
outgoing is not None and context is outgoing[0] and el is not outgoing[1] and el._key is not None and not el.is_shared and key in context.used_keys
|
|
1333
|
+
)
|
|
1263
1334
|
|
|
1264
1335
|
def __init__(self, element: Element, container: widgets.Widget = None, children_trait="children", handle_error: bool = True, initial_state=None):
|
|
1265
1336
|
self.element = element
|
|
@@ -1274,7 +1345,11 @@ class _RenderContext:
|
|
|
1274
1345
|
self.last_root_widget: widgets.Widget = None
|
|
1275
1346
|
self._is_rendering = False
|
|
1276
1347
|
self._rerender_needed = False
|
|
1277
|
-
|
|
1348
|
+
# the reasons are only read for the "too many renders" error message, and a reason
|
|
1349
|
+
# holds the previous and next state value: keeping all of them kept every old state
|
|
1350
|
+
# value alive until close(). REACTON_RERENDER_REASONS keeps more, for debugging.
|
|
1351
|
+
max_reasons = max(1, int(os.environ.get("REACTON_RERENDER_REASONS", "2")))
|
|
1352
|
+
self._rerender_needed_reasons: Deque[RerenderReason] = collections.deque(maxlen=max_reasons)
|
|
1278
1353
|
self.thread_lock = threading.Lock()
|
|
1279
1354
|
self._closing = False
|
|
1280
1355
|
self.tracebacks: List[TracebackType] = []
|
|
@@ -1654,10 +1729,10 @@ class _RenderContext:
|
|
|
1654
1729
|
f += f"Triggered at: {''.join(reason.trigger_stack)}\n"
|
|
1655
1730
|
return f
|
|
1656
1731
|
|
|
1657
|
-
self._rerender_needed_reasons[-1]
|
|
1658
1732
|
msg = f"Too many renders triggered, your render loop does not stop\nLast reason: {format(self._rerender_needed_reasons[-1])}\n"
|
|
1659
1733
|
if len(self._rerender_needed_reasons) >= 2:
|
|
1660
|
-
|
|
1734
|
+
previous = reversed(list(self._rerender_needed_reasons)[:-1])
|
|
1735
|
+
msg += f"Previous reasons: {''.join(format(reason) for reason in previous)}\n"
|
|
1661
1736
|
raise RuntimeError(msg)
|
|
1662
1737
|
logger.info("Entering nested render phase: %r", self._rerender_needed_reasons[-1])
|
|
1663
1738
|
self._rerender_needed = False
|
|
@@ -1760,6 +1835,10 @@ class _RenderContext:
|
|
|
1760
1835
|
finally:
|
|
1761
1836
|
local.rc = prev_rc # type: ignore
|
|
1762
1837
|
self._is_rendering = False
|
|
1838
|
+
# clear before the lock is released: a stale _lock_thread makes the
|
|
1839
|
+
# recursion guard above fire for a thread that merely rendered last,
|
|
1840
|
+
# while a *different* thread holds the lock (false "Recursive render")
|
|
1841
|
+
self._lock_thread = None
|
|
1763
1842
|
assert self.context is self.context_root
|
|
1764
1843
|
|
|
1765
1844
|
exceptions = [*self.context.exceptions_children, *self.context_root.exceptions_self]
|
|
@@ -2040,7 +2119,7 @@ class _RenderContext:
|
|
|
2040
2119
|
try:
|
|
2041
2120
|
if isinstance(el.component, ComponentFunction):
|
|
2042
2121
|
if el_prev and isinstance(el_prev.component, ComponentWidget):
|
|
2043
|
-
self.
|
|
2122
|
+
self._remove_outgoing(el_prev, key, parent_key=parent_key)
|
|
2044
2123
|
new_parent_key = join_key(parent_key, key)
|
|
2045
2124
|
try:
|
|
2046
2125
|
# TODO: test suite passes when this block if commented out
|
|
@@ -2086,7 +2165,7 @@ class _RenderContext:
|
|
|
2086
2165
|
if removed:
|
|
2087
2166
|
for key_remove in removed:
|
|
2088
2167
|
el_remove = elements[key_remove]
|
|
2089
|
-
self.
|
|
2168
|
+
self._remove_outgoing(el_remove, key_remove, parent_key)
|
|
2090
2169
|
for effect_index, effect in enumerate(child_context.effects):
|
|
2091
2170
|
if effect.next:
|
|
2092
2171
|
# if we have a next, it means that effect itself is executed
|
|
@@ -2101,6 +2180,7 @@ class _RenderContext:
|
|
|
2101
2180
|
try:
|
|
2102
2181
|
effect.cleanup()
|
|
2103
2182
|
except BaseException as e:
|
|
2183
|
+
logger.exception("Effect cleanup %r raised exception %r", effect.callable, e)
|
|
2104
2184
|
context.exceptions_self.append(e)
|
|
2105
2185
|
self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during effect"))
|
|
2106
2186
|
self._rerender_needed = True
|
|
@@ -2112,6 +2192,7 @@ class _RenderContext:
|
|
|
2112
2192
|
continue
|
|
2113
2193
|
effect()
|
|
2114
2194
|
except BaseException as e:
|
|
2195
|
+
logger.exception("Effect %r raised exception %r", effect.callable, e)
|
|
2115
2196
|
context.exceptions_self.append(e)
|
|
2116
2197
|
self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during effect"))
|
|
2117
2198
|
self._rerender_needed = True
|
|
@@ -2123,6 +2204,7 @@ class _RenderContext:
|
|
|
2123
2204
|
continue
|
|
2124
2205
|
effect()
|
|
2125
2206
|
except BaseException as e:
|
|
2207
|
+
logger.exception("Effect %r raised exception %r", effect.callable, e)
|
|
2126
2208
|
context.exceptions_self.append(e)
|
|
2127
2209
|
self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during effect"))
|
|
2128
2210
|
self._rerender_needed = True
|
|
@@ -2214,7 +2296,7 @@ class _RenderContext:
|
|
|
2214
2296
|
else:
|
|
2215
2297
|
assert el_prev is not None, "widget_previous is not None, but el_prev is"
|
|
2216
2298
|
logger.debug("Replacing widget: %r → %r %r", el_prev, el, key)
|
|
2217
|
-
self.
|
|
2299
|
+
self._remove_outgoing(el_prev, key, parent_key=parent_key)
|
|
2218
2300
|
kwargs = reconsolidate_children()
|
|
2219
2301
|
widget = None
|
|
2220
2302
|
if not context.exceptions_children:
|
|
@@ -2291,7 +2373,7 @@ class _RenderContext:
|
|
|
2291
2373
|
if extra:
|
|
2292
2374
|
for key in list(extra):
|
|
2293
2375
|
if key in self.context.elements:
|
|
2294
|
-
self.
|
|
2376
|
+
self._remove_outgoing(self.context.elements[key], key, parent_key=parent_key)
|
|
2295
2377
|
|
|
2296
2378
|
# keeping this for debugging
|
|
2297
2379
|
# logger.debug("Current:")
|
|
@@ -2310,6 +2392,9 @@ class _RenderContext:
|
|
|
2310
2392
|
context = self.context
|
|
2311
2393
|
logger.debug("Remove: (%s, %s) %r", parent_key, key, el)
|
|
2312
2394
|
|
|
2395
|
+
if self._keep_keyed_child(context, el, key):
|
|
2396
|
+
return
|
|
2397
|
+
|
|
2313
2398
|
if el.is_shared:
|
|
2314
2399
|
if el not in self._shared_elements:
|
|
2315
2400
|
return
|
|
@@ -2336,6 +2421,7 @@ class _RenderContext:
|
|
|
2336
2421
|
if not effect._cleaned_up:
|
|
2337
2422
|
effect.cleanup()
|
|
2338
2423
|
except BaseException as e:
|
|
2424
|
+
logger.exception("Effect cleanup %r raised exception %r", effect.callable, e)
|
|
2339
2425
|
child_context.exceptions_self.append(e)
|
|
2340
2426
|
self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during effect"))
|
|
2341
2427
|
self._rerender_needed = True
|
|
@@ -2446,10 +2532,20 @@ class _RenderContextFast(_RenderContext):
|
|
|
2446
2532
|
# without walking either.
|
|
2447
2533
|
# ------------------------------------------------------------------
|
|
2448
2534
|
|
|
2535
|
+
# > 0 while the render phase walks the new children of a widget, or the arguments of a
|
|
2536
|
+
# shared element, that replaces an element of another type (see _render_arguments)
|
|
2537
|
+
_replacing = 0
|
|
2538
|
+
|
|
2449
2539
|
def _set_rerender_needed(self, reason: str):
|
|
2450
2540
|
self._rerender_needed_reasons.append(RerenderReason(reason=reason))
|
|
2451
2541
|
self._rerender_needed = True
|
|
2452
2542
|
|
|
2543
|
+
def _has_effect_work(self, context: "ComponentContext") -> bool:
|
|
2544
|
+
for effect in context.effects:
|
|
2545
|
+
if not effect.executed or effect.next is not None:
|
|
2546
|
+
return True
|
|
2547
|
+
return False
|
|
2548
|
+
|
|
2453
2549
|
def _render(self, element: Element, default_key: str, parent_key: str):
|
|
2454
2550
|
if not isinstance(element, Element):
|
|
2455
2551
|
raise TypeError(f"Expected element, not {element}")
|
|
@@ -2462,6 +2558,8 @@ class _RenderContextFast(_RenderContext):
|
|
|
2462
2558
|
# the root element of a component determines which keys are in use,
|
|
2463
2559
|
# everything else is stale and gets removed during reconciliation
|
|
2464
2560
|
context.used_keys.clear()
|
|
2561
|
+
context.fast_stale_effect_keys = None
|
|
2562
|
+
context.fast_stale_effects_cleaned.clear()
|
|
2465
2563
|
|
|
2466
2564
|
el = element
|
|
2467
2565
|
key = el._key
|
|
@@ -2494,7 +2592,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2494
2592
|
del context.children_next[key]
|
|
2495
2593
|
# the element arguments are part of this component's element tree
|
|
2496
2594
|
if el.kwargs:
|
|
2497
|
-
self.
|
|
2595
|
+
self._render_arguments(el, key, parent_key)
|
|
2498
2596
|
return
|
|
2499
2597
|
|
|
2500
2598
|
assert isinstance(el.component, ComponentFunction)
|
|
@@ -2502,7 +2600,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2502
2600
|
# arguments of a shared element belong to the context it is rendered in;
|
|
2503
2601
|
# for non-shared component elements the component function decides
|
|
2504
2602
|
# what ends up in the tree
|
|
2505
|
-
self.
|
|
2603
|
+
self._render_arguments(el, key, parent_key)
|
|
2506
2604
|
|
|
2507
2605
|
context_previous = context.children_next.get(key)
|
|
2508
2606
|
if context_previous is None:
|
|
@@ -2512,10 +2610,12 @@ class _RenderContextFast(_RenderContext):
|
|
|
2512
2610
|
not self._walk_all
|
|
2513
2611
|
and el is el_prev
|
|
2514
2612
|
and not el.is_shared
|
|
2613
|
+
and not self._replacing
|
|
2515
2614
|
and context_previous is not None
|
|
2516
2615
|
and context.children.get(key) is context_previous
|
|
2517
2616
|
and not context_previous.needs_render
|
|
2518
2617
|
and not context_previous.needs_render_descendant
|
|
2618
|
+
and not self._has_effect_work(context_previous)
|
|
2519
2619
|
and not context_previous.exceptions_self
|
|
2520
2620
|
and not context_previous.exceptions_children
|
|
2521
2621
|
and context_previous.root_element is not None
|
|
@@ -2697,7 +2797,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2697
2797
|
|
|
2698
2798
|
if el_prev and isinstance(el_prev.component, ComponentWidget):
|
|
2699
2799
|
# a widget element was replaced by a component element at this key
|
|
2700
|
-
self.
|
|
2800
|
+
self._remove_outgoing(el_prev, key, parent_key=parent_key)
|
|
2701
2801
|
new_parent_key = join_key(parent_key, key)
|
|
2702
2802
|
try:
|
|
2703
2803
|
if el.is_shared and (el.args or el.kwargs):
|
|
@@ -2733,7 +2833,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2733
2833
|
logger.info("elements to be removed: %r", stale_keys)
|
|
2734
2834
|
for stale_key in stale_keys:
|
|
2735
2835
|
if stale_key in child_context.elements:
|
|
2736
|
-
self.
|
|
2836
|
+
self._remove_outgoing(child_context.elements[stale_key], stale_key, new_parent_key)
|
|
2737
2837
|
|
|
2738
2838
|
self._process_effects(child_context, context)
|
|
2739
2839
|
|
|
@@ -2790,20 +2890,22 @@ class _RenderContextFast(_RenderContext):
|
|
|
2790
2890
|
# update the existing widget in place
|
|
2791
2891
|
kwargs = self._visit_children_values(el.kwargs, key, parent_key, self._reconsolidate)
|
|
2792
2892
|
if not context.exceptions_children:
|
|
2793
|
-
|
|
2893
|
+
# the same element whose kwargs hold no elements: nothing can have changed.
|
|
2894
|
+
# With elements (a container), the kwargs resolve to widgets, so compare
|
|
2895
|
+
# them with what the widget holds: equal means setting them is a no-op
|
|
2896
|
+
# (and a value changed from the frontend is still set back)
|
|
2897
|
+
if el is not el_prev or not (_values_identical(kwargs, el.kwargs) or _widget_holds_values(widget_previous, kwargs)):
|
|
2794
2898
|
try:
|
|
2795
2899
|
el._update_widget(widget_previous, el_prev, kwargs)
|
|
2796
2900
|
except BaseException as e:
|
|
2797
2901
|
context.exceptions_self.append(e)
|
|
2798
2902
|
self._set_rerender_needed("Exception ocurred during reconciliation (updating widget)")
|
|
2799
2903
|
_mark_needs_render_ancestors(context)
|
|
2800
|
-
# else: identical element and all children reconciled to the
|
|
2801
|
-
# same widgets, nothing can have changed
|
|
2802
2904
|
self._store_widget(context, el, key, widget_previous)
|
|
2803
2905
|
else:
|
|
2804
2906
|
assert el_prev is not None, "widget_previous is not None, but el_prev is"
|
|
2805
2907
|
# a different widget type at the same key: replace
|
|
2806
|
-
self.
|
|
2908
|
+
self._remove_outgoing(el_prev, key, parent_key=parent_key)
|
|
2807
2909
|
kwargs = self._visit_children_values(el.kwargs, key, parent_key, self._reconsolidate)
|
|
2808
2910
|
widget = None
|
|
2809
2911
|
if not context.exceptions_children:
|
|
@@ -2858,6 +2960,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2858
2960
|
self._shared_elements.add(el)
|
|
2859
2961
|
assert el in self._shared_elements_next
|
|
2860
2962
|
self._shared_elements_next.remove(el)
|
|
2963
|
+
self._cleanup_stale_effects_for_context(context, parent_key)
|
|
2861
2964
|
|
|
2862
2965
|
def _process_effects(self, child_context: "ComponentContext", context: "ComponentContext"):
|
|
2863
2966
|
# NOTE: effect/cleanup exceptions are recorded on the context of the
|
|
@@ -2876,6 +2979,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2876
2979
|
try:
|
|
2877
2980
|
effect.cleanup()
|
|
2878
2981
|
except BaseException as e:
|
|
2982
|
+
logger.exception("Effect cleanup %r raised exception %r", effect.callable, e)
|
|
2879
2983
|
context.exceptions_self.append(e)
|
|
2880
2984
|
self._set_rerender_needed("Exception ocurred during effect")
|
|
2881
2985
|
_mark_needs_render_ancestors(context)
|
|
@@ -2887,6 +2991,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2887
2991
|
try:
|
|
2888
2992
|
effect()
|
|
2889
2993
|
except BaseException as e:
|
|
2994
|
+
logger.exception("Effect %r raised exception %r", effect.callable, e)
|
|
2890
2995
|
context.exceptions_self.append(e)
|
|
2891
2996
|
self._set_rerender_needed("Exception ocurred during effect")
|
|
2892
2997
|
_mark_needs_render_ancestors(context)
|
|
@@ -2898,6 +3003,74 @@ class _RenderContextFast(_RenderContext):
|
|
|
2898
3003
|
else:
|
|
2899
3004
|
context.widgets[key] = widget
|
|
2900
3005
|
|
|
3006
|
+
def _cleanup_stale_effects_for_context(self, context: "ComponentContext", parent_key: str):
|
|
3007
|
+
if context.fast_stale_effect_keys is None:
|
|
3008
|
+
# reversed, so we can pop from the end and still go in sorted order
|
|
3009
|
+
context.fast_stale_effect_keys = sorted(set(context.elements) - context.used_keys, reverse=True)
|
|
3010
|
+
context_prev = self.context
|
|
3011
|
+
try:
|
|
3012
|
+
while context.fast_stale_effect_keys:
|
|
3013
|
+
stale_key = context.fast_stale_effect_keys.pop()
|
|
3014
|
+
if stale_key not in context.elements or stale_key in context.fast_stale_effects_cleaned:
|
|
3015
|
+
continue
|
|
3016
|
+
self.context = context
|
|
3017
|
+
el = context.elements[stale_key]
|
|
3018
|
+
# keyed children that the new tree still uses stay, with their effects
|
|
3019
|
+
# (see _keep_keyed_child)
|
|
3020
|
+
outgoing = self._outgoing
|
|
3021
|
+
self._outgoing = (context, el)
|
|
3022
|
+
try:
|
|
3023
|
+
self._cleanup_stale_effects(el, stale_key, parent_key)
|
|
3024
|
+
finally:
|
|
3025
|
+
self._outgoing = outgoing
|
|
3026
|
+
finally:
|
|
3027
|
+
self.context = context_prev
|
|
3028
|
+
|
|
3029
|
+
def _cleanup_stale_effects(self, el: Element, default_key: str, parent_key: str):
|
|
3030
|
+
key = el._key
|
|
3031
|
+
if key is None:
|
|
3032
|
+
key = default_key
|
|
3033
|
+
assert key is not None
|
|
3034
|
+
context = self.context
|
|
3035
|
+
assert context is not None
|
|
3036
|
+
if key in context.fast_stale_effects_cleaned:
|
|
3037
|
+
return
|
|
3038
|
+
if self._keep_keyed_child(context, el, key):
|
|
3039
|
+
return
|
|
3040
|
+
if el.is_shared and (el in self._shared_elements_next or el not in self._shared_elements):
|
|
3041
|
+
return
|
|
3042
|
+
context.fast_stale_effects_cleaned.add(key)
|
|
3043
|
+
|
|
3044
|
+
if isinstance(el.component, ComponentFunction):
|
|
3045
|
+
if el.is_shared:
|
|
3046
|
+
self._visit_children(el, key, parent_key, self._cleanup_stale_effects)
|
|
3047
|
+
child_context = context.children.get(key)
|
|
3048
|
+
if child_context is None:
|
|
3049
|
+
return
|
|
3050
|
+
try:
|
|
3051
|
+
self.context = child_context
|
|
3052
|
+
child_context.exceptions_self = []
|
|
3053
|
+
child_context.exceptions_children = []
|
|
3054
|
+
for effect in child_context.effects:
|
|
3055
|
+
try:
|
|
3056
|
+
if not effect._cleaned_up:
|
|
3057
|
+
effect.cleanup()
|
|
3058
|
+
except BaseException as e:
|
|
3059
|
+
effect._cleaned_up = True
|
|
3060
|
+
logger.exception("Effect cleanup %r raised exception %r", effect.callable, e)
|
|
3061
|
+
child_context.exceptions_self.append(e)
|
|
3062
|
+
self._set_rerender_needed("Exception ocurred during effect")
|
|
3063
|
+
_mark_needs_render_ancestors(child_context)
|
|
3064
|
+
assert child_context.root_element is not None
|
|
3065
|
+
self._cleanup_stale_effects(child_context.root_element, "/", parent_key=join_key(parent_key, key))
|
|
3066
|
+
finally:
|
|
3067
|
+
self.context = context
|
|
3068
|
+
if child_context.exceptions_self or child_context.exceptions_children and not child_context.exception_handler:
|
|
3069
|
+
context.exceptions_children.extend(child_context.exceptions_self)
|
|
3070
|
+
context.exceptions_children.extend(child_context.exceptions_children)
|
|
3071
|
+
else:
|
|
3072
|
+
self._visit_children(el, key, parent_key, self._cleanup_stale_effects)
|
|
3073
|
+
|
|
2901
3074
|
def _remove_element(self, el: Element, default_key: str, parent_key):
|
|
2902
3075
|
key = el._key
|
|
2903
3076
|
if key is None:
|
|
@@ -2906,6 +3079,9 @@ class _RenderContextFast(_RenderContext):
|
|
|
2906
3079
|
context = self.context
|
|
2907
3080
|
assert context is not None
|
|
2908
3081
|
|
|
3082
|
+
if self._keep_keyed_child(context, el, key):
|
|
3083
|
+
return
|
|
3084
|
+
|
|
2909
3085
|
if el.is_shared:
|
|
2910
3086
|
if el not in self._shared_elements:
|
|
2911
3087
|
# another use of this element keeps it alive, or it was already removed
|
|
@@ -2930,6 +3106,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2930
3106
|
if not effect._cleaned_up:
|
|
2931
3107
|
effect.cleanup()
|
|
2932
3108
|
except BaseException as e:
|
|
3109
|
+
logger.exception("Effect cleanup %r raised exception %r", effect.callable, e)
|
|
2933
3110
|
child_context.exceptions_self.append(e)
|
|
2934
3111
|
self._set_rerender_needed("Exception ocurred during effect")
|
|
2935
3112
|
_mark_needs_render_ancestors(child_context)
|
|
@@ -3001,18 +3178,31 @@ class _RenderContextFast(_RenderContext):
|
|
|
3001
3178
|
else:
|
|
3002
3179
|
return value
|
|
3003
3180
|
|
|
3181
|
+
def _render_arguments(self, el: Element, key: str, parent_key: str):
|
|
3182
|
+
"""Render the elements in the arguments of a widget element or a shared element."""
|
|
3183
|
+
assert self.context is not None
|
|
3184
|
+
el_reconciled = self.context.elements.get(key)
|
|
3185
|
+
replaced = el_reconciled is not None and el_reconciled.component != el.component
|
|
3186
|
+
if replaced:
|
|
3187
|
+
# a different component at this key: reconciliation removes the old element and
|
|
3188
|
+
# everything below it, so nothing below the new one may be skipped as clean
|
|
3189
|
+
self._replacing += 1
|
|
3190
|
+
try:
|
|
3191
|
+
self._visit_children(el, key, parent_key, self._render)
|
|
3192
|
+
finally:
|
|
3193
|
+
if replaced:
|
|
3194
|
+
self._replacing -= 1
|
|
3195
|
+
|
|
3004
3196
|
def _remove_stale_root_elements(self, parent_key):
|
|
3005
3197
|
# remove stale elements of the root context itself
|
|
3006
3198
|
# (child contexts are swept during their reconciliation)
|
|
3007
3199
|
stale_keys = sorted(set(self.context_root.elements) - self.context_root.used_keys)
|
|
3008
3200
|
for stale_key in stale_keys:
|
|
3009
3201
|
if stale_key in self.context_root.elements:
|
|
3010
|
-
self.
|
|
3202
|
+
self._remove_outgoing(self.context_root.elements[stale_key], stale_key, parent_key)
|
|
3011
3203
|
|
|
3012
3204
|
|
|
3013
3205
|
def _render_context_class():
|
|
3014
|
-
import os
|
|
3015
|
-
|
|
3016
3206
|
return _RenderContextFast if os.environ.get("REACTON_FAST") == "1" else _RenderContext
|
|
3017
3207
|
|
|
3018
3208
|
|