Skip to content

Commit 1cb8ff0

Browse files
Merge pull request #406 from DataLab-Platform/fix/release-closed-main-window
Destroy the widgets of each test before the next one
2 parents 9161e0e + 52340fb commit 1cb8ff0

4 files changed

Lines changed: 44 additions & 5 deletions

File tree

‎datalab/tests/conftest.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
executed before running any tests.
99
"""
1010

11+
import gc
1112
import os
1213
import os.path as osp
1314

@@ -23,6 +24,7 @@
2324
import skimage
2425
from guidata.config import ValidationMode, set_validation_mode
2526
from guidata.utils.gitreport import format_git_info_for_pytest, get_git_info_for_modules
27+
from qtpy import QtCore as QC
2628
from sigima.tests import helpers
2729

2830
import datalab
@@ -143,3 +145,11 @@ def reset_cwd(request): # pylint: disable=unused-argument
143145
def pytest_runtest_teardown(item, nextitem): # pylint: disable=unused-argument
144146
"""Run teardown after each test."""
145147
PluginRegistry.unregister_all_plugins()
148+
# Destroy the widgets of the test now, rather than during a later test: closed
149+
# widgets deleted later (e.g. the internal console) are only deleted by the
150+
# next event loop, and main windows are part of reference cycles, only
151+
# destroyed when the garbage collector runs, possibly amid Qt operations
152+
if QC.QCoreApplication.instance() is not None:
153+
QC.QCoreApplication.processEvents()
154+
QC.QCoreApplication.sendPostedEvents(None, QC.QEvent.DeferredDelete)
155+
gc.collect()

‎datalab/webapi/actions.py‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -279,9 +279,21 @@ def _on_server_error(self, message: str) -> None:
279279
self._on_server_stopped()
280280

281281
def cleanup(self) -> None:
282-
"""Clean up resources on shutdown."""
283-
if self._controller is not None and self._controller.is_running:
284-
self._controller.stop()
282+
"""Clean up resources on shutdown.
283+
284+
The Web API controller is shared by all main windows: once this one is
285+
closed, the controller must neither keep it alive nor notify its actions.
286+
"""
287+
controller = self._controller
288+
if controller is None:
289+
return
290+
if controller.is_running:
291+
controller.stop()
292+
controller.server_started.disconnect(self._on_server_started)
293+
controller.server_stopped.disconnect(self._on_server_stopped)
294+
controller.server_error.disconnect(self._on_server_error)
295+
controller.release_main_window(self._main_window)
296+
self._controller = None
285297

286298
def show_connection_info(self) -> None:
287299
"""Show connection info dialog or copy to clipboard.

‎datalab/webapi/controller.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,18 @@ def set_main_window(self, main_window: DLMainWindow) -> None:
106106
"""
107107
self._main_window = main_window
108108

109+
def release_main_window(self, main_window: DLMainWindow) -> None:
110+
"""Release the main window reference, if it is the given one.
111+
112+
The controller being shared by all main windows, a closed main window
113+
must not be kept alive by its reference.
114+
115+
Args:
116+
main_window: The DataLab main window being closed.
117+
"""
118+
if self._main_window is main_window:
119+
self._main_window = None
120+
109121
@property
110122
def is_running(self) -> bool:
111123
"""Check if the server is running."""
@@ -337,6 +349,11 @@ def stop(self) -> None:
337349

338350
self._uvicorn_server = None
339351
self._adapter = None
352+
# pylint: disable=import-outside-toplevel
353+
from datalab.webapi.routes import set_adapter
354+
355+
# The adapter references the main window
356+
set_adapter(None)
340357
self._url = None
341358
self._token = None
342359

‎datalab/webapi/routes.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@
6060
_LOCALHOST_NO_TOKEN: bool = False
6161

6262

63-
def set_adapter(adapter: WorkspaceAdapter) -> None:
64-
"""Set the workspace adapter for route handlers."""
63+
def set_adapter(adapter: WorkspaceAdapter | None) -> None:
64+
"""Set the workspace adapter for route handlers (None when stopped)."""
6565
global _ADAPTER # noqa: PLW0603 # pylint: disable=global-statement
6666
_ADAPTER = adapter
6767

0 commit comments

Comments
 (0)