Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PySide crashing Python when emitting None between threads

[edit] This is not a pure duplicate of the PySide emit signal causes python to crash question. This question relates specifically to a (now) known bug in PySide preventing None from being passed across threads. The other question relates to hooking up signals to a spinner box. I've updated the title of this question to better reflect the problem I was facing. [/edit]

I've banged my head against a situation where PySide behaves subtly different from PyQt. Well, I say subtly but actually PySide crashes Python whereas PyQt works as I expect.

Under PySide my app crashes

I'm completely new to PySide and still fairly new to PyQt so maybe I'm making some basic mistake, but damned if I can figure it out... really hoping one of you fine folks can give some pointers!

The full app is a batch processing tool and much too cumbersome to describe here, but I've stripped the problem down to its bare essentials in the code-sample below:

import threading

try:
    # raise ImportError()  # Uncomment this line to show PyQt works correctly
    from PySide import QtCore, QtGui
except ImportError:
    from PyQt4 import QtCore, QtGui
    QtCore.Signal = QtCore.pyqtSignal
    QtCore.Slot = QtCore.pyqtSlot


class _ThreadsafeCallbackHelper(QtCore.QObject):
    finished = QtCore.Signal(object)


def Dummy():
    print "Ran Dummy"
    # return ''  # Uncomment this to show PySide *not* crashing
    return None


class BatchProcessingWindow(QtGui.QMainWindow):
    def __init__(self):
        QtGui.QMainWindow.__init__(self, None)

        btn = QtGui.QPushButton('do it', self)
        btn.clicked.connect(lambda: self._BatchProcess())

    def _BatchProcess(self):
        def postbatch():
            pass
        helper = _ThreadsafeCallbackHelper()
        helper.finished.connect(postbatch)

        def cb():
            res = Dummy()
            helper.finished.emit(res)  # `None` crashes Python under PySide??!
        t = threading.Thread(target=cb)
        t.start()


if __name__ == '__main__':  # pragma: no cover
    app = QtGui.QApplication([])
    BatchProcessingWindow().show()
    app.exec_()

Running this displays a window with a "do it" button. Clicking it crashes Python if running under PySide. Uncomment the ImportError on line 4 to see PyQt* correctly run the Dummy function. Or uncomment the return statement on line 20 to see PySide correctly run.

I don't understand why emitting None makes Python/PySide fail so badly?

The goal is to offload the processing (whatever Dummy does) to another thread, keeping the main GUI thread responsive. Again this has worked fine with PyQt but clearly not so much with PySide.

Any and all advice will be super appreciated.

This is under:

    Python 2.7 (r27:82525, Jul  4 2010, 09:01:59) [MSC v.1500 32 bit (Intel)] on win32

    >>> import PySide
    >>> PySide.__version_info__
    (1, 1, 0, 'final', 1)

    >>> from PyQt4 import Qt
    >>> Qt.qVersion()
    '4.8.2'
like image 404
Jon Lauridsen Avatar asked May 19 '14 01:05

Jon Lauridsen


1 Answers

So, if the argument is that PySide is neglected and this really is a bug, we might as well come up with a workaround, right?

By introducing a sentinel to replace None, and emitting it the problem can be circumvented, then the sentinel just has to be swapped back to None in the callbacks and the problem is bypassed.

Good grief though. I'll post the code I've ended up with to invite further comments, but if you got better alternatives or actual solutions then do give a shout. In the meantime I guess this'll do:

_PYSIDE_NONE_SENTINEL = object()


def pyside_none_wrap(var):
    """None -> sentinel. Wrap this around out-of-thread emitting."""
    if var is None:
        return _PYSIDE_NONE_SENTINEL
    return var


def pyside_none_deco(func):
    """sentinel -> None. Decorate callbacks that react to out-of-thread
    signal emitting.

    Modifies the function such that any sentinels passed in
    are transformed into None.
    """

    def sentinel_guard(arg):
        if arg is _PYSIDE_NONE_SENTINEL:
            return None
        return arg

    def inner(*args, **kwargs):
        newargs = map(sentinel_guard, args)
        newkwargs = {k: sentinel_guard(v) for k, v in kwargs.iteritems()}
        return func(*newargs, **newkwargs)

    return inner

Modifying my original code we arrive at this solution:

class _ThreadsafeCallbackHelper(QtCore.QObject):
    finished = QtCore.Signal(object)


def Dummy():
    print "Ran Dummy"
    return None


def _BatchProcess():
    @pyside_none_deco
    def postbatch(result):
        print "Post batch result: %s" % result

    helper = _ThreadsafeCallbackHelper()
    helper.finished.connect(postbatch)

    def cb():
        res = Dummy()
        helper.finished.emit(pyside_none_wrap(res))

    t = threading.Thread(target=cb)
    t.start()


class BatchProcessingWindow(QtGui.QDialog):
    def __init__(self):
        super(BatchProcessingWindow, self).__init__(None)

        btn = QtGui.QPushButton('do it', self)
        btn.clicked.connect(_BatchProcess)


if __name__ == '__main__':  # pragma: no cover
    app = QtGui.QApplication([])
    window = BatchProcessingWindow()
    window.show()
    sys.exit(app.exec_())

I doubt that'll win any awards, but it does seem to fix the issue.

like image 83
Jon Lauridsen Avatar answered Oct 02 '22 08:10

Jon Lauridsen