thread-safetypython-multithreadingpyside6python-logging

How can I make a threadsafe logging handler subclass that triggers events in the main Qt loop?


I have a PySide6 application that uses threading and logging. My logger has a custom handler defined below, which pops up a messagebox that requires user attention for higher level logging events:

import logging from PySide6.QtWidgets import QMessageBox

class QtHandler(logging.Handler):
    def emit(self, record):
        if record.levelno >= logging.ERROR:
            QMessageBox.critical(None, "Error", self.format(record))
        elif record.levelno >= logging.WARNING:
            QMessageBox.warning(None, "Warning", self.format(record))

This works fine for single-threaded operations, but fails when a thread needs to log something that would trigger the messagebox, since that occurs in the main loop and it is not safe to trigger main loop events from threads.

My first attempt to fix this was to try to make the popup box appear only through emission of a Signal mediated by Qt.QueuedConnection, like so:

import logging
from PySide6.QtCore import QObject, Signal, Slot, Qt
from PySide6.QtWidgets import QMessageBox

class QtHandler(logging.Handler, QObject):
    queue_messagebox = Signal(object)
    
    def __init__(self, parent=None):
        # Initialize both QObject and logging.Handler
        QObject.__init__(self, parent)
        logging.Handler.__init__(self)
        # Connect the signal to the slot using a queued connection to ensure thread safety
        self.queue_messagebox.connect(self.show_message_box, Qt.QueuedConnection)

    def emit(self, record):
        # This is where the logging record is processed
        self.queue_messagebox.emit(record)  # Emit the signal with the log record

    @Slot(object)
    def show_message_box(self, record):
        # Create the message box based on the log level
        if record.levelno >= logging.ERROR:
            QMessageBox.critical(None, "Error", self.format(record))
        elif record.levelno >= logging.WARNING:
            QMessageBox.warning(None, "Warning", self.format(record))

But this fails with the statement that my emit() function is getting too many arguments. My assumption is that this is due to the fact that both logging.Handler and QObject have methods called emit and I am overriding one of them, when I need both of them to work.

How can I address this conflict, or is there another way I could go about making this logging handler threadsafe?


Solution

  • For posterity, this worked in the end, thank you @mahkitah for the suggested framework:

    import logging
    from PySide6.QtCore import QObject, Signal, Slot, Qt
    from PySide6.QtWidgets import QMessageBox
    
    #QObject designed solely to emit signals
    class MessageBoxEmitter(QObject):
        emit_message = Signal(object)
    
    class QtHandler(logging.Handler):
        def __init__(self, parent=None):
            super().__init__()
            # Create an instance of the internal QObject to handle signal emissions
            self.emitter = MessageBoxEmitter()
            # Connect the internal signal to the message box displaying slot - queued connection to ensure thread safety
            self.emitter.emit_message.connect(self.show_message_box, Qt.QueuedConnection)
    
        def emit(self, record):
            # Emit the signal with the log record
            self.emitter.emit_message.emit(record)
    
        @Slot(object)
        def show_message_box(self, record):
            # Create the message box based on the log level
            if record.levelno >= logging.ERROR:
                QMessageBox.critical(None, "Error", self.format(record))
            elif record.levelno >= logging.WARNING:
                QMessageBox.warning(None, "Warning", self.format(record))