reacton 1.10.0__tar.gz → 1.10.2__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 (33) hide show
  1. {reacton-1.10.0 → reacton-1.10.2}/PKG-INFO +1 -1
  2. reacton-1.10.2/reacton/_version.py +1 -0
  3. {reacton-1.10.0 → reacton-1.10.2}/reacton/core.py +97 -19
  4. {reacton-1.10.0 → reacton-1.10.2}/reacton/core_test.py +84 -0
  5. reacton-1.10.0/reacton/_version.py +0 -1
  6. {reacton-1.10.0 → reacton-1.10.2}/.gitignore +0 -0
  7. {reacton-1.10.0 → reacton-1.10.2}/LICENSE +0 -0
  8. {reacton-1.10.0 → reacton-1.10.2}/pyproject.toml +0 -0
  9. {reacton-1.10.0 → reacton-1.10.2}/react_ipywidgets/__init__.py +0 -0
  10. {reacton-1.10.0 → reacton-1.10.2}/react_ipywidgets/core.py +0 -0
  11. {reacton-1.10.0 → reacton-1.10.2}/reacton/__init__.py +0 -0
  12. {reacton-1.10.0 → reacton-1.10.2}/reacton/bqplot.py +0 -0
  13. {reacton-1.10.0 → reacton-1.10.2}/reacton/deprecated/__init__.py +0 -0
  14. {reacton-1.10.0 → reacton-1.10.2}/reacton/deprecated/ipyvuetify.py +0 -0
  15. {reacton-1.10.0 → reacton-1.10.2}/reacton/find.py +0 -0
  16. {reacton-1.10.0 → reacton-1.10.2}/reacton/find_test.py +0 -0
  17. {reacton-1.10.0 → reacton-1.10.2}/reacton/generate.py +0 -0
  18. {reacton-1.10.0 → reacton-1.10.2}/reacton/generate_test.py +0 -0
  19. {reacton-1.10.0 → reacton-1.10.2}/reacton/ipycanvas.py +0 -0
  20. {reacton-1.10.0 → reacton-1.10.2}/reacton/ipyvue.py +0 -0
  21. {reacton-1.10.0 → reacton-1.10.2}/reacton/ipyvue_test.py +0 -0
  22. {reacton-1.10.0 → reacton-1.10.2}/reacton/ipyvuetify.py +0 -0
  23. {reacton-1.10.0 → reacton-1.10.2}/reacton/ipywidgets.py +0 -0
  24. {reacton-1.10.0 → reacton-1.10.2}/reacton/logging.py +0 -0
  25. {reacton-1.10.0 → reacton-1.10.2}/reacton/patch.py +0 -0
  26. {reacton-1.10.0 → reacton-1.10.2}/reacton/patch_display.py +0 -0
  27. {reacton-1.10.0 → reacton-1.10.2}/reacton/py.typed +0 -0
  28. {reacton-1.10.0 → reacton-1.10.2}/reacton/rx.py +0 -0
  29. {reacton-1.10.0 → reacton-1.10.2}/reacton/test.vue +0 -0
  30. {reacton-1.10.0 → reacton-1.10.2}/reacton/test_rx.py +0 -0
  31. {reacton-1.10.0 → reacton-1.10.2}/reacton/utils.py +0 -0
  32. {reacton-1.10.0 → reacton-1.10.2}/reacton/utils_test.py +0 -0
  33. {reacton-1.10.0 → reacton-1.10.2}/reacton/work.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reacton
3
- Version: 1.10.0
3
+ Version: 1.10.2
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.2"
@@ -1166,6 +1166,46 @@ class ComponentContext:
1166
1166
  TEffect = TypeVar("TEffect", bound="Effect")
1167
1167
 
1168
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
+
1169
1209
  @dataclass
1170
1210
  class RerenderReason:
1171
1211
  reason: str
@@ -1267,20 +1307,48 @@ class _RenderContext:
1267
1307
  def close(self):
1268
1308
  with self.thread_lock:
1269
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)
1270
1321
  logger.info("Removing elements...")
1271
1322
  self._remove_element(self.element, default_key="/", parent_key=ROOT_KEY)
1272
1323
  logger.info("Removing elements done.")
1273
1324
  assert self.context is self.context_root
1274
- if self.container:
1275
- self.container.close()
1276
- if isinstance(self.container, widgets.DOMWidget) and self.container.layout is not None:
1277
- self.container.layout.close()
1278
- if self._shared_elements:
1279
- raise RuntimeError(f"Element not cleaned up: {self._shared_elements}")
1280
- if self._orphans:
1281
- orphan_widgets = set([_get_widgets_dict()[k] for k in self._orphans])
1282
- raise RuntimeError(f"Orphan widgets not cleaned up for widgets: {orphan_widgets}")
1283
- exceptions = [*self.context.exceptions_children, *self.context_root.exceptions_self]
1325
+ # everything below used to run outside the lock: a render() that was
1326
+ # blocked on the lock could then interleave with this teardown and
1327
+ # find self.context None mid-render (AssertionError in
1328
+ # Element.__exit__, seen in production behind a prompt kernel close)
1329
+ if self.container:
1330
+ self.container.close()
1331
+ if isinstance(self.container, widgets.DOMWidget) and self.container.layout is not None:
1332
+ self.container.layout.close()
1333
+ if self._shared_elements:
1334
+ raise RuntimeError(f"Element not cleaned up: {self._shared_elements}")
1335
+ if self._orphans:
1336
+ orphan_widgets = set([_get_widgets_dict()[k] for k in self._orphans])
1337
+ raise RuntimeError(f"Orphan widgets not cleaned up for widgets: {orphan_widgets}")
1338
+ exceptions = [*self.context.exceptions_children, *self.context_root.exceptions_self]
1339
+ # break the reference cycles through the tree (see _teardown_component_context);
1340
+ # _closing stays True, making stray setters and event handlers no-ops
1341
+ for context in all_contexts:
1342
+ _teardown_component_context(context)
1343
+ self.context = None
1344
+ self.context_root = None # type: ignore
1345
+ # the root element, container and root widget keep the widget tree alive, and
1346
+ # widgets reference their elements, whose kwargs hold user callbacks - which
1347
+ # capture use_state setters and therefore this render context
1348
+ self.element = None # type: ignore
1349
+ self.container = None
1350
+ self.last_root_widget = None
1351
+ self._old_element_ids.clear()
1284
1352
  if exceptions:
1285
1353
  raise exceptions[0]
1286
1354
 
@@ -1346,12 +1414,18 @@ class _RenderContext:
1346
1414
  if DEBUG:
1347
1415
  created_stack = traceback.format_stack()
1348
1416
 
1349
- # if we do not use a weakref, the memory test fails
1350
- # I am not sure exactly why, but this will at least make
1351
- # it easier for Python to garbage collect, since it will
1352
- # avoid circular references
1417
+ # NOTE: set_ captures self and context strongly, and that is a requirement:
1418
+ # a setter may be the ONLY reference keeping its component context alive
1419
+ # (see cf61378, where weak references lost live state). The resulting
1420
+ # reference cycles through the closed tree are broken by close() emptying
1421
+ # the component contexts instead.
1353
1422
 
1354
1423
  def set_(value):
1424
+ if self._closing:
1425
+ # the render context is closed (or closing) and the tree is (being)
1426
+ # torn down: there is nothing to update. This check must come first:
1427
+ # after close, context.state is empty and reading it would fail.
1428
+ return
1355
1429
  if callable(value):
1356
1430
  value = value(context.state[key])
1357
1431
  logger.info("Set state = %r for key %r (previous value was %r) (%r)", value, key, context.state[key], id(self.context))
@@ -1384,11 +1458,6 @@ class _RenderContext:
1384
1458
  equals = eq or utils.equals
1385
1459
  should_update = not equals(context.state[key], value) or should_update
1386
1460
 
1387
- # if a cleanup during close trigger a state update in a different component, we want to ignore that
1388
- # otherwise we get a deadlock
1389
- if self._closing:
1390
- should_update = False
1391
-
1392
1461
  if should_update:
1393
1462
  prev_value = context.state[key]
1394
1463
  context.state[key] = value
@@ -1421,6 +1490,9 @@ class _RenderContext:
1421
1490
  return set_
1422
1491
 
1423
1492
  def force_update(self):
1493
+ if self._closing:
1494
+ # e.g. an event handler on a closed tree routing an exception to us
1495
+ return
1424
1496
  # a forced update re-walks the whole tree, no subtree skipping
1425
1497
  self._walk_all = True
1426
1498
  if not self._is_rendering:
@@ -1479,6 +1551,12 @@ class _RenderContext:
1479
1551
  self._lock_thread = threading.current_thread()
1480
1552
  if was_locked:
1481
1553
  logger.info("Mutex released, continuing render phase")
1554
+ if self._closing or self.context is None:
1555
+ # close() won the race for the lock (a disconnect can close the
1556
+ # kernel while an update was waiting to render): the tree is
1557
+ # torn down, there is nothing to render into anymore
1558
+ logger.info("Render requested on a closing/closed render context, ignoring")
1559
+ return container
1482
1560
  prev_rc = getattr(local, "rc", None)
1483
1561
  try:
1484
1562
  local.rc = self
@@ -2963,6 +2963,66 @@ def test_close_when_overridden():
2963
2963
  rc.close()
2964
2964
 
2965
2965
 
2966
+ def test_render_after_close_is_noop():
2967
+ # A disconnect can close the kernel while an update is waiting on the render
2968
+ # lock: the late render used to proceed into the torn-down tree and hit
2969
+ # "assert rc.context is not None" in Element.__exit__ (seen in production).
2970
+ # A render on a closed/closing context must be a no-op instead.
2971
+ @react.component
2972
+ def App():
2973
+ value, _set_value = react.use_state(1)
2974
+ return w.Label(value=str(value))
2975
+
2976
+ box, rc = react.render(App(), handle_error=False)
2977
+ rc.close()
2978
+ rc.render(App(), box) # must not raise, must not render
2979
+
2980
+
2981
+ def test_close_render_race():
2982
+ # close() in one thread while renders queue behind the lock in another:
2983
+ # neither thread may raise (the close teardown must not interleave with a
2984
+ # render that was blocked on the lock).
2985
+ import threading
2986
+
2987
+ for _ in range(20):
2988
+
2989
+ @react.component
2990
+ def App():
2991
+ value, _set_value = react.use_state(1)
2992
+ return w.Label(value=str(value))
2993
+
2994
+ box, rc = react.render(App(), handle_error=False)
2995
+ el = App()
2996
+ errors: List[BaseException] = []
2997
+ barrier = threading.Barrier(2)
2998
+
2999
+ def do_render(rc=rc, el=el, box=box, errors=errors, barrier=barrier):
3000
+ try:
3001
+ barrier.wait()
3002
+ rc.render(el, box)
3003
+ except BaseException as e: # noqa
3004
+ errors.append(e)
3005
+
3006
+ def do_close(rc=rc, errors=errors, barrier=barrier):
3007
+ try:
3008
+ barrier.wait()
3009
+ rc.close()
3010
+ except BaseException as e: # noqa
3011
+ errors.append(e)
3012
+
3013
+ t1 = threading.Thread(target=do_render)
3014
+ t2 = threading.Thread(target=do_close)
3015
+ t1.start()
3016
+ t2.start()
3017
+ t1.join()
3018
+ t2.join()
3019
+ # whichever won the lock, the loser must not blow up; if render won,
3020
+ # close ran after and the context must still close cleanly
3021
+ if not rc._closing:
3022
+ rc.close()
3023
+ assert not errors, errors
3024
+
3025
+
2966
3026
  def test_render_perf_child_only():
2967
3027
  render_child = unittest.mock.Mock()
2968
3028
  render_main = unittest.mock.Mock()
@@ -3162,6 +3222,30 @@ def test_memory_leak(component):
3162
3222
  assert weak_rc() is None
3163
3223
 
3164
3224
 
3225
+ @reacton.component
3226
+ def UseStateComponent():
3227
+ value, set_value = reacton.use_state(0)
3228
+ return w.Button(description=str(value), on_click=lambda: set_value(value + 1))
3229
+
3230
+
3231
+ @pytest.mark.parametrize("component", [w.Button, ButtonComponentFunction, UseMemoComponent, UseStateComponent])
3232
+ def test_no_reference_cycles_after_close(component):
3233
+ # Stricter than test_memory_leak: the closed render tree must be freed by plain
3234
+ # reference counting, without a garbage collection. If this fails, a closure (a
3235
+ # use_state setter or an event handler) holds the render context strongly and the
3236
+ # tree became one big reference cycle that only a gen-2 gc can free - on a server
3237
+ # that shows up as a memory sawtooth that scales with per-kernel state.
3238
+ box, rc = react.render(component(), handle_error=False)
3239
+ rc.close()
3240
+ weak_rc = weakref.ref(rc)
3241
+ gc.disable()
3242
+ try:
3243
+ del rc
3244
+ assert weak_rc() is None
3245
+ finally:
3246
+ gc.enable()
3247
+
3248
+
3165
3249
  def test_fragment():
3166
3250
  @reacton.component
3167
3251
  def Children():
@@ -1 +0,0 @@
1
- __version__ = "1.10.0"
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