6

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!!

sarbjit
  • 3,786
  • 9
  • 38
  • 60

4 Answers4

17

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()

The result

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
Stevoisiak
  • 23,794
  • 27
  • 122
  • 225
Maxim Kochurov
  • 301
  • 2
  • 4
  • If you want to report the full traceback for an error, use `message = traceback.format_exc()` ([credit to James](https://stackoverflow.com/a/37493588/3357935)) – Stevoisiak Mar 01 '18 at 21:17
5

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()
Ocaso Protal
  • 19,362
  • 8
  • 76
  • 83
A. Rodas
  • 20,171
  • 8
  • 62
  • 72
  • I have included this in my code, but some how it doesn't seems to work. On double clicking the code, it juts simply exits without writing any file. See my updated code. – sarbjit Mar 06 '13 at 14:50
  • @sarbjit I didn't try the code above, but it seems to work only if the import is named. I've updated the answer. – A. Rodas Mar 06 '13 at 18:47
  • With named import, it works fine. Though I had to add some code to cancel all the pending Tk after events and close mainloop explicitly. – sarbjit Mar 07 '13 at 05:28
3

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.

bgusach
  • 14,527
  • 14
  • 51
  • 68
  • 1
    I am actually looking for a generic solution for handling exceptions in case of Tkinter App's. Like in this case, I am aware of this solution. What I want to know is that if there is a way by which any exception raised in Python Tkinter app to be dumped in an text file. – sarbjit Mar 06 '13 at 12:42
  • @sarbjit I extended my solution. Check it out, now I understand what you wanted and I pretty sure it'll help. – bgusach Mar 06 '13 at 13:34
  • Thanks for the solution, this is what I am looking for. Since I am learning Python, I have few doubts and hope you would help to clarify:- 1) When you used `@ProvideException` in code, what does that mean, what do we call this in Python (I want to read about it). 2) By this method, is it possible to dump the complete stack trace. So I want whenever any exception is raised, should be dumped in a text file. As of now, it is showing the string error message only. Also doing this will handle all kind of exceptions, Right? – sarbjit Mar 06 '13 at 13:51
  • the @ProvideException is a decorator, which consists of sending your function to another function and work with the result of this (which is another function). In this case, copydir is sent to the class ProvideException, and stored as internal var, and then called, which fires the __call__ method, but adding a try-except block. I'll update the answer. – bgusach Mar 06 '13 at 14:22
  • If I use decorator in my code, will it be applicable for all the functions in the program (every function route through this decortar) or only the function above which this decorator is used? – sarbjit Mar 06 '13 at 14:29
  • 1
    That is the point of using decorators. You just have to add @DecoratorName before each routine definition and you're good to go =). No need to hardcode each function. – bgusach Mar 06 '13 at 14:35
0

You can install a global handler for exception with a except-hook(). An example can be found here.

krase
  • 1,006
  • 7
  • 18