4

I am writing some scripts that I want to share with my team, so I have been building in a bunch of logging so that it will be easier to debug if they encounter a crash somewhere since then I can see what exactly crashed.

General logging to a file is no issue, but I have an issue with uncaught exceptions. I have tried various things to get it to work, see for example this and this. It seems that the sys.excepthook is just not getting called, either when I run it from IDLE or from the Command Prompt.

Yesterday it log correctly if the exception occurred in the main module, but not if an exception occurred in an imported class. Now the exceptions don't get logged at all, I have no clue what I changed (so I installed Git today :-P)

Here is the code that I am trying to get to work, the main module:

import tkinter as tk
import sys
import traceback
import logging
import datetime

import exception_logging_test_imported_class as impclass

# Main Class
class ExceptMain(tk.Frame):
    def __init__(self, parent):
        logging.info('Start main')
        tk.Frame.__init__(self, parent, relief='groove', bd=4)
        self.parent = parent
        self.pack()
        tk.Label(self, text='This is the main class', bg='white').pack()
        tk.Button(self, text='Start subframe', command=self.run).pack()
        tk.Button(self, text='Throw main exception', command=self.throwex).pack()
        tk.Button(self, text='Start imported class', command=self.start_import).pack()

    # Function to start another frame, from this same file
    def run(self):
        logging.info('Run main function')
        subclass = ExceptSubclass(self)
        subclass.pack()

    # Function to start an imported frame class
    def start_import(self):
        imported_class = impclass.ExtraClass(self)
        imported_class.pack()

    # Throw an exception
    def throwex(self):
        raise ValueError("Main is burning down")

#Another class in this file
class ExceptSubclass(tk.Frame):
    def __init__(self, parent):
        logging.info('Start subframe')
        tk.Frame.__init__(self, parent, relief='groove', bd=4)
        self.parent = parent
        self.pack()
        tk.Label(self, text='This is the subclass', bg='white').pack()
        tk.Button(self, text='run sub function', command=self.script).pack()
        tk.Button(self, text='Throw sub exception', command=self.throwexsub).pack()

    # Run something
    def script(self):
        logging.info('Run subfunction')
        tk.Label(self, text='Script has run').pack()

    # Throw an exception
    def throwexsub(self):
        thing = []
        thing[1]

# Use a logger object or just logging
logger_not_just_logging = False
if logger_not_just_logging:
    logger = logging.getLogger()
    logger.setLevel(logging.DEBUG)
    logger.addHandler(logging.FileHandler("ZZ_logging_test.log"))
else:
    logging.basicConfig(filename='ZZ_logging_test.log', level=logging.DEBUG)

logging.info('<<<<< STARTING NEW INSTANCE >>>>>')

# This function needs to be called somehow when an uncaught exception happens
def exception_handler(etype, value, tb):
    logging.exception("Uncaught exception: {0}".format(str(value)))
    with open('ZZ_exception_test.log', 'a') as file:
        file.write("<<< Exception occurred at " + str(datetime.datetime.now()) + " >>>")
        traceback.print_exception(etype, value, tb, file=file)

# Install the hook  
sys.excepthook = exception_handler

# Run the app
root = tk.Tk()
app = ExceptMain(root)
root.mainloop()

And a submodule:

import tkinter as tk
import logging

# This is an imported frame
class ExtraClass(tk.Frame):
    def __init__(self, parent):
        logging.info('Start imported frame')
        tk.Frame.__init__(self, parent, relief='groove', bd=4)
        self.parent = parent
        self.pack()
        tk.Label(self, text='This is an imported frame', bg='white').pack()
        tk.Button(self, text='run imported frame function', command=self.script).pack()
        tk.Button(self, text='Throw imported exception', command=self.throwexsub).pack()

    # Imported frame has a function
    def script(self):
        logging.info('Run imported frame function')
        tk.Label(self, text='Imported script has run').pack()

    # Imported frame throws an exception
    def throwexsub(self):
        thing = []
        thing[1]

What I expect to get is one file with all the general logging and a separate file with the exceptions and stacktraces.

I will be eternally grateful if you can help me figure this out!

Community
  • 1
  • 1
Bob
  • 614
  • 1
  • 7
  • 19

1 Answers1

5

Aha, I have found the issue. The excepthook that is installed in the aforementioned way is only working on a higher level. I figured that out when I noticed that the exceptions that my class was throwing were still logging in the console while a keyboard interrupt did log in the file as I wanted.

Next thing I found was that you can have similar issues when using threads in Python. The main thread would still log as wanted but the exceptions in the other thread would not. Then I figured that classes can override my overriding of what to do with exceptions. Since all of my code is running in the tkinter mainloop I might need to tell that bit to do what I want it to do. This insight led me to this SO answer and with one line of code everything was fixed, all the way at the end of the main class:

# Run the app
root = tk.Tk()
root.report_callback_exception = exception_handler # this here.
app = ExceptMain(root)
root.mainloop()
Community
  • 1
  • 1
Bob
  • 614
  • 1
  • 7
  • 19
  • I've been stuck on this issue for a while, and this was finally the solution I was looking for. Who would have guessed that tkinter was setting its own exception hook? Excellent summary of how to fix this. – Chris Collett Jul 23 '21 at 16:12