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.
Files changed (34) hide show
  1. {reacton-1.10.3 → reacton-1.10.4}/PKG-INFO +1 -1
  2. reacton-1.10.4/reacton/_version.py +1 -0
  3. {reacton-1.10.3 → reacton-1.10.4}/reacton/core.py +194 -15
  4. {reacton-1.10.3 → reacton-1.10.4}/reacton/core_test.py +799 -1
  5. reacton-1.10.4/reacton/fuzz_test.py +182 -0
  6. {reacton-1.10.3 → reacton-1.10.4}/reacton/ipyvue.py +1 -0
  7. reacton-1.10.3/reacton/_version.py +0 -1
  8. {reacton-1.10.3 → reacton-1.10.4}/.gitignore +0 -0
  9. {reacton-1.10.3 → reacton-1.10.4}/LICENSE +0 -0
  10. {reacton-1.10.3 → reacton-1.10.4}/pyproject.toml +0 -0
  11. {reacton-1.10.3 → reacton-1.10.4}/react_ipywidgets/__init__.py +0 -0
  12. {reacton-1.10.3 → reacton-1.10.4}/react_ipywidgets/core.py +0 -0
  13. {reacton-1.10.3 → reacton-1.10.4}/reacton/__init__.py +0 -0
  14. {reacton-1.10.3 → reacton-1.10.4}/reacton/bqplot.py +0 -0
  15. {reacton-1.10.3 → reacton-1.10.4}/reacton/deprecated/__init__.py +0 -0
  16. {reacton-1.10.3 → reacton-1.10.4}/reacton/deprecated/ipyvuetify.py +0 -0
  17. {reacton-1.10.3 → reacton-1.10.4}/reacton/find.py +0 -0
  18. {reacton-1.10.3 → reacton-1.10.4}/reacton/find_test.py +0 -0
  19. {reacton-1.10.3 → reacton-1.10.4}/reacton/generate.py +0 -0
  20. {reacton-1.10.3 → reacton-1.10.4}/reacton/generate_test.py +0 -0
  21. {reacton-1.10.3 → reacton-1.10.4}/reacton/ipycanvas.py +0 -0
  22. {reacton-1.10.3 → reacton-1.10.4}/reacton/ipyvue_test.py +0 -0
  23. {reacton-1.10.3 → reacton-1.10.4}/reacton/ipyvuetify.py +0 -0
  24. {reacton-1.10.3 → reacton-1.10.4}/reacton/ipywidgets.py +0 -0
  25. {reacton-1.10.3 → reacton-1.10.4}/reacton/logging.py +0 -0
  26. {reacton-1.10.3 → reacton-1.10.4}/reacton/patch.py +0 -0
  27. {reacton-1.10.3 → reacton-1.10.4}/reacton/patch_display.py +0 -0
  28. {reacton-1.10.3 → reacton-1.10.4}/reacton/py.typed +0 -0
  29. {reacton-1.10.3 → reacton-1.10.4}/reacton/rx.py +0 -0
  30. {reacton-1.10.3 → reacton-1.10.4}/reacton/test.vue +0 -0
  31. {reacton-1.10.3 → reacton-1.10.4}/reacton/test_rx.py +0 -0
  32. {reacton-1.10.3 → reacton-1.10.4}/reacton/utils.py +0 -0
  33. {reacton-1.10.3 → reacton-1.10.4}/reacton/utils_test.py +0 -0
  34. {reacton-1.10.3 → reacton-1.10.4}/reacton/work.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: reacton
3
- Version: 1.10.3
3
+ Version: 1.10.4
4
4
  Dynamic: Summary
5
5
  Project-URL: Home, https://www.github.com/widgetti/reacton
6
6
  Project-URL: Documentation, https://reacton.solara.dev
@@ -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 and not parent.needs_render_descendant:
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
- self._rerender_needed_reasons: List[RerenderReason] = []
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
- msg += f"Previous reasons: {format(self._rerender_needed_reasons[-2])}\n"
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._remove_element(el_prev, default_key=key, parent_key=parent_key)
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._remove_element(el_prev, key, parent_key=parent_key)
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._visit_children(el, key, parent_key, self._render)
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._visit_children(el, key, parent_key, self._render)
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._remove_element(el_prev, default_key=key, parent_key=parent_key)
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
- if el is not el_prev or not _values_identical(kwargs, el.kwargs):
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._remove_element(el_prev, key, parent_key=parent_key)
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