reacton 1.10.4__tar.gz → 1.11.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.10.4 → reacton-1.11.0}/PKG-INFO +1 -1
- reacton-1.11.0/reacton/_version.py +1 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/core.py +182 -97
- {reacton-1.10.4 → reacton-1.11.0}/reacton/core_test.py +192 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/fuzz_test.py +34 -3
- reacton-1.11.0/reacton/threads_test.py +562 -0
- reacton-1.10.4/reacton/_version.py +0 -1
- {reacton-1.10.4 → reacton-1.11.0}/.gitignore +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/LICENSE +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/pyproject.toml +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/react_ipywidgets/__init__.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/react_ipywidgets/core.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/__init__.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/bqplot.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/deprecated/__init__.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/deprecated/ipyvuetify.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/find.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/find_test.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/generate.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/generate_test.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/ipycanvas.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/ipyvue.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/ipyvue_test.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/ipyvuetify.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/ipywidgets.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/logging.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/patch.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/patch_display.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/py.typed +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/rx.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/test.vue +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/test_rx.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/utils.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/utils_test.py +0 -0
- {reacton-1.10.4 → reacton-1.11.0}/reacton/work.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.11.0"
|
|
@@ -88,30 +88,43 @@ _get_widgets_dict = _widgets_dict_getter()
|
|
|
88
88
|
# via the widget constructed hook: diffing the global widgets dict per widget
|
|
89
89
|
# creation is O(total widgets), and solara replaces that dict with a context
|
|
90
90
|
# aware mapping we should not depend on.
|
|
91
|
-
|
|
91
|
+
# The recording is per thread, so no lock is held while a widget is constructed:
|
|
92
|
+
# construction opens a comm, and in a server that send can wait for a slow client,
|
|
93
|
+
# which with a process wide lock stalled widget creation on every other thread.
|
|
94
|
+
class _ConstructionRecording(threading.local):
|
|
95
|
+
# where the widgets constructed on this thread go, between _start_recording_constructed and
|
|
96
|
+
# _stop_recording_constructed
|
|
97
|
+
constructed: Optional[List["widgets.Widget"]] = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
_construction_recording = _ConstructionRecording()
|
|
92
101
|
_chained_construction_callback: Optional[Callable] = None
|
|
93
102
|
|
|
94
103
|
|
|
95
104
|
def _record_constructed_widget(widget: "widgets.Widget"):
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
constructed = _construction_recording.constructed
|
|
106
|
+
if constructed is not None:
|
|
107
|
+
constructed.append(widget)
|
|
98
108
|
if _chained_construction_callback is not None:
|
|
99
109
|
_chained_construction_callback(widget)
|
|
100
110
|
|
|
101
111
|
|
|
102
|
-
def _start_recording_constructed(recording: List["widgets.Widget"]):
|
|
103
|
-
|
|
112
|
+
def _start_recording_constructed(recording: List["widgets.Widget"]) -> Optional[List["widgets.Widget"]]:
|
|
113
|
+
"""Record the widgets this thread constructs; returns what to pass to _stop_recording_constructed."""
|
|
114
|
+
global _chained_construction_callback
|
|
104
115
|
current = getattr(widgets.Widget, "_widget_construction_callback", None)
|
|
105
116
|
if current is not _record_constructed_widget:
|
|
106
117
|
# first time, or someone else registered a callback after us: chain it
|
|
107
118
|
_chained_construction_callback = current
|
|
108
119
|
widgets.Widget.on_widget_constructed(_record_constructed_widget)
|
|
109
|
-
|
|
120
|
+
# a widget whose constructor renders records in its own list; the outer recording continues afterwards
|
|
121
|
+
previous = _construction_recording.constructed
|
|
122
|
+
_construction_recording.constructed = recording
|
|
123
|
+
return previous
|
|
110
124
|
|
|
111
125
|
|
|
112
|
-
def _stop_recording_constructed():
|
|
113
|
-
|
|
114
|
-
_construction_recording = None
|
|
126
|
+
def _stop_recording_constructed(previous: Optional[List["widgets.Widget"]]):
|
|
127
|
+
_construction_recording.constructed = previous
|
|
115
128
|
|
|
116
129
|
|
|
117
130
|
_last_rc = None # used for testing
|
|
@@ -334,7 +347,6 @@ class Element(Generic[W]):
|
|
|
334
347
|
# to make every unique on_value callback to a unique wrapper
|
|
335
348
|
# so that we can remove the listeners
|
|
336
349
|
_callback_wrappers: Dict[Tuple[str, str, Callable], Callable] = {}
|
|
337
|
-
create_lock: ContextManager = threading.Lock()
|
|
338
350
|
_shared = False
|
|
339
351
|
|
|
340
352
|
def __init__(self, component, args=None, kwargs=None):
|
|
@@ -499,32 +511,30 @@ class Element(Generic[W]):
|
|
|
499
511
|
# we can't use our own kwarg, since that contains elements, not widgets
|
|
500
512
|
kwargs, listeners = self._split_kwargs(kwargs)
|
|
501
513
|
assert isinstance(self.component, ComponentWidget)
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
_start_recording_constructed(recorded)
|
|
514
|
+
rc = get_render_context(required=True)
|
|
515
|
+
recorded: List[widgets.Widget] = []
|
|
516
|
+
previous_recording = _start_recording_constructed(recorded)
|
|
517
|
+
try:
|
|
507
518
|
try:
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
_stop_recording_constructed()
|
|
519
|
+
widget = self.component.widget(**kwargs)
|
|
520
|
+
hold_trait_notifications = widget.hold_trait_notifications
|
|
521
|
+
|
|
522
|
+
@contextlib.contextmanager
|
|
523
|
+
def hold_trait_notifications_extra(*args, **kwargs):
|
|
524
|
+
with rc, hold_trait_notifications(*args, **kwargs):
|
|
525
|
+
yield
|
|
526
|
+
|
|
527
|
+
widget.hold_trait_notifications = hold_trait_notifications_extra
|
|
528
|
+
|
|
529
|
+
if self._meta:
|
|
530
|
+
widget._react_meta = dict(self._meta)
|
|
531
|
+
except Exception as e:
|
|
532
|
+
raise RuntimeError(f"Could not create widget {self.component.widget} with {kwargs}") from e
|
|
533
|
+
for name, callback in listeners.items():
|
|
534
|
+
if callback is not None:
|
|
535
|
+
self._add_widget_event_listener(widget, name, callback)
|
|
536
|
+
finally:
|
|
537
|
+
_stop_recording_constructed(previous_recording)
|
|
528
538
|
widgets_dict = _get_widgets_dict()
|
|
529
539
|
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}
|
|
530
540
|
return widget, orphans
|
|
@@ -1306,33 +1316,37 @@ class Effect:
|
|
|
1306
1316
|
|
|
1307
1317
|
class _RenderContext:
|
|
1308
1318
|
context: Optional[ComponentContext] = None
|
|
1309
|
-
# the element that
|
|
1310
|
-
#
|
|
1311
|
-
|
|
1319
|
+
# the element that reconciliation removes because the new tree replaced it by an element
|
|
1320
|
+
# of another type, or no longer has it, and its context (see _remove_outgoing)
|
|
1321
|
+
_outgoing: Optional[Tuple[ComponentContext, Element]] = None
|
|
1312
1322
|
|
|
1313
|
-
def
|
|
1314
|
-
"""Remove the element at key, which
|
|
1323
|
+
def _remove_outgoing(self, el: Element, key: str, parent_key: str):
|
|
1324
|
+
"""Remove the element at key, which the new tree replaced or no longer has."""
|
|
1315
1325
|
assert self.context is not None
|
|
1316
|
-
|
|
1317
|
-
self.
|
|
1326
|
+
outgoing = self._outgoing
|
|
1327
|
+
self._outgoing = (self.context, el)
|
|
1318
1328
|
try:
|
|
1319
|
-
self._remove_element(
|
|
1329
|
+
self._remove_element(el, key, parent_key=parent_key)
|
|
1320
1330
|
finally:
|
|
1321
|
-
self.
|
|
1331
|
+
self._outgoing = outgoing
|
|
1322
1332
|
|
|
1323
1333
|
def _keep_keyed_child(self, context: ComponentContext, el: Element, key: str) -> bool:
|
|
1324
1334
|
# An explicit key is the same in any container of a component, so a child with an
|
|
1325
|
-
# explicit key that the new tree still uses (it moved out of the
|
|
1326
|
-
#
|
|
1327
|
-
#
|
|
1328
|
-
#
|
|
1329
|
-
|
|
1335
|
+
# explicit key that the new tree still uses (it moved out of the outgoing element, for
|
|
1336
|
+
# instance a wrapper that went away, or stays under the element that replaces it) is
|
|
1337
|
+
# not removed with the outgoing element: reconciliation updates it where the new tree
|
|
1338
|
+
# has it. Shared elements keep the old behavior: their bookkeeping (_shared_elements,
|
|
1339
|
+
# _shared_widgets) needs the removal.
|
|
1340
|
+
outgoing = self._outgoing
|
|
1330
1341
|
return (
|
|
1331
|
-
|
|
1342
|
+
outgoing is not None and context is outgoing[0] and el is not outgoing[1] and el._key is not None and not el.is_shared and key in context.used_keys
|
|
1332
1343
|
)
|
|
1333
1344
|
|
|
1334
1345
|
def __init__(self, element: Element, container: widgets.Widget = None, children_trait="children", handle_error: bool = True, initial_state=None):
|
|
1335
1346
|
self.element = element
|
|
1347
|
+
# the last requested root element: the thread that holds the render lock copies it to
|
|
1348
|
+
# self.element at the start of each pass (see render)
|
|
1349
|
+
self._element_next = element
|
|
1336
1350
|
self.container = container
|
|
1337
1351
|
self.children_next_trait = children_trait
|
|
1338
1352
|
self.first_render = True
|
|
@@ -1344,6 +1358,9 @@ class _RenderContext:
|
|
|
1344
1358
|
self.last_root_widget: widgets.Widget = None
|
|
1345
1359
|
self._is_rendering = False
|
|
1346
1360
|
self._rerender_needed = False
|
|
1361
|
+
self._state_set_by_other_thread = False
|
|
1362
|
+
# used for tests only: called when a render loop is done, right after its last look at _rerender_needed
|
|
1363
|
+
self._on_render_loop_done: Optional[Callable[[], None]] = None
|
|
1347
1364
|
# the reasons are only read for the "too many renders" error message, and a reason
|
|
1348
1365
|
# holds the previous and next state value: keeping all of them kept every old state
|
|
1349
1366
|
# value alive until close(). REACTON_RERENDER_REASONS keeps more, for debugging.
|
|
@@ -1399,15 +1416,27 @@ class _RenderContext:
|
|
|
1399
1416
|
_find = find # for backward compatibility
|
|
1400
1417
|
|
|
1401
1418
|
def close(self):
|
|
1419
|
+
# close() waits for the render lock, so it cannot run in its own render: it would wait for itself
|
|
1420
|
+
if self._lock_thread == threading.current_thread():
|
|
1421
|
+
raise RuntimeError("close() called during a render of this render context, current thread: %r" % threading.current_thread())
|
|
1422
|
+
if self._closing:
|
|
1423
|
+
# closed or closing: a second close(), or an effect cleanup that calls close() during close()
|
|
1424
|
+
return
|
|
1402
1425
|
with self.thread_lock:
|
|
1426
|
+
if self._closing:
|
|
1427
|
+
return # another thread closed us while we waited for the lock
|
|
1403
1428
|
self._closing = True
|
|
1404
1429
|
# snapshot the component contexts before _remove_element detaches them from
|
|
1405
1430
|
# their parents: detached contexts would escape the teardown below while the
|
|
1406
1431
|
# setter/handler closures in their state still reference them and us
|
|
1407
|
-
|
|
1432
|
+
# by id: after a failed render, a context can be in both children and children_next, and
|
|
1433
|
+
# a visit per path to it is 2**depth visits
|
|
1434
|
+
all_contexts: Dict[int, ComponentContext] = {}
|
|
1408
1435
|
|
|
1409
1436
|
def collect(context: ComponentContext):
|
|
1410
|
-
|
|
1437
|
+
if id(context) in all_contexts:
|
|
1438
|
+
return
|
|
1439
|
+
all_contexts[id(context)] = context
|
|
1411
1440
|
for child in list(context.children.values()) + list(context.children_next.values()):
|
|
1412
1441
|
collect(child)
|
|
1413
1442
|
|
|
@@ -1432,7 +1461,7 @@ class _RenderContext:
|
|
|
1432
1461
|
exceptions = [*self.context.exceptions_children, *self.context_root.exceptions_self]
|
|
1433
1462
|
# break the reference cycles through the tree (see _teardown_component_context);
|
|
1434
1463
|
# _closing stays True, making stray setters and event handlers no-ops
|
|
1435
|
-
for context in all_contexts:
|
|
1464
|
+
for context in all_contexts.values():
|
|
1436
1465
|
_teardown_component_context(context)
|
|
1437
1466
|
self.context = None
|
|
1438
1467
|
self.context_root = None # type: ignore
|
|
@@ -1440,6 +1469,7 @@ class _RenderContext:
|
|
|
1440
1469
|
# widgets reference their elements, whose kwargs hold user callbacks - which
|
|
1441
1470
|
# capture use_state setters and therefore this render context
|
|
1442
1471
|
self.element = None # type: ignore
|
|
1472
|
+
self._element_next = None # type: ignore
|
|
1443
1473
|
self.container = None
|
|
1444
1474
|
self.last_root_widget = None
|
|
1445
1475
|
self._old_element_ids.clear()
|
|
@@ -1563,6 +1593,9 @@ class _RenderContext:
|
|
|
1563
1593
|
# TODO: enable
|
|
1564
1594
|
context.needs_render = True
|
|
1565
1595
|
_mark_needs_render_ancestors(context)
|
|
1596
|
+
if self._lock_thread is not threading.current_thread():
|
|
1597
|
+
# not part of the render loop of the thread that renders (see render)
|
|
1598
|
+
self._state_set_by_other_thread = True
|
|
1566
1599
|
if self._rerender_needed is False:
|
|
1567
1600
|
if DEBUG:
|
|
1568
1601
|
trigger_stack = traceback.format_stack()
|
|
@@ -1589,8 +1622,13 @@ class _RenderContext:
|
|
|
1589
1622
|
return
|
|
1590
1623
|
# a forced update re-walks the whole tree, no subtree skipping
|
|
1591
1624
|
self._walk_all = True
|
|
1625
|
+
# mark before we look at _is_rendering, like a setter: a render that is ending then sees the
|
|
1626
|
+
# mark after it released the lock (see render), or we see that no render runs
|
|
1627
|
+
if self._rerender_needed is False:
|
|
1628
|
+
self._rerender_needed_reasons.append(RerenderReason(reason="forced update"))
|
|
1629
|
+
self._rerender_needed = True
|
|
1592
1630
|
if not self._is_rendering:
|
|
1593
|
-
self.render(
|
|
1631
|
+
self.render()
|
|
1594
1632
|
|
|
1595
1633
|
def use_effect(self, effect: EffectCallable, dependencies=None):
|
|
1596
1634
|
assert self.context is not None
|
|
@@ -1612,17 +1650,13 @@ class _RenderContext:
|
|
|
1612
1650
|
self.context.effect_index += 1
|
|
1613
1651
|
|
|
1614
1652
|
def update(self, element: Element):
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
self._rerender_needed_reasons.append(RerenderReason(reason="root element changed"))
|
|
1619
|
-
self._rerender_needed = True
|
|
1620
|
-
else:
|
|
1621
|
-
self.render(element, self.container)
|
|
1653
|
+
# the element first: force_update marks, and the render takes _element_next after it saw the mark
|
|
1654
|
+
self._element_next = element
|
|
1655
|
+
self.force_update()
|
|
1622
1656
|
|
|
1623
1657
|
def _possible_rerender(self):
|
|
1624
1658
|
if not self._is_rendering and self._batch_counter.current() == 0:
|
|
1625
|
-
self.render(
|
|
1659
|
+
self.render()
|
|
1626
1660
|
else:
|
|
1627
1661
|
logger.info("No render phase triggered, already rendering")
|
|
1628
1662
|
|
|
@@ -1639,8 +1673,12 @@ class _RenderContext:
|
|
|
1639
1673
|
# state), and a context created by the aborted pass owns no widgets yet, it is simply
|
|
1640
1674
|
# reused or pruned by the next render
|
|
1641
1675
|
contexts: List[ComponentContext] = [self.context_root]
|
|
1676
|
+
seen: Set[int] = set() # a context can be in both children and children_next (see close)
|
|
1642
1677
|
while contexts:
|
|
1643
1678
|
context = contexts.pop()
|
|
1679
|
+
if id(context) in seen:
|
|
1680
|
+
continue
|
|
1681
|
+
seen.add(id(context))
|
|
1644
1682
|
for effect in context.effects:
|
|
1645
1683
|
effect.next = None
|
|
1646
1684
|
context.root_element_next = None
|
|
@@ -1655,41 +1693,60 @@ class _RenderContext:
|
|
|
1655
1693
|
self._shared_elements_next = set()
|
|
1656
1694
|
self.context = self.context_root
|
|
1657
1695
|
|
|
1658
|
-
def render(self, element: Element, container: widgets.Widget = None):
|
|
1696
|
+
def render(self, element: Optional[Element] = None, container: widgets.Widget = None):
|
|
1659
1697
|
# render + consolidate
|
|
1698
|
+
# We never wait for the render lock: the caller may hold a lock that the render needs (a
|
|
1699
|
+
# deadlock). So we first mark the request, then only try the lock. When another thread holds
|
|
1700
|
+
# it, that thread renders the request: it takes _element_next at the start of each pass, and
|
|
1701
|
+
# looks at _rerender_needed again after it released the lock (below).
|
|
1702
|
+
widget, rendered = self._render_once(element, container)
|
|
1703
|
+
# A request from another thread (a setter, update(), render()) sets _rerender_needed, and leaves
|
|
1704
|
+
# the render to us while we render or hold the render lock. We released the lock, and look again
|
|
1705
|
+
# now: either we see the request here, or that thread got the lock and renders it itself. A loop,
|
|
1706
|
+
# not a recursion: this can repeat many times in a row while another thread changes state.
|
|
1707
|
+
while rendered and self._rerender_needed and not self._is_rendering and self._batch_counter.current() == 0:
|
|
1708
|
+
widget, rendered = self._render_once(None, container)
|
|
1709
|
+
return widget
|
|
1710
|
+
|
|
1711
|
+
def _render_once(self, element: Optional[Element], container: widgets.Widget) -> Tuple[Any, bool]:
|
|
1712
|
+
# returns the root widget, and whether we rendered (False: another thread holds the render
|
|
1713
|
+
# lock and renders our request, or we are closed, or we rendered an error message)
|
|
1714
|
+
if self._lock_thread == threading.current_thread():
|
|
1715
|
+
raise RuntimeError("Recursive render detected (avoided deadlock), current thread: %r" % threading.current_thread())
|
|
1660
1716
|
widget = None
|
|
1661
1717
|
if container is None:
|
|
1662
1718
|
container = self.container
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
)
|
|
1672
|
-
|
|
1673
|
-
|
|
1719
|
+
if element is not None:
|
|
1720
|
+
self._element_next = element
|
|
1721
|
+
if self._rerender_needed is False:
|
|
1722
|
+
self._rerender_needed_reasons.append(RerenderReason(reason="render requested"))
|
|
1723
|
+
self._rerender_needed = True
|
|
1724
|
+
locked = False
|
|
1725
|
+
try:
|
|
1726
|
+
# acquire inside the try, in one statement with the assignment: an exception raised
|
|
1727
|
+
# on the next line (an interrupt, a cancel from a trace function) still releases the lock
|
|
1728
|
+
locked = self.thread_lock.acquire(blocking=False)
|
|
1729
|
+
if not locked:
|
|
1730
|
+
logger.info("Render phase in progress in thread %r, leaving the render to it", self._lock_thread)
|
|
1731
|
+
return container, False
|
|
1674
1732
|
self._lock_thread = threading.current_thread()
|
|
1675
|
-
if was_locked:
|
|
1676
|
-
logger.info("Mutex released, continuing render phase")
|
|
1677
1733
|
if self._closing or self.context is None:
|
|
1678
1734
|
# close() won the race for the lock (a disconnect can close the
|
|
1679
1735
|
# kernel while an update was waiting to render): the tree is
|
|
1680
1736
|
# torn down, there is nothing to render into anymore
|
|
1681
1737
|
logger.info("Render requested on a closing/closed render context, ignoring")
|
|
1682
|
-
return container
|
|
1738
|
+
return container, False
|
|
1683
1739
|
prev_rc = getattr(local, "rc", None)
|
|
1684
1740
|
# an exception that escapes while this is True aborted a render pass (see the except below)
|
|
1685
1741
|
in_render_phase = True
|
|
1686
1742
|
try:
|
|
1687
1743
|
local.rc = self
|
|
1688
|
-
self.element = element
|
|
1689
|
-
del element
|
|
1690
1744
|
main_render_phase = not self._is_rendering
|
|
1691
1745
|
render_count = self.render_count # make a copy
|
|
1746
|
+
# clear before taking the element: a request that comes in between is seen by the loop
|
|
1692
1747
|
self._rerender_needed = False
|
|
1748
|
+
self._state_set_by_other_thread = False
|
|
1749
|
+
self.element = self._element_next
|
|
1693
1750
|
logger.info("Render phase: %r %r of %r", self.render_count, "main" if main_render_phase else "(nested)", self.element)
|
|
1694
1751
|
self.render_count += 1
|
|
1695
1752
|
self._is_rendering = True
|
|
@@ -1714,11 +1771,18 @@ class _RenderContext:
|
|
|
1714
1771
|
if main_render_phase:
|
|
1715
1772
|
stable = False
|
|
1716
1773
|
render_counts = 0
|
|
1774
|
+
render_limit = 50
|
|
1717
1775
|
while not stable and not self.context_root.exceptions_children:
|
|
1718
1776
|
# we started the rendering loop (main_render_phase is True), so we keep going
|
|
1719
1777
|
# but if an exception bubbled up, we should stop
|
|
1720
1778
|
while self._rerender_needed and not self.context_root.exceptions_children:
|
|
1721
|
-
if
|
|
1779
|
+
if self._state_set_by_other_thread:
|
|
1780
|
+
# another thread changed state during a pass (a progress update for
|
|
1781
|
+
# instance): that is not a render loop of our own, so allow more passes.
|
|
1782
|
+
# But not without end: our caller would not return, and close() waits.
|
|
1783
|
+
self._state_set_by_other_thread = False
|
|
1784
|
+
render_limit = 100
|
|
1785
|
+
if render_counts > render_limit:
|
|
1722
1786
|
|
|
1723
1787
|
def format(reason: RerenderReason):
|
|
1724
1788
|
f = f"Reason: {reason.reason}\nValue changed from {reason.prev_value} to {reason.next_value}\n"
|
|
@@ -1728,13 +1792,17 @@ class _RenderContext:
|
|
|
1728
1792
|
f += f"Triggered at: {''.join(reason.trigger_stack)}\n"
|
|
1729
1793
|
return f
|
|
1730
1794
|
|
|
1731
|
-
msg =
|
|
1795
|
+
msg = "Too many renders triggered, your render loop does not stop\n"
|
|
1796
|
+
if render_limit > 50:
|
|
1797
|
+
msg += "Another thread changes state during every render pass: add a pause between its changes\n"
|
|
1798
|
+
msg += f"Last reason: {format(self._rerender_needed_reasons[-1])}\n"
|
|
1732
1799
|
if len(self._rerender_needed_reasons) >= 2:
|
|
1733
1800
|
previous = reversed(list(self._rerender_needed_reasons)[:-1])
|
|
1734
1801
|
msg += f"Previous reasons: {''.join(format(reason) for reason in previous)}\n"
|
|
1735
1802
|
raise RuntimeError(msg)
|
|
1736
1803
|
logger.info("Entering nested render phase: %r", self._rerender_needed_reasons[-1])
|
|
1737
1804
|
self._rerender_needed = False
|
|
1805
|
+
self.element = self._element_next
|
|
1738
1806
|
self._shared_elements_next = set()
|
|
1739
1807
|
self.context.exception_handler = False
|
|
1740
1808
|
self.context.exceptions_children = []
|
|
@@ -1808,6 +1876,8 @@ class _RenderContext:
|
|
|
1808
1876
|
stable = False
|
|
1809
1877
|
else:
|
|
1810
1878
|
stable = True
|
|
1879
|
+
if self._on_render_loop_done is not None:
|
|
1880
|
+
self._on_render_loop_done()
|
|
1811
1881
|
|
|
1812
1882
|
self._is_rendering = False
|
|
1813
1883
|
self.context = context_prev
|
|
@@ -1834,13 +1904,18 @@ class _RenderContext:
|
|
|
1834
1904
|
finally:
|
|
1835
1905
|
local.rc = prev_rc # type: ignore
|
|
1836
1906
|
self._is_rendering = False
|
|
1837
|
-
# clear before the lock is released: a stale _lock_thread makes the
|
|
1838
|
-
# recursion guard above fire for a thread that merely rendered last,
|
|
1839
|
-
# while a *different* thread holds the lock (false "Recursive render")
|
|
1840
|
-
self._lock_thread = None
|
|
1841
1907
|
assert self.context is self.context_root
|
|
1908
|
+
# read the errors of this render while we hold the lock: once it is released, close()
|
|
1909
|
+
# on another thread can tear the tree down (self.context is None then)
|
|
1910
|
+
exceptions = [*self.context.exceptions_children, *self.context_root.exceptions_self]
|
|
1911
|
+
finally:
|
|
1912
|
+
if locked:
|
|
1913
|
+
# clear before the lock is released, on every way out (also the return for a closed
|
|
1914
|
+
# render context): a stale _lock_thread makes the checks for a render or close() in
|
|
1915
|
+
# its own render fire for a thread that merely rendered last
|
|
1916
|
+
self._lock_thread = None
|
|
1917
|
+
self.thread_lock.release()
|
|
1842
1918
|
|
|
1843
|
-
exceptions = [*self.context.exceptions_children, *self.context_root.exceptions_self]
|
|
1844
1919
|
if exceptions:
|
|
1845
1920
|
exc = exceptions[0]
|
|
1846
1921
|
if DEBUG:
|
|
@@ -1857,10 +1932,10 @@ class _RenderContext:
|
|
|
1857
1932
|
value = html.escape(error)
|
|
1858
1933
|
from . import ipywidgets as w
|
|
1859
1934
|
|
|
1860
|
-
return self.render(w.HTML(value="<pre>" + value + "</pre>", layout=w.Layout(overflow="auto")), self.container)
|
|
1935
|
+
return self.render(w.HTML(value="<pre>" + value + "</pre>", layout=w.Layout(overflow="auto")), self.container), False
|
|
1861
1936
|
else:
|
|
1862
1937
|
raise exc
|
|
1863
|
-
return widget
|
|
1938
|
+
return widget, True
|
|
1864
1939
|
|
|
1865
1940
|
def _render(self, element: Element, default_key: str, parent_key: str):
|
|
1866
1941
|
if not isinstance(element, Element):
|
|
@@ -2118,7 +2193,7 @@ class _RenderContext:
|
|
|
2118
2193
|
try:
|
|
2119
2194
|
if isinstance(el.component, ComponentFunction):
|
|
2120
2195
|
if el_prev and isinstance(el_prev.component, ComponentWidget):
|
|
2121
|
-
self.
|
|
2196
|
+
self._remove_outgoing(el_prev, key, parent_key=parent_key)
|
|
2122
2197
|
new_parent_key = join_key(parent_key, key)
|
|
2123
2198
|
try:
|
|
2124
2199
|
# TODO: test suite passes when this block if commented out
|
|
@@ -2164,7 +2239,7 @@ class _RenderContext:
|
|
|
2164
2239
|
if removed:
|
|
2165
2240
|
for key_remove in removed:
|
|
2166
2241
|
el_remove = elements[key_remove]
|
|
2167
|
-
self.
|
|
2242
|
+
self._remove_outgoing(el_remove, key_remove, parent_key)
|
|
2168
2243
|
for effect_index, effect in enumerate(child_context.effects):
|
|
2169
2244
|
if effect.next:
|
|
2170
2245
|
# if we have a next, it means that effect itself is executed
|
|
@@ -2295,7 +2370,7 @@ class _RenderContext:
|
|
|
2295
2370
|
else:
|
|
2296
2371
|
assert el_prev is not None, "widget_previous is not None, but el_prev is"
|
|
2297
2372
|
logger.debug("Replacing widget: %r → %r %r", el_prev, el, key)
|
|
2298
|
-
self.
|
|
2373
|
+
self._remove_outgoing(el_prev, key, parent_key=parent_key)
|
|
2299
2374
|
kwargs = reconsolidate_children()
|
|
2300
2375
|
widget = None
|
|
2301
2376
|
if not context.exceptions_children:
|
|
@@ -2372,7 +2447,7 @@ class _RenderContext:
|
|
|
2372
2447
|
if extra:
|
|
2373
2448
|
for key in list(extra):
|
|
2374
2449
|
if key in self.context.elements:
|
|
2375
|
-
self.
|
|
2450
|
+
self._remove_outgoing(self.context.elements[key], key, parent_key=parent_key)
|
|
2376
2451
|
|
|
2377
2452
|
# keeping this for debugging
|
|
2378
2453
|
# logger.debug("Current:")
|
|
@@ -2796,7 +2871,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2796
2871
|
|
|
2797
2872
|
if el_prev and isinstance(el_prev.component, ComponentWidget):
|
|
2798
2873
|
# a widget element was replaced by a component element at this key
|
|
2799
|
-
self.
|
|
2874
|
+
self._remove_outgoing(el_prev, key, parent_key=parent_key)
|
|
2800
2875
|
new_parent_key = join_key(parent_key, key)
|
|
2801
2876
|
try:
|
|
2802
2877
|
if el.is_shared and (el.args or el.kwargs):
|
|
@@ -2832,7 +2907,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2832
2907
|
logger.info("elements to be removed: %r", stale_keys)
|
|
2833
2908
|
for stale_key in stale_keys:
|
|
2834
2909
|
if stale_key in child_context.elements:
|
|
2835
|
-
self.
|
|
2910
|
+
self._remove_outgoing(child_context.elements[stale_key], stale_key, new_parent_key)
|
|
2836
2911
|
|
|
2837
2912
|
self._process_effects(child_context, context)
|
|
2838
2913
|
|
|
@@ -2904,7 +2979,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2904
2979
|
else:
|
|
2905
2980
|
assert el_prev is not None, "widget_previous is not None, but el_prev is"
|
|
2906
2981
|
# a different widget type at the same key: replace
|
|
2907
|
-
self.
|
|
2982
|
+
self._remove_outgoing(el_prev, key, parent_key=parent_key)
|
|
2908
2983
|
kwargs = self._visit_children_values(el.kwargs, key, parent_key, self._reconsolidate)
|
|
2909
2984
|
widget = None
|
|
2910
2985
|
if not context.exceptions_children:
|
|
@@ -3013,7 +3088,15 @@ class _RenderContextFast(_RenderContext):
|
|
|
3013
3088
|
if stale_key not in context.elements or stale_key in context.fast_stale_effects_cleaned:
|
|
3014
3089
|
continue
|
|
3015
3090
|
self.context = context
|
|
3016
|
-
|
|
3091
|
+
el = context.elements[stale_key]
|
|
3092
|
+
# keyed children that the new tree still uses stay, with their effects
|
|
3093
|
+
# (see _keep_keyed_child)
|
|
3094
|
+
outgoing = self._outgoing
|
|
3095
|
+
self._outgoing = (context, el)
|
|
3096
|
+
try:
|
|
3097
|
+
self._cleanup_stale_effects(el, stale_key, parent_key)
|
|
3098
|
+
finally:
|
|
3099
|
+
self._outgoing = outgoing
|
|
3017
3100
|
finally:
|
|
3018
3101
|
self.context = context_prev
|
|
3019
3102
|
|
|
@@ -3026,6 +3109,8 @@ class _RenderContextFast(_RenderContext):
|
|
|
3026
3109
|
assert context is not None
|
|
3027
3110
|
if key in context.fast_stale_effects_cleaned:
|
|
3028
3111
|
return
|
|
3112
|
+
if self._keep_keyed_child(context, el, key):
|
|
3113
|
+
return
|
|
3029
3114
|
if el.is_shared and (el in self._shared_elements_next or el not in self._shared_elements):
|
|
3030
3115
|
return
|
|
3031
3116
|
context.fast_stale_effects_cleaned.add(key)
|
|
@@ -3188,7 +3273,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
3188
3273
|
stale_keys = sorted(set(self.context_root.elements) - self.context_root.used_keys)
|
|
3189
3274
|
for stale_key in stale_keys:
|
|
3190
3275
|
if stale_key in self.context_root.elements:
|
|
3191
|
-
self.
|
|
3276
|
+
self._remove_outgoing(self.context_root.elements[stale_key], stale_key, parent_key)
|
|
3192
3277
|
|
|
3193
3278
|
|
|
3194
3279
|
def _render_context_class():
|