6
votes

[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'
1
I'm not sure it is a duplicate of that question. For instance, emitting a tuple containing (None,) does not cause a crash but directly emitting None does crash. It looks to be an issue where None is not considered to be of type object, maybe? I would have expected that to raise an exception though, not hard crash Python. - three_pineapples
My colleague found this PySide bug which seems to exactly cover the problem: bugreports.qt-project.org/browse/PYSIDE-17 > Segfault when emitting a signal with a None parameter from another I somehow didn't find that in my googlin'. Created back in 2012, so.. hm. - Jon Lauridsen
Good find. I doubt it will get fixed any time soon (PySide development seems pretty stagnent). My suggestion would be to wrap everything up I'm a tuple, emit the signal, and unpack on the other side. Or stick with using PyQt4 :p - three_pineapples
Yup, a valid suggestion. I've added a similar workaround below, idunno if it's bad form to answer own questions but unless something else pops up I'll tick this one off. - Jon Lauridsen
It is encouraged to answer your own questions :) also I've tried to request this question be reopened. It really isn't a duplicate.... - three_pineapples

1 Answers

2
votes

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.