reacton 1.10.0__tar.gz → 1.10.1__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.1}/PKG-INFO +1 -1
  2. reacton-1.10.1/reacton/_version.py +1 -0
  3. {reacton-1.10.0 → reacton-1.10.1}/reacton/core.py +77 -9
  4. {reacton-1.10.0 → reacton-1.10.1}/reacton/core_test.py +24 -0
  5. reacton-1.10.0/reacton/_version.py +0 -1
  6. {reacton-1.10.0 → reacton-1.10.1}/.gitignore +0 -0
  7. {reacton-1.10.0 → reacton-1.10.1}/LICENSE +0 -0
  8. {reacton-1.10.0 → reacton-1.10.1}/pyproject.toml +0 -0
  9. {reacton-1.10.0 → reacton-1.10.1}/react_ipywidgets/__init__.py +0 -0
  10. {reacton-1.10.0 → reacton-1.10.1}/react_ipywidgets/core.py +0 -0
  11. {reacton-1.10.0 → reacton-1.10.1}/reacton/__init__.py +0 -0
  12. {reacton-1.10.0 → reacton-1.10.1}/reacton/bqplot.py +0 -0
  13. {reacton-1.10.0 → reacton-1.10.1}/reacton/deprecated/__init__.py +0 -0
  14. {reacton-1.10.0 → reacton-1.10.1}/reacton/deprecated/ipyvuetify.py +0 -0
  15. {reacton-1.10.0 → reacton-1.10.1}/reacton/find.py +0 -0
  16. {reacton-1.10.0 → reacton-1.10.1}/reacton/find_test.py +0 -0
  17. {reacton-1.10.0 → reacton-1.10.1}/reacton/generate.py +0 -0
  18. {reacton-1.10.0 → reacton-1.10.1}/reacton/generate_test.py +0 -0
  19. {reacton-1.10.0 → reacton-1.10.1}/reacton/ipycanvas.py +0 -0
  20. {reacton-1.10.0 → reacton-1.10.1}/reacton/ipyvue.py +0 -0
  21. {reacton-1.10.0 → reacton-1.10.1}/reacton/ipyvue_test.py +0 -0
  22. {reacton-1.10.0 → reacton-1.10.1}/reacton/ipyvuetify.py +0 -0
  23. {reacton-1.10.0 → reacton-1.10.1}/reacton/ipywidgets.py +0 -0
  24. {reacton-1.10.0 → reacton-1.10.1}/reacton/logging.py +0 -0
  25. {reacton-1.10.0 → reacton-1.10.1}/reacton/patch.py +0 -0
  26. {reacton-1.10.0 → reacton-1.10.1}/reacton/patch_display.py +0 -0
  27. {reacton-1.10.0 → reacton-1.10.1}/reacton/py.typed +0 -0
  28. {reacton-1.10.0 → reacton-1.10.1}/reacton/rx.py +0 -0
  29. {reacton-1.10.0 → reacton-1.10.1}/reacton/test.vue +0 -0
  30. {reacton-1.10.0 → reacton-1.10.1}/reacton/test_rx.py +0 -0
  31. {reacton-1.10.0 → reacton-1.10.1}/reacton/utils.py +0 -0
  32. {reacton-1.10.0 → reacton-1.10.1}/reacton/utils_test.py +0 -0
  33. {reacton-1.10.0 → reacton-1.10.1}/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.1
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.1"
@@ -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,6 +1307,17 @@ 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.")
@@ -1281,6 +1332,19 @@ class _RenderContext:
1281
1332
  orphan_widgets = set([_get_widgets_dict()[k] for k in self._orphans])
1282
1333
  raise RuntimeError(f"Orphan widgets not cleaned up for widgets: {orphan_widgets}")
1283
1334
  exceptions = [*self.context.exceptions_children, *self.context_root.exceptions_self]
1335
+ # break the reference cycles through the tree (see _teardown_component_context);
1336
+ # _closing stays True, making stray setters and event handlers no-ops
1337
+ for context in all_contexts:
1338
+ _teardown_component_context(context)
1339
+ self.context = None
1340
+ self.context_root = None # type: ignore
1341
+ # the root element, container and root widget keep the widget tree alive, and
1342
+ # widgets reference their elements, whose kwargs hold user callbacks - which
1343
+ # capture use_state setters and therefore this render context
1344
+ self.element = None # type: ignore
1345
+ self.container = None
1346
+ self.last_root_widget = None
1347
+ self._old_element_ids.clear()
1284
1348
  if exceptions:
1285
1349
  raise exceptions[0]
1286
1350
 
@@ -1346,12 +1410,18 @@ class _RenderContext:
1346
1410
  if DEBUG:
1347
1411
  created_stack = traceback.format_stack()
1348
1412
 
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
1413
+ # NOTE: set_ captures self and context strongly, and that is a requirement:
1414
+ # a setter may be the ONLY reference keeping its component context alive
1415
+ # (see cf61378, where weak references lost live state). The resulting
1416
+ # reference cycles through the closed tree are broken by close() emptying
1417
+ # the component contexts instead.
1353
1418
 
1354
1419
  def set_(value):
1420
+ if self._closing:
1421
+ # the render context is closed (or closing) and the tree is (being)
1422
+ # torn down: there is nothing to update. This check must come first:
1423
+ # after close, context.state is empty and reading it would fail.
1424
+ return
1355
1425
  if callable(value):
1356
1426
  value = value(context.state[key])
1357
1427
  logger.info("Set state = %r for key %r (previous value was %r) (%r)", value, key, context.state[key], id(self.context))
@@ -1384,11 +1454,6 @@ class _RenderContext:
1384
1454
  equals = eq or utils.equals
1385
1455
  should_update = not equals(context.state[key], value) or should_update
1386
1456
 
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
1457
  if should_update:
1393
1458
  prev_value = context.state[key]
1394
1459
  context.state[key] = value
@@ -1421,6 +1486,9 @@ class _RenderContext:
1421
1486
  return set_
1422
1487
 
1423
1488
  def force_update(self):
1489
+ if self._closing:
1490
+ # e.g. an event handler on a closed tree routing an exception to us
1491
+ return
1424
1492
  # a forced update re-walks the whole tree, no subtree skipping
1425
1493
  self._walk_all = True
1426
1494
  if not self._is_rendering:
@@ -3162,6 +3162,30 @@ def test_memory_leak(component):
3162
3162
  assert weak_rc() is None
3163
3163
 
3164
3164
 
3165
+ @reacton.component
3166
+ def UseStateComponent():
3167
+ value, set_value = reacton.use_state(0)
3168
+ return w.Button(description=str(value), on_click=lambda: set_value(value + 1))
3169
+
3170
+
3171
+ @pytest.mark.parametrize("component", [w.Button, ButtonComponentFunction, UseMemoComponent, UseStateComponent])
3172
+ def test_no_reference_cycles_after_close(component):
3173
+ # Stricter than test_memory_leak: the closed render tree must be freed by plain
3174
+ # reference counting, without a garbage collection. If this fails, a closure (a
3175
+ # use_state setter or an event handler) holds the render context strongly and the
3176
+ # tree became one big reference cycle that only a gen-2 gc can free - on a server
3177
+ # that shows up as a memory sawtooth that scales with per-kernel state.
3178
+ box, rc = react.render(component(), handle_error=False)
3179
+ rc.close()
3180
+ weak_rc = weakref.ref(rc)
3181
+ gc.disable()
3182
+ try:
3183
+ del rc
3184
+ assert weak_rc() is None
3185
+ finally:
3186
+ gc.enable()
3187
+
3188
+
3165
3189
  def test_fragment():
3166
3190
  @reacton.component
3167
3191
  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