enthought / traitsui

TraitsUI: Traits-capable windowing framework
http://docs.enthought.com/traitsui
Other
297 stars 95 forks source link

TableEditor leaks a TableModel object under PySide. #319

Open mdickinson opened 7 years ago

mdickinson commented 7 years ago

Under Qt4 / PySide, the TableEditor creates an uncollectable TableModel object.

Here's some code that exercises the bug:

from traits.api import HasStrictTraits, List, Str
from traitsui.api import TableEditor, UItem, View
from traitsui.table_column import ObjectColumn

class Employee(HasStrictTraits):
    first_name = Str
    last_name = Str

class Department(HasStrictTraits):
    employees = List(Employee)

    traits_view = View(
        UItem(
            'employees',
            editor=TableEditor(
                columns=[
                    ObjectColumn(name='first_name'),
                    ObjectColumn(name='last_name'),
                ],
            ),
        ),
    )

def main():
    demo = Department(
        employees=[
            Employee(first_name='Jason', last_name='Smith'),
            Employee(first_name='Lyn', last_name='Spitz'),
        ]
    )
    demo.configure_traits()

if __name__ == '__main__':
    import gc
    for _ in xrange(4):
        main()

    while gc.collect():
        pass

    table_models = [
        obj for obj in gc.get_objects()
        if type(obj).__name__ == 'TableModel'
    ]
    print "Number of leaked table models: ", len(table_models)

When I run the above, after dismissing the UI four times, I get:

Number of leaked table models:  4

The TableModel objects in question have no references to them (excluding the table_models list itself), according to gc.get_referrers. I suspect that some PySide signal handler is incrementing the reference count of the TableModel and never doing the corresponding decrement.

mdickinson commented 7 years ago

Note: this was on OS X 10.9. I haven't tested on other platforms, but I don't have any reason to suspect that the bug is platform-dependent.

mdickinson commented 7 years ago

Here's the leak, reduced to its PySide essentials. I'm looking for some sort of cleanup operation I can do to undo the leak, but I haven't found anything yet.

from PySide import QtCore
from PySide import QtGui

demonstrate_refleak = True

class MyTableModel(QtCore.QAbstractTableModel):
    def rowCount(self, mi):
        return 2

    def columnCount(self, mi):
        return 2

    def data(self, mi, role):
        return 'spam' if role == QtCore.Qt.DisplayRole else None

class MyWindow(QtGui.QWidget):
    def __init__(self, *args):
        QtGui.QWidget.__init__(self, *args)
        self.table_model = MyTableModel()
        self.table_view = QtGui.QTableView()

        if demonstrate_refleak:
            # The use of the proxy seems to lead to the refleak.
            self.proxy_model = QtGui.QSortFilterProxyModel()
            self.proxy_model.setSourceModel(self.table_model)
            self.table_view.setModel(self.proxy_model)
        else:
            # Version that doesn't leak.
            self.table_view.setModel(self.table_model)

        layout = QtGui.QVBoxLayout(self)
        layout.addWidget(self.table_view)
        self.setLayout(layout)

def main():
    app = QtGui.QApplication.instance()
    if app is None:
        app = QtGui.QApplication([])

    win = MyWindow()
    win.show()
    app.exec_()

if __name__ == '__main__':
    import gc
    for _ in range(4):
        main()

    while gc.collect():
        pass
    table_models = [obj for obj in gc.get_objects() if isinstance(obj, MyTableModel)]
    print "Number of live MyTableModel instances: ", len(table_models)
mdickinson commented 7 years ago

The leak doesn't occur under PyQt4 (though I do get error messages of the form "QObject::startTimer: QTimer can only be used with threads started with QThread").

corranwebster commented 7 years ago

Possible fix is to handle the sorting/filtering ourselves rather than relying on PySide to do it. Downsides would be additional complexity on the Python side, plus possibly less-nice user experience (eg. when you sort, does the selection update correctly?)