Search code examples
python-2.7pyqtstdoutpysidestderr

Duplicate stdout, stderr in QTextEdit widget


I've been having difficulty with my PySide program for a few days now. I don't think the problem is incredibly difficult because there are answer out there. Problem I have is none of them seem to work for me.

I want to 'listen' to the file objects stdout and stderr and output the contents to QText Edit widget while my PySide program is running. Now, I already realise this question (or something similar) has been asked before on here but like I said, can't get it to work for me for some reason and most other solutions out there are based on the one that I can't get working, so a very frustrating last few days for me. This solution (OutLog), is included in my code snippet below, just in case one of you guys can see a botched implementation on my part.

Things to remember:

1 I'm doing this on Windows 7(duuuh, da, da, duh)

2 I'm using eclipse and running it from inside the IDE(duh, da, da, duh, DUUUUH: It would be really handy if the suggestions worked with either commandline or an IDE)

3 I really just want to duplicate the output of stdout and stderr to the widget while the program runs. For this to happen line-by-line would be a dream but even if it all comes out as a chunk at the end of a loop or something, that would be fab.

4 Oh, and also regarding OutLog, could somebody tell me how, if self.out is set to 'None' in the init, this class can actually work? I mean, self.out is always a NoneType object, right???

Any help would be appreciated, even if it's just pointers to where I could find more information. I've been trying to build my own solution (I'm a bit of a sadist that way) but I've found it hard to find relevant info on how these objects work to do that.

Anyway, whine over. Here's my code:

#!/usr/bin/env python
import sys
import logging
import system_utilities

log = logging.getLogger()
log.setLevel("DEBUG")
log.addHandler(system_utilities.SystemLogger())

import matplotlib
matplotlib.use("Qt4Agg")
matplotlib.rcParams["backend.qt4"] = "PySide"
import subprocess
import plot_widget

from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure

from PySide import QtCore, QtGui


class MainWindow(QtGui.QMainWindow):
    """This is the main window class and displays the primary UI when launched.
    Inherits from QMainWindow.
    """

    def __init__(self):
        """Init function. 
        """
        super(MainWindow, self).__init__()
        self.x = None
        self.y = None
        self.data_plot = None
        self.plot_layout = None
        self.terminal = None
        self.setup_plot()
        self.setup_interface()

    def  setup_plot(self):
        """Member function to setup the graph window in the main UI.
        """

        #Create a PlotWidget object
        self.data_plot = plot_widget.PlotWidget()

        #Create a BoxLayout element to hold PlotWidget
        self.plot_layout = QtGui.QVBoxLayout()
        self.plot_layout.addWidget(self.data_plot)


    def setup_interface(self):
        """Member function to instantiate and build the composite elements of the 
        UI."""

        #Main widget houses layout elements (Layout cannot be placed directly in a QMainWindow).
        central_widget = QtGui.QWidget()
        test_splitter = QtGui.QSplitter(QtCore.Qt.Vertical)
        button_splitter = QtGui.QSplitter(QtCore.Qt.Horizontal)

        #UI BoxLayout elements
        central_layout = QtGui.QVBoxLayout()
        #button_layout = QtGui.QHBoxLayout()

        #UI PushButton elements
        exit_button = QtGui.QPushButton("Close")
        run_button = QtGui.QPushButton("Run...")

        #UI Text output
        self.editor = QtGui.QTextEdit()
        self.editor.setReadOnly(True)
        self.terminal = QtGui.QTextBrowser()
        self.terminal.setReadOnly(True)


        #UI PushButton signals
        run_button.clicked.connect(self.run_c_program)
        run_button.clicked.connect(self.data_plot.redraw_plot)
        exit_button.clicked.connect(QtCore.QCoreApplication.instance().quit)

        #Build the UI from composite elements
        central_layout.addLayout(self.plot_layout)
        central_layout.addWidget(self.editor)
        button_splitter.addWidget(run_button)
        button_splitter.addWidget(exit_button)
        test_splitter.addWidget(button_splitter)
        test_splitter.addWidget(self.terminal)
        test_splitter.setCollapsible(1, True)
        central_layout.addWidget(test_splitter)
        central_widget.setLayout(central_layout)
        self.setCentralWidget(central_widget)


        self.show()

class OutLog:
    def __init__(self, edit, out=None, color=None):
        """(edit, out=None, color=None) -> can write stdout, stderr to a
        QTextEdit.
        edit = QTextEdit
        out = alternate stream ( can be the original sys.stdout )
        color = alternate color (i.e. color stderr a different color)
        """
        self.edit = edit
        self.out = None
        self.color = color

    def write(self, m):
        if self.color:
            tc = self.edit.textColor()
            self.edit.setTextColor(self.color)

        self.edit.moveCursor(QtGui.QTextCursor.End)
        log.debug("this is m {}".format(m))
        self.edit.insertPlainText( m )

        if self.color:
            self.edit.setTextColor(tc)

        if self.out:
            self.out.write(m)




def main():


    app = QtGui.QApplication(sys.argv)

    log.debug("Window starting.")
    window = MainWindow()
    sys.stdout = OutLog(window.terminal, sys.stdout)
    sys.stderr = OutLog(window.terminal, sys.stderr, QtGui.QColor(255,0,0))
    window.show()

    sys.exit(app.exec_())
    log.info("System shutdown.")


if __name__ == '__main__':
    main()

"Help me Obi-Wan..."

Thanks in advance guys (and gals :-))


Solution

  • It seems that all you need to do is override sys.stderr and sys.stdout with a wrapper object that emits a signal whenever output is written.

    Below is a demo script that should do more or less what you want. Note that the wrapper class does not restore sys.stdout/sys.stderr from sys.__stdout__/sys.__stderr__, because the latter objects may not be same as the ones that were orignally replaced.

    PyQt5:

    import sys
    from PyQt5 import QtWidgets, QtGui, QtCore
    
    class OutputWrapper(QtCore.QObject):
        outputWritten = QtCore.pyqtSignal(object, object)
    
        def __init__(self, parent, stdout=True):
            super().__init__(parent)
            if stdout:
                self._stream = sys.stdout
                sys.stdout = self
            else:
                self._stream = sys.stderr
                sys.stderr = self
            self._stdout = stdout
    
        def write(self, text):
            self._stream.write(text)
            self.outputWritten.emit(text, self._stdout)
    
        def __getattr__(self, name):
            return getattr(self._stream, name)
    
        def __del__(self):
            try:
                if self._stdout:
                    sys.stdout = self._stream
                else:
                    sys.stderr = self._stream
            except AttributeError:
                pass
    
    class Window(QtWidgets.QMainWindow):
        def __init__(self):
            super().__init__(   )
            widget = QtWidgets.QWidget(self)
            layout = QtWidgets.QVBoxLayout(widget)
            self.setCentralWidget(widget)
            self.terminal = QtWidgets.QTextBrowser(self)
            self._err_color = QtCore.Qt.red
            self.button = QtWidgets.QPushButton('Test', self)
            self.button.clicked.connect(self.handleButton)
            layout.addWidget(self.terminal)
            layout.addWidget(self.button)
            stdout = OutputWrapper(self, True)
            stdout.outputWritten.connect(self.handleOutput)
            stderr = OutputWrapper(self, False)
            stderr.outputWritten.connect(self.handleOutput)
    
        def handleOutput(self, text, stdout):
            color = self.terminal.textColor()
            self.terminal.moveCursor(QtGui.QTextCursor.End)
            self.terminal.setTextColor(color if stdout else self._err_color)
            self.terminal.insertPlainText(text)
            self.terminal.setTextColor(color)
    
        def handleButton(self):
            if QtCore.QTime.currentTime().second() % 2:
                print('Printing to stdout...')
            else:
                print('Printing to stderr...', file=sys.stderr)
    
    if __name__ == '__main__':
    
        app = QtWidgets.QApplication(sys.argv)
        window = Window()
        window.setGeometry(500, 300, 300, 200)
        window.show()
        sys.exit(app.exec_())
    

    PyQt4:

    import sys
    from PyQt4 import QtGui, QtCore
    
    class OutputWrapper(QtCore.QObject):
        outputWritten = QtCore.pyqtSignal(object, object)
    
        def __init__(self, parent, stdout=True):
            QtCore.QObject.__init__(self, parent)
            if stdout:
                self._stream = sys.stdout
                sys.stdout = self
            else:
                self._stream = sys.stderr
                sys.stderr = self
            self._stdout = stdout
    
        def write(self, text):
            self._stream.write(text)
            self.outputWritten.emit(text, self._stdout)
    
        def __getattr__(self, name):
            return getattr(self._stream, name)
    
        def __del__(self):
            try:
                if self._stdout:
                    sys.stdout = self._stream
                else:
                    sys.stderr = self._stream
            except AttributeError:
                pass
    
    class Window(QtGui.QMainWindow):
        def __init__(self):
            QtGui.QMainWindow.__init__(self)
            widget = QtGui.QWidget(self)
            layout = QtGui.QVBoxLayout(widget)
            self.setCentralWidget(widget)
            self.terminal = QtGui.QTextBrowser(self)
            self._err_color = QtCore.Qt.red
            self.button = QtGui.QPushButton('Test', self)
            self.button.clicked.connect(self.handleButton)
            layout.addWidget(self.terminal)
            layout.addWidget(self.button)
            stdout = OutputWrapper(self, True)
            stdout.outputWritten.connect(self.handleOutput)
            stderr = OutputWrapper(self, False)
            stderr.outputWritten.connect(self.handleOutput)
    
        def handleOutput(self, text, stdout):
            color = self.terminal.textColor()
            self.terminal.moveCursor(QtGui.QTextCursor.End)
            self.terminal.setTextColor(color if stdout else self._err_color)
            self.terminal.insertPlainText(text)
            self.terminal.setTextColor(color)
    
        def handleButton(self):
            if QtCore.QTime.currentTime().second() % 2:
                print('Printing to stdout...')
            else:
                sys.stderr.write('Printing to stderr...\n')
    
    if __name__ == '__main__':
    
        app = QtGui.QApplication(sys.argv)
        window = Window()
        window.setGeometry(500, 300, 300, 200)
        window.show()
        sys.exit(app.exec_())
    

    NB:

    Instances of the OutputWrapper should be created as early as possible, so as to ensure that other modules that need sys.stdout/sys.stderr (such as the logging module) use the wrapped versions wherever necessary.