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.
Files changed (34) hide show
  1. {reacton-1.10.3 → reacton-1.10.5}/PKG-INFO +1 -1
  2. reacton-1.10.5/reacton/_version.py +1 -0
  3. {reacton-1.10.3 → reacton-1.10.5}/reacton/core.py +209 -19
  4. {reacton-1.10.3 → reacton-1.10.5}/reacton/core_test.py +858 -1
  5. reacton-1.10.5/reacton/fuzz_test.py +213 -0
  6. {reacton-1.10.3 → reacton-1.10.5}/reacton/ipyvue.py +1 -0
  7. reacton-1.10.3/reacton/_version.py +0 -1
  8. {reacton-1.10.3 → reacton-1.10.5}/.gitignore +0 -0
  9. {reacton-1.10.3 → reacton-1.10.5}/LICENSE +0 -0
  10. {reacton-1.10.3 → reacton-1.10.5}/pyproject.toml +0 -0
  11. {reacton-1.10.3 → reacton-1.10.5}/react_ipywidgets/__init__.py +0 -0
  12. {reacton-1.10.3 → reacton-1.10.5}/react_ipywidgets/core.py +0 -0
  13. {reacton-1.10.3 → reacton-1.10.5}/reacton/__init__.py +0 -0
  14. {reacton-1.10.3 → reacton-1.10.5}/reacton/bqplot.py +0 -0
  15. {reacton-1.10.3 → reacton-1.10.5}/reacton/deprecated/__init__.py +0 -0
  16. {reacton-1.10.3 → reacton-1.10.5}/reacton/deprecated/ipyvuetify.py +0 -0
  17. {reacton-1.10.3 → reacton-1.10.5}/reacton/find.py +0 -0
  18. {reacton-1.10.3 → reacton-1.10.5}/reacton/find_test.py +0 -0
  19. {reacton-1.10.3 → reacton-1.10.5}/reacton/generate.py +0 -0
  20. {reacton-1.10.3 → reacton-1.10.5}/reacton/generate_test.py +0 -0
  21. {reacton-1.10.3 → reacton-1.10.5}/reacton/ipycanvas.py +0 -0
  22. {reacton-1.10.3 → reacton-1.10.5}/reacton/ipyvue_test.py +0 -0
  23. {reacton-1.10.3 → reacton-1.10.5}/reacton/ipyvuetify.py +0 -0
  24. {reacton-1.10.3 → reacton-1.10.5}/reacton/ipywidgets.py +0 -0
  25. {reacton-1.10.3 → reacton-1.10.5}/reacton/logging.py +0 -0
  26. {reacton-1.10.3 → reacton-1.10.5}/reacton/patch.py +0 -0
  27. {reacton-1.10.3 → reacton-1.10.5}/reacton/patch_display.py +0 -0
  28. {reacton-1.10.3 → reacton-1.10.5}/reacton/py.typed +0 -0
  29. {reacton-1.10.3 → reacton-1.10.5}/reacton/rx.py +0 -0
  30. {reacton-1.10.3 → reacton-1.10.5}/reacton/test.vue +0 -0
  31. {reacton-1.10.3 → reacton-1.10.5}/reacton/test_rx.py +0 -0
  32. {reacton-1.10.3 → reacton-1.10.5}/reacton/utils.py +0 -0
  33. {reacton-1.10.3 → reacton-1.10.5}/reacton/utils_test.py +0 -0
  34. {reacton-1.10.3 → reacton-1.10.5}/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.5
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.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 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,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
- self._rerender_needed_reasons: List[RerenderReason] = []
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
- msg += f"Previous reasons: {format(self._rerender_needed_reasons[-2])}\n"
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._remove_element(el_prev, default_key=key, parent_key=parent_key)
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._remove_element(el_remove, key_remove, parent_key)
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._remove_element(el_prev, key, parent_key=parent_key)
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._remove_element(self.context.elements[key], key, parent_key=parent_key)
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._visit_children(el, key, parent_key, self._render)
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._visit_children(el, key, parent_key, self._render)
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._remove_element(el_prev, default_key=key, parent_key=parent_key)
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._remove_element(child_context.elements[stale_key], stale_key, new_parent_key)
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
- if el is not el_prev or not _values_identical(kwargs, el.kwargs):
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._remove_element(el_prev, key, parent_key=parent_key)
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._remove_element(self.context_root.elements[stale_key], stale_key, parent_key)
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