reacton 1.10.1__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.1 → reacton-1.10.2}/PKG-INFO +1 -1
  2. reacton-1.10.2/reacton/_version.py +1 -0
  3. {reacton-1.10.1 → reacton-1.10.2}/reacton/core.py +33 -23
  4. {reacton-1.10.1 → reacton-1.10.2}/reacton/core_test.py +60 -0
  5. reacton-1.10.1/reacton/_version.py +0 -1
  6. {reacton-1.10.1 → reacton-1.10.2}/.gitignore +0 -0
  7. {reacton-1.10.1 → reacton-1.10.2}/LICENSE +0 -0
  8. {reacton-1.10.1 → reacton-1.10.2}/pyproject.toml +0 -0
  9. {reacton-1.10.1 → reacton-1.10.2}/react_ipywidgets/__init__.py +0 -0
  10. {reacton-1.10.1 → reacton-1.10.2}/react_ipywidgets/core.py +0 -0
  11. {reacton-1.10.1 → reacton-1.10.2}/reacton/__init__.py +0 -0
  12. {reacton-1.10.1 → reacton-1.10.2}/reacton/bqplot.py +0 -0
  13. {reacton-1.10.1 → reacton-1.10.2}/reacton/deprecated/__init__.py +0 -0
  14. {reacton-1.10.1 → reacton-1.10.2}/reacton/deprecated/ipyvuetify.py +0 -0
  15. {reacton-1.10.1 → reacton-1.10.2}/reacton/find.py +0 -0
  16. {reacton-1.10.1 → reacton-1.10.2}/reacton/find_test.py +0 -0
  17. {reacton-1.10.1 → reacton-1.10.2}/reacton/generate.py +0 -0
  18. {reacton-1.10.1 → reacton-1.10.2}/reacton/generate_test.py +0 -0
  19. {reacton-1.10.1 → reacton-1.10.2}/reacton/ipycanvas.py +0 -0
  20. {reacton-1.10.1 → reacton-1.10.2}/reacton/ipyvue.py +0 -0
  21. {reacton-1.10.1 → reacton-1.10.2}/reacton/ipyvue_test.py +0 -0
  22. {reacton-1.10.1 → reacton-1.10.2}/reacton/ipyvuetify.py +0 -0
  23. {reacton-1.10.1 → reacton-1.10.2}/reacton/ipywidgets.py +0 -0
  24. {reacton-1.10.1 → reacton-1.10.2}/reacton/logging.py +0 -0
  25. {reacton-1.10.1 → reacton-1.10.2}/reacton/patch.py +0 -0
  26. {reacton-1.10.1 → reacton-1.10.2}/reacton/patch_display.py +0 -0
  27. {reacton-1.10.1 → reacton-1.10.2}/reacton/py.typed +0 -0
  28. {reacton-1.10.1 → reacton-1.10.2}/reacton/rx.py +0 -0
  29. {reacton-1.10.1 → reacton-1.10.2}/reacton/test.vue +0 -0
  30. {reacton-1.10.1 → reacton-1.10.2}/reacton/test_rx.py +0 -0
  31. {reacton-1.10.1 → reacton-1.10.2}/reacton/utils.py +0 -0
  32. {reacton-1.10.1 → reacton-1.10.2}/reacton/utils_test.py +0 -0
  33. {reacton-1.10.1 → 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.1
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"
@@ -1322,29 +1322,33 @@ class _RenderContext:
1322
1322
  self._remove_element(self.element, default_key="/", parent_key=ROOT_KEY)
1323
1323
  logger.info("Removing elements done.")
1324
1324
  assert self.context is self.context_root
1325
- if self.container:
1326
- self.container.close()
1327
- if isinstance(self.container, widgets.DOMWidget) and self.container.layout is not None:
1328
- self.container.layout.close()
1329
- if self._shared_elements:
1330
- raise RuntimeError(f"Element not cleaned up: {self._shared_elements}")
1331
- if self._orphans:
1332
- orphan_widgets = set([_get_widgets_dict()[k] for k in self._orphans])
1333
- raise RuntimeError(f"Orphan widgets not cleaned up for widgets: {orphan_widgets}")
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()
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()
1348
1352
  if exceptions:
1349
1353
  raise exceptions[0]
1350
1354
 
@@ -1547,6 +1551,12 @@ class _RenderContext:
1547
1551
  self._lock_thread = threading.current_thread()
1548
1552
  if was_locked:
1549
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
1550
1560
  prev_rc = getattr(local, "rc", None)
1551
1561
  try:
1552
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()
@@ -1 +0,0 @@
1
- __version__ = "1.10.1"
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