reacton 1.9.0__tar.gz → 1.10.0__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.9.0 → reacton-1.10.0}/PKG-INFO +2 -1
- {reacton-1.9.0 → reacton-1.10.0}/pyproject.toml +1 -0
- reacton-1.10.0/reacton/_version.py +1 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/core.py +695 -22
- {reacton-1.9.0 → reacton-1.10.0}/reacton/ipyvue.py +20 -1
- reacton-1.10.0/reacton/ipyvue_test.py +90 -0
- reacton-1.9.0/reacton/_version.py +0 -1
- {reacton-1.9.0 → reacton-1.10.0}/.gitignore +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/LICENSE +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/react_ipywidgets/__init__.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/react_ipywidgets/core.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/__init__.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/bqplot.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/core_test.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/deprecated/__init__.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/deprecated/ipyvuetify.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/find.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/find_test.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/generate.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/generate_test.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/ipycanvas.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/ipyvuetify.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/ipywidgets.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/logging.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/patch.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/patch_display.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/py.typed +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/rx.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/test.vue +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/test_rx.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/utils.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/utils_test.py +0 -0
- {reacton-1.9.0 → reacton-1.10.0}/reacton/work.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reacton
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.10.0
|
|
4
4
|
Dynamic: Summary
|
|
5
5
|
Project-URL: Home, https://www.github.com/widgetti/reacton
|
|
6
6
|
Project-URL: Documentation, https://reacton.solara.dev
|
|
@@ -61,3 +61,4 @@ Requires-Dist: black; extra == 'generate'
|
|
|
61
61
|
Requires-Dist: bqplot; extra == 'generate'
|
|
62
62
|
Requires-Dist: jinja2; extra == 'generate'
|
|
63
63
|
Requires-Dist: mypy; extra == 'generate'
|
|
64
|
+
Requires-Dist: ruff; (python_version > '3.6') and extra == 'generate'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.10.0"
|
|
@@ -80,6 +80,37 @@ def _widgets_dict_getter() -> Callable[[], Dict[str, widgets.Widget]]:
|
|
|
80
80
|
|
|
81
81
|
_get_widgets_dict = _widgets_dict_getter()
|
|
82
82
|
|
|
83
|
+
|
|
84
|
+
# Tracking widgets created as a side effect (like Layout and Style) is done
|
|
85
|
+
# via the widget constructed hook: diffing the global widgets dict per widget
|
|
86
|
+
# creation is O(total widgets), and solara replaces that dict with a context
|
|
87
|
+
# aware mapping we should not depend on.
|
|
88
|
+
_construction_recording: Optional[List["widgets.Widget"]] = None
|
|
89
|
+
_chained_construction_callback: Optional[Callable] = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _record_constructed_widget(widget: "widgets.Widget"):
|
|
93
|
+
if _construction_recording is not None:
|
|
94
|
+
_construction_recording.append(widget)
|
|
95
|
+
if _chained_construction_callback is not None:
|
|
96
|
+
_chained_construction_callback(widget)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _start_recording_constructed(recording: List["widgets.Widget"]):
|
|
100
|
+
global _construction_recording, _chained_construction_callback
|
|
101
|
+
current = getattr(widgets.Widget, "_widget_construction_callback", None)
|
|
102
|
+
if current is not _record_constructed_widget:
|
|
103
|
+
# first time, or someone else registered a callback after us: chain it
|
|
104
|
+
_chained_construction_callback = current
|
|
105
|
+
widgets.Widget.on_widget_constructed(_record_constructed_widget)
|
|
106
|
+
_construction_recording = recording
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _stop_recording_constructed():
|
|
110
|
+
global _construction_recording
|
|
111
|
+
_construction_recording = None
|
|
112
|
+
|
|
113
|
+
|
|
83
114
|
_last_rc = None # used for testing
|
|
84
115
|
local = threading.local()
|
|
85
116
|
T = TypeVar("T")
|
|
@@ -178,6 +209,32 @@ def same_component(c1, c2):
|
|
|
178
209
|
return c1 == c2
|
|
179
210
|
|
|
180
211
|
|
|
212
|
+
def _mark_needs_render_ancestors(context: "ComponentContext"):
|
|
213
|
+
"""Let the render phase find its way down to a context that needs work, without walking subtrees that do not."""
|
|
214
|
+
parent = context.parent
|
|
215
|
+
while parent is not None and not parent.needs_render_descendant:
|
|
216
|
+
parent.needs_render_descendant = True
|
|
217
|
+
parent = parent.parent
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _values_identical(a, b):
|
|
221
|
+
"""Are two (possibly nested in containers) values the same objects?
|
|
222
|
+
|
|
223
|
+
Used to detect that reconciliation of an identical element resolved to the
|
|
224
|
+
same widgets, meaning the widget cannot need an update.
|
|
225
|
+
"""
|
|
226
|
+
if a is b:
|
|
227
|
+
return True
|
|
228
|
+
type_a = type(a)
|
|
229
|
+
if type_a is not type(b):
|
|
230
|
+
return False
|
|
231
|
+
if type_a is list or type_a is tuple:
|
|
232
|
+
return len(a) == len(b) and all(x is y or _values_identical(x, y) for x, y in zip(a, b))
|
|
233
|
+
if type_a is dict:
|
|
234
|
+
return len(a) == len(b) and all(k in b and (v is b[k] or _values_identical(v, b[k])) for k, v in a.items())
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
|
|
181
238
|
def _with_tracebacks(e, tracebacks):
|
|
182
239
|
# copy it, and we need with_traceback for unknown reasons not to cause
|
|
183
240
|
# an infinite loop
|
|
@@ -383,31 +440,34 @@ class Element(Generic[W]):
|
|
|
383
440
|
# we can't use our own kwarg, since that contains elements, not widgets
|
|
384
441
|
kwargs, listeners = self._split_kwargs(kwargs)
|
|
385
442
|
assert isinstance(self.component, ComponentWidget)
|
|
386
|
-
#
|
|
387
|
-
# A different implementation might avoid this.
|
|
443
|
+
# The recording is global state, so we need a lock.
|
|
388
444
|
with self.create_lock:
|
|
389
445
|
rc = get_render_context(required=True)
|
|
390
|
-
|
|
446
|
+
recorded: List[widgets.Widget] = []
|
|
447
|
+
_start_recording_constructed(recorded)
|
|
391
448
|
try:
|
|
392
|
-
|
|
393
|
-
|
|
449
|
+
try:
|
|
450
|
+
widget = self.component.widget(**kwargs)
|
|
451
|
+
hold_trait_notifications = widget.hold_trait_notifications
|
|
394
452
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
453
|
+
@contextlib.contextmanager
|
|
454
|
+
def hold_trait_notifications_extra(*args, **kwargs):
|
|
455
|
+
with rc, hold_trait_notifications(*args, **kwargs):
|
|
456
|
+
yield
|
|
399
457
|
|
|
400
|
-
|
|
458
|
+
widget.hold_trait_notifications = hold_trait_notifications_extra
|
|
401
459
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
460
|
+
if self._meta:
|
|
461
|
+
widget._react_meta = dict(self._meta)
|
|
462
|
+
except Exception as e:
|
|
463
|
+
raise RuntimeError(f"Could not create widget {self.component.widget} with {kwargs}") from e
|
|
464
|
+
for name, callback in listeners.items():
|
|
465
|
+
if callback is not None:
|
|
466
|
+
self._add_widget_event_listener(widget, name, callback)
|
|
467
|
+
finally:
|
|
468
|
+
_stop_recording_constructed()
|
|
469
|
+
widgets_dict = _get_widgets_dict()
|
|
470
|
+
orphans = {w.model_id for w in recorded if w is not widget and w.comm is not None and w.model_id in widgets_dict}
|
|
411
471
|
return widget, orphans
|
|
412
472
|
|
|
413
473
|
def _update_widget(self, widget: widgets.Widget, el_prev: "Element", kwargs):
|
|
@@ -632,7 +692,7 @@ class ComponentFunction(Component):
|
|
|
632
692
|
for name in normal_kwargs:
|
|
633
693
|
self.observe(self._rerender, name)
|
|
634
694
|
el = self._create_el()
|
|
635
|
-
self.rc =
|
|
695
|
+
self.rc = _render_context_class()(el, self)
|
|
636
696
|
self.rc.render(el, self)
|
|
637
697
|
|
|
638
698
|
def _create_el(self):
|
|
@@ -1080,6 +1140,12 @@ class ComponentContext:
|
|
|
1080
1140
|
used_keys: Set[str] = field(default_factory=set)
|
|
1081
1141
|
# if a child component's state if changed, it needs a rerender
|
|
1082
1142
|
needs_render: bool = True
|
|
1143
|
+
# some context in this subtree may need a render (set by setters walking up,
|
|
1144
|
+
# cleared when the render phase walks this context)
|
|
1145
|
+
needs_render_descendant: bool = True
|
|
1146
|
+
# the render phase skipped this whole subtree (nothing changed), so the
|
|
1147
|
+
# reconciliation phase can reuse the previous result without walking
|
|
1148
|
+
clean_subtree: bool = False
|
|
1083
1149
|
|
|
1084
1150
|
# elements created in this context go there
|
|
1085
1151
|
owns: Set[Element] = field(default_factory=set)
|
|
@@ -1155,6 +1221,9 @@ class _RenderContext:
|
|
|
1155
1221
|
self.handle_error = handle_error
|
|
1156
1222
|
self.reconsolidating = False
|
|
1157
1223
|
self._batch_counter = utils.ThreadSafeCounter()
|
|
1224
|
+
# when set, the next render phase walks the whole tree instead of
|
|
1225
|
+
# skipping subtrees in which no state changed (see _render)
|
|
1226
|
+
self._walk_all = True
|
|
1158
1227
|
if initial_state:
|
|
1159
1228
|
self.state_set(self.context_root, initial_state)
|
|
1160
1229
|
|
|
@@ -1330,6 +1399,7 @@ class _RenderContext:
|
|
|
1330
1399
|
context.state_metadata[key] = new_metadata
|
|
1331
1400
|
# TODO: enable
|
|
1332
1401
|
context.needs_render = True
|
|
1402
|
+
_mark_needs_render_ancestors(context)
|
|
1333
1403
|
if self._rerender_needed is False:
|
|
1334
1404
|
if DEBUG:
|
|
1335
1405
|
trigger_stack = traceback.format_stack()
|
|
@@ -1351,6 +1421,8 @@ class _RenderContext:
|
|
|
1351
1421
|
return set_
|
|
1352
1422
|
|
|
1353
1423
|
def force_update(self):
|
|
1424
|
+
# a forced update re-walks the whole tree, no subtree skipping
|
|
1425
|
+
self._walk_all = True
|
|
1354
1426
|
if not self._is_rendering:
|
|
1355
1427
|
self.render(self.element, self.container)
|
|
1356
1428
|
|
|
@@ -1374,6 +1446,7 @@ class _RenderContext:
|
|
|
1374
1446
|
self.context.effect_index += 1
|
|
1375
1447
|
|
|
1376
1448
|
def update(self, element: Element):
|
|
1449
|
+
self._walk_all = True
|
|
1377
1450
|
if self._is_rendering:
|
|
1378
1451
|
self.element = element
|
|
1379
1452
|
self._rerender_needed_reasons.append(RerenderReason(reason="root element changed"))
|
|
@@ -1430,6 +1503,7 @@ class _RenderContext:
|
|
|
1430
1503
|
self._shared_elements_next = set()
|
|
1431
1504
|
self._render(self.element, "/", parent_key=ROOT_KEY)
|
|
1432
1505
|
self.first_render = False
|
|
1506
|
+
self._walk_all = False
|
|
1433
1507
|
except BaseException:
|
|
1434
1508
|
self._is_rendering = False
|
|
1435
1509
|
raise
|
|
@@ -1464,6 +1538,7 @@ class _RenderContext:
|
|
|
1464
1538
|
self.context.exceptions_self = []
|
|
1465
1539
|
|
|
1466
1540
|
self._render(self.element, "/", parent_key=ROOT_KEY)
|
|
1541
|
+
self._walk_all = False
|
|
1467
1542
|
logger.info("Render done: %r %r", self._rerender_needed, self._rerender_needed_reasons[-1])
|
|
1468
1543
|
assert self.context is self.context_root
|
|
1469
1544
|
render_counts += 1
|
|
@@ -1488,6 +1563,10 @@ class _RenderContext:
|
|
|
1488
1563
|
self.context.root_element = self.context.root_element_next
|
|
1489
1564
|
self.context.root_element_next = None
|
|
1490
1565
|
|
|
1566
|
+
# remove stale elements of the root context itself
|
|
1567
|
+
# (child contexts are swept during their reconciliation)
|
|
1568
|
+
self._remove_stale_root_elements(ROOT_KEY)
|
|
1569
|
+
|
|
1491
1570
|
if self._shared_elements_next:
|
|
1492
1571
|
raise RuntimeError(f"Element not reconsolidated: {self._shared_elements_next}")
|
|
1493
1572
|
logger.debug("Reconsolidate phase resulted in elements:")
|
|
@@ -2194,6 +2273,600 @@ class _RenderContext:
|
|
|
2194
2273
|
else:
|
|
2195
2274
|
return value
|
|
2196
2275
|
|
|
2276
|
+
def _remove_stale_root_elements(self, parent_key):
|
|
2277
|
+
# The original _reconsolidate already removes stale elements itself,
|
|
2278
|
+
# so for the base (original) render context this is a no-op.
|
|
2279
|
+
pass
|
|
2280
|
+
|
|
2281
|
+
|
|
2282
|
+
class _RenderContextFast(_RenderContext):
|
|
2283
|
+
# Opt-in fast reimplementation of the tree-walking, selectable via the
|
|
2284
|
+
# REACTON_FAST=1 environment variable.
|
|
2285
|
+
#
|
|
2286
|
+
# ------------------------------------------------------------------
|
|
2287
|
+
# The tree walking: one render() call alternates two kinds of phases.
|
|
2288
|
+
#
|
|
2289
|
+
# * render phase (_render): execute component functions depth first
|
|
2290
|
+
# and build the next element tree (elements_next/children_next).
|
|
2291
|
+
# * reconciliation phase (_reconsolidate): walk the new element tree,
|
|
2292
|
+
# create/update/close widgets, run effects, and move the *_next
|
|
2293
|
+
# bookkeeping to current.
|
|
2294
|
+
#
|
|
2295
|
+
# A component subtree in which no state changed, no exception is
|
|
2296
|
+
# pending, and whose element is identical to the previous render is
|
|
2297
|
+
# skipped in both phases: setters mark the path from their context up
|
|
2298
|
+
# to the root (ComponentContext.needs_render_descendant), so the walk
|
|
2299
|
+
# only descends where work can exist. _render marks skipped contexts
|
|
2300
|
+
# (clean_subtree) so _reconsolidate can reuse the previous widget
|
|
2301
|
+
# without walking either.
|
|
2302
|
+
# ------------------------------------------------------------------
|
|
2303
|
+
|
|
2304
|
+
def _set_rerender_needed(self, reason: str):
|
|
2305
|
+
self._rerender_needed_reasons.append(RerenderReason(reason=reason))
|
|
2306
|
+
self._rerender_needed = True
|
|
2307
|
+
|
|
2308
|
+
def _render(self, element: Element, default_key: str, parent_key: str):
|
|
2309
|
+
if not isinstance(element, Element):
|
|
2310
|
+
raise TypeError(f"Expected element, not {element}")
|
|
2311
|
+
# for tracking stale elements when using get_widget
|
|
2312
|
+
self._old_element_ids.add(id(element))
|
|
2313
|
+
context = self.context
|
|
2314
|
+
assert context is not None
|
|
2315
|
+
|
|
2316
|
+
if default_key == "/":
|
|
2317
|
+
# the root element of a component determines which keys are in use,
|
|
2318
|
+
# everything else is stale and gets removed during reconciliation
|
|
2319
|
+
context.used_keys.clear()
|
|
2320
|
+
|
|
2321
|
+
el = element
|
|
2322
|
+
key = el._key
|
|
2323
|
+
if key is None:
|
|
2324
|
+
key = default_key
|
|
2325
|
+
el._key_frozen = True
|
|
2326
|
+
|
|
2327
|
+
if key in context.used_keys:
|
|
2328
|
+
if DEBUG:
|
|
2329
|
+
self.tracebacks.append(el.traceback)
|
|
2330
|
+
raise KeyError(f"Duplicate key {key!r}")
|
|
2331
|
+
context.used_keys.add(key)
|
|
2332
|
+
|
|
2333
|
+
if el.is_shared:
|
|
2334
|
+
# a shared element renders a single widget, process it once per phase
|
|
2335
|
+
if el in self._shared_elements_next:
|
|
2336
|
+
return
|
|
2337
|
+
self._shared_elements_next.add(el)
|
|
2338
|
+
|
|
2339
|
+
el_prev = context.elements_next.get(key)
|
|
2340
|
+
if el_prev is None:
|
|
2341
|
+
el_prev = context.elements.get(key)
|
|
2342
|
+
context.elements_next[key] = el
|
|
2343
|
+
el._render_count += 1 # for testing only
|
|
2344
|
+
|
|
2345
|
+
if isinstance(el.component, ComponentWidget):
|
|
2346
|
+
assert not el.args, "no positional args supported for widgets"
|
|
2347
|
+
if key in context.children_next:
|
|
2348
|
+
# a function component lived at this key before
|
|
2349
|
+
del context.children_next[key]
|
|
2350
|
+
# the element arguments are part of this component's element tree
|
|
2351
|
+
if el.kwargs:
|
|
2352
|
+
self._visit_children(el, key, parent_key, self._render)
|
|
2353
|
+
return
|
|
2354
|
+
|
|
2355
|
+
assert isinstance(el.component, ComponentFunction)
|
|
2356
|
+
if el.is_shared and (el.args or el.kwargs):
|
|
2357
|
+
# arguments of a shared element belong to the context it is rendered in;
|
|
2358
|
+
# for non-shared component elements the component function decides
|
|
2359
|
+
# what ends up in the tree
|
|
2360
|
+
self._visit_children(el, key, parent_key, self._render)
|
|
2361
|
+
|
|
2362
|
+
context_previous = context.children_next.get(key)
|
|
2363
|
+
if context_previous is None:
|
|
2364
|
+
context_previous = context.children.get(key)
|
|
2365
|
+
|
|
2366
|
+
if (
|
|
2367
|
+
not self._walk_all
|
|
2368
|
+
and el is el_prev
|
|
2369
|
+
and not el.is_shared
|
|
2370
|
+
and context_previous is not None
|
|
2371
|
+
and context.children.get(key) is context_previous
|
|
2372
|
+
and not context_previous.needs_render
|
|
2373
|
+
and not context_previous.needs_render_descendant
|
|
2374
|
+
and not context_previous.exceptions_self
|
|
2375
|
+
and not context_previous.exceptions_children
|
|
2376
|
+
and context_previous.root_element is not None
|
|
2377
|
+
and context_previous.root_element_next is None
|
|
2378
|
+
):
|
|
2379
|
+
# fast path: same element, no state changes or pending exceptions
|
|
2380
|
+
# anywhere in this subtree, and fully reconciled: the previous
|
|
2381
|
+
# result stands, skip the subtree in both phases
|
|
2382
|
+
context_previous.clean_subtree = True
|
|
2383
|
+
context.children_next[key] = context_previous
|
|
2384
|
+
return
|
|
2385
|
+
|
|
2386
|
+
parent_context = context
|
|
2387
|
+
del context
|
|
2388
|
+
if context_previous is None:
|
|
2389
|
+
context = ComponentContext(parent=parent_context, context_managers=[cm(el) for cm in _component_context_manager_classes])
|
|
2390
|
+
el_prev = None # do not compare against an element of a different component
|
|
2391
|
+
elif context_previous.root_element is None and context_previous.root_element_next is None:
|
|
2392
|
+
# pre-created, carrying initial state (state_set), but never rendered
|
|
2393
|
+
context = context_previous
|
|
2394
|
+
context.context_managers = [cm(el) for cm in _component_context_manager_classes]
|
|
2395
|
+
else:
|
|
2396
|
+
assert context_previous.invoke_element is not None
|
|
2397
|
+
if not same_component(context_previous.invoke_element.component, el.component):
|
|
2398
|
+
# a different component took this key, start fresh
|
|
2399
|
+
# (the old context is cleaned up during reconciliation)
|
|
2400
|
+
context = ComponentContext(parent=parent_context, context_managers=[cm(el) for cm in _component_context_manager_classes])
|
|
2401
|
+
el_prev = None
|
|
2402
|
+
else:
|
|
2403
|
+
context = context_previous
|
|
2404
|
+
context.parent = parent_context
|
|
2405
|
+
context.clean_subtree = False
|
|
2406
|
+
context.invoke_element = el
|
|
2407
|
+
|
|
2408
|
+
needs_render = context.needs_render
|
|
2409
|
+
if not needs_render and el_prev is not None and context_previous is context:
|
|
2410
|
+
assert not isinstance(el_prev.component, ComponentWidget)
|
|
2411
|
+
needs_render = el._arguments_changed(el_prev)
|
|
2412
|
+
if not needs_render and context.exceptions_children:
|
|
2413
|
+
# we have exceptions, so we need to render
|
|
2414
|
+
needs_render = True
|
|
2415
|
+
if not needs_render:
|
|
2416
|
+
assert el_prev is not None
|
|
2417
|
+
|
|
2418
|
+
self.container_adders = []
|
|
2419
|
+
self.context = context
|
|
2420
|
+
render_count_check = self.render_count
|
|
2421
|
+
try:
|
|
2422
|
+
context.exceptions_self = []
|
|
2423
|
+
root_element: Optional[Element] = None
|
|
2424
|
+
if needs_render:
|
|
2425
|
+
context.state_index = 0
|
|
2426
|
+
context.effect_index = 0
|
|
2427
|
+
context.memo_index = 0
|
|
2428
|
+
context.user_contexts = {}
|
|
2429
|
+
context.exception_handler = False
|
|
2430
|
+
# we reset it before calling the component function,
|
|
2431
|
+
# which might set it to true again
|
|
2432
|
+
context.needs_render = False
|
|
2433
|
+
try:
|
|
2434
|
+
with contextlib.ExitStack() as stack:
|
|
2435
|
+
for cm in context.context_managers:
|
|
2436
|
+
stack.enter_context(cm)
|
|
2437
|
+
if _default_container is not None:
|
|
2438
|
+
with _default_container() as container:
|
|
2439
|
+
el.component.render_count += 1
|
|
2440
|
+
root_element = el.component.f(*el.args, **el.kwargs)
|
|
2441
|
+
if root_element is None:
|
|
2442
|
+
if len(container.kwargs["children"]) == 1:
|
|
2443
|
+
root_element = container.kwargs["children"][0]
|
|
2444
|
+
else:
|
|
2445
|
+
root_element = container
|
|
2446
|
+
else:
|
|
2447
|
+
el.component.render_count += 1
|
|
2448
|
+
root_element = el.component.f(*el.args, **el.kwargs)
|
|
2449
|
+
assert root_element is not None
|
|
2450
|
+
except BaseException as e:
|
|
2451
|
+
if DEBUG:
|
|
2452
|
+
# we might be interested in the traceback inside the call...
|
|
2453
|
+
if len(self.tracebacks) == 0:
|
|
2454
|
+
assert e.__traceback__ is not None
|
|
2455
|
+
traceback = cast(TracebackType, e.__traceback__)
|
|
2456
|
+
if traceback.tb_next: # is there an error inside the call
|
|
2457
|
+
self.tracebacks.append(traceback.tb_next)
|
|
2458
|
+
self.tracebacks.append(el.traceback)
|
|
2459
|
+
logger.exception("Component %r raised exception %r", el.component, e)
|
|
2460
|
+
context.exceptions_self.append(e)
|
|
2461
|
+
self._set_rerender_needed("Exception ocurred during render")
|
|
2462
|
+
context.needs_render = True
|
|
2463
|
+
if root_element is None and not context.exceptions_self:
|
|
2464
|
+
raise ValueError(f"Component {el.component} returned None")
|
|
2465
|
+
else:
|
|
2466
|
+
root_element = context.root_element_next or context.root_element
|
|
2467
|
+
|
|
2468
|
+
if self.render_count != render_count_check:
|
|
2469
|
+
raise RuntimeError("Recursive render detected, possible a bug in react")
|
|
2470
|
+
|
|
2471
|
+
# the subtree walk below will mark this again when state changes
|
|
2472
|
+
context.needs_render_descendant = False
|
|
2473
|
+
if root_element is not None:
|
|
2474
|
+
self._render(root_element, "/", parent_key=join_key(parent_key, key)) # depth first
|
|
2475
|
+
context.root_element_next = root_element
|
|
2476
|
+
elif el.is_shared:
|
|
2477
|
+
self._shared_elements_next.remove(el)
|
|
2478
|
+
|
|
2479
|
+
try:
|
|
2480
|
+
# when an exception interrupted the component function, fewer hook calls are allowed
|
|
2481
|
+
if not ((context.effect_index == len(context.effects)) or (context.exceptions_self and context.effect_index <= len(context.effects))):
|
|
2482
|
+
raise RuntimeError(
|
|
2483
|
+
f"Previously render had {len(context.effects)} effects, this run {context.effect_index} "
|
|
2484
|
+
f"(in element/component: {el}/{el.component}). "
|
|
2485
|
+
"Are you using conditional hooks?"
|
|
2486
|
+
)
|
|
2487
|
+
if not ((context.memo_index == len(context.memo)) or (context.exceptions_self and context.memo_index <= len(context.memo))):
|
|
2488
|
+
raise RuntimeError(
|
|
2489
|
+
f"Previously render had {len(context.memo)} calls to use_memo, this run {context.memo_index} "
|
|
2490
|
+
f"(in element/component: {el}/{el.component}). "
|
|
2491
|
+
"Are you using conditional hooks?"
|
|
2492
|
+
)
|
|
2493
|
+
except RuntimeError as e:
|
|
2494
|
+
logger.exception("Exception in hook count check")
|
|
2495
|
+
context.exceptions_self.append(e)
|
|
2496
|
+
self._set_rerender_needed("Exception ocurred during render (hook count check)")
|
|
2497
|
+
context.needs_render = True
|
|
2498
|
+
|
|
2499
|
+
# only expose to the parent when we get this far
|
|
2500
|
+
parent_context.children_next[key] = context
|
|
2501
|
+
# drop children/elements from a previous render pass that are no longer used
|
|
2502
|
+
used_keys = context.used_keys
|
|
2503
|
+
for unused in [k for k in context.children_next if k not in used_keys]:
|
|
2504
|
+
del context.children_next[unused]
|
|
2505
|
+
for unused in [k for k in context.elements_next if k not in used_keys]:
|
|
2506
|
+
del context.elements_next[unused]
|
|
2507
|
+
context.user_contexts_prev = context.user_contexts
|
|
2508
|
+
finally:
|
|
2509
|
+
assert context.parent is parent_context
|
|
2510
|
+
self.context = parent_context
|
|
2511
|
+
if context.exceptions_self or context.exceptions_children and not context.exception_handler:
|
|
2512
|
+
# this component does not handle the exceptions, bubble up
|
|
2513
|
+
parent_context.exceptions_children.extend(context.exceptions_self)
|
|
2514
|
+
parent_context.exceptions_children.extend(context.exceptions_children)
|
|
2515
|
+
if context.exceptions_self or context.exceptions_children:
|
|
2516
|
+
# make sure the next render pass walks down to this context
|
|
2517
|
+
# (e.g. so a parent with use_exception gets a chance to handle it)
|
|
2518
|
+
_mark_needs_render_ancestors(context)
|
|
2519
|
+
if parent_context.exceptions_self or parent_context.exceptions_children:
|
|
2520
|
+
if not self._rerender_needed:
|
|
2521
|
+
# this happens when an exception was added from an event handler:
|
|
2522
|
+
# no exception was raised during this render phase, but we still
|
|
2523
|
+
# need to rerender until a component catches the exception
|
|
2524
|
+
self._set_rerender_needed("Exception ocurred during render")
|
|
2525
|
+
parent_context.needs_render = True
|
|
2526
|
+
|
|
2527
|
+
def _reconsolidate(self, el: Element, default_key: str, parent_key: str):
|
|
2528
|
+
key = el._key
|
|
2529
|
+
if key is None:
|
|
2530
|
+
key = default_key
|
|
2531
|
+
assert key is not None
|
|
2532
|
+
context = self.context
|
|
2533
|
+
assert context is not None
|
|
2534
|
+
|
|
2535
|
+
if el.is_shared and el in self._shared_elements and el is not self.element:
|
|
2536
|
+
# shared elements reconcile once, all other uses share the widget
|
|
2537
|
+
return self._shared_widgets[el]
|
|
2538
|
+
|
|
2539
|
+
el_prev = context.elements.get(key)
|
|
2540
|
+
try:
|
|
2541
|
+
if isinstance(el.component, ComponentFunction):
|
|
2542
|
+
child_context_next = context.children_next.get(key)
|
|
2543
|
+
if child_context_next is not None and child_context_next.clean_subtree:
|
|
2544
|
+
# subtree was skipped during the render phase: the previous
|
|
2545
|
+
# reconciliation result stands
|
|
2546
|
+
child_context_next.clean_subtree = False
|
|
2547
|
+
context.children[key] = context.children_next.pop(key)
|
|
2548
|
+
return context.widgets[key]
|
|
2549
|
+
|
|
2550
|
+
if el_prev and isinstance(el_prev.component, ComponentWidget):
|
|
2551
|
+
# a widget element was replaced by a component element at this key
|
|
2552
|
+
self._remove_element(el_prev, default_key=key, parent_key=parent_key)
|
|
2553
|
+
new_parent_key = join_key(parent_key, key)
|
|
2554
|
+
try:
|
|
2555
|
+
if el.is_shared and (el.args or el.kwargs):
|
|
2556
|
+
self._visit_children(el, key, parent_key, self._reconsolidate)
|
|
2557
|
+
assert self.context is context
|
|
2558
|
+
|
|
2559
|
+
child_context_prev = context.children.get(key)
|
|
2560
|
+
child_context = context.children_next[key]
|
|
2561
|
+
if child_context_prev is not None and child_context_prev is not child_context:
|
|
2562
|
+
# the component type changed, remove the old subtree
|
|
2563
|
+
assert el_prev is not None, "prev child is not None, but element is"
|
|
2564
|
+
self._remove_element(el_prev, default_key=key, parent_key=parent_key)
|
|
2565
|
+
|
|
2566
|
+
self.context = child_context
|
|
2567
|
+
assert child_context.root_element_next
|
|
2568
|
+
widget = self._reconsolidate(child_context.root_element_next, "/", new_parent_key)
|
|
2569
|
+
child_context.root_element = child_context.root_element_next
|
|
2570
|
+
child_context.root_element_next = None
|
|
2571
|
+
# merge the meta of the component element onto the root widget,
|
|
2572
|
+
# e.g. SomeComponent().meta(name="a") should appear on the widget
|
|
2573
|
+
if el._meta or getattr(widget, "_react_meta", {}):
|
|
2574
|
+
widget._react_meta = {**getattr(widget, "_react_meta", {}), **el._meta}
|
|
2575
|
+
|
|
2576
|
+
if el.is_shared:
|
|
2577
|
+
self._shared_widgets[el] = widget
|
|
2578
|
+
else:
|
|
2579
|
+
context.widgets[key] = widget
|
|
2580
|
+
|
|
2581
|
+
# remove elements that are no longer part of this component's tree
|
|
2582
|
+
# NOTE: sorted for reproducibility
|
|
2583
|
+
stale_keys = sorted(set(child_context.elements) - child_context.used_keys)
|
|
2584
|
+
if stale_keys:
|
|
2585
|
+
logger.info("elements to be removed: %r", stale_keys)
|
|
2586
|
+
for stale_key in stale_keys:
|
|
2587
|
+
if stale_key in child_context.elements:
|
|
2588
|
+
self._remove_element(child_context.elements[stale_key], stale_key, new_parent_key)
|
|
2589
|
+
|
|
2590
|
+
self._process_effects(child_context, context)
|
|
2591
|
+
|
|
2592
|
+
if child_context.children_next:
|
|
2593
|
+
# if we had two render phases, we can have old contexts left over
|
|
2594
|
+
child_context.children_next.clear()
|
|
2595
|
+
if child_context.elements_next:
|
|
2596
|
+
# we can still have elements that are not used as a 'widget' in this
|
|
2597
|
+
# context, but are passed down as an element (when shared)
|
|
2598
|
+
unreferenced = []
|
|
2599
|
+
for child_key, child_el in list(child_context.elements_next.items()):
|
|
2600
|
+
if child_el not in self._shared_elements:
|
|
2601
|
+
unreferenced.append(child_el)
|
|
2602
|
+
else:
|
|
2603
|
+
child_context.elements[child_key] = child_context.elements_next.pop(child_key)
|
|
2604
|
+
if unreferenced:
|
|
2605
|
+
raise RuntimeError(f"Unused elements and unreferenced elements {unreferenced}")
|
|
2606
|
+
if child_context.exceptions_self or child_context.exceptions_children and not child_context.exception_handler:
|
|
2607
|
+
# child does not handle exceptions, so bubble up
|
|
2608
|
+
context.exceptions_children.extend(child_context.exceptions_self)
|
|
2609
|
+
context.exceptions_children.extend(child_context.exceptions_children)
|
|
2610
|
+
finally:
|
|
2611
|
+
# restore context
|
|
2612
|
+
self.context = context
|
|
2613
|
+
context.children[key] = context.children_next.pop(key)
|
|
2614
|
+
else:
|
|
2615
|
+
assert isinstance(el.component, ComponentWidget)
|
|
2616
|
+
if el.args:
|
|
2617
|
+
raise TypeError("Widget element only take keyword arguments")
|
|
2618
|
+
|
|
2619
|
+
widget_previous = None
|
|
2620
|
+
if el_prev is not None:
|
|
2621
|
+
if el_prev.is_shared:
|
|
2622
|
+
widget_previous = self._shared_widgets[el_prev]
|
|
2623
|
+
else:
|
|
2624
|
+
widget_previous = context.widgets[key]
|
|
2625
|
+
|
|
2626
|
+
orphan_ids: Set[str] = set()
|
|
2627
|
+
if widget_previous is None:
|
|
2628
|
+
# initial create
|
|
2629
|
+
kwargs = self._visit_children_values(el.kwargs, key, parent_key, self._reconsolidate)
|
|
2630
|
+
if el.is_shared and el in self._shared_widgets:
|
|
2631
|
+
raise RuntimeError(f"Element ({el}) was already in self._shared_widgets")
|
|
2632
|
+
widget = None
|
|
2633
|
+
if not context.exceptions_children:
|
|
2634
|
+
try:
|
|
2635
|
+
widget, orphan_ids = el._create_widget(kwargs)
|
|
2636
|
+
except BaseException as e:
|
|
2637
|
+
context.exceptions_self.append(e)
|
|
2638
|
+
self._set_rerender_needed("Exception ocurred during reconciliation (creating widget)")
|
|
2639
|
+
_mark_needs_render_ancestors(context)
|
|
2640
|
+
self._store_widget(context, el, key, widget)
|
|
2641
|
+
elif el_prev is not None and el_prev.component == el.component:
|
|
2642
|
+
# update the existing widget in place
|
|
2643
|
+
kwargs = self._visit_children_values(el.kwargs, key, parent_key, self._reconsolidate)
|
|
2644
|
+
if not context.exceptions_children:
|
|
2645
|
+
if el is not el_prev or not _values_identical(kwargs, el.kwargs):
|
|
2646
|
+
try:
|
|
2647
|
+
el._update_widget(widget_previous, el_prev, kwargs)
|
|
2648
|
+
except BaseException as e:
|
|
2649
|
+
context.exceptions_self.append(e)
|
|
2650
|
+
self._set_rerender_needed("Exception ocurred during reconciliation (updating widget)")
|
|
2651
|
+
_mark_needs_render_ancestors(context)
|
|
2652
|
+
# else: identical element and all children reconciled to the
|
|
2653
|
+
# same widgets, nothing can have changed
|
|
2654
|
+
self._store_widget(context, el, key, widget_previous)
|
|
2655
|
+
else:
|
|
2656
|
+
assert el_prev is not None, "widget_previous is not None, but el_prev is"
|
|
2657
|
+
# a different widget type at the same key: replace
|
|
2658
|
+
self._remove_element(el_prev, key, parent_key=parent_key)
|
|
2659
|
+
kwargs = self._visit_children_values(el.kwargs, key, parent_key, self._reconsolidate)
|
|
2660
|
+
widget = None
|
|
2661
|
+
if not context.exceptions_children:
|
|
2662
|
+
try:
|
|
2663
|
+
widget, orphan_ids = el._create_widget(kwargs)
|
|
2664
|
+
except BaseException as e:
|
|
2665
|
+
context.exceptions_self.append(e)
|
|
2666
|
+
self._set_rerender_needed("Exception ocurred during reconciliation (updating widget)")
|
|
2667
|
+
_mark_needs_render_ancestors(context)
|
|
2668
|
+
self._store_widget(context, el, key, widget)
|
|
2669
|
+
if orphan_ids:
|
|
2670
|
+
# widgets created as a side effect (like Layout and Style); we track
|
|
2671
|
+
# them by model id (not all widgets are hashable) for cleanup
|
|
2672
|
+
orphan_widgets = set([_get_widgets_dict()[k] for k in orphan_ids])
|
|
2673
|
+
for orphan_widget in orphan_widgets:
|
|
2674
|
+
# these are shared between widgets
|
|
2675
|
+
if orphan_widget.__class__.__name__ == "Template" and orphan_widget.__class__.__module__ == "ipyvue.Template":
|
|
2676
|
+
orphan_ids -= {orphan_widget.model_id}
|
|
2677
|
+
widget = self._shared_widgets[el] if el.is_shared else context.widgets[key]
|
|
2678
|
+
if widget.model_id not in self._orphans:
|
|
2679
|
+
self._orphans[widget.model_id] = set()
|
|
2680
|
+
self._orphans[widget.model_id].update(orphan_ids)
|
|
2681
|
+
|
|
2682
|
+
if el.is_shared:
|
|
2683
|
+
widget = self._shared_widgets[el]
|
|
2684
|
+
else:
|
|
2685
|
+
widget = context.widgets[key]
|
|
2686
|
+
# used in get_widget
|
|
2687
|
+
if el_prev in context.element_to_widget:
|
|
2688
|
+
del context.element_to_widget[el_prev]
|
|
2689
|
+
context.element_to_widget[el] = widget
|
|
2690
|
+
return widget
|
|
2691
|
+
except Exception as e:
|
|
2692
|
+
if DEBUG:
|
|
2693
|
+
# we don't care about the traceback of the root element
|
|
2694
|
+
if self.element is not el:
|
|
2695
|
+
# we might be interested in the traceback inside the call...
|
|
2696
|
+
if len(self.tracebacks) == 0:
|
|
2697
|
+
assert e.__traceback__ is not None
|
|
2698
|
+
traceback = cast(TracebackType, e.__traceback__)
|
|
2699
|
+
if traceback.tb_next: # is there an error inside the call
|
|
2700
|
+
self.tracebacks.append(traceback.tb_next)
|
|
2701
|
+
self.tracebacks.append(el.traceback)
|
|
2702
|
+
raise
|
|
2703
|
+
finally:
|
|
2704
|
+
# this marks the work as 'done'
|
|
2705
|
+
context.elements[key] = context.elements_next.pop(key)
|
|
2706
|
+
if el_prev in self._shared_elements:
|
|
2707
|
+
self._shared_elements.remove(el_prev)
|
|
2708
|
+
if el.is_shared:
|
|
2709
|
+
assert el not in self._shared_elements
|
|
2710
|
+
self._shared_elements.add(el)
|
|
2711
|
+
assert el in self._shared_elements_next
|
|
2712
|
+
self._shared_elements_next.remove(el)
|
|
2713
|
+
|
|
2714
|
+
def _process_effects(self, child_context: "ComponentContext", context: "ComponentContext"):
|
|
2715
|
+
# NOTE: effect/cleanup exceptions are recorded on the context of the
|
|
2716
|
+
# component's *parent* (`context`), unlike render exceptions: this is
|
|
2717
|
+
# historical behavior (see notes.md)
|
|
2718
|
+
for effect_index, effect in enumerate(child_context.effects):
|
|
2719
|
+
if effect.next:
|
|
2720
|
+
# if we have a next, the effect itself already executed
|
|
2721
|
+
# TODO: custom equals
|
|
2722
|
+
if effect.next.dependencies is not None and utils.equals(effect.dependencies, effect.next.dependencies):
|
|
2723
|
+
# dependencies are the same, just remove the reference
|
|
2724
|
+
effect.next = None
|
|
2725
|
+
continue
|
|
2726
|
+
# dependencies changed: cleanup and execute next
|
|
2727
|
+
if not effect._cleaned_up:
|
|
2728
|
+
try:
|
|
2729
|
+
effect.cleanup()
|
|
2730
|
+
except BaseException as e:
|
|
2731
|
+
context.exceptions_self.append(e)
|
|
2732
|
+
self._set_rerender_needed("Exception ocurred during effect")
|
|
2733
|
+
_mark_needs_render_ancestors(context)
|
|
2734
|
+
context.needs_render = True
|
|
2735
|
+
effect = child_context.effects[effect_index] = effect.next
|
|
2736
|
+
if child_context.exceptions_self or child_context.exceptions_children:
|
|
2737
|
+
# we had an exception, so we skip the effect (not the cleanup)
|
|
2738
|
+
continue
|
|
2739
|
+
try:
|
|
2740
|
+
effect()
|
|
2741
|
+
except BaseException as e:
|
|
2742
|
+
context.exceptions_self.append(e)
|
|
2743
|
+
self._set_rerender_needed("Exception ocurred during effect")
|
|
2744
|
+
_mark_needs_render_ancestors(context)
|
|
2745
|
+
context.needs_render = True
|
|
2746
|
+
|
|
2747
|
+
def _store_widget(self, context: "ComponentContext", el: Element, key: str, widget: Optional[widgets.Widget]):
|
|
2748
|
+
if el.is_shared:
|
|
2749
|
+
self._shared_widgets[el] = widget
|
|
2750
|
+
else:
|
|
2751
|
+
context.widgets[key] = widget
|
|
2752
|
+
|
|
2753
|
+
def _remove_element(self, el: Element, default_key: str, parent_key):
|
|
2754
|
+
key = el._key
|
|
2755
|
+
if key is None:
|
|
2756
|
+
key = default_key
|
|
2757
|
+
assert key is not None
|
|
2758
|
+
context = self.context
|
|
2759
|
+
assert context is not None
|
|
2760
|
+
|
|
2761
|
+
if el.is_shared:
|
|
2762
|
+
if el not in self._shared_elements:
|
|
2763
|
+
# another use of this element keeps it alive, or it was already removed
|
|
2764
|
+
return
|
|
2765
|
+
self._shared_elements.remove(el)
|
|
2766
|
+
|
|
2767
|
+
if key not in context.elements:
|
|
2768
|
+
# already removed, e.g. as part of the removal of a parent element
|
|
2769
|
+
return
|
|
2770
|
+
|
|
2771
|
+
if isinstance(el.component, ComponentFunction):
|
|
2772
|
+
if el.is_shared:
|
|
2773
|
+
self._visit_children(el, key, parent_key, self._remove_element)
|
|
2774
|
+
child_context = context.children[key]
|
|
2775
|
+
try:
|
|
2776
|
+
self.context = child_context
|
|
2777
|
+
# the element is going away, pending exceptions only matter if cleanup fails
|
|
2778
|
+
child_context.exceptions_self = []
|
|
2779
|
+
child_context.exceptions_children = []
|
|
2780
|
+
for effect in child_context.effects:
|
|
2781
|
+
try:
|
|
2782
|
+
if not effect._cleaned_up:
|
|
2783
|
+
effect.cleanup()
|
|
2784
|
+
except BaseException as e:
|
|
2785
|
+
child_context.exceptions_self.append(e)
|
|
2786
|
+
self._set_rerender_needed("Exception ocurred during effect")
|
|
2787
|
+
_mark_needs_render_ancestors(child_context)
|
|
2788
|
+
assert child_context.root_element is not None
|
|
2789
|
+
self._remove_element(child_context.root_element, "/", parent_key=join_key(parent_key, key))
|
|
2790
|
+
finally:
|
|
2791
|
+
try:
|
|
2792
|
+
assert not child_context.elements, f"left over elements {child_context.elements}"
|
|
2793
|
+
assert not child_context.element_to_widget, f"left over element_to_widget {child_context.element_to_widget}"
|
|
2794
|
+
assert not child_context.widgets, f"left over widgets {child_context.widgets}"
|
|
2795
|
+
assert not child_context.children, f"left over children {child_context.children}"
|
|
2796
|
+
assert not child_context.owns, f"left over owns {child_context.owns}"
|
|
2797
|
+
except Exception as e:
|
|
2798
|
+
child_context.exceptions_self.append(e)
|
|
2799
|
+
# restore context
|
|
2800
|
+
self.context = context
|
|
2801
|
+
if child_context.exceptions_self or child_context.exceptions_children and not child_context.exception_handler:
|
|
2802
|
+
# child does not handle exceptions, so bubble up
|
|
2803
|
+
context.exceptions_children.extend(child_context.exceptions_self)
|
|
2804
|
+
context.exceptions_children.extend(child_context.exceptions_children)
|
|
2805
|
+
del context.children[key]
|
|
2806
|
+
else:
|
|
2807
|
+
self._visit_children(el, key, parent_key, self._remove_element)
|
|
2808
|
+
widget = self._shared_widgets[el] if el.is_shared else context.widgets[key]
|
|
2809
|
+
if widget is not None:
|
|
2810
|
+
assert widget.comm is not None
|
|
2811
|
+
assert widget.model_id in _get_widgets_dict()
|
|
2812
|
+
for orphan in self._orphans.get(widget.model_id, set()):
|
|
2813
|
+
orphan_widget = _get_widgets_dict().get(orphan)
|
|
2814
|
+
if orphan_widget:
|
|
2815
|
+
close_widget(orphan_widget)
|
|
2816
|
+
self._orphans.pop(widget.model_id, None)
|
|
2817
|
+
el._cleanup_callbacks(widget)
|
|
2818
|
+
el._close_widget(widget)
|
|
2819
|
+
if el.is_shared:
|
|
2820
|
+
del self._shared_widgets[el]
|
|
2821
|
+
else:
|
|
2822
|
+
del context.widgets[key]
|
|
2823
|
+
# elements can be removed multiple times, since they can be added multiple times
|
|
2824
|
+
# (even non-shared elements can)
|
|
2825
|
+
if el in context.element_to_widget:
|
|
2826
|
+
del context.element_to_widget[el]
|
|
2827
|
+
del context.elements[key]
|
|
2828
|
+
|
|
2829
|
+
def _visit_children(self, el: Element, default_key: str, parent_key: str, f: Callable):
|
|
2830
|
+
key = el._key
|
|
2831
|
+
if key is None:
|
|
2832
|
+
key = default_key
|
|
2833
|
+
assert key is not None
|
|
2834
|
+
self._visit_children_values(el.kwargs, key, parent_key, f)
|
|
2835
|
+
self._visit_children_values(el.args, key, parent_key, f)
|
|
2836
|
+
|
|
2837
|
+
def _visit_children_values(self, value: Any, key: str, parent_key: str, f: Callable):
|
|
2838
|
+
if isinstance(value, Element):
|
|
2839
|
+
return f(value, key, parent_key)
|
|
2840
|
+
elif isinstance(value, (list, tuple)):
|
|
2841
|
+
values = []
|
|
2842
|
+
for index, v in enumerate(value):
|
|
2843
|
+
new_value = self._visit_children_values(v, f"{key}{index}/", parent_key, f)
|
|
2844
|
+
if isinstance(new_value, FragmentWidget):
|
|
2845
|
+
values.extend(new_value.children)
|
|
2846
|
+
else:
|
|
2847
|
+
values.append(new_value)
|
|
2848
|
+
if isinstance(value, tuple):
|
|
2849
|
+
return tuple(values)
|
|
2850
|
+
return values
|
|
2851
|
+
elif isinstance(value, dict):
|
|
2852
|
+
return {k: self._visit_children_values(v, f"{key}{k}/", parent_key, f) for k, v in value.items()}
|
|
2853
|
+
else:
|
|
2854
|
+
return value
|
|
2855
|
+
|
|
2856
|
+
def _remove_stale_root_elements(self, parent_key):
|
|
2857
|
+
# remove stale elements of the root context itself
|
|
2858
|
+
# (child contexts are swept during their reconciliation)
|
|
2859
|
+
stale_keys = sorted(set(self.context_root.elements) - self.context_root.used_keys)
|
|
2860
|
+
for stale_key in stale_keys:
|
|
2861
|
+
if stale_key in self.context_root.elements:
|
|
2862
|
+
self._remove_element(self.context_root.elements[stale_key], stale_key, parent_key)
|
|
2863
|
+
|
|
2864
|
+
|
|
2865
|
+
def _render_context_class():
|
|
2866
|
+
import os
|
|
2867
|
+
|
|
2868
|
+
return _RenderContextFast if os.environ.get("REACTON_FAST") == "1" else _RenderContext
|
|
2869
|
+
|
|
2197
2870
|
|
|
2198
2871
|
@overload
|
|
2199
2872
|
def render(
|
|
@@ -2209,14 +2882,14 @@ def render(
|
|
|
2209
2882
|
|
|
2210
2883
|
def render(element: Element[T], container: widgets.Widget = None, children_trait="children", handle_error: bool = True, initial_state=None):
|
|
2211
2884
|
container = container or widgets.VBox()
|
|
2212
|
-
_rc =
|
|
2885
|
+
_rc = _render_context_class()(element, container, children_trait=children_trait, handle_error=handle_error, initial_state=initial_state)
|
|
2213
2886
|
_rc.render(element, _rc.container)
|
|
2214
2887
|
local.last_rc = weakref.ref(_rc)
|
|
2215
2888
|
return container, _rc
|
|
2216
2889
|
|
|
2217
2890
|
|
|
2218
2891
|
def render_fixed(element: Element[T], handle_error: bool = True) -> Tuple[T, _RenderContext]:
|
|
2219
|
-
_rc =
|
|
2892
|
+
_rc = _render_context_class()(element, handle_error=handle_error)
|
|
2220
2893
|
widget = _rc.render(element)
|
|
2221
2894
|
local.last_rc = weakref.ref(_rc)
|
|
2222
2895
|
return widget, _rc
|
|
@@ -3,7 +3,7 @@ from typing import Any, Callable, cast
|
|
|
3
3
|
import ipyvue
|
|
4
4
|
|
|
5
5
|
import reacton as react
|
|
6
|
-
from reacton.core import get_render_context
|
|
6
|
+
from reacton.core import ComponentWidget, get_render_context
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def use_event(el: react.core.Element, event_and_modifiers, callback: Callable[[Any], Any]):
|
|
@@ -11,6 +11,20 @@ def use_event(el: react.core.Element, event_and_modifiers, callback: Callable[[A
|
|
|
11
11
|
callback_ref = react.use_ref(callback)
|
|
12
12
|
callback_ref.current = callback
|
|
13
13
|
|
|
14
|
+
# Put the event name in the widget constructor arguments: the synced _events
|
|
15
|
+
# trait then goes along with the comm open message. The later on_event call
|
|
16
|
+
# (in the effect below) only updates _events when the event set differs, so
|
|
17
|
+
# this saves one update message per widget per event. When the element is
|
|
18
|
+
# reused from a previous render (memoized) and the widget already exists,
|
|
19
|
+
# on_event falls back to syncing _events itself.
|
|
20
|
+
if isinstance(el.component, ComponentWidget) and issubclass(el.component.widget, ipyvue.VueWidget):
|
|
21
|
+
events = el.kwargs.get("_events")
|
|
22
|
+
if events is None:
|
|
23
|
+
el.kwargs["_events"] = [event_and_modifiers]
|
|
24
|
+
elif event_and_modifiers not in events:
|
|
25
|
+
# do not mutate the list, it could be shared with a previous element
|
|
26
|
+
el.kwargs["_events"] = [*events, event_and_modifiers]
|
|
27
|
+
|
|
14
28
|
def add_event_handler():
|
|
15
29
|
vue_widget = cast(ipyvue.VueWidget, react.core.get_widget(el))
|
|
16
30
|
# we are basically copying the logic from reacton.core._event_handler_exception_wrapper
|
|
@@ -32,6 +46,11 @@ def use_event(el: react.core.Element, event_and_modifiers, callback: Callable[[A
|
|
|
32
46
|
vue_widget.on_event(event_and_modifiers, handler)
|
|
33
47
|
|
|
34
48
|
def cleanup():
|
|
49
|
+
if rc._closing:
|
|
50
|
+
# the whole tree is going away: removing the handler would sync
|
|
51
|
+
# the _events trait to the frontend (one message per widget)
|
|
52
|
+
# right before the comm is closed anyway
|
|
53
|
+
return
|
|
35
54
|
vue_widget.on_event(event_and_modifiers, handler, remove=True)
|
|
36
55
|
|
|
37
56
|
return cleanup
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import unittest.mock
|
|
2
|
+
|
|
3
|
+
import ipyvuetify
|
|
4
|
+
import ipyvue
|
|
5
|
+
|
|
6
|
+
import reacton as react
|
|
7
|
+
|
|
8
|
+
from . import ipyvuetify as v
|
|
9
|
+
from .ipyvue import use_event
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_use_event_no_sync_messages():
|
|
13
|
+
"""The _events trait should be set at construction and never re-assigned.
|
|
14
|
+
|
|
15
|
+
Re-assigning it (by on_event during the effect, or on_event(remove=True)
|
|
16
|
+
during close) sends one widget update message per widget per event.
|
|
17
|
+
"""
|
|
18
|
+
on_click = unittest.mock.Mock()
|
|
19
|
+
|
|
20
|
+
@react.component
|
|
21
|
+
def Test():
|
|
22
|
+
btn = v.Btn(children=["click me"])
|
|
23
|
+
use_event(btn, "click", on_click)
|
|
24
|
+
return btn
|
|
25
|
+
|
|
26
|
+
box, rc = react.render(Test(), handle_error=False)
|
|
27
|
+
btn = rc.find(ipyvuetify.Btn).widget
|
|
28
|
+
|
|
29
|
+
events_changes = unittest.mock.Mock()
|
|
30
|
+
btn.observe(events_changes, "_events")
|
|
31
|
+
|
|
32
|
+
# the event name went along with the constructor arguments
|
|
33
|
+
assert btn._events == ["click"]
|
|
34
|
+
# and the handler works
|
|
35
|
+
btn.fire_event("click", {})
|
|
36
|
+
on_click.assert_called_once()
|
|
37
|
+
|
|
38
|
+
rc.force_update()
|
|
39
|
+
events_changes.assert_not_called()
|
|
40
|
+
|
|
41
|
+
rc.close()
|
|
42
|
+
events_changes.assert_not_called()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_use_event_removed_on_rerender():
|
|
46
|
+
"""When the widget persists but the event changes, _events must sync."""
|
|
47
|
+
on_event = unittest.mock.Mock()
|
|
48
|
+
set_event_name = None
|
|
49
|
+
|
|
50
|
+
@react.component
|
|
51
|
+
def Test():
|
|
52
|
+
nonlocal set_event_name
|
|
53
|
+
event_name, set_event_name = react.use_state("click")
|
|
54
|
+
btn = v.Btn(children=["click me"])
|
|
55
|
+
use_event(btn, event_name, on_event)
|
|
56
|
+
return btn
|
|
57
|
+
|
|
58
|
+
box, rc = react.render(Test(), handle_error=False)
|
|
59
|
+
btn = rc.find(ipyvuetify.Btn).widget
|
|
60
|
+
assert btn._events == ["click"]
|
|
61
|
+
assert set_event_name is not None
|
|
62
|
+
set_event_name("dblclick")
|
|
63
|
+
assert btn._events == ["dblclick"]
|
|
64
|
+
btn.fire_event("dblclick", {})
|
|
65
|
+
on_event.assert_called_once()
|
|
66
|
+
rc.close()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_use_event_component_element():
|
|
70
|
+
"""use_event on a component element (not a widget element) keeps working."""
|
|
71
|
+
on_click = unittest.mock.Mock()
|
|
72
|
+
|
|
73
|
+
@react.component
|
|
74
|
+
def Button():
|
|
75
|
+
return v.Btn(children=["click me"])
|
|
76
|
+
|
|
77
|
+
@react.component
|
|
78
|
+
def Test():
|
|
79
|
+
btn = Button()
|
|
80
|
+
use_event(btn, "click", on_click)
|
|
81
|
+
return btn
|
|
82
|
+
|
|
83
|
+
box, rc = react.render(Test(), handle_error=False)
|
|
84
|
+
btn = rc.find(ipyvuetify.Btn).widget
|
|
85
|
+
assert isinstance(btn, ipyvue.VueWidget)
|
|
86
|
+
# falls back to syncing _events from the effect
|
|
87
|
+
assert btn._events == ["click"]
|
|
88
|
+
btn.fire_event("click", {})
|
|
89
|
+
on_click.assert_called_once()
|
|
90
|
+
rc.close()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.9.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|