reacton 1.9.1__tar.gz → 1.10.1__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.1 → reacton-1.10.1}/PKG-INFO +1 -1
- reacton-1.10.1/reacton/_version.py +1 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/core.py +772 -31
- {reacton-1.9.1 → reacton-1.10.1}/reacton/core_test.py +24 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/ipyvue.py +20 -1
- reacton-1.10.1/reacton/ipyvue_test.py +90 -0
- reacton-1.9.1/reacton/_version.py +0 -1
- {reacton-1.9.1 → reacton-1.10.1}/.gitignore +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/LICENSE +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/pyproject.toml +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/react_ipywidgets/__init__.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/react_ipywidgets/core.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/__init__.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/bqplot.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/deprecated/__init__.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/deprecated/ipyvuetify.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/find.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/find_test.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/generate.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/generate_test.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/ipycanvas.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/ipyvuetify.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/ipywidgets.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/logging.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/patch.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/patch_display.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/py.typed +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/rx.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/test.vue +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/test_rx.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/utils.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/utils_test.py +0 -0
- {reacton-1.9.1 → reacton-1.10.1}/reacton/work.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.10.1"
|
|
@@ -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)
|
|
@@ -1100,6 +1166,46 @@ class ComponentContext:
|
|
|
1100
1166
|
TEffect = TypeVar("TEffect", bound="Effect")
|
|
1101
1167
|
|
|
1102
1168
|
|
|
1169
|
+
def _teardown_component_context(context: ComponentContext):
|
|
1170
|
+
"""Empty a component context so a closed render context can be freed by refcounting.
|
|
1171
|
+
|
|
1172
|
+
Closures created during rendering (use_state setters, event handlers) capture their
|
|
1173
|
+
component context and the render context strongly - they must, a setter can be the
|
|
1174
|
+
only reference keeping its component context alive (cf61378). Since those closures
|
|
1175
|
+
are also stored in the hook state and on widget callback dispatchers, a closed tree
|
|
1176
|
+
is one big reference cycle that refcounting cannot free: it waits for a gen-2
|
|
1177
|
+
garbage collection, which on a server shows up as a memory sawtooth scaling with
|
|
1178
|
+
per-kernel state. Emptying the contexts breaks the cycles: whoever still holds a
|
|
1179
|
+
setter or handler keeps only a small hollow context alive, and set_/force_update
|
|
1180
|
+
are no-ops once the render context is closing.
|
|
1181
|
+
"""
|
|
1182
|
+
# replace the containers instead of clearing them: state_get() hands out the live
|
|
1183
|
+
# state dicts (test_state_get closes and re-renders with them), and in general we
|
|
1184
|
+
# only want to drop OUR references, not destroy objects someone else captured
|
|
1185
|
+
context.parent = None
|
|
1186
|
+
context.invoke_element = None
|
|
1187
|
+
context.root_element = None
|
|
1188
|
+
context.root_element_next = None
|
|
1189
|
+
context.elements = {}
|
|
1190
|
+
context.elements_next = {}
|
|
1191
|
+
context.children = {}
|
|
1192
|
+
context.children_next = {}
|
|
1193
|
+
context.widgets = {}
|
|
1194
|
+
context.element_to_widget = {}
|
|
1195
|
+
context.state = {}
|
|
1196
|
+
context.state_metadata = {}
|
|
1197
|
+
context.effects = []
|
|
1198
|
+
context.memo = []
|
|
1199
|
+
context.user_contexts = {}
|
|
1200
|
+
context.user_contexts_prev = {}
|
|
1201
|
+
context.context_listeners = defaultdict(set)
|
|
1202
|
+
context.used_keys = set()
|
|
1203
|
+
context.owns = set()
|
|
1204
|
+
context.exceptions_self = []
|
|
1205
|
+
context.exceptions_children = []
|
|
1206
|
+
context.context_managers = []
|
|
1207
|
+
|
|
1208
|
+
|
|
1103
1209
|
@dataclass
|
|
1104
1210
|
class RerenderReason:
|
|
1105
1211
|
reason: str
|
|
@@ -1155,6 +1261,9 @@ class _RenderContext:
|
|
|
1155
1261
|
self.handle_error = handle_error
|
|
1156
1262
|
self.reconsolidating = False
|
|
1157
1263
|
self._batch_counter = utils.ThreadSafeCounter()
|
|
1264
|
+
# when set, the next render phase walks the whole tree instead of
|
|
1265
|
+
# skipping subtrees in which no state changed (see _render)
|
|
1266
|
+
self._walk_all = True
|
|
1158
1267
|
if initial_state:
|
|
1159
1268
|
self.state_set(self.context_root, initial_state)
|
|
1160
1269
|
|
|
@@ -1198,6 +1307,17 @@ class _RenderContext:
|
|
|
1198
1307
|
def close(self):
|
|
1199
1308
|
with self.thread_lock:
|
|
1200
1309
|
self._closing = True
|
|
1310
|
+
# snapshot the component contexts before _remove_element detaches them from
|
|
1311
|
+
# their parents: detached contexts would escape the teardown below while the
|
|
1312
|
+
# setter/handler closures in their state still reference them and us
|
|
1313
|
+
all_contexts: List[ComponentContext] = []
|
|
1314
|
+
|
|
1315
|
+
def collect(context: ComponentContext):
|
|
1316
|
+
all_contexts.append(context)
|
|
1317
|
+
for child in list(context.children.values()) + list(context.children_next.values()):
|
|
1318
|
+
collect(child)
|
|
1319
|
+
|
|
1320
|
+
collect(self.context_root)
|
|
1201
1321
|
logger.info("Removing elements...")
|
|
1202
1322
|
self._remove_element(self.element, default_key="/", parent_key=ROOT_KEY)
|
|
1203
1323
|
logger.info("Removing elements done.")
|
|
@@ -1212,6 +1332,19 @@ class _RenderContext:
|
|
|
1212
1332
|
orphan_widgets = set([_get_widgets_dict()[k] for k in self._orphans])
|
|
1213
1333
|
raise RuntimeError(f"Orphan widgets not cleaned up for widgets: {orphan_widgets}")
|
|
1214
1334
|
exceptions = [*self.context.exceptions_children, *self.context_root.exceptions_self]
|
|
1335
|
+
# break the reference cycles through the tree (see _teardown_component_context);
|
|
1336
|
+
# _closing stays True, making stray setters and event handlers no-ops
|
|
1337
|
+
for context in all_contexts:
|
|
1338
|
+
_teardown_component_context(context)
|
|
1339
|
+
self.context = None
|
|
1340
|
+
self.context_root = None # type: ignore
|
|
1341
|
+
# the root element, container and root widget keep the widget tree alive, and
|
|
1342
|
+
# widgets reference their elements, whose kwargs hold user callbacks - which
|
|
1343
|
+
# capture use_state setters and therefore this render context
|
|
1344
|
+
self.element = None # type: ignore
|
|
1345
|
+
self.container = None
|
|
1346
|
+
self.last_root_widget = None
|
|
1347
|
+
self._old_element_ids.clear()
|
|
1215
1348
|
if exceptions:
|
|
1216
1349
|
raise exceptions[0]
|
|
1217
1350
|
|
|
@@ -1277,12 +1410,18 @@ class _RenderContext:
|
|
|
1277
1410
|
if DEBUG:
|
|
1278
1411
|
created_stack = traceback.format_stack()
|
|
1279
1412
|
|
|
1280
|
-
#
|
|
1281
|
-
#
|
|
1282
|
-
#
|
|
1283
|
-
#
|
|
1413
|
+
# NOTE: set_ captures self and context strongly, and that is a requirement:
|
|
1414
|
+
# a setter may be the ONLY reference keeping its component context alive
|
|
1415
|
+
# (see cf61378, where weak references lost live state). The resulting
|
|
1416
|
+
# reference cycles through the closed tree are broken by close() emptying
|
|
1417
|
+
# the component contexts instead.
|
|
1284
1418
|
|
|
1285
1419
|
def set_(value):
|
|
1420
|
+
if self._closing:
|
|
1421
|
+
# the render context is closed (or closing) and the tree is (being)
|
|
1422
|
+
# torn down: there is nothing to update. This check must come first:
|
|
1423
|
+
# after close, context.state is empty and reading it would fail.
|
|
1424
|
+
return
|
|
1286
1425
|
if callable(value):
|
|
1287
1426
|
value = value(context.state[key])
|
|
1288
1427
|
logger.info("Set state = %r for key %r (previous value was %r) (%r)", value, key, context.state[key], id(self.context))
|
|
@@ -1315,11 +1454,6 @@ class _RenderContext:
|
|
|
1315
1454
|
equals = eq or utils.equals
|
|
1316
1455
|
should_update = not equals(context.state[key], value) or should_update
|
|
1317
1456
|
|
|
1318
|
-
# if a cleanup during close trigger a state update in a different component, we want to ignore that
|
|
1319
|
-
# otherwise we get a deadlock
|
|
1320
|
-
if self._closing:
|
|
1321
|
-
should_update = False
|
|
1322
|
-
|
|
1323
1457
|
if should_update:
|
|
1324
1458
|
prev_value = context.state[key]
|
|
1325
1459
|
context.state[key] = value
|
|
@@ -1330,6 +1464,7 @@ class _RenderContext:
|
|
|
1330
1464
|
context.state_metadata[key] = new_metadata
|
|
1331
1465
|
# TODO: enable
|
|
1332
1466
|
context.needs_render = True
|
|
1467
|
+
_mark_needs_render_ancestors(context)
|
|
1333
1468
|
if self._rerender_needed is False:
|
|
1334
1469
|
if DEBUG:
|
|
1335
1470
|
trigger_stack = traceback.format_stack()
|
|
@@ -1351,6 +1486,11 @@ class _RenderContext:
|
|
|
1351
1486
|
return set_
|
|
1352
1487
|
|
|
1353
1488
|
def force_update(self):
|
|
1489
|
+
if self._closing:
|
|
1490
|
+
# e.g. an event handler on a closed tree routing an exception to us
|
|
1491
|
+
return
|
|
1492
|
+
# a forced update re-walks the whole tree, no subtree skipping
|
|
1493
|
+
self._walk_all = True
|
|
1354
1494
|
if not self._is_rendering:
|
|
1355
1495
|
self.render(self.element, self.container)
|
|
1356
1496
|
|
|
@@ -1374,6 +1514,7 @@ class _RenderContext:
|
|
|
1374
1514
|
self.context.effect_index += 1
|
|
1375
1515
|
|
|
1376
1516
|
def update(self, element: Element):
|
|
1517
|
+
self._walk_all = True
|
|
1377
1518
|
if self._is_rendering:
|
|
1378
1519
|
self.element = element
|
|
1379
1520
|
self._rerender_needed_reasons.append(RerenderReason(reason="root element changed"))
|
|
@@ -1430,6 +1571,7 @@ class _RenderContext:
|
|
|
1430
1571
|
self._shared_elements_next = set()
|
|
1431
1572
|
self._render(self.element, "/", parent_key=ROOT_KEY)
|
|
1432
1573
|
self.first_render = False
|
|
1574
|
+
self._walk_all = False
|
|
1433
1575
|
except BaseException:
|
|
1434
1576
|
self._is_rendering = False
|
|
1435
1577
|
raise
|
|
@@ -1464,6 +1606,7 @@ class _RenderContext:
|
|
|
1464
1606
|
self.context.exceptions_self = []
|
|
1465
1607
|
|
|
1466
1608
|
self._render(self.element, "/", parent_key=ROOT_KEY)
|
|
1609
|
+
self._walk_all = False
|
|
1467
1610
|
logger.info("Render done: %r %r", self._rerender_needed, self._rerender_needed_reasons[-1])
|
|
1468
1611
|
assert self.context is self.context_root
|
|
1469
1612
|
render_counts += 1
|
|
@@ -1488,6 +1631,10 @@ class _RenderContext:
|
|
|
1488
1631
|
self.context.root_element = self.context.root_element_next
|
|
1489
1632
|
self.context.root_element_next = None
|
|
1490
1633
|
|
|
1634
|
+
# remove stale elements of the root context itself
|
|
1635
|
+
# (child contexts are swept during their reconciliation)
|
|
1636
|
+
self._remove_stale_root_elements(ROOT_KEY)
|
|
1637
|
+
|
|
1491
1638
|
if self._shared_elements_next:
|
|
1492
1639
|
raise RuntimeError(f"Element not reconsolidated: {self._shared_elements_next}")
|
|
1493
1640
|
logger.debug("Reconsolidate phase resulted in elements:")
|
|
@@ -2194,6 +2341,600 @@ class _RenderContext:
|
|
|
2194
2341
|
else:
|
|
2195
2342
|
return value
|
|
2196
2343
|
|
|
2344
|
+
def _remove_stale_root_elements(self, parent_key):
|
|
2345
|
+
# The original _reconsolidate already removes stale elements itself,
|
|
2346
|
+
# so for the base (original) render context this is a no-op.
|
|
2347
|
+
pass
|
|
2348
|
+
|
|
2349
|
+
|
|
2350
|
+
class _RenderContextFast(_RenderContext):
|
|
2351
|
+
# Opt-in fast reimplementation of the tree-walking, selectable via the
|
|
2352
|
+
# REACTON_FAST=1 environment variable.
|
|
2353
|
+
#
|
|
2354
|
+
# ------------------------------------------------------------------
|
|
2355
|
+
# The tree walking: one render() call alternates two kinds of phases.
|
|
2356
|
+
#
|
|
2357
|
+
# * render phase (_render): execute component functions depth first
|
|
2358
|
+
# and build the next element tree (elements_next/children_next).
|
|
2359
|
+
# * reconciliation phase (_reconsolidate): walk the new element tree,
|
|
2360
|
+
# create/update/close widgets, run effects, and move the *_next
|
|
2361
|
+
# bookkeeping to current.
|
|
2362
|
+
#
|
|
2363
|
+
# A component subtree in which no state changed, no exception is
|
|
2364
|
+
# pending, and whose element is identical to the previous render is
|
|
2365
|
+
# skipped in both phases: setters mark the path from their context up
|
|
2366
|
+
# to the root (ComponentContext.needs_render_descendant), so the walk
|
|
2367
|
+
# only descends where work can exist. _render marks skipped contexts
|
|
2368
|
+
# (clean_subtree) so _reconsolidate can reuse the previous widget
|
|
2369
|
+
# without walking either.
|
|
2370
|
+
# ------------------------------------------------------------------
|
|
2371
|
+
|
|
2372
|
+
def _set_rerender_needed(self, reason: str):
|
|
2373
|
+
self._rerender_needed_reasons.append(RerenderReason(reason=reason))
|
|
2374
|
+
self._rerender_needed = True
|
|
2375
|
+
|
|
2376
|
+
def _render(self, element: Element, default_key: str, parent_key: str):
|
|
2377
|
+
if not isinstance(element, Element):
|
|
2378
|
+
raise TypeError(f"Expected element, not {element}")
|
|
2379
|
+
# for tracking stale elements when using get_widget
|
|
2380
|
+
self._old_element_ids.add(id(element))
|
|
2381
|
+
context = self.context
|
|
2382
|
+
assert context is not None
|
|
2383
|
+
|
|
2384
|
+
if default_key == "/":
|
|
2385
|
+
# the root element of a component determines which keys are in use,
|
|
2386
|
+
# everything else is stale and gets removed during reconciliation
|
|
2387
|
+
context.used_keys.clear()
|
|
2388
|
+
|
|
2389
|
+
el = element
|
|
2390
|
+
key = el._key
|
|
2391
|
+
if key is None:
|
|
2392
|
+
key = default_key
|
|
2393
|
+
el._key_frozen = True
|
|
2394
|
+
|
|
2395
|
+
if key in context.used_keys:
|
|
2396
|
+
if DEBUG:
|
|
2397
|
+
self.tracebacks.append(el.traceback)
|
|
2398
|
+
raise KeyError(f"Duplicate key {key!r}")
|
|
2399
|
+
context.used_keys.add(key)
|
|
2400
|
+
|
|
2401
|
+
if el.is_shared:
|
|
2402
|
+
# a shared element renders a single widget, process it once per phase
|
|
2403
|
+
if el in self._shared_elements_next:
|
|
2404
|
+
return
|
|
2405
|
+
self._shared_elements_next.add(el)
|
|
2406
|
+
|
|
2407
|
+
el_prev = context.elements_next.get(key)
|
|
2408
|
+
if el_prev is None:
|
|
2409
|
+
el_prev = context.elements.get(key)
|
|
2410
|
+
context.elements_next[key] = el
|
|
2411
|
+
el._render_count += 1 # for testing only
|
|
2412
|
+
|
|
2413
|
+
if isinstance(el.component, ComponentWidget):
|
|
2414
|
+
assert not el.args, "no positional args supported for widgets"
|
|
2415
|
+
if key in context.children_next:
|
|
2416
|
+
# a function component lived at this key before
|
|
2417
|
+
del context.children_next[key]
|
|
2418
|
+
# the element arguments are part of this component's element tree
|
|
2419
|
+
if el.kwargs:
|
|
2420
|
+
self._visit_children(el, key, parent_key, self._render)
|
|
2421
|
+
return
|
|
2422
|
+
|
|
2423
|
+
assert isinstance(el.component, ComponentFunction)
|
|
2424
|
+
if el.is_shared and (el.args or el.kwargs):
|
|
2425
|
+
# arguments of a shared element belong to the context it is rendered in;
|
|
2426
|
+
# for non-shared component elements the component function decides
|
|
2427
|
+
# what ends up in the tree
|
|
2428
|
+
self._visit_children(el, key, parent_key, self._render)
|
|
2429
|
+
|
|
2430
|
+
context_previous = context.children_next.get(key)
|
|
2431
|
+
if context_previous is None:
|
|
2432
|
+
context_previous = context.children.get(key)
|
|
2433
|
+
|
|
2434
|
+
if (
|
|
2435
|
+
not self._walk_all
|
|
2436
|
+
and el is el_prev
|
|
2437
|
+
and not el.is_shared
|
|
2438
|
+
and context_previous is not None
|
|
2439
|
+
and context.children.get(key) is context_previous
|
|
2440
|
+
and not context_previous.needs_render
|
|
2441
|
+
and not context_previous.needs_render_descendant
|
|
2442
|
+
and not context_previous.exceptions_self
|
|
2443
|
+
and not context_previous.exceptions_children
|
|
2444
|
+
and context_previous.root_element is not None
|
|
2445
|
+
and context_previous.root_element_next is None
|
|
2446
|
+
):
|
|
2447
|
+
# fast path: same element, no state changes or pending exceptions
|
|
2448
|
+
# anywhere in this subtree, and fully reconciled: the previous
|
|
2449
|
+
# result stands, skip the subtree in both phases
|
|
2450
|
+
context_previous.clean_subtree = True
|
|
2451
|
+
context.children_next[key] = context_previous
|
|
2452
|
+
return
|
|
2453
|
+
|
|
2454
|
+
parent_context = context
|
|
2455
|
+
del context
|
|
2456
|
+
if context_previous is None:
|
|
2457
|
+
context = ComponentContext(parent=parent_context, context_managers=[cm(el) for cm in _component_context_manager_classes])
|
|
2458
|
+
el_prev = None # do not compare against an element of a different component
|
|
2459
|
+
elif context_previous.root_element is None and context_previous.root_element_next is None:
|
|
2460
|
+
# pre-created, carrying initial state (state_set), but never rendered
|
|
2461
|
+
context = context_previous
|
|
2462
|
+
context.context_managers = [cm(el) for cm in _component_context_manager_classes]
|
|
2463
|
+
else:
|
|
2464
|
+
assert context_previous.invoke_element is not None
|
|
2465
|
+
if not same_component(context_previous.invoke_element.component, el.component):
|
|
2466
|
+
# a different component took this key, start fresh
|
|
2467
|
+
# (the old context is cleaned up during reconciliation)
|
|
2468
|
+
context = ComponentContext(parent=parent_context, context_managers=[cm(el) for cm in _component_context_manager_classes])
|
|
2469
|
+
el_prev = None
|
|
2470
|
+
else:
|
|
2471
|
+
context = context_previous
|
|
2472
|
+
context.parent = parent_context
|
|
2473
|
+
context.clean_subtree = False
|
|
2474
|
+
context.invoke_element = el
|
|
2475
|
+
|
|
2476
|
+
needs_render = context.needs_render
|
|
2477
|
+
if not needs_render and el_prev is not None and context_previous is context:
|
|
2478
|
+
assert not isinstance(el_prev.component, ComponentWidget)
|
|
2479
|
+
needs_render = el._arguments_changed(el_prev)
|
|
2480
|
+
if not needs_render and context.exceptions_children:
|
|
2481
|
+
# we have exceptions, so we need to render
|
|
2482
|
+
needs_render = True
|
|
2483
|
+
if not needs_render:
|
|
2484
|
+
assert el_prev is not None
|
|
2485
|
+
|
|
2486
|
+
self.container_adders = []
|
|
2487
|
+
self.context = context
|
|
2488
|
+
render_count_check = self.render_count
|
|
2489
|
+
try:
|
|
2490
|
+
context.exceptions_self = []
|
|
2491
|
+
root_element: Optional[Element] = None
|
|
2492
|
+
if needs_render:
|
|
2493
|
+
context.state_index = 0
|
|
2494
|
+
context.effect_index = 0
|
|
2495
|
+
context.memo_index = 0
|
|
2496
|
+
context.user_contexts = {}
|
|
2497
|
+
context.exception_handler = False
|
|
2498
|
+
# we reset it before calling the component function,
|
|
2499
|
+
# which might set it to true again
|
|
2500
|
+
context.needs_render = False
|
|
2501
|
+
try:
|
|
2502
|
+
with contextlib.ExitStack() as stack:
|
|
2503
|
+
for cm in context.context_managers:
|
|
2504
|
+
stack.enter_context(cm)
|
|
2505
|
+
if _default_container is not None:
|
|
2506
|
+
with _default_container() as container:
|
|
2507
|
+
el.component.render_count += 1
|
|
2508
|
+
root_element = el.component.f(*el.args, **el.kwargs)
|
|
2509
|
+
if root_element is None:
|
|
2510
|
+
if len(container.kwargs["children"]) == 1:
|
|
2511
|
+
root_element = container.kwargs["children"][0]
|
|
2512
|
+
else:
|
|
2513
|
+
root_element = container
|
|
2514
|
+
else:
|
|
2515
|
+
el.component.render_count += 1
|
|
2516
|
+
root_element = el.component.f(*el.args, **el.kwargs)
|
|
2517
|
+
assert root_element is not None
|
|
2518
|
+
except BaseException as e:
|
|
2519
|
+
if DEBUG:
|
|
2520
|
+
# we might be interested in the traceback inside the call...
|
|
2521
|
+
if len(self.tracebacks) == 0:
|
|
2522
|
+
assert e.__traceback__ is not None
|
|
2523
|
+
traceback = cast(TracebackType, e.__traceback__)
|
|
2524
|
+
if traceback.tb_next: # is there an error inside the call
|
|
2525
|
+
self.tracebacks.append(traceback.tb_next)
|
|
2526
|
+
self.tracebacks.append(el.traceback)
|
|
2527
|
+
logger.exception("Component %r raised exception %r", el.component, e)
|
|
2528
|
+
context.exceptions_self.append(e)
|
|
2529
|
+
self._set_rerender_needed("Exception ocurred during render")
|
|
2530
|
+
context.needs_render = True
|
|
2531
|
+
if root_element is None and not context.exceptions_self:
|
|
2532
|
+
raise ValueError(f"Component {el.component} returned None")
|
|
2533
|
+
else:
|
|
2534
|
+
root_element = context.root_element_next or context.root_element
|
|
2535
|
+
|
|
2536
|
+
if self.render_count != render_count_check:
|
|
2537
|
+
raise RuntimeError("Recursive render detected, possible a bug in react")
|
|
2538
|
+
|
|
2539
|
+
# the subtree walk below will mark this again when state changes
|
|
2540
|
+
context.needs_render_descendant = False
|
|
2541
|
+
if root_element is not None:
|
|
2542
|
+
self._render(root_element, "/", parent_key=join_key(parent_key, key)) # depth first
|
|
2543
|
+
context.root_element_next = root_element
|
|
2544
|
+
elif el.is_shared:
|
|
2545
|
+
self._shared_elements_next.remove(el)
|
|
2546
|
+
|
|
2547
|
+
try:
|
|
2548
|
+
# when an exception interrupted the component function, fewer hook calls are allowed
|
|
2549
|
+
if not ((context.effect_index == len(context.effects)) or (context.exceptions_self and context.effect_index <= len(context.effects))):
|
|
2550
|
+
raise RuntimeError(
|
|
2551
|
+
f"Previously render had {len(context.effects)} effects, this run {context.effect_index} "
|
|
2552
|
+
f"(in element/component: {el}/{el.component}). "
|
|
2553
|
+
"Are you using conditional hooks?"
|
|
2554
|
+
)
|
|
2555
|
+
if not ((context.memo_index == len(context.memo)) or (context.exceptions_self and context.memo_index <= len(context.memo))):
|
|
2556
|
+
raise RuntimeError(
|
|
2557
|
+
f"Previously render had {len(context.memo)} calls to use_memo, this run {context.memo_index} "
|
|
2558
|
+
f"(in element/component: {el}/{el.component}). "
|
|
2559
|
+
"Are you using conditional hooks?"
|
|
2560
|
+
)
|
|
2561
|
+
except RuntimeError as e:
|
|
2562
|
+
logger.exception("Exception in hook count check")
|
|
2563
|
+
context.exceptions_self.append(e)
|
|
2564
|
+
self._set_rerender_needed("Exception ocurred during render (hook count check)")
|
|
2565
|
+
context.needs_render = True
|
|
2566
|
+
|
|
2567
|
+
# only expose to the parent when we get this far
|
|
2568
|
+
parent_context.children_next[key] = context
|
|
2569
|
+
# drop children/elements from a previous render pass that are no longer used
|
|
2570
|
+
used_keys = context.used_keys
|
|
2571
|
+
for unused in [k for k in context.children_next if k not in used_keys]:
|
|
2572
|
+
del context.children_next[unused]
|
|
2573
|
+
for unused in [k for k in context.elements_next if k not in used_keys]:
|
|
2574
|
+
del context.elements_next[unused]
|
|
2575
|
+
context.user_contexts_prev = context.user_contexts
|
|
2576
|
+
finally:
|
|
2577
|
+
assert context.parent is parent_context
|
|
2578
|
+
self.context = parent_context
|
|
2579
|
+
if context.exceptions_self or context.exceptions_children and not context.exception_handler:
|
|
2580
|
+
# this component does not handle the exceptions, bubble up
|
|
2581
|
+
parent_context.exceptions_children.extend(context.exceptions_self)
|
|
2582
|
+
parent_context.exceptions_children.extend(context.exceptions_children)
|
|
2583
|
+
if context.exceptions_self or context.exceptions_children:
|
|
2584
|
+
# make sure the next render pass walks down to this context
|
|
2585
|
+
# (e.g. so a parent with use_exception gets a chance to handle it)
|
|
2586
|
+
_mark_needs_render_ancestors(context)
|
|
2587
|
+
if parent_context.exceptions_self or parent_context.exceptions_children:
|
|
2588
|
+
if not self._rerender_needed:
|
|
2589
|
+
# this happens when an exception was added from an event handler:
|
|
2590
|
+
# no exception was raised during this render phase, but we still
|
|
2591
|
+
# need to rerender until a component catches the exception
|
|
2592
|
+
self._set_rerender_needed("Exception ocurred during render")
|
|
2593
|
+
parent_context.needs_render = True
|
|
2594
|
+
|
|
2595
|
+
def _reconsolidate(self, el: Element, default_key: str, parent_key: str):
|
|
2596
|
+
key = el._key
|
|
2597
|
+
if key is None:
|
|
2598
|
+
key = default_key
|
|
2599
|
+
assert key is not None
|
|
2600
|
+
context = self.context
|
|
2601
|
+
assert context is not None
|
|
2602
|
+
|
|
2603
|
+
if el.is_shared and el in self._shared_elements and el is not self.element:
|
|
2604
|
+
# shared elements reconcile once, all other uses share the widget
|
|
2605
|
+
return self._shared_widgets[el]
|
|
2606
|
+
|
|
2607
|
+
el_prev = context.elements.get(key)
|
|
2608
|
+
try:
|
|
2609
|
+
if isinstance(el.component, ComponentFunction):
|
|
2610
|
+
child_context_next = context.children_next.get(key)
|
|
2611
|
+
if child_context_next is not None and child_context_next.clean_subtree:
|
|
2612
|
+
# subtree was skipped during the render phase: the previous
|
|
2613
|
+
# reconciliation result stands
|
|
2614
|
+
child_context_next.clean_subtree = False
|
|
2615
|
+
context.children[key] = context.children_next.pop(key)
|
|
2616
|
+
return context.widgets[key]
|
|
2617
|
+
|
|
2618
|
+
if el_prev and isinstance(el_prev.component, ComponentWidget):
|
|
2619
|
+
# a widget element was replaced by a component element at this key
|
|
2620
|
+
self._remove_element(el_prev, default_key=key, parent_key=parent_key)
|
|
2621
|
+
new_parent_key = join_key(parent_key, key)
|
|
2622
|
+
try:
|
|
2623
|
+
if el.is_shared and (el.args or el.kwargs):
|
|
2624
|
+
self._visit_children(el, key, parent_key, self._reconsolidate)
|
|
2625
|
+
assert self.context is context
|
|
2626
|
+
|
|
2627
|
+
child_context_prev = context.children.get(key)
|
|
2628
|
+
child_context = context.children_next[key]
|
|
2629
|
+
if child_context_prev is not None and child_context_prev is not child_context:
|
|
2630
|
+
# the component type changed, remove the old subtree
|
|
2631
|
+
assert el_prev is not None, "prev child is not None, but element is"
|
|
2632
|
+
self._remove_element(el_prev, default_key=key, parent_key=parent_key)
|
|
2633
|
+
|
|
2634
|
+
self.context = child_context
|
|
2635
|
+
assert child_context.root_element_next
|
|
2636
|
+
widget = self._reconsolidate(child_context.root_element_next, "/", new_parent_key)
|
|
2637
|
+
child_context.root_element = child_context.root_element_next
|
|
2638
|
+
child_context.root_element_next = None
|
|
2639
|
+
# merge the meta of the component element onto the root widget,
|
|
2640
|
+
# e.g. SomeComponent().meta(name="a") should appear on the widget
|
|
2641
|
+
if el._meta or getattr(widget, "_react_meta", {}):
|
|
2642
|
+
widget._react_meta = {**getattr(widget, "_react_meta", {}), **el._meta}
|
|
2643
|
+
|
|
2644
|
+
if el.is_shared:
|
|
2645
|
+
self._shared_widgets[el] = widget
|
|
2646
|
+
else:
|
|
2647
|
+
context.widgets[key] = widget
|
|
2648
|
+
|
|
2649
|
+
# remove elements that are no longer part of this component's tree
|
|
2650
|
+
# NOTE: sorted for reproducibility
|
|
2651
|
+
stale_keys = sorted(set(child_context.elements) - child_context.used_keys)
|
|
2652
|
+
if stale_keys:
|
|
2653
|
+
logger.info("elements to be removed: %r", stale_keys)
|
|
2654
|
+
for stale_key in stale_keys:
|
|
2655
|
+
if stale_key in child_context.elements:
|
|
2656
|
+
self._remove_element(child_context.elements[stale_key], stale_key, new_parent_key)
|
|
2657
|
+
|
|
2658
|
+
self._process_effects(child_context, context)
|
|
2659
|
+
|
|
2660
|
+
if child_context.children_next:
|
|
2661
|
+
# if we had two render phases, we can have old contexts left over
|
|
2662
|
+
child_context.children_next.clear()
|
|
2663
|
+
if child_context.elements_next:
|
|
2664
|
+
# we can still have elements that are not used as a 'widget' in this
|
|
2665
|
+
# context, but are passed down as an element (when shared)
|
|
2666
|
+
unreferenced = []
|
|
2667
|
+
for child_key, child_el in list(child_context.elements_next.items()):
|
|
2668
|
+
if child_el not in self._shared_elements:
|
|
2669
|
+
unreferenced.append(child_el)
|
|
2670
|
+
else:
|
|
2671
|
+
child_context.elements[child_key] = child_context.elements_next.pop(child_key)
|
|
2672
|
+
if unreferenced:
|
|
2673
|
+
raise RuntimeError(f"Unused elements and unreferenced elements {unreferenced}")
|
|
2674
|
+
if child_context.exceptions_self or child_context.exceptions_children and not child_context.exception_handler:
|
|
2675
|
+
# child does not handle exceptions, so bubble up
|
|
2676
|
+
context.exceptions_children.extend(child_context.exceptions_self)
|
|
2677
|
+
context.exceptions_children.extend(child_context.exceptions_children)
|
|
2678
|
+
finally:
|
|
2679
|
+
# restore context
|
|
2680
|
+
self.context = context
|
|
2681
|
+
context.children[key] = context.children_next.pop(key)
|
|
2682
|
+
else:
|
|
2683
|
+
assert isinstance(el.component, ComponentWidget)
|
|
2684
|
+
if el.args:
|
|
2685
|
+
raise TypeError("Widget element only take keyword arguments")
|
|
2686
|
+
|
|
2687
|
+
widget_previous = None
|
|
2688
|
+
if el_prev is not None:
|
|
2689
|
+
if el_prev.is_shared:
|
|
2690
|
+
widget_previous = self._shared_widgets[el_prev]
|
|
2691
|
+
else:
|
|
2692
|
+
widget_previous = context.widgets[key]
|
|
2693
|
+
|
|
2694
|
+
orphan_ids: Set[str] = set()
|
|
2695
|
+
if widget_previous is None:
|
|
2696
|
+
# initial create
|
|
2697
|
+
kwargs = self._visit_children_values(el.kwargs, key, parent_key, self._reconsolidate)
|
|
2698
|
+
if el.is_shared and el in self._shared_widgets:
|
|
2699
|
+
raise RuntimeError(f"Element ({el}) was already in self._shared_widgets")
|
|
2700
|
+
widget = None
|
|
2701
|
+
if not context.exceptions_children:
|
|
2702
|
+
try:
|
|
2703
|
+
widget, orphan_ids = el._create_widget(kwargs)
|
|
2704
|
+
except BaseException as e:
|
|
2705
|
+
context.exceptions_self.append(e)
|
|
2706
|
+
self._set_rerender_needed("Exception ocurred during reconciliation (creating widget)")
|
|
2707
|
+
_mark_needs_render_ancestors(context)
|
|
2708
|
+
self._store_widget(context, el, key, widget)
|
|
2709
|
+
elif el_prev is not None and el_prev.component == el.component:
|
|
2710
|
+
# update the existing widget in place
|
|
2711
|
+
kwargs = self._visit_children_values(el.kwargs, key, parent_key, self._reconsolidate)
|
|
2712
|
+
if not context.exceptions_children:
|
|
2713
|
+
if el is not el_prev or not _values_identical(kwargs, el.kwargs):
|
|
2714
|
+
try:
|
|
2715
|
+
el._update_widget(widget_previous, el_prev, kwargs)
|
|
2716
|
+
except BaseException as e:
|
|
2717
|
+
context.exceptions_self.append(e)
|
|
2718
|
+
self._set_rerender_needed("Exception ocurred during reconciliation (updating widget)")
|
|
2719
|
+
_mark_needs_render_ancestors(context)
|
|
2720
|
+
# else: identical element and all children reconciled to the
|
|
2721
|
+
# same widgets, nothing can have changed
|
|
2722
|
+
self._store_widget(context, el, key, widget_previous)
|
|
2723
|
+
else:
|
|
2724
|
+
assert el_prev is not None, "widget_previous is not None, but el_prev is"
|
|
2725
|
+
# a different widget type at the same key: replace
|
|
2726
|
+
self._remove_element(el_prev, key, parent_key=parent_key)
|
|
2727
|
+
kwargs = self._visit_children_values(el.kwargs, key, parent_key, self._reconsolidate)
|
|
2728
|
+
widget = None
|
|
2729
|
+
if not context.exceptions_children:
|
|
2730
|
+
try:
|
|
2731
|
+
widget, orphan_ids = el._create_widget(kwargs)
|
|
2732
|
+
except BaseException as e:
|
|
2733
|
+
context.exceptions_self.append(e)
|
|
2734
|
+
self._set_rerender_needed("Exception ocurred during reconciliation (updating widget)")
|
|
2735
|
+
_mark_needs_render_ancestors(context)
|
|
2736
|
+
self._store_widget(context, el, key, widget)
|
|
2737
|
+
if orphan_ids:
|
|
2738
|
+
# widgets created as a side effect (like Layout and Style); we track
|
|
2739
|
+
# them by model id (not all widgets are hashable) for cleanup
|
|
2740
|
+
orphan_widgets = set([_get_widgets_dict()[k] for k in orphan_ids])
|
|
2741
|
+
for orphan_widget in orphan_widgets:
|
|
2742
|
+
# these are shared between widgets
|
|
2743
|
+
if orphan_widget.__class__.__name__ == "Template" and orphan_widget.__class__.__module__ == "ipyvue.Template":
|
|
2744
|
+
orphan_ids -= {orphan_widget.model_id}
|
|
2745
|
+
widget = self._shared_widgets[el] if el.is_shared else context.widgets[key]
|
|
2746
|
+
if widget.model_id not in self._orphans:
|
|
2747
|
+
self._orphans[widget.model_id] = set()
|
|
2748
|
+
self._orphans[widget.model_id].update(orphan_ids)
|
|
2749
|
+
|
|
2750
|
+
if el.is_shared:
|
|
2751
|
+
widget = self._shared_widgets[el]
|
|
2752
|
+
else:
|
|
2753
|
+
widget = context.widgets[key]
|
|
2754
|
+
# used in get_widget
|
|
2755
|
+
if el_prev in context.element_to_widget:
|
|
2756
|
+
del context.element_to_widget[el_prev]
|
|
2757
|
+
context.element_to_widget[el] = widget
|
|
2758
|
+
return widget
|
|
2759
|
+
except Exception as e:
|
|
2760
|
+
if DEBUG:
|
|
2761
|
+
# we don't care about the traceback of the root element
|
|
2762
|
+
if self.element is not el:
|
|
2763
|
+
# we might be interested in the traceback inside the call...
|
|
2764
|
+
if len(self.tracebacks) == 0:
|
|
2765
|
+
assert e.__traceback__ is not None
|
|
2766
|
+
traceback = cast(TracebackType, e.__traceback__)
|
|
2767
|
+
if traceback.tb_next: # is there an error inside the call
|
|
2768
|
+
self.tracebacks.append(traceback.tb_next)
|
|
2769
|
+
self.tracebacks.append(el.traceback)
|
|
2770
|
+
raise
|
|
2771
|
+
finally:
|
|
2772
|
+
# this marks the work as 'done'
|
|
2773
|
+
context.elements[key] = context.elements_next.pop(key)
|
|
2774
|
+
if el_prev in self._shared_elements:
|
|
2775
|
+
self._shared_elements.remove(el_prev)
|
|
2776
|
+
if el.is_shared:
|
|
2777
|
+
assert el not in self._shared_elements
|
|
2778
|
+
self._shared_elements.add(el)
|
|
2779
|
+
assert el in self._shared_elements_next
|
|
2780
|
+
self._shared_elements_next.remove(el)
|
|
2781
|
+
|
|
2782
|
+
def _process_effects(self, child_context: "ComponentContext", context: "ComponentContext"):
|
|
2783
|
+
# NOTE: effect/cleanup exceptions are recorded on the context of the
|
|
2784
|
+
# component's *parent* (`context`), unlike render exceptions: this is
|
|
2785
|
+
# historical behavior (see notes.md)
|
|
2786
|
+
for effect_index, effect in enumerate(child_context.effects):
|
|
2787
|
+
if effect.next:
|
|
2788
|
+
# if we have a next, the effect itself already executed
|
|
2789
|
+
# TODO: custom equals
|
|
2790
|
+
if effect.next.dependencies is not None and utils.equals(effect.dependencies, effect.next.dependencies):
|
|
2791
|
+
# dependencies are the same, just remove the reference
|
|
2792
|
+
effect.next = None
|
|
2793
|
+
continue
|
|
2794
|
+
# dependencies changed: cleanup and execute next
|
|
2795
|
+
if not effect._cleaned_up:
|
|
2796
|
+
try:
|
|
2797
|
+
effect.cleanup()
|
|
2798
|
+
except BaseException as e:
|
|
2799
|
+
context.exceptions_self.append(e)
|
|
2800
|
+
self._set_rerender_needed("Exception ocurred during effect")
|
|
2801
|
+
_mark_needs_render_ancestors(context)
|
|
2802
|
+
context.needs_render = True
|
|
2803
|
+
effect = child_context.effects[effect_index] = effect.next
|
|
2804
|
+
if child_context.exceptions_self or child_context.exceptions_children:
|
|
2805
|
+
# we had an exception, so we skip the effect (not the cleanup)
|
|
2806
|
+
continue
|
|
2807
|
+
try:
|
|
2808
|
+
effect()
|
|
2809
|
+
except BaseException as e:
|
|
2810
|
+
context.exceptions_self.append(e)
|
|
2811
|
+
self._set_rerender_needed("Exception ocurred during effect")
|
|
2812
|
+
_mark_needs_render_ancestors(context)
|
|
2813
|
+
context.needs_render = True
|
|
2814
|
+
|
|
2815
|
+
def _store_widget(self, context: "ComponentContext", el: Element, key: str, widget: Optional[widgets.Widget]):
|
|
2816
|
+
if el.is_shared:
|
|
2817
|
+
self._shared_widgets[el] = widget
|
|
2818
|
+
else:
|
|
2819
|
+
context.widgets[key] = widget
|
|
2820
|
+
|
|
2821
|
+
def _remove_element(self, el: Element, default_key: str, parent_key):
|
|
2822
|
+
key = el._key
|
|
2823
|
+
if key is None:
|
|
2824
|
+
key = default_key
|
|
2825
|
+
assert key is not None
|
|
2826
|
+
context = self.context
|
|
2827
|
+
assert context is not None
|
|
2828
|
+
|
|
2829
|
+
if el.is_shared:
|
|
2830
|
+
if el not in self._shared_elements:
|
|
2831
|
+
# another use of this element keeps it alive, or it was already removed
|
|
2832
|
+
return
|
|
2833
|
+
self._shared_elements.remove(el)
|
|
2834
|
+
|
|
2835
|
+
if key not in context.elements:
|
|
2836
|
+
# already removed, e.g. as part of the removal of a parent element
|
|
2837
|
+
return
|
|
2838
|
+
|
|
2839
|
+
if isinstance(el.component, ComponentFunction):
|
|
2840
|
+
if el.is_shared:
|
|
2841
|
+
self._visit_children(el, key, parent_key, self._remove_element)
|
|
2842
|
+
child_context = context.children[key]
|
|
2843
|
+
try:
|
|
2844
|
+
self.context = child_context
|
|
2845
|
+
# the element is going away, pending exceptions only matter if cleanup fails
|
|
2846
|
+
child_context.exceptions_self = []
|
|
2847
|
+
child_context.exceptions_children = []
|
|
2848
|
+
for effect in child_context.effects:
|
|
2849
|
+
try:
|
|
2850
|
+
if not effect._cleaned_up:
|
|
2851
|
+
effect.cleanup()
|
|
2852
|
+
except BaseException as e:
|
|
2853
|
+
child_context.exceptions_self.append(e)
|
|
2854
|
+
self._set_rerender_needed("Exception ocurred during effect")
|
|
2855
|
+
_mark_needs_render_ancestors(child_context)
|
|
2856
|
+
assert child_context.root_element is not None
|
|
2857
|
+
self._remove_element(child_context.root_element, "/", parent_key=join_key(parent_key, key))
|
|
2858
|
+
finally:
|
|
2859
|
+
try:
|
|
2860
|
+
assert not child_context.elements, f"left over elements {child_context.elements}"
|
|
2861
|
+
assert not child_context.element_to_widget, f"left over element_to_widget {child_context.element_to_widget}"
|
|
2862
|
+
assert not child_context.widgets, f"left over widgets {child_context.widgets}"
|
|
2863
|
+
assert not child_context.children, f"left over children {child_context.children}"
|
|
2864
|
+
assert not child_context.owns, f"left over owns {child_context.owns}"
|
|
2865
|
+
except Exception as e:
|
|
2866
|
+
child_context.exceptions_self.append(e)
|
|
2867
|
+
# restore context
|
|
2868
|
+
self.context = context
|
|
2869
|
+
if child_context.exceptions_self or child_context.exceptions_children and not child_context.exception_handler:
|
|
2870
|
+
# child does not handle exceptions, so bubble up
|
|
2871
|
+
context.exceptions_children.extend(child_context.exceptions_self)
|
|
2872
|
+
context.exceptions_children.extend(child_context.exceptions_children)
|
|
2873
|
+
del context.children[key]
|
|
2874
|
+
else:
|
|
2875
|
+
self._visit_children(el, key, parent_key, self._remove_element)
|
|
2876
|
+
widget = self._shared_widgets[el] if el.is_shared else context.widgets[key]
|
|
2877
|
+
if widget is not None:
|
|
2878
|
+
assert widget.comm is not None
|
|
2879
|
+
assert widget.model_id in _get_widgets_dict()
|
|
2880
|
+
for orphan in self._orphans.get(widget.model_id, set()):
|
|
2881
|
+
orphan_widget = _get_widgets_dict().get(orphan)
|
|
2882
|
+
if orphan_widget:
|
|
2883
|
+
close_widget(orphan_widget)
|
|
2884
|
+
self._orphans.pop(widget.model_id, None)
|
|
2885
|
+
el._cleanup_callbacks(widget)
|
|
2886
|
+
el._close_widget(widget)
|
|
2887
|
+
if el.is_shared:
|
|
2888
|
+
del self._shared_widgets[el]
|
|
2889
|
+
else:
|
|
2890
|
+
del context.widgets[key]
|
|
2891
|
+
# elements can be removed multiple times, since they can be added multiple times
|
|
2892
|
+
# (even non-shared elements can)
|
|
2893
|
+
if el in context.element_to_widget:
|
|
2894
|
+
del context.element_to_widget[el]
|
|
2895
|
+
del context.elements[key]
|
|
2896
|
+
|
|
2897
|
+
def _visit_children(self, el: Element, default_key: str, parent_key: str, f: Callable):
|
|
2898
|
+
key = el._key
|
|
2899
|
+
if key is None:
|
|
2900
|
+
key = default_key
|
|
2901
|
+
assert key is not None
|
|
2902
|
+
self._visit_children_values(el.kwargs, key, parent_key, f)
|
|
2903
|
+
self._visit_children_values(el.args, key, parent_key, f)
|
|
2904
|
+
|
|
2905
|
+
def _visit_children_values(self, value: Any, key: str, parent_key: str, f: Callable):
|
|
2906
|
+
if isinstance(value, Element):
|
|
2907
|
+
return f(value, key, parent_key)
|
|
2908
|
+
elif isinstance(value, (list, tuple)):
|
|
2909
|
+
values = []
|
|
2910
|
+
for index, v in enumerate(value):
|
|
2911
|
+
new_value = self._visit_children_values(v, f"{key}{index}/", parent_key, f)
|
|
2912
|
+
if isinstance(new_value, FragmentWidget):
|
|
2913
|
+
values.extend(new_value.children)
|
|
2914
|
+
else:
|
|
2915
|
+
values.append(new_value)
|
|
2916
|
+
if isinstance(value, tuple):
|
|
2917
|
+
return tuple(values)
|
|
2918
|
+
return values
|
|
2919
|
+
elif isinstance(value, dict):
|
|
2920
|
+
return {k: self._visit_children_values(v, f"{key}{k}/", parent_key, f) for k, v in value.items()}
|
|
2921
|
+
else:
|
|
2922
|
+
return value
|
|
2923
|
+
|
|
2924
|
+
def _remove_stale_root_elements(self, parent_key):
|
|
2925
|
+
# remove stale elements of the root context itself
|
|
2926
|
+
# (child contexts are swept during their reconciliation)
|
|
2927
|
+
stale_keys = sorted(set(self.context_root.elements) - self.context_root.used_keys)
|
|
2928
|
+
for stale_key in stale_keys:
|
|
2929
|
+
if stale_key in self.context_root.elements:
|
|
2930
|
+
self._remove_element(self.context_root.elements[stale_key], stale_key, parent_key)
|
|
2931
|
+
|
|
2932
|
+
|
|
2933
|
+
def _render_context_class():
|
|
2934
|
+
import os
|
|
2935
|
+
|
|
2936
|
+
return _RenderContextFast if os.environ.get("REACTON_FAST") == "1" else _RenderContext
|
|
2937
|
+
|
|
2197
2938
|
|
|
2198
2939
|
@overload
|
|
2199
2940
|
def render(
|
|
@@ -2209,14 +2950,14 @@ def render(
|
|
|
2209
2950
|
|
|
2210
2951
|
def render(element: Element[T], container: widgets.Widget = None, children_trait="children", handle_error: bool = True, initial_state=None):
|
|
2211
2952
|
container = container or widgets.VBox()
|
|
2212
|
-
_rc =
|
|
2953
|
+
_rc = _render_context_class()(element, container, children_trait=children_trait, handle_error=handle_error, initial_state=initial_state)
|
|
2213
2954
|
_rc.render(element, _rc.container)
|
|
2214
2955
|
local.last_rc = weakref.ref(_rc)
|
|
2215
2956
|
return container, _rc
|
|
2216
2957
|
|
|
2217
2958
|
|
|
2218
2959
|
def render_fixed(element: Element[T], handle_error: bool = True) -> Tuple[T, _RenderContext]:
|
|
2219
|
-
_rc =
|
|
2960
|
+
_rc = _render_context_class()(element, handle_error=handle_error)
|
|
2220
2961
|
widget = _rc.render(element)
|
|
2221
2962
|
local.last_rc = weakref.ref(_rc)
|
|
2222
2963
|
return widget, _rc
|
|
@@ -3162,6 +3162,30 @@ def test_memory_leak(component):
|
|
|
3162
3162
|
assert weak_rc() is None
|
|
3163
3163
|
|
|
3164
3164
|
|
|
3165
|
+
@reacton.component
|
|
3166
|
+
def UseStateComponent():
|
|
3167
|
+
value, set_value = reacton.use_state(0)
|
|
3168
|
+
return w.Button(description=str(value), on_click=lambda: set_value(value + 1))
|
|
3169
|
+
|
|
3170
|
+
|
|
3171
|
+
@pytest.mark.parametrize("component", [w.Button, ButtonComponentFunction, UseMemoComponent, UseStateComponent])
|
|
3172
|
+
def test_no_reference_cycles_after_close(component):
|
|
3173
|
+
# Stricter than test_memory_leak: the closed render tree must be freed by plain
|
|
3174
|
+
# reference counting, without a garbage collection. If this fails, a closure (a
|
|
3175
|
+
# use_state setter or an event handler) holds the render context strongly and the
|
|
3176
|
+
# tree became one big reference cycle that only a gen-2 gc can free - on a server
|
|
3177
|
+
# that shows up as a memory sawtooth that scales with per-kernel state.
|
|
3178
|
+
box, rc = react.render(component(), handle_error=False)
|
|
3179
|
+
rc.close()
|
|
3180
|
+
weak_rc = weakref.ref(rc)
|
|
3181
|
+
gc.disable()
|
|
3182
|
+
try:
|
|
3183
|
+
del rc
|
|
3184
|
+
assert weak_rc() is None
|
|
3185
|
+
finally:
|
|
3186
|
+
gc.enable()
|
|
3187
|
+
|
|
3188
|
+
|
|
3165
3189
|
def test_fragment():
|
|
3166
3190
|
@reacton.component
|
|
3167
3191
|
def Children():
|
|
@@ -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.1"
|
|
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
|