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.
Files changed (34) hide show
  1. {reacton-1.10.4 → reacton-1.10.5}/PKG-INFO +1 -1
  2. reacton-1.10.5/reacton/_version.py +1 -0
  3. {reacton-1.10.4 → reacton-1.10.5}/reacton/core.py +35 -24
  4. {reacton-1.10.4 → reacton-1.10.5}/reacton/core_test.py +59 -0
  5. {reacton-1.10.4 → reacton-1.10.5}/reacton/fuzz_test.py +34 -3
  6. reacton-1.10.4/reacton/_version.py +0 -1
  7. {reacton-1.10.4 → reacton-1.10.5}/.gitignore +0 -0
  8. {reacton-1.10.4 → reacton-1.10.5}/LICENSE +0 -0
  9. {reacton-1.10.4 → reacton-1.10.5}/pyproject.toml +0 -0
  10. {reacton-1.10.4 → reacton-1.10.5}/react_ipywidgets/__init__.py +0 -0
  11. {reacton-1.10.4 → reacton-1.10.5}/react_ipywidgets/core.py +0 -0
  12. {reacton-1.10.4 → reacton-1.10.5}/reacton/__init__.py +0 -0
  13. {reacton-1.10.4 → reacton-1.10.5}/reacton/bqplot.py +0 -0
  14. {reacton-1.10.4 → reacton-1.10.5}/reacton/deprecated/__init__.py +0 -0
  15. {reacton-1.10.4 → reacton-1.10.5}/reacton/deprecated/ipyvuetify.py +0 -0
  16. {reacton-1.10.4 → reacton-1.10.5}/reacton/find.py +0 -0
  17. {reacton-1.10.4 → reacton-1.10.5}/reacton/find_test.py +0 -0
  18. {reacton-1.10.4 → reacton-1.10.5}/reacton/generate.py +0 -0
  19. {reacton-1.10.4 → reacton-1.10.5}/reacton/generate_test.py +0 -0
  20. {reacton-1.10.4 → reacton-1.10.5}/reacton/ipycanvas.py +0 -0
  21. {reacton-1.10.4 → reacton-1.10.5}/reacton/ipyvue.py +0 -0
  22. {reacton-1.10.4 → reacton-1.10.5}/reacton/ipyvue_test.py +0 -0
  23. {reacton-1.10.4 → reacton-1.10.5}/reacton/ipyvuetify.py +0 -0
  24. {reacton-1.10.4 → reacton-1.10.5}/reacton/ipywidgets.py +0 -0
  25. {reacton-1.10.4 → reacton-1.10.5}/reacton/logging.py +0 -0
  26. {reacton-1.10.4 → reacton-1.10.5}/reacton/patch.py +0 -0
  27. {reacton-1.10.4 → reacton-1.10.5}/reacton/patch_display.py +0 -0
  28. {reacton-1.10.4 → reacton-1.10.5}/reacton/py.typed +0 -0
  29. {reacton-1.10.4 → reacton-1.10.5}/reacton/rx.py +0 -0
  30. {reacton-1.10.4 → reacton-1.10.5}/reacton/test.vue +0 -0
  31. {reacton-1.10.4 → reacton-1.10.5}/reacton/test_rx.py +0 -0
  32. {reacton-1.10.4 → reacton-1.10.5}/reacton/utils.py +0 -0
  33. {reacton-1.10.4 → reacton-1.10.5}/reacton/utils_test.py +0 -0
  34. {reacton-1.10.4 → reacton-1.10.5}/reacton/work.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: reacton
3
- Version: 1.10.4
3
+ Version: 1.10.5
4
4
  Dynamic: Summary
5
5
  Project-URL: Home, https://www.github.com/widgetti/reacton
6
6
  Project-URL: Documentation, https://reacton.solara.dev
@@ -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 an element of another type replaces, and its context, while
1310
- # reconciliation removes it (see _remove_replaced)
1311
- _replaced: Optional[Tuple[ComponentContext, Element]] = None
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 _remove_replaced(self, el_prev: Element, key: str, parent_key: str):
1314
- """Remove the element at key, which an element of another type replaces."""
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
- replaced = self._replaced
1317
- self._replaced = (self.context, el_prev)
1316
+ outgoing = self._outgoing
1317
+ self._outgoing = (self.context, el)
1318
1318
  try:
1319
- self._remove_element(el_prev, key, parent_key=parent_key)
1319
+ self._remove_element(el, key, parent_key=parent_key)
1320
1320
  finally:
1321
- self._replaced = replaced
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 replaced element, or
1326
- # stays under the new one) is not removed with the replaced element: reconciliation
1327
- # updates it where the new tree has it. Shared elements keep the old behavior: their
1328
- # bookkeeping (_shared_elements, _shared_widgets) needs the removal.
1329
- replaced = self._replaced
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
- replaced is not None and context is replaced[0] and el is not replaced[1] and el._key is not None and not el.is_shared and key in context.used_keys
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._remove_replaced(el_prev, key, parent_key=parent_key)
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._remove_element(el_remove, key_remove, parent_key)
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._remove_replaced(el_prev, key, parent_key=parent_key)
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._remove_element(self.context.elements[key], key, parent_key=parent_key)
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._remove_replaced(el_prev, key, parent_key=parent_key)
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._remove_element(child_context.elements[stale_key], stale_key, new_parent_key)
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._remove_replaced(el_prev, key, parent_key=parent_key)
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
- self._cleanup_stale_effects(context.elements[stale_key], stale_key, parent_key)
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._remove_element(self.context_root.elements[stale_key], stale_key, parent_key)
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: Leaf(id * 3 + 2).key(f"moved {id}"), [])
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