2

I'm using a strategy based around os.dup2 (similar to examples on this site) to redirect C/fortran level output into a temporary file for capturing.

The only problem I've noticed is, if you use this code from an interactive shell in windows (either python.exe or ipython) it has the strange side effect of enabling output buffering in the console.

Before capture sys.stdout is some kind of file object that returns True for istty(). Typing print('hi') causes hi to be output directly. After capture sys.stdout points to exactly the same file object but print('hi') no longer shows anything until sys.stdout.flush() is called.

Below is a minimal example script "test.py"

import os, sys, tempfile

class Capture(object):
    def __init__(self):
        super(Capture, self).__init__()
        self._org = None    # Original stdout stream
        self._dup = None    # Original system stdout descriptor
        self._file = None   # Temporary file to write stdout to

    def start(self):
        self._org = sys.stdout
        sys.stdout = sys.__stdout__
        fdout = sys.stdout.fileno()
        self._file = tempfile.TemporaryFile()
        self._dup = None
        if fdout >= 0:
            self._dup = os.dup(fdout)
            os.dup2(self._file.fileno(), fdout)

    def stop(self):
        sys.stdout.flush()
        if self._dup is not None:
            os.dup2(self._dup, sys.stdout.fileno())
            os.close(self._dup)
        sys.stdout = self._org
        self._file.seek(0)
        out = self._file.readlines()
        self._file.close()
        return out

def run():
    c = Capture()
    c.start()
    os.system('echo 10')
    print('20')
    x = c.stop()
    print(x)

if __name__ == '__main__':
    run()

Opening a command prompt and running the script works fine. This produces the expected output:

python.exe test.py

Running it from a python shell does not:

python.exe
>>> import test.py
>>> test.run()
>>> print('hello?')

No output is shown until stdout is flushed:

>>> import sys
>>> sys.stdout.flush()

Does anybody have any idea what's going on?


Quick info:

  • The issue appears on Windows, not on linux (so probably not on mac).
  • Same behaviour in both Python 2.7.6 and Python 2.7.9
  • The script should capture C/fortran output, not just python output
  • It runs without errors on windows, but afterwards print() no longer flushes
Michael Clerx
  • 2,928
  • 2
  • 33
  • 47

1 Answers1

2

I could confirm a related problem with Python 2 in Linux, but not with Python 3

The basic problem is

>>> sys.stdout is sys.__stdout__
True

Thus you are using the original sys.stdout object all the time. And when you do the first output, in Python 2 it executes the isatty() system call once for the underlying file, and stores the result.

You should open an altogether new file and replace sys.stdout with it.


Thus the proper way to write the Capture class would be

import sys
import tempfile
import time
import os

class Capture(object):
    def __init__(self):
        super(Capture, self).__init__()

    def start(self):
        self._old_stdout = sys.stdout
        self._stdout_fd = self._old_stdout.fileno()
        self._saved_stdout_fd = os.dup(self._stdout_fd)
        self._file = sys.stdout = tempfile.TemporaryFile(mode='w+t')
        os.dup2(self._file.fileno(), self._stdout_fd)

    def stop(self):
        os.dup2(self._saved_stdout_fd, self._stdout_fd)
        os.close(self._saved_stdout_fd)
        sys.stdout = self._old_stdout
        self._file.seek(0)
        out = self._file.readlines()
        self._file.close()
        return out

def run():
    c = Capture()
    c.start()
    os.system('echo 10')
    x = c.stop()
    print(x)
    time.sleep(1)
    print("finished")

run()

With this program, in both Python 2 and Python 3, the output will be:

['10\n']
finished

with the first line appearing on the terminal instantaneously, and the second after one second delay.


This would fail for code that import stdout from sys, however. Luckily not much code does that.

  • Hi Antti, I think you misunderstood: there is no issue with python 2.7 on linux. _Only_ on windows. On linux the script does exactly what it needs to do, and afterwards printing works fine. Secondly, the capture class is supposed to capture subprocess level output (ie output from C or fortran extensions), which this solution doesn't do. – Michael Clerx Mar 23 '15 at 08:23
  • 1
    @MichaelClerx in any case I modded your question so that it does not use sys.stdout but instead the actual filehandle – Antti Haapala -- Слава Україні Mar 23 '15 at 09:49
  • Which linux/python are you using? And are you executing the script or running from a terminal? – Michael Clerx Mar 23 '15 at 10:18
  • I mean, are you doing "python test.py" or are you opening a terminal and then doing "import test". It matters for the windows version... – Michael Clerx Mar 23 '15 at 11:07
  • Ok, redirecting sys.stdout to the file as well fixes the issue. However, I'd still change the second line of start() to ``self._stdout_fd = sys.__stdout__.fileno()`` to prevent issues when stdout has been redirected already (possibly to something without a fileno() method). Also, the windows issue in my example stops occurring if you leave out a print() from inside the capturing bit, so I'd add a print('20') after the os call to make it a fair comparison. – Michael Clerx Mar 23 '15 at 12:00
  • ah indeed :D, yes, the print is needed within the windows/linux for this issue to occur (and no prints before that) – Antti Haapala -- Слава Україні Mar 23 '15 at 13:39
  • Came across this via a pytest issue (https://github.com/pytest-dev/pytest/issues/5134#issuecomment-546523358). There it is still a problem when `sys.__stdout__` gets used, or the iostream via a C++ program. While the former could be fixed by replacing `sys.__stdout__` also, the latter likely needs closing of `fd 1` really or something similar. But it appears to be a problem with py2 only after all. – blueyed Oct 25 '19 at 23:34