From 85c82f3089386d0e8f302323dbbd96e17910893a Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:22:23 +0200 Subject: [PATCH] fix(mainwindow): release the closed main window as singleton instance The instance returned by `get_instance` kept a closed main window alive, with all its widgets and data, until another main window was created. In test suites, the previous window was therefore destroyed by the garbage collector at an arbitrary point of the next test. A main window closed for good (i.e. not only hidden, see `hide_on_close`) is no longer the singleton instance: `get_instance` creates a new window. Assisted-by: Claude Opus 5.5 --- .../LC_MESSAGES/release_notes/release_1.01.po | 3 +++ doc/release_notes/release_1.01.md | 1 + sigimax/mainwindow.py | 11 +++++++++- .../tests/mainwindow/test_lifecycle_hooks.py | 20 +++++++++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.01.po b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.01.po index e9c9d04..4d5b7a4 100644 --- a/doc/locale/fr/LC_MESSAGES/release_notes/release_1.01.po +++ b/doc/locale/fr/LC_MESSAGES/release_notes/release_1.01.po @@ -36,6 +36,9 @@ msgstr "" msgid "With Qt 5, the dock widget sizes saved on exit are now restored at startup: the window is sized and laid out before its state is restored, so tabified docks no longer shrink to the default window size and leave the extra width to the central widget." msgstr "" +msgid "Closing the main window (unless it is only hidden, see `hide_on_close`) now releases it as the instance returned by `get_instance`, which creates a new window afterwards: the closed window, with its widgets and data, is no longer kept in memory until another main window is created." +msgstr "La fermeture de la fenêtre principale (sauf si elle est seulement masquée, voir `hide_on_close`) la libère désormais en tant qu'instance renvoyée par `get_instance`, qui crée ensuite une nouvelle fenêtre : la fenêtre fermée, avec ses widgets et ses données, n'est plus conservée en mémoire jusqu'à la création d'une autre fenêtre principale." + msgid "Qt helpers" msgstr "" diff --git a/doc/release_notes/release_1.01.md b/doc/release_notes/release_1.01.md index a8b1e0a..58585d0 100644 --- a/doc/release_notes/release_1.01.md +++ b/doc/release_notes/release_1.01.md @@ -18,6 +18,7 @@ ### Main window ### * With Qt 5, the dock widget sizes saved on exit are now restored at startup: the window is sized and laid out before its state is restored, so tabified docks no longer shrink to the default window size and leave the extra width to the central widget. +* Closing the main window (unless it is only hidden, see `hide_on_close`) now releases it as the instance returned by `get_instance`, which creates a new window afterwards: the closed window, with its widgets and data, is no longer kept in memory until another main window is created. ### Qt helpers ### diff --git a/sigimax/mainwindow.py b/sigimax/mainwindow.py index dfb46a5..5a01dbb 100644 --- a/sigimax/mainwindow.py +++ b/sigimax/mainwindow.py @@ -96,7 +96,12 @@ class SGMXMainWindow(QW.QMainWindow, metaclass=SGMXMainWindowMeta): @classmethod def get_instance(cls, console=None, hide_on_close=False): - """Return the singleton instance for this window class.""" + """Return the singleton instance for this window class. + + A new window is created if there is no instance of this class, e.g. once + the previous one has been closed (unless it was only hidden, see + ``hide_on_close``). + """ if not isinstance(SGMXMainWindow.__instance, cls): return cls(console, hide_on_close) return SGMXMainWindow.__instance @@ -1372,6 +1377,10 @@ def closeEvent(self, event: QG.QCloseEvent) -> None: else: if self.close_properly(): self.SIG_CLOSING.emit() + # A closed window must not be kept alive (with all its widgets and + # data) as the instance returned by `get_instance` + if SGMXMainWindow.__instance is self: + SGMXMainWindow.__instance = None event.accept() else: event.ignore() diff --git a/sigimax/tests/mainwindow/test_lifecycle_hooks.py b/sigimax/tests/mainwindow/test_lifecycle_hooks.py index 0d442bc..1b7a7fe 100644 --- a/sigimax/tests/mainwindow/test_lifecycle_hooks.py +++ b/sigimax/tests/mainwindow/test_lifecycle_hooks.py @@ -4,6 +4,9 @@ from __future__ import annotations +import gc +import weakref + import pytest from qtpy import QtCore as QC from qtpy import QtWidgets as QW @@ -156,6 +159,23 @@ def test_get_instance_preserves_derived_window_class() -> None: derived_window.close() +def test_get_instance_after_close() -> None: + """Test that a closed window is released as instance, unlike a hidden one.""" + with qth.sigimax_app_context(exec_loop=False): + window = SGMXMainWindow(console=False, hide_on_close=True) + window.close() + assert SGMXMainWindow.get_instance() is window + window.hide_on_close = False + window.close() + window_ref = weakref.ref(window) + del window + gc.collect() + assert window_ref() is None, "Closed window is still alive" + new_window = SGMXMainWindow.get_instance(console=False) + assert isinstance(new_window, SGMXMainWindow) + new_window.close() + + def test_restored_dock_width() -> None: """Test that a restarted window keeps the central width chosen by the user.""" with qth.sigimax_app_context(exec_loop=False):