Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PyQt event handlers snarf exceptions

Here's the code that illustrates the problem:

from PyQt4 import QtGui
app = QtGui.QApplication([])
dialog = QtGui.QDialog()
button = QtGui.QPushButton('I crash')
layout = QtGui.QHBoxLayout()
layout.addWidget(button)
dialog.setLayout(layout)
def crash(): raise Exception('Crash!')
button.clicked.connect(crash)
button.click()
print 'I should not happen'

When I run that, PyQt4 handles the error for me. My console displays a stack trace with 'Crash!' etc. in it, and I see 'I should not happen'.

This is not useful, because I've been handed a large application with many handlers, and I need to force all their errors up into my face (and into my - ahem - automated tests). Each time I run, errors escape my nets, and they would require excessive and useless try:except blocks, inside every handler, just to catch them all.

Put another way, I want good code to be very good, and bad code to be very bad. Not whitewashed.

Apologies if this is already asked, but when I e-search for it, I naturally get thousands of newbies asking basic error handling questions (or, worse, I get newbies asking how to turn OFF their wayward exceptions!;)

How do I override PyQt4's default error handling, so I can propagate or log errors myself? And please don't answer sys.excepthook, either - it catches the errors that PyQt4 doesn't catch.

like image 362
Phlip Avatar asked Jan 24 '13 02:01

Phlip


2 Answers

This is not the answer, you silly website. Stop forcing us to fit into a preconceived notion of an idealized thread template.

The un-answer is to use my test framework setUp() to hook in an exception handler:

def setUp(self):
    self.no_exceptions = True

    def testExceptionHook(type, value, tback):
        self.no_exceptions = False
        sys.__excepthook__(type, value, tback)

    sys.excepthook = testExceptionHook

Where that says "self.no_exceptions = False", I would much rather simply say self.fail(''). However, because Python's unit test library insists on throwing exceptions just to register test failures, and because PyQt insists on snarfing all exceptions, we have a deadlock.

To fit into unittest.TestCase's silly preconceived notion of an idealized test case, I have to instead set a variable, then detect it in the teardown:

def tearDown(self):
    self.assertTrue(self.no_exceptions)

This is still not ideal, but at least it will force me to spend more time paying attention to the errors, instead of spending that time complaining about them on technical websites.

The root question: How to turn off PyQt's magic error handler? - remains unanswered...

like image 199
Phlip Avatar answered Sep 23 '22 19:09

Phlip


I think the answer is that this isn't a 'feature' of PyQt, but a consequence inherent to the design that lets signals/slots work (remember that the signal/slot communication is going through a c++ layer as well).

This is ugly, but does a bit of and end-run around your problem

from PyQt4 import QtGui
import time
app = QtGui.QApplication([])

class exception_munger(object):
    def __init__(self):
        self.flag = True
        self.txt = ''
        self.type = None

    def indicate_fail(self,etype=None, txt=None):
        self.flag = False
        if txt is not None:
            self.txt = txt
        self.type = etype
    def reset(self):

        tmp_txt = self.txt
        tmp_type = self.type
        tmp_flag = self.flag
        self.flag = True
        self.txt = ''
        self.type = None
        return tmp_flag, tmp_type, tmp_txt

class e_manager():
    def __init__(self):
        self.old_hook = None

    def __enter__(self):
        em = exception_munger()
        def my_hook(type, value, tback):
            em.indicate_fail(type, value)
            sys.__excepthook__(type, value, tback) 
        self.old_hook = sys.excepthook
        sys.excepthook = my_hook
        self.em = em
        return self

    def __exit__(self,*args,**kwargs):
        sys.excepthook = self.old_hook

def mang_fac():
    return e_manager()

def assert_dec(original_fun):

    def new_fun(*args,**kwargs):
        with mang_fac() as mf:
            res = original_fun(*args, **kwargs)
            flag, etype, txt = mf.em.reset()
            if not flag:
                raise etype(txt)
            return res
    return new_fun


@assert_dec
def my_test_fun():
    dialog = QtGui.QDialog()
    button = QtGui.QPushButton('I crash')
    layout = QtGui.QHBoxLayout()
    layout.addWidget(button)
    dialog.setLayout(layout)
    def crash(): 
        time.sleep(1)
        raise Exception('Crash!')
    button.clicked.connect(crash)


    button.click()

my_test_fun()
print 'should not happen'

This will not print 'should not happen' and gives you something to catch with your automated tests (with the correct exception type).

In [11]: Traceback (most recent call last):
  File "/tmp/ipython2-3426rwB.py", line 68, in crash
Exception: Crash!
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-11-6ef4090ab3de> in <module>()
----> 1 execfile(r'/tmp/ipython2-3426rwB.py') # PYTHON-MODE

/tmp/ipython2-3426rwB.py in <module>()

/tmp/ipython2-3426rwB.py in new_fun(*args, **kwargs)

Exception: Crash!

In [12]: 

The stack trace is jacked up, but you can still read the first one that was printed out.

like image 29
tacaswell Avatar answered Sep 20 '22 19:09

tacaswell