1

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 :-))

kssmyw
  • 13
  • 1
  • 4
  • Just so it's clear, at the moment when I run this program, nothing shows in the QTextEdit I want the output to go to (self.terminal). Once again, thanks – kssmyw Nov 08 '13 at 09:18
  • possible duplicate of [How to capture output of Python's interpreter and show in a Text widget?](http://stackoverflow.com/questions/8356336/how-to-capture-output-of-pythons-interpreter-and-show-in-a-text-widget) – Aaron Digulla Nov 08 '13 at 09:23

2 Answers2

7

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.

ekhumoro
  • 115,249
  • 20
  • 229
  • 336
  • Hi!! Thanks for this, it's almost exactly what I was after! There is a slight thing on which I'm still confused: I can click the button and the print statement appears in the widget and stdout/stderr, but I don't get all of stdout/stderr in the widget. For example, none of my logging messages appear in the widget but the still appear on the terminal screen. Does this mean I have two concurrent stdout's at the same time?? I want _all_ stdout output to appear in the widget, not just when I click the qbutton. Any suggestions? – kssmyw Nov 12 '13 at 12:41
  • 1
    @kssmyw. The [logging.StreamHandler](http://docs.python.org/2/library/logging.handlers.html#streamhandler) class keeps its own reference to `sys.stderr` (or the stream object passed to its constructor). So you will have to ensure that the output wrappers are created *before* the logging is setup. – ekhumoro Nov 12 '13 at 18:11
  • Yeah, thanks again! At first I had a few problems with it but then I moved OutputWrapper into another module and imported it into my MainWindow module. I was then able to import and instantiate it before my logging call, and hey presto! I had to make one minor change to the __init__: I set parent=None but then it worked a treat. Thanks, alot of frustration has been alleviated today :-) – kssmyw Nov 13 '13 at 10:45
0

self.out = None is probably a typo and should be self.out = out. That way, you can see anything that printed in the console as well. This is the first step to be sure that the code prints anything at all.

The next thing is that you need to realize which output you're redirecting. A subprocess get its own stdio, so no amount of redirection of the parent's stdout is going to have any effect.

Getting stdio correct with a subprocess isn't trivial. I suggest to start with subprocess.communicate() which gives you all the output as a single string. That's usually good enough.

Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
  • Hi Aaron on self.out, yeah I thought as much but everywhere that this code (or a derivative) is used on the internet, nobody points that out, so I'd started to think that I was missing something. On you second point, how would I be able to use subprocess to capture the stdio from running my PySide program? Would I, for instance, be able to pass sys.argv to subprocess.communicate() to listen to the output of the running program? (because you need to pass commandline program name, with necessary arguments to get the output from subprocess, from what I understand...) – kssmyw Nov 08 '13 at 10:22
  • `subprocess.communicate()` will do everything for you. When it returns, you will get the output of the subprocess as a string as return value from the method. Please look at the examples in the subprocess module's documentation. – Aaron Digulla Nov 08 '13 at 14:10
  • I'm still a little confused. If I use communicate(), then I do this: return = subprocess.Popen(system_cmd), then: return.communicate(). I'm unsure what value I am using for "system_cmd". When I input ["python", path_to_py_file], then the program starts an infinite loop and I have to restart my computer. Please elaborate on which command I should use for subprocess. I've already looked through the doc several times, but I'll keep re-reading... Also I'm trying to listen in on the stdout from MainWindow, not from one of the member functions... – kssmyw Nov 08 '13 at 14:56
  • Use the same value for `system_cmd` that you had before: `"path/to/C/program.exe"` – Aaron Digulla Nov 08 '13 at 15:23
  • Sorry, I have not been very clear. I can already get that output no problem. What I want is to duplicate the stdout from my QMainWindow parent process in the QTextEdit while my QMainWindow program is running.... – kssmyw Nov 08 '13 at 15:53