Thread doesn't log into tkinter window if it is in an imported file

Viewed 63

I have a tkinter app running alongside two different threads that are logging into it using a queue.

One of the threads is in the same code file as the tkinter app. The other one is imported from another file, even thought their code is similar. What I verify is that only the thread defined in the same file manages to write into the UI. Do you know why this happens?

The code for the main file is:

import time
import queue
import threading
import tkinter as tk
from tkinter.scrolledtext import ScrolledText
from tkinter import ttk
import logging
from logging.handlers import QueueHandler

from foo import ImportedLoggerThread


logger = logging.getLogger(__name__)


class LoggerThread(threading.Thread):

    def __init__(self):
        super().__init__()
        self._stop_event = threading.Event()

    def run(self):
        logger.debug('LoggerThread: running')
        i = 0
        while not self._stop_event.is_set():
            logger.info("LoggerThread: iteration %d" % i)
            i += 1
            time.sleep(1)

    def stop(self):
        self._stop_event.set()


class LoggingWindow:
    def __init__(self, frame):
        self.frame = frame
        self.scrolled_text = ScrolledText(frame, height=12)
        self.scrolled_text.pack()
        self.log_queue = queue.Queue()
        self.queue_handler = QueueHandler(self.log_queue)
        logger.addHandler(self.queue_handler)
        # start polling
        self.frame.after(100, self.poll_log_queue)

    def write(self, record):
        msg = self.queue_handler.format(record)
        self.scrolled_text.insert(tk.END, msg + '\n')
        # Scroll to the bottom
        self.scrolled_text.yview(tk.END)

    def poll_log_queue(self):
        # Poll every 100ms
        while True:
            try:
                record = self.log_queue.get(block=False)
            except queue.Empty:
                break
            else:
                self.write(record)
        self.frame.after(100, self.poll_log_queue)


class App:

    def __init__(self, root):
        self.root = root
        frame = ttk.Labelframe(text="Log")
        frame.pack()
        self.console = LoggingWindow(frame)
        self.th = LoggerThread()
        self.th.start()
        self.imported = ImportedLoggerThread()
        self.imported.start()
        self.root.protocol('WM_DELETE_WINDOW', self.quit)

    def quit(self):
        self.th.stop()
        self.imported.stop()
        self.root.destroy()


def main():
    logging.basicConfig(level=logging.DEBUG)
    root = tk.Tk()
    app = App(root)
    app.root.mainloop()


if __name__ == '__main__':
    main()

and for the second file foo.py:

import threading
import logging
import time

logger = logging.getLogger(__name__)


class ImportedLoggerThread(threading.Thread):

    def __init__(self):
        super().__init__()
        self._stop_event = threading.Event()

    def run(self):
        logger.debug('Imported: running')
        i = 0
        while not self._stop_event.is_set():
            logger.info("Imported: iteration %d" % i)
            i += 1
            time.sleep(2)

    def stop(self):
        self._stop_event.set()

Thanks in advance!

1 Answers

You define 2 logger instances in your files (logger = logging.getLogger(__name__)) and it causes your issue. If you use the same logger instance, it should work. It means in your case, you should pass the logger instance from your main file to the imported module (foo.py). Please see below the fixed foo.py and the fixed App class in the main file.

foo.py:

import threading
import time


class ImportedLoggerThread(threading.Thread):

    def __init__(self, my_logger):
        super().__init__()
        self._stop_event = threading.Event()
        self.my_logger = my_logger  # Should be passed from caller side.

    def run(self):
        self.my_logger.debug('Imported: running')
        i = 0
        while not self._stop_event.is_set():
            self.my_logger.info("Imported: iteration %d" % i)
            i += 1
            time.sleep(2)

    def stop(self):
        self._stop_event.set()

As you can see above the "imported" module uses a getting logger (It should comes from the "main" file)

App class:

class App:

    def __init__(self, root):
        self.root = root
        frame = ttk.Labelframe(text="Log")
        frame.pack()
        self.console = LoggingWindow(frame)
        self.th = LoggerThread()
        self.th.start()
        self.imported = ImportedLoggerThread(my_logger=logger)  # Should be passed the defined logger instance.
        self.imported.start()
        self.root.protocol('WM_DELETE_WINDOW', self.quit)

    def quit(self):
        self.th.stop()
        self.imported.stop()
        self.root.destroy()

As you can see in the App class, the defined logger instance is passed to the imported ImportedLoggerThread class.

Output:

>>> python3 test.py 
DEBUG:__main__:LoggerThread: running
DEBUG:__main__:Imported: running
INFO:__main__:LoggerThread: iteration 0
INFO:__main__:Imported: iteration 0
INFO:__main__:LoggerThread: iteration 1
INFO:__main__:Imported: iteration 1
INFO:__main__:LoggerThread: iteration 2

GUI:

GUI

Related