¿Cómo obtener un comportamiento sin locking / en tiempo real del módulo de registro de Python? (salida a PyQt QTextBrowser)

Descripción : He escrito un controlador de registro personalizado para capturar eventos de registro y escribirlos en un objeto QTextBrowser (muestra el código de ejemplo que se muestra a continuación).

Problema : al presionar el botón se invoca someProcess() . Esto escribe dos cadenas en el objeto logger . Sin embargo, las cadenas solo aparecen después de que someProcess() .

Pregunta : ¿Cómo puedo hacer que las cadenas registradas aparezcan en el objeto QTextBrowser inmediatamente / en tiempo real? (es decir, tan pronto como se invoca un método de salida del logger )

 from PyQt4 import QtCore, QtGui import sys import time import logging logger = logging.getLogger(__name__) class ConsoleWindowLogHandler(logging.Handler): def __init__(self, textBox): super(ConsoleWindowLogHandler, self).__init__() self.textBox = textBox def emit(self, logRecord): self.textBox.append(str(logRecord.getMessage())) def someProcess(): logger.error("line1") time.sleep(5) logger.error("line2") if __name__ == "__main__": app = QtGui.QApplication(sys.argv) window = QtGui.QWidget() textBox = QtGui.QTextBrowser() button = QtGui.QPushButton() button.clicked.connect(someProcess) vertLayout = QtGui.QVBoxLayout() vertLayout.addWidget(textBox) vertLayout.addWidget(button) window.setLayout(vertLayout) window.show() consoleHandler = ConsoleWindowLogHandler(textBox) logger.addHandler(consoleHandler) sys.exit(app.exec_()) 

EDITAR : gracias a la respuesta de @abarnert, logré escribir este código de trabajo utilizando QThread. QThread para ejecutar alguna función someProcess en un hilo de fondo. Para la señalización, tuve que recurrir a la señal y ranuras de estilo antiguo (no estoy seguro de cómo hacerlo en el estilo nuevo). Creé un QObject ficticio para poder emitir señales desde el controlador de registro.

 from PyQt4 import QtCore, QtGui import sys import time import logging logger = logging.getLogger(__name__) #------------------------------------------------------------------------------ class ConsoleWindowLogHandler(logging.Handler): def __init__(self, sigEmitter): super(ConsoleWindowLogHandler, self).__init__() self.sigEmitter = sigEmitter def emit(self, logRecord): message = str(logRecord.getMessage()) self.sigEmitter.emit(QtCore.SIGNAL("logMsg(QString)"), message) #------------------------------------------------------------------------------ class Window(QtGui.QWidget): def __init__(self): super(Window, self).__init__() # Layout textBox = QtGui.QTextBrowser() self.button = QtGui.QPushButton() vertLayout = QtGui.QVBoxLayout() vertLayout.addWidget(textBox) vertLayout.addWidget(self.button) self.setLayout(vertLayout) # Connect button self.button.clicked.connect(self.buttonPressed) # Thread self.bee = Worker(self.someProcess, ()) self.bee.finished.connect(self.restreUi) self.bee.terminated.connect(self.restreUi) # Console handler dummyEmitter = QtCore.QObject() self.connect(dummyEmitter, QtCore.SIGNAL("logMsg(QString)"), textBox.append) consoleHandler = ConsoleWindowLogHandler(dummyEmitter) logger.addHandler(consoleHandler) def buttonPressed(self): self.button.setEnabled(False) self.bee.start() def someProcess(self): logger.error("starting") for i in xrange(10): logger.error("line%d" % i) time.sleep(2) def restreUi(self): self.button.setEnabled(True) #------------------------------------------------------------------------------ class Worker(QtCore.QThread): def __init__(self, func, args): super(Worker, self).__init__() self.func = func self.args = args def run(self): self.func(*self.args) #------------------------------------------------------------------------------ if __name__ == "__main__": app = QtGui.QApplication(sys.argv) window = Window() window.show() sys.exit(app.exec_()) 

El problema real aquí es que está bloqueando la GUI completa durante 5 segundos al dormir en el hilo principal. No puede hacer eso o, de lo contrario, no se mostrarán actualizaciones, el usuario no podrá interactuar con su aplicación, etc. El problema de registro es solo una consecuencia secundaria de ese problema mayor.

Y si su progtwig real está llamando a algún código de un módulo de terceros que demora 5 segundos o hace algo que bloquea, tendrá exactamente el mismo problema.

En general, hay dos formas de hacer cosas lentas, bloqueando cosas sin bloquear una GUI (u otra aplicación basada en bucles de eventos):

  1. Hacer el trabajo en un hilo de fondo. Dependiendo de su marco de GUI, desde un subproceso en segundo plano, generalmente no puede llamar a funciones directamente en la GUI o modificar sus objetos; en su lugar, tiene que usar algún mecanismo para publicar mensajes en el bucle de eventos. En Qt, normalmente lo hace a través del mecanismo de la ranura de señal. Vea esta pregunta para más detalles.

  2. Divida el trabajo en trabajos sin locking o con locking garantizado de muy corto plazo que se devuelven rápidamente, cada uno progtwigndo el siguiente derecho antes de volver. (Con algunos marcos de GUI, puede hacer el equivalente en línea llamando a algo como safeYield o llamando al bucle de eventos de forma recursiva, pero no lo hace con Qt).

Dado que someProcess es un código externo que no puede modificar, que puede tardar unos segundos en finalizar o hace algo que bloquea, no puede usar la opción 2. Por lo tanto, la opción 1 es: ejecútelo en un subproceso en segundo plano.

Afortunadamente, esto es fácil. Qt tiene maneras de hacer esto, pero las formas de Python son aún más fáciles:

 t = threading.Thread(target=someProcess) t.start() 

Ahora, necesita cambiar ConsoleWindowLogHandler.emit para que, en lugar de modificar directamente textBox , envíe una señal para hacerlo en el hilo principal. Vea Temas y QObjects para todos los detalles, y algunos buenos ejemplos.

Más concretamente: el ejemplo de Mandelbrot utiliza un RenderThread que en realidad no dibuja nada, sino que envía una señal de imagen renderedImage ; El MandelbrotWidget luego tiene una ranura updatePixmap que se conecta a la señal de imagen renderedImage . De la misma manera, su manejador de registros no actualizaría realmente el cuadro de texto, sino que enviaría una señal gotLogMessage ; entonces tendrías un LogTextWidget con una ranura de updateLog que se conecta a esa señal. Por supuesto, para su caso simple, puede mantenerlos juntos en una sola clase, siempre y cuando conecte los dos lados con una conexión de ranura de señal en lugar de una llamada de método directo.

Probablemente desee mantener t en algún lugar y join él durante el cierre, o configurar t.daemon = True .

De cualquier manera, si desea saber cuándo se realiza someProcess , debe utilizar otros medios de comunicación con su hilo principal cuando haya terminado; de nuevo, con Qt, la respuesta habitual es enviar una señal. Y esto también le permite recuperar un resultado de someProcess . Y no es necesario modificar algunos someProcess para hacer esto; simplemente defina una función de envoltura que llame a someProcess y someProcess su resultado, y llame a esa función de envoltura desde el hilo de fondo.

Aquí hay otro método. En este ejemplo, agrego un StreamHandler al registrador que escribe en un búfer heredando tanto de QObject como de StringIO : cuando el controlador encuentra una cadena no vacía, la señal bufferMessage se emite y se captura en la ranura on_bufferMessage .

 #!/usr/bin/env python #-*- coding:utf-8 -*- import logging, StringIO, time from PyQt4 import QtCore, QtGui class logBuffer(QtCore.QObject, StringIO.StringIO): bufferMessage = QtCore.pyqtSignal(str) def __init__(self, *args, **kwargs): QtCore.QObject.__init__(self) StringIO.StringIO.__init__(self, *args, **kwargs) def write(self, message): if message: self.bufferMessage.emit(unicode(message)) StringIO.StringIO.write(self, message) class myThread(QtCore.QThread): def __init__(self, parent=None): super(myThread, self).__init__(parent) self.iteration = None def start(self): self.iteration = 3 return super(myThread, self).start() def run(self): while self.iteration: logging.info("Hello from thread {0}! {1}".format(0, self.iteration)) self.iteration -= 1 time.sleep(3) class myThread1(QtCore.QThread): def __init__(self, parent=None): super(myThread1, self).__init__(parent) self.iteration = None self.logger = logging.getLogger(__name__) def start(self): self.iteration = 3 return super(myThread1, self).start() def run(self): time.sleep(1) while self.iteration: self.logger.info("Hello from thread {0}! {1}".format(1, self.iteration)) self.iteration -= 1 time.sleep(3) class myWindow(QtGui.QWidget): def __init__(self, parent=None): super(myWindow, self).__init__(parent) self.pushButton = QtGui.QPushButton(self) self.pushButton.setText("Send Log Message") self.pushButton.clicked.connect(self.on_pushButton_clicked) self.pushButtonThread = QtGui.QPushButton(self) self.pushButtonThread.setText("Start Threading") self.pushButtonThread.clicked.connect(self.on_pushButtonThread_clicked) self.lineEdit = QtGui.QLineEdit(self) self.lineEdit.setText("Hello!") self.label = QtGui.QLabel(self) self.layout = QtGui.QVBoxLayout(self) self.layout.addWidget(self.lineEdit) self.layout.addWidget(self.pushButton) self.layout.addWidget(self.pushButtonThread) self.layout.addWidget(self.label) self.logBuffer = logBuffer() self.logBuffer.bufferMessage.connect(self.on_logBuffer_bufferMessage) logFormatter = logging.Formatter('%(levelname)s: %(message)s') logHandler = logging.StreamHandler(self.logBuffer) logHandler.setFormatter(logFormatter) self.logger = logging.getLogger() self.logger.setLevel(logging.INFO) self.logger.addHandler(logHandler) self.thread = myThread(self) self.thread1 = myThread1(self) @QtCore.pyqtSlot() def on_pushButtonThread_clicked(self): self.thread.start() self.thread1.start() @QtCore.pyqtSlot(str) def on_logBuffer_bufferMessage(self, message): self.label.setText(message) @QtCore.pyqtSlot() def on_pushButton_clicked(self): message = self.lineEdit.text() self.logger.info(message if message else "No new messages") if __name__ == "__main__": import sys app = QtGui.QApplication(sys.argv) app.setApplicationName('myWindow') main = myWindow() main.show() sys.exit(app.exec_()) 

Lo mejor de este método es que puede registrar mensajes desde los módulos / subprocesos de su aplicación principal sin tener que mantener ninguna referencia al registrador, por ejemplo, llamando a logging.log(logging.INFO, logging_message) o logging.info(logging_message)

Sobre la base del código de QTextBrowser y las QTextBrowser de QTextEdit , actualizo el código cambiando el estilo antiguo al nuevo estilo de señal / ranura y cambiando el QTextBrowser a QTextEdit .

 import sys import time import logging from qtpy.QtCore import QObject, Signal, QThread from qtpy.QtWidgets import QWidget, QTextEdit, QPushButton, QVBoxLayout logger = logging.getLogger(__name__) class ConsoleWindowLogHandler(logging.Handler, QObject): sigLog = Signal(str) def __init__(self): logging.Handler.__init__(self) QObject.__init__(self) def emit(self, logRecord): message = str(logRecord.getMessage()) self.sigLog.emit(message) class Window(QWidget): def __init__(self): super(Window, self).__init__() # Layout textBox = QTextEdit() textBox.setReadOnly(True) self.button = QPushButton('Click') vertLayout = QVBoxLayout() vertLayout.addWidget(textBox) vertLayout.addWidget(self.button) self.setLayout(vertLayout) # Connect button #self.button.clicked.connect(self.someProcess) # blocking self.button.clicked.connect(self.buttonPressed) # Thread self.bee = Worker(self.someProcess, ()) self.bee.finished.connect(self.restreUi) self.bee.terminated.connect(self.restreUi) # Console handler consoleHandler = ConsoleWindowLogHandler() consoleHandler.sigLog.connect(textBox.append) logger.addHandler(consoleHandler) def buttonPressed(self): self.button.setEnabled(False) self.bee.start() def someProcess(self): logger.error("starting") for i in range(10): logger.error("line%d" % i) time.sleep(2) def restreUi(self): self.button.setEnabled(True) class Worker(QThread): def __init__(self, func, args): super(Worker, self).__init__() self.func = func self.args = args def run(self): self.func(*self.args) def main(): from qtpy.QtWidgets import QApplication app = QApplication(sys.argv) window = Window() window.show() sys.exit(app.exec_()) if __name__ == "__main__": main()