1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-03-04 16:02:51 +01:00

wip weakrefs to methods

This commit is contained in:
2026-01-19 14:32:11 +01:00
parent 4218fe6845
commit 74c592e48b
2 changed files with 134 additions and 8 deletions

View File

@@ -1,17 +1,20 @@
# pylint: disable = no-name-in-module,missing-module-docstring
from __future__ import annotations
import inspect
import os
import time
import traceback
import uuid
import weakref
from datetime import datetime
from typing import TYPE_CHECKING, Callable, Final, Optional
from weakref import WeakValueDictionary
from weakref import WeakMethod, WeakValueDictionary
from bec_lib.logger import bec_logger
from bec_lib.utils.import_utils import lazy_import_from
from pydantic import BaseModel, Field, field_validator
from louie import saferef
from pydantic import BaseModel, ConfigDict, Field, field_validator
from qtpy.QtCore import QObject, QRunnable, QThreadPool, QTimer, Signal
from qtpy.QtWidgets import QApplication
@@ -43,7 +46,7 @@ class ConnectionConfig(BaseModel):
gui_id: Optional[str] = Field(
default=None, validate_default=True, description="The GUI ID of the widget."
)
model_config: dict = {"validate_assignment": True}
model_config: ConfigDict = {"validate_assignment": True}
@field_validator("gui_id")
@classmethod
@@ -86,6 +89,7 @@ class BECConnector:
USER_ACCESS = ["_config_dict", "_get_all_rpc", "_rpc_id"]
EXIT_HANDLERS: WeakValueDictionary[int, Callable[[],]] = WeakValueDictionary()
_exit_handler: Callable[[],] | None = None
_method_handlers: set[Callable[[],]] = set()
widget_removed = Signal()
name_established = Signal(str)
@@ -199,6 +203,15 @@ class BECConnector:
@classmethod
def _add_exit_handler(cls, handler: Callable, priority: int):
"""Private to allow use of priority 0"""
if inspect.ismethod(handler):
_h = saferef.safe_ref(handler)
def handler():
if h := _h():
h()
# cls._method_handlers.add(handler) # hold any instance methods in safe refs
cls.EXIT_HANDLERS[priority] = handler
@classmethod
@@ -226,8 +239,8 @@ class BECConnector:
@SafeSlot()
def _run_exit_handlers(self):
"""Run all exit handlers from highest to lowest priority. Should be connected to AboutToQuit once and only once."""
handlers = reversed(
list(handler for _, handler in sorted(BECConnector.EXIT_HANDLERS.items()))
handlers = list(
reversed(list(handler for _, handler in sorted(BECConnector.EXIT_HANDLERS.items())))
)
for handler in handlers:
handler()

View File

@@ -1,12 +1,18 @@
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
import gc
import time
from unittest.mock import MagicMock
from functools import partial
from multiprocessing import Process
from unittest.mock import MagicMock, call, patch
import pytest
from PySide6.QtWidgets import QWidget
from qtpy.QtCore import QObject
from qtpy.QtWidgets import QApplication
from bec_widgets.utils import BECConnector
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeSlot as Slot
from .client_mocks import mocked_client
@@ -138,7 +144,7 @@ def test_bec_connector_terminate_run_on_about_to_quit(qtbot, bec_connector):
assert BECConnector.EXIT_HANDLERS.get(0) is not None
terminate_mock = MagicMock()
bec_connector.__class__.EXIT_HANDLERS[0] = terminate_mock
QApplication.instance().aboutToQuit.emit()
bec_connector._run_exit_handlers()
qtbot.waitUntil(lambda: terminate_mock.call_count == 1)
@@ -147,5 +153,112 @@ def test_bec_connector_terminate_run_once_and_only_once(qtbot, bec_connector):
bec_connector.__class__.EXIT_HANDLERS[0] = terminate_mock
_conn_2 = BECConnectorQObject(client=mocked_client)
_conn_3 = BECConnectorQObject(client=mocked_client)
QApplication.instance().aboutToQuit.emit()
bec_connector._run_exit_handlers()
qtbot.waitUntil(lambda: terminate_mock.call_count == 1)
def test_bec_connector_exit_handlers_run_in_order(qtbot, bec_connector):
handler = MagicMock()
bec_connector.__class__.EXIT_HANDLERS[0] = handler
def h1():
handler(prio=1)
def h2():
handler(prio=2)
def h3():
handler(prio=3)
bec_connector._add_exit_handler(h3, 5)
bec_connector._add_exit_handler(h2, 10)
bec_connector._add_exit_handler(h1, 15)
bec_connector._run_exit_handlers()
qtbot.waitUntil(lambda: handler.call_count == 4)
handler.assert_has_calls([call(prio=1), call(prio=2), call(prio=3), call()])
@pytest.fixture
def mock_widget_with_exit_handlers(bec_connector, mocked_client):
with patch.object(mocked_client, "connector", bec_connector):
handler = MagicMock()
bec_connector.__class__.EXIT_HANDLERS[0] = handler
class DropWeakrefWidget(BECWidget, QWidget):
def __init__(
self,
client=None,
config: ConnectionConfig = None,
gui_id: str | None = None,
theme_update: bool = False,
start_busy: bool = False,
busy_text: str = "Loading…",
**kwargs,
):
super().__init__(
client, config, gui_id, theme_update, start_busy, busy_text, **kwargs
)
self.setup_on_exit()
self.client.connector.add_exit_handler(self._on_exit_stored_ref, 5)
self.client.connector.add_exit_handler(self.instance_on_exit, 7)
def setup_on_exit(self):
def _on_exit():
self.backgroundRole() # access some Qt thing just to fail test if c++ object is deleted
handler("called by DropWeakrefWidget in stored reference to function")
self._on_exit_stored_ref = _on_exit
def instance_on_exit(self):
self.backgroundRole() # access some Qt thing just to fail test if c++ object is deleted
handler("called by DropWeakrefWidget in instance method")
widget = DropWeakrefWidget(client=mocked_client)
return widget, handler
def test_connector_exit_handlers_doesnt_drop_when_widget_lives(
qtbot, bec_connector, mock_widget_with_exit_handlers
):
widget, handler = mock_widget_with_exit_handlers
qtbot.addWidget(widget)
def h1():
handler(prio=1)
bec_connector._add_exit_handler(h1, 15)
bec_connector._run_exit_handlers()
qtbot.waitUntil(lambda: handler.call_count == 4)
handler.assert_has_calls(
[
call(prio=1),
call("called by DropWeakrefWidget in instance method"),
call("called by DropWeakrefWidget in stored reference to function"),
call(), # from root cleanup
]
)
def test_connector_exit_handlers_drops_when_widget_dies(
qtbot, bec_connector, mock_widget_with_exit_handlers
):
widget, handler = mock_widget_with_exit_handlers
qtbot.addWidget(widget)
def h1():
handler(prio=1)
bec_connector._add_exit_handler(h1, 15)
widget.deleteLater()
qtbot.wait(100)
QApplication.processEvents()
del widget
qtbot.wait(100)
gc.collect()
qtbot.wait(100)
bec_connector._run_exit_handlers()
qtbot.waitUntil(lambda: handler.call_count == 2)
handler.assert_has_calls([call(prio=1), call()])