0

I am using a PySide (Qt) Gui that should exit a loop on a button click. A button click for PySide emits a signal and the signal calls the connected functions. However, when the signal calls the functions it uses it's own system for Error handling.

I don't really want to do the fix for that Signal Error handling.

Is there a way to break out of a "with" statement without raising errors.

import sys
import time
from PySide import QtGui, QtCore

class MyClassError(Exception): pass

class MyClass(QtGui.QProgressDialog):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Properties
        self.setWindowFlags(QtCore.Qt.Dialog | QtCore.Qt.WindowTitleHint)
        self.setWindowModality(QtCore.Qt.WindowModal)
        self.setMinimumDuration(2000) # if progress is finished in 2 sec it wont show
        self.setRange(0, 99)
    # end Constructor

    def breakOut(self):
        print("break out")
        self.__exit__(MyClassError, "User cancel!", "") # does not break out of "with"
        raise MyClassError("User cancel!") # PySide just prints Exception
    # end breakOut

    def __enter__(self):
        self.canceled.connect(self.breakOut)
        return self
    # end enter (with)

    def __exit__(self, etype, value, traceback):
#         self.canceled.disconnect(self.breakOut)
        if etype is None or etype == MyClassError:
            return True
        raise
    # end exit
# end class MyClass

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)

    window = QtGui.QMainWindow()
    window.show()

    myinst = MyClass()
    window.setCentralWidget(myinst)

    val = 0
    with myinst:
        for i in range(100):
            myinst.setLabelText(str(i))
            myinst.setValue(i)
            val = i+1
            time.sleep(0.1)
    # end with
    print(val)

    sys.exit(app.exec_())

The fixes that I have seen for PySide/Qt error handling in signals doesn't look nice. It overrides QApplication which means that someone else using this class has to use the new QApplication.

Is there an easy way around PySide/Qt signal error handling or is there an alternative way of breaking out of a "with" statement.

justengel
  • 6,132
  • 4
  • 26
  • 42

2 Answers2

2

Firstly, I've interpreted your comment on @dano's answer to mean you want to do some sort of long running operations within the with statement.

I think you have a few fundamental misunderstandings.

  1. Your with statement (and the code within) is before the call to app.exec_(). The Qt Event loop does not start running until app.exec_() is called. So all your with statement is doing is queuing up events to be processed by the event loop once it is started. Thus, a qt signal to cancel the code within the with statement will never work because the signal will not be processed until the event loop is started, which is after your loop has finished (at least in the test case you provide above(.

    The way to fix this is to place your with statement in a callback that is executed by the QT Event loop (eg a function that is queued up by a QTimer or similar)

  2. Even assuming you fix the above, you will never be able to interrupt the long running task with a button click (or a signal of any kind) because the long running task is in the same thread as the Qt Event loop, and thus the Qt Event loop cannot process any new commands (like pressing the cancel button) until your long running task finishes. While your long running task is

    To fix that, you need to put your long running task in a thread (python thread or QThread) or another process. However, these approaches come with their own difficulties, largely centred around the fact that you should never attempt to update the GUI directly from a thread. There are some options to get around this, see here and here for questions that update the GUI safely from a thread.. I've wrapped some of this stuff up in a library if it helps, called qtutils.

I realise this doesn't directly solve your question of how to interrupt a with statement, however I hope I've made it clear that your current approach will not work. If you switch to threads or processes, you should be able to kill the thread/process immediately (if you wish), rather than waiting for the thread to read a condition and exit itself (though of course hard killing something may have unintended side effects, but since that is what you seem to want to do, I'll assume you've thought about this and have decided it will always be fine).

Community
  • 1
  • 1
three_pineapples
  • 11,579
  • 5
  • 38
  • 75
  • Sorry for the misunderstanding, but the sample code does not reflect everything that is going on. A QProgressDialog's setValue method does process GUI events, so even if event loop has not been called the event items in the Queue are being processed correctly. I am aware about the threading, and I thank you for your input. You have given me some good insight on a few things. I realize now that an Error only breaks the `with` statement if that error is raised directly from the lines of code in the with. You are correct my approach is wrong. – justengel Jul 23 '14 at 11:41
0

You can just check to see if it's been cancelled in the loop itself:

class MyClass(QtGui.QProgressDialog):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.cancelled = False
        self.setWindowFlags(QtCore.Qt.Dialog | QtCore.Qt.WindowTitleHint)
        self.setWindowModality(QtCore.Qt.WindowModal)
        self.setMinimumDuration(2000) # if progress is finished in 2 sec it wont show
        self.setRange(0, 99) 

    def breakOut(self):
        print("break out")
        self.cancelled = True

    def __enter__(self):
        self.canceled.connect(self.breakOut)
        return self

    def __exit__(self, etype, value, traceback):
        self.canceled.disconnect(self.breakOut)


if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)

    window = QtGui.QMainWindow()
    window.show()

    myinst = MyClass()
    window.setCentralWidget(myinst)

    val = 0 
    with myinst:
        for i in range(100):
            if myinst.cancelled:
                break
            myinst.setLabelText(str(i))
            myinst.setValue(i)
            val = i+1 
            time.sleep(0.1)

Raising an exception in breakOut will not have the affect you want, because it's being called in a completely different context from the for loop. Calling __exit__ won't make a with statement, regardless of PySide's error handling. I think you have the causation backwards: __exit__ gets called when you leave a with block, calling __exit__ will not force a with block to abort.

dano
  • 91,354
  • 19
  • 222
  • 219
  • Raising the exception is being handled by pyside. Whenever there is an exception raise in the "with" statement it will exit the code (for loop as well) and come to the exit method. PySide is stopping this. Unfortunately this is for handling very large operations, and the value that is being set does always happen that quickly. I want the cancel button to work immediately. Thank you for your time. – justengel Jul 22 '14 at 17:06