reacton 1.10.2__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.2 → reacton-1.10.4}/PKG-INFO +2 -2
- reacton-1.10.4/reacton/_version.py +1 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/core.py +271 -22
- {reacton-1.10.2 → reacton-1.10.4}/reacton/core_test.py +958 -1
- reacton-1.10.4/reacton/fuzz_test.py +182 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/ipyvue.py +1 -0
- reacton-1.10.2/reacton/_version.py +0 -1
- {reacton-1.10.2 → reacton-1.10.4}/.gitignore +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/LICENSE +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/pyproject.toml +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/react_ipywidgets/__init__.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/react_ipywidgets/core.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/__init__.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/bqplot.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/deprecated/__init__.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/deprecated/ipyvuetify.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/find.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/find_test.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/generate.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/generate_test.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/ipycanvas.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/ipyvue_test.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/ipyvuetify.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/ipywidgets.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/logging.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/patch.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/patch_display.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/py.typed +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/rx.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/test.vue +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/test_rx.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/utils.py +0 -0
- {reacton-1.10.2 → reacton-1.10.4}/reacton/utils_test.py +0 -0
- {reacton-1.10.2 → 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,
|
|
@@ -170,6 +173,26 @@ def close_widget(widget: widgets.Widget):
|
|
|
170
173
|
logger.warning("Widget %r does not have a close method, possibly a close trait was added", widget)
|
|
171
174
|
|
|
172
175
|
|
|
176
|
+
def _is_shared_ipyvue_template(widget: widgets.Widget) -> bool:
|
|
177
|
+
"""Is this widget an ipyvue Template that is shared between VueTemplate instances?
|
|
178
|
+
|
|
179
|
+
ipyvue keeps a per-file registry of Template widgets (ipyvue/Template.py), so several
|
|
180
|
+
VueTemplate widgets can point at the same Template. Such a Template outlives the
|
|
181
|
+
VueTemplate that created it, and we must not close it as an orphan.
|
|
182
|
+
ipyvue 3 only puts a Template in that registry when there is a real comm, so an
|
|
183
|
+
unregistered Template belongs to a single VueTemplate and is a normal orphan.
|
|
184
|
+
"""
|
|
185
|
+
cls = widget.__class__
|
|
186
|
+
if cls.__name__ != "Template" or cls.__module__ != "ipyvue.Template":
|
|
187
|
+
return False
|
|
188
|
+
module = sys.modules.get(cls.__module__)
|
|
189
|
+
registry = getattr(module, "template_registry", None)
|
|
190
|
+
if registry is None:
|
|
191
|
+
# unknown ipyvue version: keep the old, conservative behaviour
|
|
192
|
+
return True
|
|
193
|
+
return any(template is widget for template in registry.values())
|
|
194
|
+
|
|
195
|
+
|
|
173
196
|
def _event_handler_exception_wrapper(f):
|
|
174
197
|
"""Wrap an event handler to catch exceptions and put them in a reacton context.
|
|
175
198
|
|
|
@@ -187,6 +210,10 @@ def _event_handler_exception_wrapper(f):
|
|
|
187
210
|
# we add it to exceptions_children, not exception_self
|
|
188
211
|
# this allows a component to catch the exception of a direct child
|
|
189
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)
|
|
190
217
|
rc.force_update()
|
|
191
218
|
|
|
192
219
|
return wrapper
|
|
@@ -211,8 +238,12 @@ def same_component(c1, c2):
|
|
|
211
238
|
|
|
212
239
|
def _mark_needs_render_ancestors(context: "ComponentContext"):
|
|
213
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
|
|
214
245
|
parent = context.parent
|
|
215
|
-
while parent is not None
|
|
246
|
+
while parent is not None:
|
|
216
247
|
parent.needs_render_descendant = True
|
|
217
248
|
parent = parent.parent
|
|
218
249
|
|
|
@@ -235,6 +266,34 @@ def _values_identical(a, b):
|
|
|
235
266
|
return False
|
|
236
267
|
|
|
237
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
|
+
|
|
238
297
|
def _with_tracebacks(e, tracebacks):
|
|
239
298
|
# copy it, and we need with_traceback for unknown reasons not to cause
|
|
240
299
|
# an infinite loop
|
|
@@ -1146,6 +1205,11 @@ class ComponentContext:
|
|
|
1146
1205
|
# the render phase skipped this whole subtree (nothing changed), so the
|
|
1147
1206
|
# reconciliation phase can reuse the previous result without walking
|
|
1148
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)
|
|
1149
1213
|
|
|
1150
1214
|
# elements created in this context go there
|
|
1151
1215
|
owns: Set[Element] = field(default_factory=set)
|
|
@@ -1204,6 +1268,8 @@ def _teardown_component_context(context: ComponentContext):
|
|
|
1204
1268
|
context.exceptions_self = []
|
|
1205
1269
|
context.exceptions_children = []
|
|
1206
1270
|
context.context_managers = []
|
|
1271
|
+
context.fast_stale_effect_keys = None
|
|
1272
|
+
context.fast_stale_effects_cleaned = set()
|
|
1207
1273
|
|
|
1208
1274
|
|
|
1209
1275
|
@dataclass
|
|
@@ -1240,6 +1306,30 @@ class Effect:
|
|
|
1240
1306
|
|
|
1241
1307
|
class _RenderContext:
|
|
1242
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
|
+
)
|
|
1243
1333
|
|
|
1244
1334
|
def __init__(self, element: Element, container: widgets.Widget = None, children_trait="children", handle_error: bool = True, initial_state=None):
|
|
1245
1335
|
self.element = element
|
|
@@ -1254,7 +1344,11 @@ class _RenderContext:
|
|
|
1254
1344
|
self.last_root_widget: widgets.Widget = None
|
|
1255
1345
|
self._is_rendering = False
|
|
1256
1346
|
self._rerender_needed = False
|
|
1257
|
-
|
|
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)
|
|
1258
1352
|
self.thread_lock = threading.Lock()
|
|
1259
1353
|
self._closing = False
|
|
1260
1354
|
self.tracebacks: List[TracebackType] = []
|
|
@@ -1532,6 +1626,35 @@ class _RenderContext:
|
|
|
1532
1626
|
else:
|
|
1533
1627
|
logger.info("No render phase triggered, already rendering")
|
|
1534
1628
|
|
|
1629
|
+
def _discard_aborted_pass(self):
|
|
1630
|
+
"""Forget everything a render pass staged when it raised before reconciliation.
|
|
1631
|
+
|
|
1632
|
+
The *_next bookkeeping and the chained effects (previous_effect.next) of every
|
|
1633
|
+
context belong to that pass. Mark every context dirty, so the next render()
|
|
1634
|
+
re-executes the components and re-queues their effects from a consistent state.
|
|
1635
|
+
Only for aborts in the render phase: a reconciliation that raised halfway already
|
|
1636
|
+
created widgets, and those need the normal removal path.
|
|
1637
|
+
"""
|
|
1638
|
+
# children_next is kept: it also holds contexts pre-created by state_set() (restored
|
|
1639
|
+
# state), and a context created by the aborted pass owns no widgets yet, it is simply
|
|
1640
|
+
# reused or pruned by the next render
|
|
1641
|
+
contexts: List[ComponentContext] = [self.context_root]
|
|
1642
|
+
while contexts:
|
|
1643
|
+
context = contexts.pop()
|
|
1644
|
+
for effect in context.effects:
|
|
1645
|
+
effect.next = None
|
|
1646
|
+
context.root_element_next = None
|
|
1647
|
+
context.elements_next = {}
|
|
1648
|
+
context.exceptions_self = []
|
|
1649
|
+
context.exceptions_children = []
|
|
1650
|
+
context.needs_render = True
|
|
1651
|
+
context.needs_render_descendant = True
|
|
1652
|
+
context.clean_subtree = False
|
|
1653
|
+
contexts.extend(context.children.values())
|
|
1654
|
+
contexts.extend(context.children_next.values())
|
|
1655
|
+
self._shared_elements_next = set()
|
|
1656
|
+
self.context = self.context_root
|
|
1657
|
+
|
|
1535
1658
|
def render(self, element: Element, container: widgets.Widget = None):
|
|
1536
1659
|
# render + consolidate
|
|
1537
1660
|
widget = None
|
|
@@ -1558,6 +1681,8 @@ class _RenderContext:
|
|
|
1558
1681
|
logger.info("Render requested on a closing/closed render context, ignoring")
|
|
1559
1682
|
return container
|
|
1560
1683
|
prev_rc = getattr(local, "rc", None)
|
|
1684
|
+
# an exception that escapes while this is True aborted a render pass (see the except below)
|
|
1685
|
+
in_render_phase = True
|
|
1561
1686
|
try:
|
|
1562
1687
|
local.rc = self
|
|
1563
1688
|
self.element = element
|
|
@@ -1603,10 +1728,10 @@ class _RenderContext:
|
|
|
1603
1728
|
f += f"Triggered at: {''.join(reason.trigger_stack)}\n"
|
|
1604
1729
|
return f
|
|
1605
1730
|
|
|
1606
|
-
self._rerender_needed_reasons[-1]
|
|
1607
1731
|
msg = f"Too many renders triggered, your render loop does not stop\nLast reason: {format(self._rerender_needed_reasons[-1])}\n"
|
|
1608
1732
|
if len(self._rerender_needed_reasons) >= 2:
|
|
1609
|
-
|
|
1733
|
+
previous = reversed(list(self._rerender_needed_reasons)[:-1])
|
|
1734
|
+
msg += f"Previous reasons: {''.join(format(reason) for reason in previous)}\n"
|
|
1610
1735
|
raise RuntimeError(msg)
|
|
1611
1736
|
logger.info("Entering nested render phase: %r", self._rerender_needed_reasons[-1])
|
|
1612
1737
|
self._rerender_needed = False
|
|
@@ -1633,10 +1758,12 @@ class _RenderContext:
|
|
|
1633
1758
|
|
|
1634
1759
|
logger.info("Render reconsolidate...")
|
|
1635
1760
|
self.reconsolidating = True
|
|
1761
|
+
in_render_phase = False
|
|
1636
1762
|
try:
|
|
1637
1763
|
widget = self._reconsolidate(self.element, default_key="/", parent_key=ROOT_KEY)
|
|
1638
1764
|
finally:
|
|
1639
1765
|
self.reconsolidating = False
|
|
1766
|
+
in_render_phase = True
|
|
1640
1767
|
logger.info("Render reconsolidate done")
|
|
1641
1768
|
self.context.root_element = self.context.root_element_next
|
|
1642
1769
|
self.context.root_element_next = None
|
|
@@ -1685,7 +1812,16 @@ class _RenderContext:
|
|
|
1685
1812
|
self._is_rendering = False
|
|
1686
1813
|
self.context = context_prev
|
|
1687
1814
|
logger.info("Done with render phase: %r", render_count)
|
|
1688
|
-
except
|
|
1815
|
+
except BaseException as e:
|
|
1816
|
+
# Exceptions raised by components are collected in exceptions_self, so an
|
|
1817
|
+
# exception here comes from the render machinery itself (duplicate key,
|
|
1818
|
+
# hook misuse, ...) or is a cancellation, and aborted a pass halfway. Drop
|
|
1819
|
+
# what that pass staged: otherwise the next render reconciles the last
|
|
1820
|
+
# committed elements, but runs the effects the aborted pass chained, closed
|
|
1821
|
+
# over elements that were never reconciled (get_widget then fails with
|
|
1822
|
+
# "found in a previous render").
|
|
1823
|
+
if in_render_phase:
|
|
1824
|
+
self._discard_aborted_pass()
|
|
1689
1825
|
if DEBUG:
|
|
1690
1826
|
# construct a fake traceback (showing how the elements were constructed)
|
|
1691
1827
|
if not self.tracebacks:
|
|
@@ -1698,6 +1834,10 @@ class _RenderContext:
|
|
|
1698
1834
|
finally:
|
|
1699
1835
|
local.rc = prev_rc # type: ignore
|
|
1700
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
|
|
1701
1841
|
assert self.context is self.context_root
|
|
1702
1842
|
|
|
1703
1843
|
exceptions = [*self.context.exceptions_children, *self.context_root.exceptions_self]
|
|
@@ -1825,8 +1965,13 @@ class _RenderContext:
|
|
|
1825
1965
|
needs_render = context.needs_render
|
|
1826
1966
|
if not needs_render:
|
|
1827
1967
|
if el_prev is not None and context_previous is context:
|
|
1828
|
-
|
|
1829
|
-
|
|
1968
|
+
if isinstance(el_prev.component, ComponentWidget):
|
|
1969
|
+
# an earlier pass of this render() call put a widget element at this
|
|
1970
|
+
# slot (elements_next), while the context is from the last reconciled
|
|
1971
|
+
# render: nothing to compare the arguments against, so render
|
|
1972
|
+
needs_render = True
|
|
1973
|
+
else:
|
|
1974
|
+
needs_render = el._arguments_changed(el_prev)
|
|
1830
1975
|
if context.exceptions_children:
|
|
1831
1976
|
# we have exceptions, so we need to render
|
|
1832
1977
|
needs_render = True
|
|
@@ -1973,7 +2118,7 @@ class _RenderContext:
|
|
|
1973
2118
|
try:
|
|
1974
2119
|
if isinstance(el.component, ComponentFunction):
|
|
1975
2120
|
if el_prev and isinstance(el_prev.component, ComponentWidget):
|
|
1976
|
-
self.
|
|
2121
|
+
self._remove_replaced(el_prev, key, parent_key=parent_key)
|
|
1977
2122
|
new_parent_key = join_key(parent_key, key)
|
|
1978
2123
|
try:
|
|
1979
2124
|
# TODO: test suite passes when this block if commented out
|
|
@@ -2034,6 +2179,7 @@ class _RenderContext:
|
|
|
2034
2179
|
try:
|
|
2035
2180
|
effect.cleanup()
|
|
2036
2181
|
except BaseException as e:
|
|
2182
|
+
logger.exception("Effect cleanup %r raised exception %r", effect.callable, e)
|
|
2037
2183
|
context.exceptions_self.append(e)
|
|
2038
2184
|
self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during effect"))
|
|
2039
2185
|
self._rerender_needed = True
|
|
@@ -2045,6 +2191,7 @@ class _RenderContext:
|
|
|
2045
2191
|
continue
|
|
2046
2192
|
effect()
|
|
2047
2193
|
except BaseException as e:
|
|
2194
|
+
logger.exception("Effect %r raised exception %r", effect.callable, e)
|
|
2048
2195
|
context.exceptions_self.append(e)
|
|
2049
2196
|
self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during effect"))
|
|
2050
2197
|
self._rerender_needed = True
|
|
@@ -2056,6 +2203,7 @@ class _RenderContext:
|
|
|
2056
2203
|
continue
|
|
2057
2204
|
effect()
|
|
2058
2205
|
except BaseException as e:
|
|
2206
|
+
logger.exception("Effect %r raised exception %r", effect.callable, e)
|
|
2059
2207
|
context.exceptions_self.append(e)
|
|
2060
2208
|
self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during effect"))
|
|
2061
2209
|
self._rerender_needed = True
|
|
@@ -2147,7 +2295,7 @@ class _RenderContext:
|
|
|
2147
2295
|
else:
|
|
2148
2296
|
assert el_prev is not None, "widget_previous is not None, but el_prev is"
|
|
2149
2297
|
logger.debug("Replacing widget: %r → %r %r", el_prev, el, key)
|
|
2150
|
-
self.
|
|
2298
|
+
self._remove_replaced(el_prev, key, parent_key=parent_key)
|
|
2151
2299
|
kwargs = reconsolidate_children()
|
|
2152
2300
|
widget = None
|
|
2153
2301
|
if not context.exceptions_children:
|
|
@@ -2166,7 +2314,7 @@ class _RenderContext:
|
|
|
2166
2314
|
if orphan_ids:
|
|
2167
2315
|
for orphan_widget in orphan_widgets:
|
|
2168
2316
|
# these are shared widgets
|
|
2169
|
-
if orphan_widget
|
|
2317
|
+
if _is_shared_ipyvue_template(orphan_widget):
|
|
2170
2318
|
orphan_ids -= {orphan_widget.model_id}
|
|
2171
2319
|
if el.is_shared:
|
|
2172
2320
|
widget = self._shared_widgets[el]
|
|
@@ -2243,6 +2391,9 @@ class _RenderContext:
|
|
|
2243
2391
|
context = self.context
|
|
2244
2392
|
logger.debug("Remove: (%s, %s) %r", parent_key, key, el)
|
|
2245
2393
|
|
|
2394
|
+
if self._keep_keyed_child(context, el, key):
|
|
2395
|
+
return
|
|
2396
|
+
|
|
2246
2397
|
if el.is_shared:
|
|
2247
2398
|
if el not in self._shared_elements:
|
|
2248
2399
|
return
|
|
@@ -2269,6 +2420,7 @@ class _RenderContext:
|
|
|
2269
2420
|
if not effect._cleaned_up:
|
|
2270
2421
|
effect.cleanup()
|
|
2271
2422
|
except BaseException as e:
|
|
2423
|
+
logger.exception("Effect cleanup %r raised exception %r", effect.callable, e)
|
|
2272
2424
|
child_context.exceptions_self.append(e)
|
|
2273
2425
|
self._rerender_needed_reasons.append(RerenderReason(reason="Exception ocurred during effect"))
|
|
2274
2426
|
self._rerender_needed = True
|
|
@@ -2379,10 +2531,20 @@ class _RenderContextFast(_RenderContext):
|
|
|
2379
2531
|
# without walking either.
|
|
2380
2532
|
# ------------------------------------------------------------------
|
|
2381
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
|
+
|
|
2382
2538
|
def _set_rerender_needed(self, reason: str):
|
|
2383
2539
|
self._rerender_needed_reasons.append(RerenderReason(reason=reason))
|
|
2384
2540
|
self._rerender_needed = True
|
|
2385
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
|
+
|
|
2386
2548
|
def _render(self, element: Element, default_key: str, parent_key: str):
|
|
2387
2549
|
if not isinstance(element, Element):
|
|
2388
2550
|
raise TypeError(f"Expected element, not {element}")
|
|
@@ -2395,6 +2557,8 @@ class _RenderContextFast(_RenderContext):
|
|
|
2395
2557
|
# the root element of a component determines which keys are in use,
|
|
2396
2558
|
# everything else is stale and gets removed during reconciliation
|
|
2397
2559
|
context.used_keys.clear()
|
|
2560
|
+
context.fast_stale_effect_keys = None
|
|
2561
|
+
context.fast_stale_effects_cleaned.clear()
|
|
2398
2562
|
|
|
2399
2563
|
el = element
|
|
2400
2564
|
key = el._key
|
|
@@ -2427,7 +2591,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2427
2591
|
del context.children_next[key]
|
|
2428
2592
|
# the element arguments are part of this component's element tree
|
|
2429
2593
|
if el.kwargs:
|
|
2430
|
-
self.
|
|
2594
|
+
self._render_arguments(el, key, parent_key)
|
|
2431
2595
|
return
|
|
2432
2596
|
|
|
2433
2597
|
assert isinstance(el.component, ComponentFunction)
|
|
@@ -2435,7 +2599,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2435
2599
|
# arguments of a shared element belong to the context it is rendered in;
|
|
2436
2600
|
# for non-shared component elements the component function decides
|
|
2437
2601
|
# what ends up in the tree
|
|
2438
|
-
self.
|
|
2602
|
+
self._render_arguments(el, key, parent_key)
|
|
2439
2603
|
|
|
2440
2604
|
context_previous = context.children_next.get(key)
|
|
2441
2605
|
if context_previous is None:
|
|
@@ -2445,10 +2609,12 @@ class _RenderContextFast(_RenderContext):
|
|
|
2445
2609
|
not self._walk_all
|
|
2446
2610
|
and el is el_prev
|
|
2447
2611
|
and not el.is_shared
|
|
2612
|
+
and not self._replacing
|
|
2448
2613
|
and context_previous is not None
|
|
2449
2614
|
and context.children.get(key) is context_previous
|
|
2450
2615
|
and not context_previous.needs_render
|
|
2451
2616
|
and not context_previous.needs_render_descendant
|
|
2617
|
+
and not self._has_effect_work(context_previous)
|
|
2452
2618
|
and not context_previous.exceptions_self
|
|
2453
2619
|
and not context_previous.exceptions_children
|
|
2454
2620
|
and context_previous.root_element is not None
|
|
@@ -2485,8 +2651,11 @@ class _RenderContextFast(_RenderContext):
|
|
|
2485
2651
|
|
|
2486
2652
|
needs_render = context.needs_render
|
|
2487
2653
|
if not needs_render and el_prev is not None and context_previous is context:
|
|
2488
|
-
|
|
2489
|
-
|
|
2654
|
+
if isinstance(el_prev.component, ComponentWidget):
|
|
2655
|
+
# see the classic renderer: a widget element from an earlier pass of this call
|
|
2656
|
+
needs_render = True
|
|
2657
|
+
else:
|
|
2658
|
+
needs_render = el._arguments_changed(el_prev)
|
|
2490
2659
|
if not needs_render and context.exceptions_children:
|
|
2491
2660
|
# we have exceptions, so we need to render
|
|
2492
2661
|
needs_render = True
|
|
@@ -2627,7 +2796,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2627
2796
|
|
|
2628
2797
|
if el_prev and isinstance(el_prev.component, ComponentWidget):
|
|
2629
2798
|
# a widget element was replaced by a component element at this key
|
|
2630
|
-
self.
|
|
2799
|
+
self._remove_replaced(el_prev, key, parent_key=parent_key)
|
|
2631
2800
|
new_parent_key = join_key(parent_key, key)
|
|
2632
2801
|
try:
|
|
2633
2802
|
if el.is_shared and (el.args or el.kwargs):
|
|
@@ -2720,20 +2889,22 @@ class _RenderContextFast(_RenderContext):
|
|
|
2720
2889
|
# update the existing widget in place
|
|
2721
2890
|
kwargs = self._visit_children_values(el.kwargs, key, parent_key, self._reconsolidate)
|
|
2722
2891
|
if not context.exceptions_children:
|
|
2723
|
-
|
|
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)):
|
|
2724
2897
|
try:
|
|
2725
2898
|
el._update_widget(widget_previous, el_prev, kwargs)
|
|
2726
2899
|
except BaseException as e:
|
|
2727
2900
|
context.exceptions_self.append(e)
|
|
2728
2901
|
self._set_rerender_needed("Exception ocurred during reconciliation (updating widget)")
|
|
2729
2902
|
_mark_needs_render_ancestors(context)
|
|
2730
|
-
# else: identical element and all children reconciled to the
|
|
2731
|
-
# same widgets, nothing can have changed
|
|
2732
2903
|
self._store_widget(context, el, key, widget_previous)
|
|
2733
2904
|
else:
|
|
2734
2905
|
assert el_prev is not None, "widget_previous is not None, but el_prev is"
|
|
2735
2906
|
# a different widget type at the same key: replace
|
|
2736
|
-
self.
|
|
2907
|
+
self._remove_replaced(el_prev, key, parent_key=parent_key)
|
|
2737
2908
|
kwargs = self._visit_children_values(el.kwargs, key, parent_key, self._reconsolidate)
|
|
2738
2909
|
widget = None
|
|
2739
2910
|
if not context.exceptions_children:
|
|
@@ -2750,7 +2921,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2750
2921
|
orphan_widgets = set([_get_widgets_dict()[k] for k in orphan_ids])
|
|
2751
2922
|
for orphan_widget in orphan_widgets:
|
|
2752
2923
|
# these are shared between widgets
|
|
2753
|
-
if orphan_widget
|
|
2924
|
+
if _is_shared_ipyvue_template(orphan_widget):
|
|
2754
2925
|
orphan_ids -= {orphan_widget.model_id}
|
|
2755
2926
|
widget = self._shared_widgets[el] if el.is_shared else context.widgets[key]
|
|
2756
2927
|
if widget.model_id not in self._orphans:
|
|
@@ -2788,6 +2959,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2788
2959
|
self._shared_elements.add(el)
|
|
2789
2960
|
assert el in self._shared_elements_next
|
|
2790
2961
|
self._shared_elements_next.remove(el)
|
|
2962
|
+
self._cleanup_stale_effects_for_context(context, parent_key)
|
|
2791
2963
|
|
|
2792
2964
|
def _process_effects(self, child_context: "ComponentContext", context: "ComponentContext"):
|
|
2793
2965
|
# NOTE: effect/cleanup exceptions are recorded on the context of the
|
|
@@ -2806,6 +2978,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2806
2978
|
try:
|
|
2807
2979
|
effect.cleanup()
|
|
2808
2980
|
except BaseException as e:
|
|
2981
|
+
logger.exception("Effect cleanup %r raised exception %r", effect.callable, e)
|
|
2809
2982
|
context.exceptions_self.append(e)
|
|
2810
2983
|
self._set_rerender_needed("Exception ocurred during effect")
|
|
2811
2984
|
_mark_needs_render_ancestors(context)
|
|
@@ -2817,6 +2990,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2817
2990
|
try:
|
|
2818
2991
|
effect()
|
|
2819
2992
|
except BaseException as e:
|
|
2993
|
+
logger.exception("Effect %r raised exception %r", effect.callable, e)
|
|
2820
2994
|
context.exceptions_self.append(e)
|
|
2821
2995
|
self._set_rerender_needed("Exception ocurred during effect")
|
|
2822
2996
|
_mark_needs_render_ancestors(context)
|
|
@@ -2828,6 +3002,64 @@ class _RenderContextFast(_RenderContext):
|
|
|
2828
3002
|
else:
|
|
2829
3003
|
context.widgets[key] = widget
|
|
2830
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
|
+
|
|
2831
3063
|
def _remove_element(self, el: Element, default_key: str, parent_key):
|
|
2832
3064
|
key = el._key
|
|
2833
3065
|
if key is None:
|
|
@@ -2836,6 +3068,9 @@ class _RenderContextFast(_RenderContext):
|
|
|
2836
3068
|
context = self.context
|
|
2837
3069
|
assert context is not None
|
|
2838
3070
|
|
|
3071
|
+
if self._keep_keyed_child(context, el, key):
|
|
3072
|
+
return
|
|
3073
|
+
|
|
2839
3074
|
if el.is_shared:
|
|
2840
3075
|
if el not in self._shared_elements:
|
|
2841
3076
|
# another use of this element keeps it alive, or it was already removed
|
|
@@ -2860,6 +3095,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2860
3095
|
if not effect._cleaned_up:
|
|
2861
3096
|
effect.cleanup()
|
|
2862
3097
|
except BaseException as e:
|
|
3098
|
+
logger.exception("Effect cleanup %r raised exception %r", effect.callable, e)
|
|
2863
3099
|
child_context.exceptions_self.append(e)
|
|
2864
3100
|
self._set_rerender_needed("Exception ocurred during effect")
|
|
2865
3101
|
_mark_needs_render_ancestors(child_context)
|
|
@@ -2931,6 +3167,21 @@ class _RenderContextFast(_RenderContext):
|
|
|
2931
3167
|
else:
|
|
2932
3168
|
return value
|
|
2933
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
|
+
|
|
2934
3185
|
def _remove_stale_root_elements(self, parent_key):
|
|
2935
3186
|
# remove stale elements of the root context itself
|
|
2936
3187
|
# (child contexts are swept during their reconciliation)
|
|
@@ -2941,8 +3192,6 @@ class _RenderContextFast(_RenderContext):
|
|
|
2941
3192
|
|
|
2942
3193
|
|
|
2943
3194
|
def _render_context_class():
|
|
2944
|
-
import os
|
|
2945
|
-
|
|
2946
3195
|
return _RenderContextFast if os.environ.get("REACTON_FAST") == "1" else _RenderContext
|
|
2947
3196
|
|
|
2948
3197
|
|