Reputation: 3894
I have written an application in Python Tkinter. I recently noticed that for one of the operation, it sometimes closes (without giving any error) if that operation failed. I have written a small program to illustrate the problem :-
import os
from Tkinter import *
def copydir():
src = "D:\\a\\x\\y"
dest = "D:\\a\\x\\z"
os.rename(src,dest)
master = Tk()
def callback():
global master
master.after(1, callback)
copydir()
print "click!"
b = Button(master, text="OK", command=copydir)
b.pack()
master.after(100, callback)
mainloop()
To reproduce the problem, open the folder which it will rename in “ms command prompt” such that renaming it will throw exception from Tkinter code.
My original code is using threading and is performing other tasks as well, so I have tried to make the operations in this test script as similar as possible.
Now, if I run this code by double clicking it, then program simply closes without throwing any error. But If I had been running this script from console, then exception messages are dumped on the console and atleast I got to know , something is wrong.
I can fix this code by using try/catch in the code where it tried to rename but I want to inform user about this failure as well. So I just want to know what coding approaches should be followed while writing Tkinter App's and I want to know:-
1) Can I make my script dump some stack trace in a file whenever user ran this by double clicking on it. By this atleast, I would know something is wrong and fix it.
2) Can I prevent the tkinter app to exit on such error and throw any exception in some TK dialog.
Thanks for help!!
Upvotes: 6
Views: 24204
Reputation: 20679
You can override Tkinter's CallWrapper
. It is necessary to use a named import of Tkinter instead of a wildcard import in order to do so:
import Tkinter as tk
import traceback
class Catcher:
def __init__(self, func, subst, widget):
self.func = func
self.subst = subst
self.widget = widget
def __call__(self, *args):
try:
if self.subst:
args = apply(self.subst, args)
return apply(self.func, args)
except SystemExit, msg:
raise SystemExit, msg
except:
traceback.print_exc(file=open('test.log', 'a'))
# ...
tk.CallWrapper = Catcher
b = tk.Button(master, text="OK", command=copydir)
b.pack()
master.mainloop()
Upvotes: 5
Reputation: 301
I see you have a non-object oriented example, so I'll show two variants to solve the problem of exception-catching.
The key is in the in the tkinter\__init__.py
file. One can see that there is a documented method report_callback_exception
of Tk
class. Here is its description:
report_callback_exception()
Report callback exception on sys.stderr.
Applications may want to override this internal function, and should when sys.stderr is None.
So as we see it it is supposed to override this method, lets do it!
Non-object oriented solution
import tkinter as tk
from tkinter.messagebox import showerror
if __name__ == '__main__':
def bad():
raise Exception("I'm Bad!")
# any name as accepted but not signature
def report_callback_exception(self, exc, val, tb):
showerror("Error", message=str(val))
tk.Tk.report_callback_exception = report_callback_exception
# now method is overridden
app = tk.Tk()
tk.Button(master=app, text="bad", command=bad).pack()
app.mainloop()
Object oriented solution
import tkinter as tk
from tkinter.messagebox import showerror
class Bad(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# or tk.Tk.__init__(*args, **kwargs)
def bad():
raise Exception("I'm Bad!")
tk.Button(self, text="bad", command=bad).pack()
def report_callback_exception(self, exc, val, tb):
showerror("Error", message=str(val))
if __name__ == '__main__':
app = Bad()
app.mainloop()
My environment:
Python 3.5.1 |Anaconda 2.4.1 (64-bit)| (default, Dec 7 2015, 15:00:12) [MSC
v.1900 64 bit (AMD64)] on win32
tkinter.TkVersion
8.6
tkinter.TclVersion
8.6
Upvotes: 17
Reputation: 15153
I am not very sure if I have understood you well, but this simple code gives you control over the case in which the directory could not be found:
import os
from Tkinter import *
def copydir():
src = "D:\\troll"
dest = "D:\\trollo"
try:
os.rename(src, dest)
except:
print 'Sorry, I couldnt rename'
# optionally: raise YourCustomException
# or use a Tkinter popup to let the user know
master = Tk()
b = Button(master, text="OK", command=copydir)
b.pack()
mainloop()
EDIT: Since you want a general method and Tkinter does not propagate exceptions, you have to program it. There are two ways:
1) Hardcode it into the the functions as I did in the example above (horrible)
2) Use a decorator to add a try-except block.
import os
from Tkinter import *
class ProvideException(object):
def __init__(self, func):
self._func = func
def __call__(self, *args):
try:
return self._func(*args)
except Exception, e:
print 'Exception was thrown', str(e)
# Optionally raise your own exceptions, popups etc
@ProvideException
def copydir():
src = "D:\\troll"
dest = "D:\\trollo"
os.rename(src, dest)
master = Tk()
b = Button(master, text="OK", command=copydir)
b.pack()
mainloop()
EDIT: If you want to include the stack
include traceback
and in the except block:
except Exception, e:
print 'Exception was thrown', str(e)
print traceback.print_stack()
The solution that A.Rodas has proposed is cleaner and more complete, however, more complicated to understand.
Upvotes: 3