3

At the moment I have a little flask project that calls another python file. I'm fully aware that this way is kinda awful, and so, I want to swap it for a function call while maintaining the prints getting yelded to the website.

def get_Checks():
    root = request.url_root

    def func():
        yield ("Inicio <br>")
        with subprocess.Popen(r"python somefile.py", stdout=subprocess.PIPE, bufsize=1,
                              universal_newlines=True) as p:
            for line in p.stdout:
                yield (line + "<br>")

    return Response(func())

I've tryed to replace the file call with the function directly but it just prints it to the console.

I really appreciate any help you can provide.

Dimitris Fasarakis Hilliard
  • 150,925
  • 31
  • 268
  • 253
EndermanAPM
  • 327
  • 2
  • 22

2 Answers2

1

A simple way would be to temporarily change sys.stdout to a file-like object, call the function, then restore sys.stdout. The output will be available in the file-like object.

Here is a working Flask app that demonstrates the method:

import sys
from io import StringIO
from flask import Flask, request, Response
import somefile

app = Flask(__name__)

@app.route("/")
def hello():
    def func():
        yield ("Inicio <br>")

        try:
            _stdout = sys.stdout
            sys.stdout = output = StringIO()
            somefile.main()
            output.seek(0)
            for line in output:
                sys.stdout = _stdout
                yield '{}<br>'.format(line.rstrip())
                sys.stdout = output
        finally:
            sys.stdout.close()    # close the StringIO object
            sys.stdout = _stdout  # restore sys.stdout

    return Response(func())

if __name__ == "__main__":
    app.run()

Here a io.StringIO object is used to collect the standard output produced by the function, and then the lines are yielded from that object. The finally ensures that the original sys.stdout is restored afterwards. There is some additional complexity around the yield statement because yield returns control to the calling code for which stdout must be restored in case the caller also wants to print to stdout.

It's assumed that the function in somefile.py is the "main" function, and that invocation of it is guarded by a if __name__ == '__main__': test, something like this:

def main():
    for i in range(10):
        print(i)

if __name__ == '__main__':
    main()
mhawke
  • 84,695
  • 9
  • 117
  • 138
  • This is not working for me :S It just dones't print anything anywhere. – EndermanAPM Sep 02 '16 at 11:00
  • @EndermanAPM: possibly you are trying this out in the interactive interpreter? If that is the case then fooling about with `stdout` will also affect your interpreter's `stdout`. This code is intended to be executed as a script, not interactively. It definitely works in a Flask app - see updated answer. – mhawke Sep 02 '16 at 11:48
  • @EndermanAPM: BTW, is there a reason that you can not simply update the function in `somefile.py` to have it yield those lines rather than print them? Then you could just call it as a generator. – mhawke Sep 02 '16 at 11:50
  • No, I'm not running from the interactive interpreter and I suppose i could rewrite `somefile.py`. – EndermanAPM Sep 02 '16 at 11:54
  • Does the full Flask app that I added to my answer work? (It does for me) – mhawke Sep 02 '16 at 12:01
  • It does not on my app, later I'll try it on its own – EndermanAPM Sep 02 '16 at 12:05
  • @EndermanAPM: don't confuse `return` and `print` statements. If you want to [call a function instead of running a module as a script](http://stackoverflow.com/q/30076185/4279) then you should use `return` (or `yield`), not `print`. If you have to use stdout as a way to return a value from a function then you could use [`contextlib.stdout_redirect()` context manager](http://stackoverflow.com/a/22434262/4279) (make sure to add `flush=True` to your `print` statements or unbuffer `sys.stdout`, to get values in time). – jfs Oct 29 '16 at 09:33
  • @J.F.Sebastian I admit that my answer may lack a reference to `contextlib.stdout_redirect()` (which I managed to overlook at that time). But could you please tell me how downvoting it and promoting your own answer to a related question improves the situation? – code_onkel Nov 01 '16 at 07:56
0

Assuming that all the printing you want to grab is done within the same module, You can monkey-patch the print function of the other module. In the example below, I use a context manager to revert the original print function after the grabbing is done.

This is mod1, the module with the misbehaving function.

def bogus_function():
    print('Hello World!')
    print('Line 2')

This is mod2, the module using mod1.bogus_function()

import io
import functools
import contextlib

import mod1

@contextlib.contextmanager
def grab_stdout(module, fd):
    def monkey_print(*args, **kwargs):
        kwargs['file'] = fd
        print(*args, **kwargs)

    setattr(module, 'print', monkey_print)
    try:
        yield
    finally:
        setattr(module, 'print', print)

def line_generator():
    fd = io.StringIO()
    with grab_stdout(mod1, fd):
        mod1.bogus_function()
    fd.seek(0)

    for line in fd:
        yield line.rstrip('\r\n') + '<br>'

for t in enumerate(line_generator()):
    print('line %d: %r' % t)

The grab_stdout() context manager redirects print calls of module to the file-like object fd. In the function line_generator(), grab_stdout() is used to store the print output of bogus_function in the StringIO object fd. The rest should be self-explanatory.

If you don't know exactly whether print is called in other modules in the call tree of the function in question, you can modify grab_stdout as follows:

import builtins
print_orig = builtins.print

@contextlib.contextmanager
def grab_stdout_global(fd):
    def monkey_print(*args, **kwargs):
        kwargs['file'] = fd
        print_orig(*args, **kwargs)

    builtins.print = monkey_print
    try:
        yield
    finally:
        builtins.print = print_orig
code_onkel
  • 2,759
  • 1
  • 16
  • 31
  • Damm, that's more complex than what I anticipated, but let me give it a try. – EndermanAPM Sep 01 '16 at 11:05
  • It's working but it prints the newline(\n) at the end of every line, any way of getting rid of it. (aside from replace at the end) – EndermanAPM Sep 01 '16 at 11:13
  • @EndermanAPM Alright, I only added the print loop with enumerate to demonstrate that it works. – code_onkel Sep 01 '16 at 11:31
  • @EndermanAPM To remove newlines, just use `line.rstrip('\r\n')` – code_onkel Sep 01 '16 at 11:33
  • It's alright, without the enumerate it's working even better than before :) The old method had some problems with the encoding of some characters :) – EndermanAPM Sep 01 '16 at 11:36
  • With my test file is working fine but when I tried to use it with the actual "prod" file it prints it's prints but not the prints that are generated from the functions that the file calls. In your example it would be like bogus_function calling another function. – EndermanAPM Sep 02 '16 at 11:02
  • grab_stdout_global replaces grab_stdout? – EndermanAPM Sep 02 '16 at 11:14
  • Indeed. Note that there is no `module` argument anymore. – code_onkel Sep 02 '16 at 11:16
  • I see.. So, if its global, when and how can I end the stdout grab? – EndermanAPM Sep 02 '16 at 11:26
  • That's why I put the replacement of the print function in a context manager. The print function is only replaced inside the `with` block (where `mod1.bogus_function()` is called). In the next line `fd.seek(0)`, the original print function is already restored, even if an exception is thrown inside the `with` block. If your application is multithreaded, you have of course a chance that the print output of unrelated function calls is captures as well. But that is the case will all solutions that replace print or stdout. There are no thread-local module attributes AFAIK. – code_onkel Sep 02 '16 at 11:32
  • Oh :( The global thingy is not working for me, it just don't print anything anywhere. Could the python native yield and the flask one be conflicting? – EndermanAPM Sep 02 '16 at 11:44
  • Well, I won't help you debugging. It should work based on the assumption that `module.__builtins__` is the very same dictionary as `builtins.__dict__` (as described [here](https://docs.python.org/3/library/builtins.html)). I checked that with on my machine with Python 3.5.2. You could, for example add the line `assert print is builtins.print` in some locations that are supposed to be executed in the `with` block. – code_onkel Sep 02 '16 at 11:55
  • `yield` is not a builtin like `print`, but a keyword. The workings of yield can not be altered. – code_onkel Sep 02 '16 at 11:57