reacton 1.10.2__tar.gz → 1.10.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {reacton-1.10.2 → reacton-1.10.3}/PKG-INFO +2 -2
- reacton-1.10.3/reacton/_version.py +1 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/core.py +77 -7
- {reacton-1.10.2 → reacton-1.10.3}/reacton/core_test.py +159 -0
- reacton-1.10.2/reacton/_version.py +0 -1
- {reacton-1.10.2 → reacton-1.10.3}/.gitignore +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/LICENSE +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/pyproject.toml +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/react_ipywidgets/__init__.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/react_ipywidgets/core.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/__init__.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/bqplot.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/deprecated/__init__.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/deprecated/ipyvuetify.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/find.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/find_test.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/generate.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/generate_test.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/ipycanvas.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/ipyvue.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/ipyvue_test.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/ipyvuetify.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/ipywidgets.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/logging.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/patch.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/patch_display.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/py.typed +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/rx.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/test.vue +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/test_rx.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/utils.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/utils_test.py +0 -0
- {reacton-1.10.2 → reacton-1.10.3}/reacton/work.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.10.3"
|
|
@@ -170,6 +170,26 @@ def close_widget(widget: widgets.Widget):
|
|
|
170
170
|
logger.warning("Widget %r does not have a close method, possibly a close trait was added", widget)
|
|
171
171
|
|
|
172
172
|
|
|
173
|
+
def _is_shared_ipyvue_template(widget: widgets.Widget) -> bool:
|
|
174
|
+
"""Is this widget an ipyvue Template that is shared between VueTemplate instances?
|
|
175
|
+
|
|
176
|
+
ipyvue keeps a per-file registry of Template widgets (ipyvue/Template.py), so several
|
|
177
|
+
VueTemplate widgets can point at the same Template. Such a Template outlives the
|
|
178
|
+
VueTemplate that created it, and we must not close it as an orphan.
|
|
179
|
+
ipyvue 3 only puts a Template in that registry when there is a real comm, so an
|
|
180
|
+
unregistered Template belongs to a single VueTemplate and is a normal orphan.
|
|
181
|
+
"""
|
|
182
|
+
cls = widget.__class__
|
|
183
|
+
if cls.__name__ != "Template" or cls.__module__ != "ipyvue.Template":
|
|
184
|
+
return False
|
|
185
|
+
module = sys.modules.get(cls.__module__)
|
|
186
|
+
registry = getattr(module, "template_registry", None)
|
|
187
|
+
if registry is None:
|
|
188
|
+
# unknown ipyvue version: keep the old, conservative behaviour
|
|
189
|
+
return True
|
|
190
|
+
return any(template is widget for template in registry.values())
|
|
191
|
+
|
|
192
|
+
|
|
173
193
|
def _event_handler_exception_wrapper(f):
|
|
174
194
|
"""Wrap an event handler to catch exceptions and put them in a reacton context.
|
|
175
195
|
|
|
@@ -1532,6 +1552,35 @@ class _RenderContext:
|
|
|
1532
1552
|
else:
|
|
1533
1553
|
logger.info("No render phase triggered, already rendering")
|
|
1534
1554
|
|
|
1555
|
+
def _discard_aborted_pass(self):
|
|
1556
|
+
"""Forget everything a render pass staged when it raised before reconciliation.
|
|
1557
|
+
|
|
1558
|
+
The *_next bookkeeping and the chained effects (previous_effect.next) of every
|
|
1559
|
+
context belong to that pass. Mark every context dirty, so the next render()
|
|
1560
|
+
re-executes the components and re-queues their effects from a consistent state.
|
|
1561
|
+
Only for aborts in the render phase: a reconciliation that raised halfway already
|
|
1562
|
+
created widgets, and those need the normal removal path.
|
|
1563
|
+
"""
|
|
1564
|
+
# children_next is kept: it also holds contexts pre-created by state_set() (restored
|
|
1565
|
+
# state), and a context created by the aborted pass owns no widgets yet, it is simply
|
|
1566
|
+
# reused or pruned by the next render
|
|
1567
|
+
contexts: List[ComponentContext] = [self.context_root]
|
|
1568
|
+
while contexts:
|
|
1569
|
+
context = contexts.pop()
|
|
1570
|
+
for effect in context.effects:
|
|
1571
|
+
effect.next = None
|
|
1572
|
+
context.root_element_next = None
|
|
1573
|
+
context.elements_next = {}
|
|
1574
|
+
context.exceptions_self = []
|
|
1575
|
+
context.exceptions_children = []
|
|
1576
|
+
context.needs_render = True
|
|
1577
|
+
context.needs_render_descendant = True
|
|
1578
|
+
context.clean_subtree = False
|
|
1579
|
+
contexts.extend(context.children.values())
|
|
1580
|
+
contexts.extend(context.children_next.values())
|
|
1581
|
+
self._shared_elements_next = set()
|
|
1582
|
+
self.context = self.context_root
|
|
1583
|
+
|
|
1535
1584
|
def render(self, element: Element, container: widgets.Widget = None):
|
|
1536
1585
|
# render + consolidate
|
|
1537
1586
|
widget = None
|
|
@@ -1558,6 +1607,8 @@ class _RenderContext:
|
|
|
1558
1607
|
logger.info("Render requested on a closing/closed render context, ignoring")
|
|
1559
1608
|
return container
|
|
1560
1609
|
prev_rc = getattr(local, "rc", None)
|
|
1610
|
+
# an exception that escapes while this is True aborted a render pass (see the except below)
|
|
1611
|
+
in_render_phase = True
|
|
1561
1612
|
try:
|
|
1562
1613
|
local.rc = self
|
|
1563
1614
|
self.element = element
|
|
@@ -1633,10 +1684,12 @@ class _RenderContext:
|
|
|
1633
1684
|
|
|
1634
1685
|
logger.info("Render reconsolidate...")
|
|
1635
1686
|
self.reconsolidating = True
|
|
1687
|
+
in_render_phase = False
|
|
1636
1688
|
try:
|
|
1637
1689
|
widget = self._reconsolidate(self.element, default_key="/", parent_key=ROOT_KEY)
|
|
1638
1690
|
finally:
|
|
1639
1691
|
self.reconsolidating = False
|
|
1692
|
+
in_render_phase = True
|
|
1640
1693
|
logger.info("Render reconsolidate done")
|
|
1641
1694
|
self.context.root_element = self.context.root_element_next
|
|
1642
1695
|
self.context.root_element_next = None
|
|
@@ -1685,7 +1738,16 @@ class _RenderContext:
|
|
|
1685
1738
|
self._is_rendering = False
|
|
1686
1739
|
self.context = context_prev
|
|
1687
1740
|
logger.info("Done with render phase: %r", render_count)
|
|
1688
|
-
except
|
|
1741
|
+
except BaseException as e:
|
|
1742
|
+
# Exceptions raised by components are collected in exceptions_self, so an
|
|
1743
|
+
# exception here comes from the render machinery itself (duplicate key,
|
|
1744
|
+
# hook misuse, ...) or is a cancellation, and aborted a pass halfway. Drop
|
|
1745
|
+
# what that pass staged: otherwise the next render reconciles the last
|
|
1746
|
+
# committed elements, but runs the effects the aborted pass chained, closed
|
|
1747
|
+
# over elements that were never reconciled (get_widget then fails with
|
|
1748
|
+
# "found in a previous render").
|
|
1749
|
+
if in_render_phase:
|
|
1750
|
+
self._discard_aborted_pass()
|
|
1689
1751
|
if DEBUG:
|
|
1690
1752
|
# construct a fake traceback (showing how the elements were constructed)
|
|
1691
1753
|
if not self.tracebacks:
|
|
@@ -1825,8 +1887,13 @@ class _RenderContext:
|
|
|
1825
1887
|
needs_render = context.needs_render
|
|
1826
1888
|
if not needs_render:
|
|
1827
1889
|
if el_prev is not None and context_previous is context:
|
|
1828
|
-
|
|
1829
|
-
|
|
1890
|
+
if isinstance(el_prev.component, ComponentWidget):
|
|
1891
|
+
# an earlier pass of this render() call put a widget element at this
|
|
1892
|
+
# slot (elements_next), while the context is from the last reconciled
|
|
1893
|
+
# render: nothing to compare the arguments against, so render
|
|
1894
|
+
needs_render = True
|
|
1895
|
+
else:
|
|
1896
|
+
needs_render = el._arguments_changed(el_prev)
|
|
1830
1897
|
if context.exceptions_children:
|
|
1831
1898
|
# we have exceptions, so we need to render
|
|
1832
1899
|
needs_render = True
|
|
@@ -2166,7 +2233,7 @@ class _RenderContext:
|
|
|
2166
2233
|
if orphan_ids:
|
|
2167
2234
|
for orphan_widget in orphan_widgets:
|
|
2168
2235
|
# these are shared widgets
|
|
2169
|
-
if orphan_widget
|
|
2236
|
+
if _is_shared_ipyvue_template(orphan_widget):
|
|
2170
2237
|
orphan_ids -= {orphan_widget.model_id}
|
|
2171
2238
|
if el.is_shared:
|
|
2172
2239
|
widget = self._shared_widgets[el]
|
|
@@ -2485,8 +2552,11 @@ class _RenderContextFast(_RenderContext):
|
|
|
2485
2552
|
|
|
2486
2553
|
needs_render = context.needs_render
|
|
2487
2554
|
if not needs_render and el_prev is not None and context_previous is context:
|
|
2488
|
-
|
|
2489
|
-
|
|
2555
|
+
if isinstance(el_prev.component, ComponentWidget):
|
|
2556
|
+
# see the classic renderer: a widget element from an earlier pass of this call
|
|
2557
|
+
needs_render = True
|
|
2558
|
+
else:
|
|
2559
|
+
needs_render = el._arguments_changed(el_prev)
|
|
2490
2560
|
if not needs_render and context.exceptions_children:
|
|
2491
2561
|
# we have exceptions, so we need to render
|
|
2492
2562
|
needs_render = True
|
|
@@ -2750,7 +2820,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2750
2820
|
orphan_widgets = set([_get_widgets_dict()[k] for k in orphan_ids])
|
|
2751
2821
|
for orphan_widget in orphan_widgets:
|
|
2752
2822
|
# these are shared between widgets
|
|
2753
|
-
if orphan_widget
|
|
2823
|
+
if _is_shared_ipyvue_template(orphan_widget):
|
|
2754
2824
|
orphan_ids -= {orphan_widget.model_id}
|
|
2755
2825
|
widget = self._shared_widgets[el] if el.is_shared else context.widgets[key]
|
|
2756
2826
|
if widget.model_id not in self._orphans:
|
|
@@ -3288,3 +3288,162 @@ def test_key_mutate_protection(Container):
|
|
|
3288
3288
|
set_state(1)
|
|
3289
3289
|
rc.render(w.HTML(value="recover").key("HTML"))
|
|
3290
3290
|
rc.close()
|
|
3291
|
+
|
|
3292
|
+
|
|
3293
|
+
def test_render_nested_pass_widget_to_component_flip():
|
|
3294
|
+
# One render() call, two passes: the first pass puts a widget element at a
|
|
3295
|
+
# slot, a state write during that pass opens a nested pass, and the nested
|
|
3296
|
+
# pass puts back the function component that held the slot in the last
|
|
3297
|
+
# committed render. _render then compares the component element against
|
|
3298
|
+
# the widget element left in elements_next by the first pass and trips
|
|
3299
|
+
# `assert not isinstance(el_prev.component, ComponentWidget)`.
|
|
3300
|
+
set_mode = cast(Callable[[str], None], None)
|
|
3301
|
+
|
|
3302
|
+
set_clicks = cast(Callable[[int], None], None)
|
|
3303
|
+
|
|
3304
|
+
@react.component
|
|
3305
|
+
def Child():
|
|
3306
|
+
nonlocal set_clicks
|
|
3307
|
+
clicks, set_clicks = react.use_state(0)
|
|
3308
|
+
return w.Button(description=f"child {clicks}")
|
|
3309
|
+
|
|
3310
|
+
@react.component
|
|
3311
|
+
def Parent():
|
|
3312
|
+
nonlocal set_mode
|
|
3313
|
+
mode, set_mode = react.use_state("component")
|
|
3314
|
+
if mode == "widget":
|
|
3315
|
+
# a set during render opens a nested pass in the same render() call
|
|
3316
|
+
set_mode("component")
|
|
3317
|
+
return w.Label(description="loading")
|
|
3318
|
+
return Child()
|
|
3319
|
+
|
|
3320
|
+
box, rc = react.render(Parent(), handle_error=False)
|
|
3321
|
+
set_clicks(3)
|
|
3322
|
+
assert box.children[0].description == "child 3"
|
|
3323
|
+
set_mode("widget")
|
|
3324
|
+
assert isinstance(box.children[0], widgets.Button)
|
|
3325
|
+
# nothing was committed in between, so Child was never unmounted and keeps its state
|
|
3326
|
+
assert box.children[0].description == "child 3"
|
|
3327
|
+
rc.close()
|
|
3328
|
+
|
|
3329
|
+
|
|
3330
|
+
def test_render_nested_pass_flip_strands_get_widget_effect():
|
|
3331
|
+
# The production chain behind the flip above (Grotto PENG-1257, through
|
|
3332
|
+
# solara's RoutingProvider): the AssertionError escapes render(), and a
|
|
3333
|
+
# parent that re-executed in the aborted nested pass already chained a new
|
|
3334
|
+
# effect closure (previous_effect.next) over its new root element, while
|
|
3335
|
+
# its root_element_next was never set. The next render that does not
|
|
3336
|
+
# re-execute the parent runs that closure, and get_widget on the never
|
|
3337
|
+
# reconciled root raises "was found to be in a previous render".
|
|
3338
|
+
set_mode = cast(Callable[[str], None], None)
|
|
3339
|
+
set_counter = cast(Callable[[int], None], None)
|
|
3340
|
+
set_other = cast(Callable[[int], None], None)
|
|
3341
|
+
|
|
3342
|
+
@react.component
|
|
3343
|
+
def Child():
|
|
3344
|
+
return w.Button(description="child")
|
|
3345
|
+
|
|
3346
|
+
@react.component
|
|
3347
|
+
def Flipper():
|
|
3348
|
+
nonlocal set_mode
|
|
3349
|
+
mode, set_mode = react.use_state("component")
|
|
3350
|
+
if mode == "widget":
|
|
3351
|
+
# a set during render opens a nested pass, and the parent's state
|
|
3352
|
+
# changes too, so the parent re-executes in that nested pass
|
|
3353
|
+
# (in production: a router.push from a subscriber)
|
|
3354
|
+
set_counter(1)
|
|
3355
|
+
set_mode("component")
|
|
3356
|
+
return w.Label(description="loading")
|
|
3357
|
+
return Child()
|
|
3358
|
+
|
|
3359
|
+
@react.component
|
|
3360
|
+
def Other():
|
|
3361
|
+
nonlocal set_other
|
|
3362
|
+
value, set_other = react.use_state(0)
|
|
3363
|
+
return w.Label(description=f"other {value}")
|
|
3364
|
+
|
|
3365
|
+
@react.component
|
|
3366
|
+
def Parent():
|
|
3367
|
+
nonlocal set_counter
|
|
3368
|
+
_counter, set_counter = react.use_state(0)
|
|
3369
|
+
|
|
3370
|
+
def effect():
|
|
3371
|
+
react.get_widget(root)
|
|
3372
|
+
|
|
3373
|
+
react.use_effect(effect)
|
|
3374
|
+
root = w.VBox(children=[Flipper(), Other()])
|
|
3375
|
+
return root
|
|
3376
|
+
|
|
3377
|
+
box, rc = react.render(Parent(), handle_error=False)
|
|
3378
|
+
root = box.children[0]
|
|
3379
|
+
assert root.children[1].description == "other 0"
|
|
3380
|
+
try:
|
|
3381
|
+
set_mode("widget")
|
|
3382
|
+
except AssertionError:
|
|
3383
|
+
# the first stage, covered by test_render_nested_pass_widget_to_component_flip
|
|
3384
|
+
pass
|
|
3385
|
+
# an unrelated state change must not run a stale effect closure
|
|
3386
|
+
set_other(1)
|
|
3387
|
+
assert root.children[1].description == "other 1"
|
|
3388
|
+
rc.close()
|
|
3389
|
+
|
|
3390
|
+
|
|
3391
|
+
@pytest.mark.parametrize("abort_in", ["first_pass", "nested_pass"])
|
|
3392
|
+
def test_render_escaped_exception_strands_chained_effect(abort_in):
|
|
3393
|
+
# An exception raised by reacton's own bookkeeping inside _render (here a
|
|
3394
|
+
# duplicate key) escapes render() and leaves the tree half updated: a
|
|
3395
|
+
# component that re-executed in the aborted pass already chained a new
|
|
3396
|
+
# effect closure (previous_effect.next) over the elements of that pass, but
|
|
3397
|
+
# its root_element_next was never set. The next render that does not
|
|
3398
|
+
# re-execute that component runs the chained effect against elements that
|
|
3399
|
+
# were never reconciled; with get_widget that is the
|
|
3400
|
+
# "was found to be in a previous render" KeyError.
|
|
3401
|
+
# (Seen in production through solara's RoutingProvider, which calls
|
|
3402
|
+
# get_widget(main) from an effect without dependencies.)
|
|
3403
|
+
set_dup = cast(Callable[[bool], None], None)
|
|
3404
|
+
set_other = cast(Callable[[int], None], None)
|
|
3405
|
+
set_counter = cast(Callable[[int], None], None)
|
|
3406
|
+
|
|
3407
|
+
@react.component
|
|
3408
|
+
def Other():
|
|
3409
|
+
nonlocal set_other
|
|
3410
|
+
value, set_other = react.use_state(0)
|
|
3411
|
+
return w.Label(description=f"other {value}")
|
|
3412
|
+
|
|
3413
|
+
@react.component
|
|
3414
|
+
def Dup(dup: bool, counter: int):
|
|
3415
|
+
aborted = react.use_ref(False)
|
|
3416
|
+
if dup and counter == 0 and abort_in == "nested_pass":
|
|
3417
|
+
# make the parent re-execute in a nested pass of this render() call
|
|
3418
|
+
set_counter(1)
|
|
3419
|
+
abort_now = counter == (1 if abort_in == "nested_pass" else 0)
|
|
3420
|
+
if dup and abort_now and not aborted.current:
|
|
3421
|
+
# abort this pass with an exception from reacton's bookkeeping
|
|
3422
|
+
aborted.current = True
|
|
3423
|
+
return w.VBox(children=[w.Label(description="a").key("dup"), w.Label(description="b").key("dup")])
|
|
3424
|
+
return w.Label(description="no dup")
|
|
3425
|
+
|
|
3426
|
+
@react.component
|
|
3427
|
+
def Parent():
|
|
3428
|
+
nonlocal set_dup, set_counter
|
|
3429
|
+
dup, set_dup = react.use_state(False)
|
|
3430
|
+
counter, set_counter = react.use_state(0)
|
|
3431
|
+
|
|
3432
|
+
def effect():
|
|
3433
|
+
react.get_widget(root)
|
|
3434
|
+
|
|
3435
|
+
react.use_effect(effect)
|
|
3436
|
+
root = w.VBox(children=[Other(), Dup(dup, counter)])
|
|
3437
|
+
return root
|
|
3438
|
+
|
|
3439
|
+
box, rc = react.render(Parent(), handle_error=False)
|
|
3440
|
+
root = box.children[0]
|
|
3441
|
+
assert root.children[0].description == "other 0"
|
|
3442
|
+
# pass 1: Dup bumps the counter, nested pass: Parent re-executes and chains
|
|
3443
|
+
# a new effect closure over its new root, then Dup aborts the pass
|
|
3444
|
+
with pytest.raises(KeyError, match="Duplicate key"):
|
|
3445
|
+
set_dup(True)
|
|
3446
|
+
# an unrelated state change must not run a stale effect closure
|
|
3447
|
+
set_other(1)
|
|
3448
|
+
assert root.children[0].description == "other 1"
|
|
3449
|
+
rc.close()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.10.2"
|
|
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
|
|
File without changes
|
|
File without changes
|