Skip to content

Commit 2829d7b

Browse files
committed
widgetsscheme: Schedule gc.collect on widget delete
1 parent 600f194 commit 2829d7b

File tree

2 files changed

+39
-16
lines changed

2 files changed

+39
-16
lines changed

orangewidget/workflow/tests/test_widgetsscheme.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import unittest
22
import unittest.mock
33
import logging
4+
import weakref
45

56
from types import SimpleNamespace
67
from typing import Type
@@ -332,6 +333,19 @@ def test_extra_actions(self):
332333
actions = wm.actions_for_context_menu(widgets.w1_node)
333334
self.assertIn(a, actions)
334335

336+
def test_widget_gc_on_delete(self):
337+
model = WidgetsScheme()
338+
node = model.new_node(widget_description(Number))
339+
wm = model.widget_manager
340+
t: QTimer = wm.findChild(QTimer, "gc-timer")
341+
widget = model.widget_for_node(node)
342+
widget._self_ref = widget # ensure cycle
343+
wref = weakref.ref(widget)
344+
del widget
345+
model.remove_node(node)
346+
assert QSignalSpy(t.timeout).wait()
347+
self.assertIsNone(wref())
348+
335349

336350
class TestSignalManager(GuiTest):
337351
def test_signalmanager(self):

orangewidget/workflow/widgetsscheme.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
:bases:
2020
"""
2121
import copy
22+
import gc
2223
import logging
2324
import enum
2425
import types
@@ -34,7 +35,7 @@
3435
from AnyQt.QtWidgets import QWidget, QAction
3536
from AnyQt.QtGui import QWhatsThisClickedEvent
3637

37-
from AnyQt.QtCore import Qt, QCoreApplication, QEvent, QByteArray
38+
from AnyQt.QtCore import Qt, QCoreApplication, QEvent, QByteArray, QTimer
3839
from AnyQt.QtCore import pyqtSlot as Slot
3940

4041
from orangecanvas.registry import WidgetDescription, OutputSignal
@@ -213,6 +214,8 @@ def __init__(self, parent=None, **kwargs):
213214

214215
# Tracks the widget in the update loop by the SignalManager
215216
self.__updating_widget = None # type: Optional[OWBaseWidget]
217+
self.__gc_timer = QTimer(self, singleShot=True, interval=100, objectName="gc-timer")
218+
self.__gc_timer.timeout.connect(self.__run_gc)
216219

217220
def set_scheme(self, scheme):
218221
"""
@@ -364,24 +367,30 @@ def __delete_item(self, item):
364367
"Deferring deletion.", widget, item.state)
365368
self.__delay_delete[widget] = item
366369
else:
367-
widget.processingStateChanged.disconnect(
368-
self.__on_widget_state_changed)
369-
widget.widgetStateChanged.disconnect(
370-
self.__on_widget_state_changed)
371-
widget.deleteLater()
372-
item.widget = None
370+
self.__delete_widget(item)
373371

374372
def __try_delete(self, item):
375373
if not item.state & WidgetManager.DelayDeleteMask:
376-
widget = item.widget
377-
log.debug("Delayed delete for widget %s", widget)
378-
widget.widgetStateChanged.disconnect(
379-
self.__on_widget_state_changed)
380-
widget.processingStateChanged.disconnect(
381-
self.__on_widget_state_changed)
382-
item.widget = None
383-
widget.deleteLater()
384-
del self.__delay_delete[widget]
374+
log.debug("Delayed delete for widget %s", item.widget)
375+
self.__delete_widget(item)
376+
377+
def __delete_widget(self, item: Item):
378+
widget = item.widget
379+
log.info("Disposing of widget '%s'", widget.captionTitle)
380+
widget.widgetStateChanged.disconnect(
381+
self.__on_widget_state_changed)
382+
widget.processingStateChanged.disconnect(
383+
self.__on_widget_state_changed)
384+
item.widget = None
385+
widget.deleteLater()
386+
self.__delay_delete.pop(widget, None)
387+
self.__gc_timer.start()
388+
389+
@Slot()
390+
def __run_gc(self):
391+
log.debug("Running gc.collect()")
392+
n = gc.collect()
393+
log.debug(f"gc.collect() collected {n} objects")
385394

386395
def create_widget_instance(self, node):
387396
# type: (SchemeNode) -> OWBaseWidget

0 commit comments

Comments
 (0)