Saving exceptions in Tkinter using Python - python

I have developed several Python programs for others that use Tkinter to receive input from the user. In order to keep things simple and user-friendly, the command line or python console are never brought up (ie. .pyw files are used), so I'm looking into using the logging library to write error text to a file when an exception occurs. However, I'm having difficulty getting it to actually catch the exceptions. For example:
We write a function that will cause an error:
def cause_an_error():
a = 3/0
Now we try to log the error normally:
import logging
logging.basicConfig(filename='errors.log', level=logging.ERROR)
try:
cause_an_error()
except:
logging.exception('simple-exception')
As expected, the program errors, and logging writes the error to errors.log. Nothing appears in the console. However, there is a different result when we implement a Tkinter interface, like so:
import logging
import Tkinter
logging.basicConfig(filename='errors.log', level=logging.ERROR)
try:
root = Tkinter.Tk()
Tkinter.Button(root, text='Test', command=cause_an_error).pack()
root.mainloop()
except:
logging.exception('simple-exception')
In this case, pressing the button in the Tkinter window causes the error. However, this time, nothing is written to the file, and the following error appears in the console:
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Python27\lib\lib-tk\Tkinter.py", line 1536, in __call__
return self.func(*args)
File "C:/Users/samk/Documents/GitHub/sandbox/sandbox2.pyw", line 8, in cause_an_error
a = 3/0
ZeroDivisionError: integer division or modulo by zero
Is there a different way to catch and log this error?

It's not very well documented, but tkinter calls a method for exceptions that happen as the result of a callback. You can write your own method to do whatever you want by setting the attribute report_callback_exception on the root window.
For example:
import tkinter as tk
def handle_exception(exception, value, traceback):
print("Caught exception:", exception)
def raise_error():
raise Exception("Sad trombone!")
root = tk.Tk()
# setup custom exception handling
root.report_callback_exception=handle_exception
# create button that causes exception
b = tk.Button(root, text="Generate Exception", command=raise_error)
b.pack(padx=20, pady=20)
root.mainloop()
For reference:
http://epydoc.sourceforge.net/stdlib/Tkinter.Tk-class.html#report_callback_exception

Since this question was about logging and being explicit where the error is occurring, I will expand on Bryan (totally correct) answer.
First, you have to import some additional useful modules.
import logging
import functools # for the logging decorator
import traceback # for printing the traceback to the log
Then, you have to configure the logger and the logging function:
logging.basicConfig(filename='/full/path/to_your/app.log',
filemode='w',
level=logging.INFO,
format='%(levelname)s: %(message)s')
def log(func):
# logging decorator
#functools.wraps(func)
def wrapper_log(*args, **kwargs):
msg = ' '.join(map(str, args))
level = kwargs.get('level', logging.INFO)
if level == logging.INFO:
logging.info(msg)
elif level == logging.ERROR:
logging.exception(msg)
traceback.print_exc()
return wrapper_log
#log
def print_to_log(s):
pass
Note that the function doing the logging is actually log and you can use it for both printing errors and regular info into your log file. How you use it: with the function print_to_log decorated with log. And instead of pass you can put some regular print(), so that you save the message to the log and print it to the console. You can replace pass with whatever commands you prefer.
Note nb. 2 we use the traceback in the log function to track where exactly your code generated an error.
To handle the exception, you do as in the already accepted answer. The addition is that you pass the message (i.e. the traceback) to the print_to_log function:
def handle_exception(exc_type, exc_value, exc_traceback):
# prints traceback in the log
message = ''.join(traceback.format_exception(exc_type,
exc_value,
exc_traceback))
print_to_log(message)
Having configured all this, now you you tell your Tkinter app that it has to use the handle_exception function:
class App(tk.Tk):
# [the rest of your app code]
if __name__ == '__main__':
app = App()
app.report_callback_exception = handle_exception
app.mainloop()
Or alternatively, if you work with multiple classes, you can even instruct the class itself to use the andle_exception function:
class App(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
tk.Tk.report_callback_exception = handle_exception
# [the rest of your app code]
if __name__ == '__main__':
app = App()
app.mainloop()
In this way, your app code looks lean, you don't need any try/except method, and you can log at info level whichever event in your app.

Related

How to prevent a console script from closing itself when an error occurs?

I made an python program that I packaged into .exe using auto-py-to-exe and if an error throws up the program shows the error and closes itself immediately, so I can't read the error.
And I don't want to make:
try:
#code
except:
#print something
I want it to print out the original python error and just make it not close immediately.
There are multiple ways to do this
Using the traceback module
Example using print_exc
import traceback
def hello():
return 1/0
def my_program():
try:
hello()
except Exception as _: # try to avoid generic exception handlers
traceback.print_exc()
if __name__ == "__main__":
my_program()
Using the logging module
Utilizing the logging module can be useful in many cases for example you can enable more verbose logging using some CLI flag or env variable. You can also dump all the logs into a file that can be parsed later.
Simple logging example
import logging
def cfg():
fmt = "%(levelname)s %(asctime)s - %(message)s"
logging.basicConfig(filename="my_program.log", filemode="w", format=fmt, level=logging.ERROR)
def hello():
return 22/0
def my_program():
try:
hello()
except Exception as _:
logging.exception("an internal error happened!")
if __name__ == "__main__":
cfg()
my_program()

Python display custom error message / traceback on every exception

Does Python support a way to display the same custom error message for every exception / raise / assert (no matter where the code broke)?
My current crack at it uses a decorator. I have a function main and it displays the traceback fine, but I want it to also print my_var (which is INSIDE the function scope) every time ANY error is thrown. So obviously there is a scope problem with this - it's just to illustrate what I want to do. Any ideas are appreciated.
import traceback
def exit_with_traceback(func, *args, **kwargs):
def wrap(*args, **kwargs):
try:
return func(*args, **kwargs)
except:
# how do I get this to print my_var AND the traceback?
print(traceback.format_exc())
return wrap
#exit_with_traceback
def main(bar=1):
my_var = 'hello world' # variable specific to main()
return bar + 1
main(bar=None) # run main() to throw the exception
You can try to override the excepthook function in sys module. From its documentation:
When an exception is raised and uncaught, the interpreter calls
sys.excepthook with three arguments, the exception class, exception
instance, and a traceback object.
So the code may look something like this (I've used your example):
import sys
# define custom exception hook
def custom_exception_hook(exc_type, value, traceback):
print('Custom exception hook')
print('Type:', exc_type)
print('Value:', value)
print('Traceback:', traceback)
lc = traceback.tb_next.tb_frame.f_locals
print(lc.get('my_var')) # this prints "hello world"
# override the default exception hook
sys.excepthook = custom_exception_hook
def main(bar=1):
my_var = 'hello world' # variable specific to main()
return bar + 1
main(bar=None) # run main() to throw the exception
The overriding of sys.excepthook won't work from IDLE, but it works just fine from command line. Hope this will be helpful.

How to handle errors in tkinter mainloop?

I have a python program which is scraping web data for a client. tkinter is used for the interface. Outline is:
Window 1 lets the user select what information to scrape.
Window 1 closes
Separate thread is started for the scraper. This thread will in turn spawn more threads to allow multiple pages to be downloaded at once.
Window 2 opens to show download progress (e.g. "downloading client 5 of 17")
User closes Window 2 to end program.
The program will work for the first few hundred pages, but then it starts spitting out the error message:
Traceback (most recent call last):
File "C:\Users\Me\AppData\Local\Programs\Python\Python35-32\lib\tkinter\__init__.py", line 248, in __del__
if self._tk.getboolean(self._tk.call("info", "exists", self._name)):
RuntimeError: main thread is not in main loop
Exception ignored in: <bound method Variable.__del__ of <tkinter.IntVar object at 0x03245510>>
multiple times until all the threads have been stopped. No idea what could be causing this error. The actual code is:
import scraper, threading
import tkinter as tk
from queue import Queue
outputQueue = Queue()
class OutputRedirect(object):
def __init__():
super().__init__()
def write(self, string):
outputQueue.put(string)
def getInformation():
stdout = sys.stdout
sys.stdout = OutputRedirect()
scraper.startThreads()
scraper.startPulling()
sys.stdout = stdout
def updateTextField(window, root):
if not outputQueue.empty():
string = outputQueue.get()
window.textArea.insert("insert", string)
outputQueue.task_done()
root.after(1, updateTextField, window, root)
'''widget/window definitions - not important'''
guiInfo = {"stuff1": [], "stuff2": []}
root = tk.Tk()
window1 = Window1(root, guiInfo)
window1.mainloop()
pullThread = threading.Thread(target=pullClaims,
args=(list(guiInfo["stuff1"]),
list(guiInfo["stuff2"])), daemon=True)
pullThread.start()
root = tk.Tk()
window2 = Window2(root)
root.after(0, updateTextField, window2, root)
window2.mainloop()
The scraper program (which works fine on its own) uses print statements for user feedback. Rather than re-write everything, I just pointed stdout to a queue. The main thread uses the "after" function to check on the queue a few times a second. If there is anything in it then it gets printed to the Text widget on the window.
I've put try/catch just about everywhere in the code, but they haven't caught a thing. I'm convinced the problem is in the mainloop itself, but I can't find any up to date information for how to stick something new in it. Any help would be greatly appreciated.
To handle tkinter errors you do the following
class TkErrorCatcher:
'''
In some cases tkinter will only print the traceback.
Enables the program to catch tkinter errors normally
To use
import tkinter
tkinter.CallWrapper = TkErrorCatcher
'''
def __init__(self, func, subst, widget):
self.func = func
self.subst = subst
self.widget = widget
def __call__(self, *args):
try:
if self.subst:
args = self.subst(*args)
return self.func(*args)
except SystemExit as msg:
raise SystemExit(msg)
except Exception as err:
raise err
import tkinter
tkinter.CallWrapper = TkErrorCatcher
But in your case please do not do this. This should only ever be done in the case of wanting to hide error messages from your users in production time. As commented above you have a nono's going on.
To spawn multiple windows you can use tkinter.Toplevel
I would recommend in general to read
http://www.tkdocs.com/tutorial/index.html
http://effbot.org/tkinterbook/tkinter-hello-tkinter.htm
http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/index.html
and for your specific problem of threading in tkinter this blog post nails it. Basically you need to have the tkinter mainloop blocking the programs main thread, then use after calls from other threads to run other code in the mainloop.
http://stupidpythonideas.blogspot.de/2013/10/why-your-gui-app-freezes.html

PyQt: No error msg (traceback) on exit

My PyQt application no longer prints the error (stderr?) to the console.
I use QtDesigner and import the UI like this:
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
from PyQt5.uic import loadUiType
Ui_MainWindow, QMainWindow = loadUiType("test.ui")
class Main(QMainWindow, Ui_MainWindow):
"""Main window"""
def __init__(self,parent=None):
super(Main, self).__init__(parent)
self.setupUi(self)
self.pushButton.clicked.connect(self.testfunc)
def testfunc(self):
print(9/0)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
main = Main()
main.show()
sys.exit(app.exec_())
test.ui contains a QPushButton and a label. When I call testfunc (which obviously gives an error) in a non-Qt application, I get the error message, traceback, etc. When I execute this code, it just exits.
I wrote a PyQt application without QtDesigner before and it printed the errors to the console as expected. What's the difference with QtDesigner and inheritance?
This is probably due to changes in the way exceptions are dealt with in PyQt-5.5. To quote from the PyQt5 Docs:
In PyQt v5.5 an unhandled Python exception will result in a call to
Qt’s qFatal() function. By default this will call abort() and the
application will terminate. Note that an application installed
exception hook will still take precedence.
When I run your example in a normal console, this is what I see:
$ python test.py
Traceback (most recent call last):
File "test.py", line 213, in testfunc
print(9/0)
ZeroDivisionError: division by zero
Aborted (core dumped)
So the main difference is that the application will now immediately abort when encountering an unhandled exception (i.e. just like a normal python script would). Of course, you can still control this behaviour by using a try/except block or globally by overriding sys.excepthook.
If you're not seeing any traceback, this may be due to an issue with the Python IDE you're using to run your application.
PS:
As a bare minimum, the old PyQt4 behaviour of simply printing the traceback to stdout/stderr can be restored like this:
def except_hook(cls, exception, traceback):
sys.__excepthook__(cls, exception, traceback)
if __name__ == "__main__":
import sys
sys.excepthook = except_hook
I've been using python's traceback module in conjunction with a try/except statement to make sure the traceback is printed before exiting:
https://docs.python.org/3/library/traceback.html
Spefically, I use traceback.print_exc()

Catch MainLoop exceptions and displaying them in MessageDialogs

I have a wxPython application that relies on an external config file. I want provide friendly message dialogs that show up if there are any config errors. I've tried to make this work by wrapping my app.MainLoop() call in a try/except statement.
The code below works for the init code in my MainWindow frame class, but doesn't catch any exceptions that occur within the MainLoop. How can I catch these exceptions as well?
if __name__ == '__main__':
app = MyApp(0)
try:
MainWindow(None, -1, 'My Cool App')
app.MainLoop()
except ConfigParser.Error, error_message:
messagebox = wx.MessageDialog(None, error_message, 'Configuration Error', wx.OK | wx.ICON_ERROR)
messagebox.ShowModal()
I've read some mention of an OnExceptionInMainLoop method that can be overridden in the wx.App class, but the source I read must be out of date (2004) since wx.App no longer seems to have a method by that name.
EDIT:
I need to be able to catch unhandled exceptions during my mainloop so that I can further handle them and display them in error dialogs, not pass silently, and not terminate the app.
The sys.excepthook solution is too low level and doesn't play nice with the wxPython mainloop thread. While the link to the other answer does the same try/except wrapping around the mainloop which doesn't work due, once again, to wxPython spawning a different thread for the app/ui.
Don't know if this will work for a wxPython application, but in the sys module you can overwrite the excepthook attribute, which is a function called with 3 arguments, (type, value, traceback), when an uncaugth exception is caught. You can install your own function in there that handles only the exceptions you want, and call the original function for all the others.
Consult: http://docs.python.org/library/sys.html#sys.excepthook
I coded something like this for Chandler, where any unhandled exceptions pop up a window with the stack and other info, and users can put in additional comments (what did they do when it happened etc.) and submit it for Chandler developers. A bit like the Mozilla Talkback (nowadays they use Breakpad I believe) feature if you will.
To do this in wxPython, you need to provide redirect parameter to wx.App. This will pop up wx.PyOnDemandOutputWindow (you will probably want to override it to provide a nicer looking implementation).
The relevant source files in Chandler are here:
Chandler.py starts the application and sets the redirect attribute, as well as tries to catch and display error dialogs in case normal application startup fails
Application.py customizes the application object, including setting up our customized wx.PyOnDemandOutputWindow
feedback.py has the implementation for the customized wx.PyOnDemandOutputWindow; it will additionally need feedback.xrc and feedback_xrc.py
Perhaps this question might be of some use, it tries to capture all exceptions.
Posting the solution that worked for me with a very similar problem.
import wx
import sys
import traceback
class Frame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None)
panel = wx.Panel(self)
m_close = wx.Button(panel, -1, "Error")
m_close.Bind(wx.EVT_BUTTON, self.OnErr)
def OnErr(self, event):
1/0
def handleGUIException(exc_type, exc_value, exc_traceback):
err_msg = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback))
err_msg += '\n Your App will now terminate'
# Here collecting traceback and some log files to be sent for debugging.
# But also possible to handle the error and continue working.
dlg = wx.MessageDialog(None, err_msg, 'Termination dialog', wx.OK | wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()
sys.exit()
sys.excepthook = handleGUIException
if __name__ == '__main__':
app = wx.App(redirect=False)
top = Frame()
top.Show()
app.MainLoop()
Using sys.excepthook is very fine to me.
I find the following article of great help: wxPython: Catching Exceptions from Anywhere.

Categories

Resources