reacton 1.10.4__tar.gz → 1.10.5__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.10.5}/PKG-INFO +1 -1
- reacton-1.10.5/reacton/_version.py +1 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/core.py +35 -24
- {reacton-1.10.4 → reacton-1.10.5}/reacton/core_test.py +59 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/fuzz_test.py +34 -3
- reacton-1.10.4/reacton/_version.py +0 -1
- {reacton-1.10.4 → reacton-1.10.5}/.gitignore +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/LICENSE +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/pyproject.toml +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/react_ipywidgets/__init__.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/react_ipywidgets/core.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/__init__.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/bqplot.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/deprecated/__init__.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/deprecated/ipyvuetify.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/find.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/find_test.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/generate.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/generate_test.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/ipycanvas.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/ipyvue.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/ipyvue_test.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/ipyvuetify.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/ipywidgets.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/logging.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/patch.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/patch_display.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/py.typed +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/rx.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/test.vue +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/test_rx.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/utils.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/utils_test.py +0 -0
- {reacton-1.10.4 → reacton-1.10.5}/reacton/work.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.10.5"
|
|
@@ -1306,29 +1306,30 @@ class Effect:
|
|
|
1306
1306
|
|
|
1307
1307
|
class _RenderContext:
|
|
1308
1308
|
context: Optional[ComponentContext] = None
|
|
1309
|
-
# the element that
|
|
1310
|
-
#
|
|
1311
|
-
|
|
1309
|
+
# the element that reconciliation removes because the new tree replaced it by an element
|
|
1310
|
+
# of another type, or no longer has it, and its context (see _remove_outgoing)
|
|
1311
|
+
_outgoing: Optional[Tuple[ComponentContext, Element]] = None
|
|
1312
1312
|
|
|
1313
|
-
def
|
|
1314
|
-
"""Remove the element at key, which
|
|
1313
|
+
def _remove_outgoing(self, el: Element, key: str, parent_key: str):
|
|
1314
|
+
"""Remove the element at key, which the new tree replaced or no longer has."""
|
|
1315
1315
|
assert self.context is not None
|
|
1316
|
-
|
|
1317
|
-
self.
|
|
1316
|
+
outgoing = self._outgoing
|
|
1317
|
+
self._outgoing = (self.context, el)
|
|
1318
1318
|
try:
|
|
1319
|
-
self._remove_element(
|
|
1319
|
+
self._remove_element(el, key, parent_key=parent_key)
|
|
1320
1320
|
finally:
|
|
1321
|
-
self.
|
|
1321
|
+
self._outgoing = outgoing
|
|
1322
1322
|
|
|
1323
1323
|
def _keep_keyed_child(self, context: ComponentContext, el: Element, key: str) -> bool:
|
|
1324
1324
|
# An explicit key is the same in any container of a component, so a child with an
|
|
1325
|
-
# explicit key that the new tree still uses (it moved out of the
|
|
1326
|
-
#
|
|
1327
|
-
#
|
|
1328
|
-
#
|
|
1329
|
-
|
|
1325
|
+
# explicit key that the new tree still uses (it moved out of the outgoing element, for
|
|
1326
|
+
# instance a wrapper that went away, or stays under the element that replaces it) is
|
|
1327
|
+
# not removed with the outgoing element: reconciliation updates it where the new tree
|
|
1328
|
+
# has it. Shared elements keep the old behavior: their bookkeeping (_shared_elements,
|
|
1329
|
+
# _shared_widgets) needs the removal.
|
|
1330
|
+
outgoing = self._outgoing
|
|
1330
1331
|
return (
|
|
1331
|
-
|
|
1332
|
+
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
1333
|
)
|
|
1333
1334
|
|
|
1334
1335
|
def __init__(self, element: Element, container: widgets.Widget = None, children_trait="children", handle_error: bool = True, initial_state=None):
|
|
@@ -2118,7 +2119,7 @@ class _RenderContext:
|
|
|
2118
2119
|
try:
|
|
2119
2120
|
if isinstance(el.component, ComponentFunction):
|
|
2120
2121
|
if el_prev and isinstance(el_prev.component, ComponentWidget):
|
|
2121
|
-
self.
|
|
2122
|
+
self._remove_outgoing(el_prev, key, parent_key=parent_key)
|
|
2122
2123
|
new_parent_key = join_key(parent_key, key)
|
|
2123
2124
|
try:
|
|
2124
2125
|
# TODO: test suite passes when this block if commented out
|
|
@@ -2164,7 +2165,7 @@ class _RenderContext:
|
|
|
2164
2165
|
if removed:
|
|
2165
2166
|
for key_remove in removed:
|
|
2166
2167
|
el_remove = elements[key_remove]
|
|
2167
|
-
self.
|
|
2168
|
+
self._remove_outgoing(el_remove, key_remove, parent_key)
|
|
2168
2169
|
for effect_index, effect in enumerate(child_context.effects):
|
|
2169
2170
|
if effect.next:
|
|
2170
2171
|
# if we have a next, it means that effect itself is executed
|
|
@@ -2295,7 +2296,7 @@ class _RenderContext:
|
|
|
2295
2296
|
else:
|
|
2296
2297
|
assert el_prev is not None, "widget_previous is not None, but el_prev is"
|
|
2297
2298
|
logger.debug("Replacing widget: %r → %r %r", el_prev, el, key)
|
|
2298
|
-
self.
|
|
2299
|
+
self._remove_outgoing(el_prev, key, parent_key=parent_key)
|
|
2299
2300
|
kwargs = reconsolidate_children()
|
|
2300
2301
|
widget = None
|
|
2301
2302
|
if not context.exceptions_children:
|
|
@@ -2372,7 +2373,7 @@ class _RenderContext:
|
|
|
2372
2373
|
if extra:
|
|
2373
2374
|
for key in list(extra):
|
|
2374
2375
|
if key in self.context.elements:
|
|
2375
|
-
self.
|
|
2376
|
+
self._remove_outgoing(self.context.elements[key], key, parent_key=parent_key)
|
|
2376
2377
|
|
|
2377
2378
|
# keeping this for debugging
|
|
2378
2379
|
# logger.debug("Current:")
|
|
@@ -2796,7 +2797,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2796
2797
|
|
|
2797
2798
|
if el_prev and isinstance(el_prev.component, ComponentWidget):
|
|
2798
2799
|
# a widget element was replaced by a component element at this key
|
|
2799
|
-
self.
|
|
2800
|
+
self._remove_outgoing(el_prev, key, parent_key=parent_key)
|
|
2800
2801
|
new_parent_key = join_key(parent_key, key)
|
|
2801
2802
|
try:
|
|
2802
2803
|
if el.is_shared and (el.args or el.kwargs):
|
|
@@ -2832,7 +2833,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2832
2833
|
logger.info("elements to be removed: %r", stale_keys)
|
|
2833
2834
|
for stale_key in stale_keys:
|
|
2834
2835
|
if stale_key in child_context.elements:
|
|
2835
|
-
self.
|
|
2836
|
+
self._remove_outgoing(child_context.elements[stale_key], stale_key, new_parent_key)
|
|
2836
2837
|
|
|
2837
2838
|
self._process_effects(child_context, context)
|
|
2838
2839
|
|
|
@@ -2904,7 +2905,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
2904
2905
|
else:
|
|
2905
2906
|
assert el_prev is not None, "widget_previous is not None, but el_prev is"
|
|
2906
2907
|
# a different widget type at the same key: replace
|
|
2907
|
-
self.
|
|
2908
|
+
self._remove_outgoing(el_prev, key, parent_key=parent_key)
|
|
2908
2909
|
kwargs = self._visit_children_values(el.kwargs, key, parent_key, self._reconsolidate)
|
|
2909
2910
|
widget = None
|
|
2910
2911
|
if not context.exceptions_children:
|
|
@@ -3013,7 +3014,15 @@ class _RenderContextFast(_RenderContext):
|
|
|
3013
3014
|
if stale_key not in context.elements or stale_key in context.fast_stale_effects_cleaned:
|
|
3014
3015
|
continue
|
|
3015
3016
|
self.context = context
|
|
3016
|
-
|
|
3017
|
+
el = context.elements[stale_key]
|
|
3018
|
+
# keyed children that the new tree still uses stay, with their effects
|
|
3019
|
+
# (see _keep_keyed_child)
|
|
3020
|
+
outgoing = self._outgoing
|
|
3021
|
+
self._outgoing = (context, el)
|
|
3022
|
+
try:
|
|
3023
|
+
self._cleanup_stale_effects(el, stale_key, parent_key)
|
|
3024
|
+
finally:
|
|
3025
|
+
self._outgoing = outgoing
|
|
3017
3026
|
finally:
|
|
3018
3027
|
self.context = context_prev
|
|
3019
3028
|
|
|
@@ -3026,6 +3035,8 @@ class _RenderContextFast(_RenderContext):
|
|
|
3026
3035
|
assert context is not None
|
|
3027
3036
|
if key in context.fast_stale_effects_cleaned:
|
|
3028
3037
|
return
|
|
3038
|
+
if self._keep_keyed_child(context, el, key):
|
|
3039
|
+
return
|
|
3029
3040
|
if el.is_shared and (el in self._shared_elements_next or el not in self._shared_elements):
|
|
3030
3041
|
return
|
|
3031
3042
|
context.fast_stale_effects_cleaned.add(key)
|
|
@@ -3188,7 +3199,7 @@ class _RenderContextFast(_RenderContext):
|
|
|
3188
3199
|
stale_keys = sorted(set(self.context_root.elements) - self.context_root.used_keys)
|
|
3189
3200
|
for stale_key in stale_keys:
|
|
3190
3201
|
if stale_key in self.context_root.elements:
|
|
3191
|
-
self.
|
|
3202
|
+
self._remove_outgoing(self.context_root.elements[stale_key], stale_key, parent_key)
|
|
3192
3203
|
|
|
3193
3204
|
|
|
3194
3205
|
def _render_context_class():
|
|
@@ -4245,3 +4245,62 @@ def test_shared_keyed_child_in_replaced_container():
|
|
|
4245
4245
|
assert rc.find(widgets.Button).widget.comm is not None
|
|
4246
4246
|
assert len(rc._shared_widgets) == 1
|
|
4247
4247
|
rc.close()
|
|
4248
|
+
|
|
4249
|
+
|
|
4250
|
+
@pytest.mark.parametrize("same_element", [True, False])
|
|
4251
|
+
def test_keyed_child_out_of_removed_wrapper(same_element):
|
|
4252
|
+
# the wrapper around a child with an explicit key goes away (and comes back): its
|
|
4253
|
+
# removal must not remove (or close) the child, which is still in the tree
|
|
4254
|
+
set_wrapped = lambda x: None # noqa
|
|
4255
|
+
|
|
4256
|
+
@react.component
|
|
4257
|
+
def Child():
|
|
4258
|
+
return w.Button(description="child")
|
|
4259
|
+
|
|
4260
|
+
child_once = Child().key("x")
|
|
4261
|
+
|
|
4262
|
+
@react.component
|
|
4263
|
+
def Test():
|
|
4264
|
+
nonlocal set_wrapped
|
|
4265
|
+
wrapped, set_wrapped = react.use_state(True)
|
|
4266
|
+
child = child_once if same_element else Child().key("x")
|
|
4267
|
+
return w.VBox(children=[w.VBox(children=[child]) if wrapped else child])
|
|
4268
|
+
|
|
4269
|
+
root, rc = react.render_fixed(Test(), handle_error=False)
|
|
4270
|
+
for wrapped in [False, True, False]:
|
|
4271
|
+
set_wrapped(wrapped)
|
|
4272
|
+
button = rc.find(widgets.Button).widget
|
|
4273
|
+
assert button.comm is not None
|
|
4274
|
+
assert (root.children[0] is button) != wrapped
|
|
4275
|
+
rc.close()
|
|
4276
|
+
|
|
4277
|
+
|
|
4278
|
+
def test_keyed_child_out_of_removed_wrapper_keeps_its_effects():
|
|
4279
|
+
# the kept child stays mounted: its effect must not be cleaned up when the wrapper goes
|
|
4280
|
+
# away (the fast renderer cleans up stale subtrees early), only when the child leaves
|
|
4281
|
+
log = []
|
|
4282
|
+
set_wrapped = lambda x: None # noqa
|
|
4283
|
+
|
|
4284
|
+
@react.component
|
|
4285
|
+
def Child():
|
|
4286
|
+
def effect():
|
|
4287
|
+
log.append("run")
|
|
4288
|
+
return lambda: log.append("cleanup")
|
|
4289
|
+
|
|
4290
|
+
react.use_effect(effect, [])
|
|
4291
|
+
return w.Button(description="child")
|
|
4292
|
+
|
|
4293
|
+
@react.component
|
|
4294
|
+
def Test():
|
|
4295
|
+
nonlocal set_wrapped
|
|
4296
|
+
wrapped, set_wrapped = react.use_state(True)
|
|
4297
|
+
child = Child().key("x")
|
|
4298
|
+
return w.VBox(children=[w.Label(value="sibling"), w.VBox(children=[child]) if wrapped else child])
|
|
4299
|
+
|
|
4300
|
+
box, rc = react.render(Test(), handle_error=False)
|
|
4301
|
+
assert log == ["run"]
|
|
4302
|
+
set_wrapped(False)
|
|
4303
|
+
set_wrapped(True)
|
|
4304
|
+
assert log == ["run"]
|
|
4305
|
+
rc.close()
|
|
4306
|
+
assert log == ["run", "cleanup"]
|
|
@@ -42,6 +42,17 @@ def _random_app(registry, log):
|
|
|
42
42
|
return reacton.Fragment(children=[w.Button(description=f"f{id}.{i}") for i in range(value % 3)])
|
|
43
43
|
return w.Button(description=f"leaf {id} {value}")
|
|
44
44
|
|
|
45
|
+
@react.component
|
|
46
|
+
def EffectLeaf(id):
|
|
47
|
+
# a leaf with an effect: however its containers change, the effect must run once
|
|
48
|
+
# while it stays in the tree, and be cleaned up once when it leaves
|
|
49
|
+
def effect():
|
|
50
|
+
log.append(("keyed effect", id))
|
|
51
|
+
return lambda: log.append(("keyed cleanup", id))
|
|
52
|
+
|
|
53
|
+
react.use_effect(effect, [])
|
|
54
|
+
return Leaf(id)
|
|
55
|
+
|
|
45
56
|
@react.component
|
|
46
57
|
def Wrapper(id):
|
|
47
58
|
return Leaf(id * 7 + 1)
|
|
@@ -75,7 +86,7 @@ def _random_app(registry, log):
|
|
|
75
86
|
# that moves between two sibling containers whose types flip
|
|
76
87
|
state, set_state = react.use_state(0)
|
|
77
88
|
registry[id] = set_state
|
|
78
|
-
child = react.use_memo(lambda:
|
|
89
|
+
child = react.use_memo(lambda: EffectLeaf(id * 3 + 2).key(f"moved {id}"), [])
|
|
79
90
|
kind = h(id, state) % 8
|
|
80
91
|
First = w.HBox if kind & 2 else w.VBox
|
|
81
92
|
Second = w.HBox if kind & 4 else w.VBox
|
|
@@ -83,6 +94,21 @@ def _random_app(registry, log):
|
|
|
83
94
|
return w.VBox(children=[First(children=[child]).key("first"), Second(children=[]).key("second")])
|
|
84
95
|
return w.VBox(children=[First(children=[]).key("first"), Second(children=[child]).key("second")])
|
|
85
96
|
|
|
97
|
+
@react.component
|
|
98
|
+
def Unwrapper(id):
|
|
99
|
+
# a child with an explicit key inside 0, 1 or 2 wrapper containers: when a wrapper
|
|
100
|
+
# goes away (or comes back), the child stays in the tree at another level
|
|
101
|
+
state, set_state = react.use_state(0)
|
|
102
|
+
registry[id] = set_state
|
|
103
|
+
memo_child = react.use_memo(lambda: EffectLeaf(id * 3 + 2).key(f"unwrapped {id}"), [])
|
|
104
|
+
kind = h(id, state) % 12
|
|
105
|
+
child = memo_child if kind & 1 else EffectLeaf(id * 3 + 2).key(f"unwrapped {id}")
|
|
106
|
+
Wrapper = w.HBox if kind & 2 else w.VBox
|
|
107
|
+
element = child
|
|
108
|
+
for i in range((kind >> 2) % 3):
|
|
109
|
+
element = Wrapper(children=[element])
|
|
110
|
+
return w.VBox(children=[element])
|
|
111
|
+
|
|
86
112
|
@react.component
|
|
87
113
|
def Node(id, depth):
|
|
88
114
|
state, set_state = react.use_state(0)
|
|
@@ -114,6 +140,8 @@ def _random_app(registry, log):
|
|
|
114
140
|
child = Catcher(child_id)
|
|
115
141
|
elif r < 0.8:
|
|
116
142
|
child = Mover(child_id)
|
|
143
|
+
elif r < 0.9:
|
|
144
|
+
child = Unwrapper(child_id)
|
|
117
145
|
else:
|
|
118
146
|
child = Leaf(child_id)
|
|
119
147
|
if rnd.random() < 0.3:
|
|
@@ -147,7 +175,7 @@ def _run_random_updates(fast: bool, seed: int, steps: int, batches: Optional[Lis
|
|
|
147
175
|
record = batches is None
|
|
148
176
|
batches = [] if batches is None else batches
|
|
149
177
|
choices = random.Random(seed)
|
|
150
|
-
results = []
|
|
178
|
+
results: List[Any] = []
|
|
151
179
|
with unittest.mock.patch.dict(os.environ, {"REACTON_FAST": "1" if fast else "0"}):
|
|
152
180
|
widget, rc = react.render_fixed(Node(1, 0), handle_error=False)
|
|
153
181
|
assert isinstance(rc, core._RenderContextFast) == fast
|
|
@@ -162,7 +190,10 @@ def _run_random_updates(fast: bool, seed: int, steps: int, batches: Optional[Lis
|
|
|
162
190
|
if id in registry:
|
|
163
191
|
registry[id](value)
|
|
164
192
|
results.append((_widget_signature(rc.last_root_widget), sorted(registry), list(log)))
|
|
193
|
+
log.clear()
|
|
165
194
|
rc.close()
|
|
195
|
+
# every effect that ran is cleaned up once, in the same order
|
|
196
|
+
results.append(("close", list(log)))
|
|
166
197
|
return results, batches
|
|
167
198
|
|
|
168
199
|
|
|
@@ -179,4 +210,4 @@ def test_renderers_agree_on_random_updates(seed):
|
|
|
179
210
|
finally:
|
|
180
211
|
core.logger.setLevel(level)
|
|
181
212
|
for step, (a, b) in enumerate(zip(expected, got)):
|
|
182
|
-
assert a == b, f"step {step}, batch {batches[step]}"
|
|
213
|
+
assert a == b, f"step {step}, batch {batches[step]}" if step < len(batches) else "close"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.10.4"
|
|
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
|