Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions datalab/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
executed before running any tests.
"""

import gc
import os
import os.path as osp

Expand All @@ -23,6 +24,7 @@
import skimage
from guidata.config import ValidationMode, set_validation_mode
from guidata.utils.gitreport import format_git_info_for_pytest, get_git_info_for_modules
from qtpy import QtCore as QC
from sigima.tests import helpers

import datalab
Expand Down Expand Up @@ -143,3 +145,11 @@ def reset_cwd(request): # pylint: disable=unused-argument
def pytest_runtest_teardown(item, nextitem): # pylint: disable=unused-argument
"""Run teardown after each test."""
PluginRegistry.unregister_all_plugins()
# Destroy the widgets of the test now, rather than during a later test: closed
# widgets deleted later (e.g. the internal console) are only deleted by the
# next event loop, and main windows are part of reference cycles, only
# destroyed when the garbage collector runs, possibly amid Qt operations
if QC.QCoreApplication.instance() is not None:
QC.QCoreApplication.processEvents()
QC.QCoreApplication.sendPostedEvents(None, QC.QEvent.DeferredDelete)
gc.collect()
18 changes: 15 additions & 3 deletions datalab/webapi/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,9 +279,21 @@ def _on_server_error(self, message: str) -> None:
self._on_server_stopped()

def cleanup(self) -> None:
"""Clean up resources on shutdown."""
if self._controller is not None and self._controller.is_running:
self._controller.stop()
"""Clean up resources on shutdown.

The Web API controller is shared by all main windows: once this one is
closed, the controller must neither keep it alive nor notify its actions.
"""
controller = self._controller
if controller is None:
return
if controller.is_running:
controller.stop()
controller.server_started.disconnect(self._on_server_started)
controller.server_stopped.disconnect(self._on_server_stopped)
controller.server_error.disconnect(self._on_server_error)
controller.release_main_window(self._main_window)
self._controller = None

def show_connection_info(self) -> None:
"""Show connection info dialog or copy to clipboard.
Expand Down
17 changes: 17 additions & 0 deletions datalab/webapi/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ def set_main_window(self, main_window: DLMainWindow) -> None:
"""
self._main_window = main_window

def release_main_window(self, main_window: DLMainWindow) -> None:
"""Release the main window reference, if it is the given one.

The controller being shared by all main windows, a closed main window
must not be kept alive by its reference.

Args:
main_window: The DataLab main window being closed.
"""
if self._main_window is main_window:
self._main_window = None

@property
def is_running(self) -> bool:
"""Check if the server is running."""
Expand Down Expand Up @@ -337,6 +349,11 @@ def stop(self) -> None:

self._uvicorn_server = None
self._adapter = None
# pylint: disable=import-outside-toplevel
from datalab.webapi.routes import set_adapter

# The adapter references the main window
set_adapter(None)
self._url = None
self._token = None

Expand Down
4 changes: 2 additions & 2 deletions datalab/webapi/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@
_LOCALHOST_NO_TOKEN: bool = False


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

Expand Down
Loading