views:

168

answers:

4

I have a context manager that captures output to a string for a block of code indented under a with statement. This context manager yields a custom result object which will, when the block has finished executing, contain the captured output.

from contextlib import contextmanager

@contextmanager
def capturing():
    "Captures output within a 'with' block."
    from cStringIO import StringIO

    class result(object):
        def __init__(self):
            self._result = None
        def __str__(self):
            return self._result

    try:
        stringio = StringIO()
        out, err, sys.stdout, sys.stderr = sys.stdout, sys.stderr, stringio, stringio
        output = result()
        yield output
    finally:
        output._result, sys.stdout, sys.stderr = stringio.getvalue(), out, err
        stringio.close()

with capturing() as text:
    print "foo bar baz",

print str(text)   # prints "foo bar baz"

I can't just return a string, of course, because strings are immutable and thus the one the user gets back from the with statement can't be changed after their block of code runs. However, it is something of a drag to have to explicitly convert the result object to a string after the fact with str (I also played with making the object callable as a bit of syntactic sugar).

So is it possible to make the result instance act like a string, in that it does in fact return a string when named? I tried implementing __get__, but that appears to only work on attributes. Or is what I want to do not really possible?

+1  A: 

I don't believe there is a clean way to do what you want. text is defined in the modules' globals() dict. You would have to modify this globals() dict from within the capturing object:

The code below would break if you tried to use the with from within a function, since then text would be in the function's scope, not the globals.

import sys
import cStringIO

class capturing(object):
    def __init__(self,varname):
        self.varname=varname
    def __enter__(self):
        self.stringio=cStringIO.StringIO()
        self.out, sys.stdout = sys.stdout, self.stringio
        self.err, sys.stderr = sys.stderr, self.stringio        
        return self
    def __exit__(self,ext_type,exc_value,traceback):
        sys.stdout = self.out
        sys.stderr = self.err
        self._result = self.stringio.getvalue()
        globals()[self.varname]=self._result
    def __str__(self):
        return self._result


with capturing('text') as text:
    print("foo bar baz")

print(text)   # prints "foo bar baz"
# foo bar baz

print(repr(text))
# 'foo bar baz\n'
unutbu
That is a cute hack and I upvoted it for that reason, but "can't be used in a function" limits its utility somewhat. :-)
kindall
A: 

This is a comment as answer for I do not have enough rep. I will delete this. So ... why mess with the internals? Why not just create a custom function named Print? Seems to be the cleanest way, because using just the same old print would mislead those who read your code later.

Hamish Grubijan
A: 

At first glance, it looked like UserString (well, actually MutableString, but that's going away in Python 3.0) was basically what I wanted. Unfortunately, UserString doesn't work quite enough like a string; I was getting some odd formatting in print statements ending in commas that worked fine with str strings. (It appears you get an extra space printed if it's not a "real" string, or something.) I had the same issue with a toy class I created to play with wrapping a string. I didn't take the time to track down the cause, but it appears UserString is most useful as an example.

I ended up using a simpler result object, one with just a text member. You can just get the captured text that way as result.text. I also wrote a separate version that splitlines() the text into a list. This works great (mutable containers FTW!) and is actually better for my immediate use case, which is removing "extra" blank lines in the concatenated output of various functions. Here's that version:

import sys
from contextlib import contextmanager

@contextmanager
def capturinglines(output=None):
    "Captures lines of output to a list."
    from cStringIO import StringIO

    try:
        output = [] if output is None else output
        stringio = StringIO()
        out, err = sys.stdout, sys.stderr
        sys.stdout, sys.stderr = stringio, stringio
        yield output
    finally:
        sys.stdout, sys.stderr = out, err
        output.extend(stringio.getvalue().splitlines())
        stringio.close()

Usage:

with capturinglines() as output:
    print "foo"
    print "bar"

print output
['foo', 'bar']

with capturinglines(output):   # append to existing list
    print "baz"

print output
['foo', 'bar', 'baz']
kindall
A: 

I think you might be able to build something like this.

import StringIO

capturing = StringIO.StringIO()
print( "foo bar baz", file= capturing )

Now 'foo bar baz\n' == capturing.getvalue()

That's the easiest. It works perfectly with no extra work, except to fix your print functions to use the file= argument.

S.Lott
The `as` variable is **always** available after the with statement, actually. :-)
kindall
@kindall: Really? There's never any use for it with the standard built-in context managers like `file`. I think perhaps I should delete this answer.
S.Lott
Really, it works. Python only has function-level scoping, so when you define a variable (even in a loop or a `with` block) it's available through the end of the function. That's why I got this crazy idea in the first place.
kindall
"crazy idea". Agreed. Since the alternative (in this answer) is trivial and requires no programming. And since the idea is so hard to explain.
S.Lott
Well, the `print` was just an example; I want this to capture the output of whatever block is inside the `with` statement -- function calls, for example. I'd have to change all the functions to accept a file I want to `print` to. If the module the functions are in doesn't already import sys I would have to add that so I could provide a default of `sys.stdout` for that parameter. And so on... easier to just redirect `stdout`. And once you're doing that, why not use a context manager so as to restore `stdout` afterward? The only issue was how to give the caller access to the captured text.
kindall
"issue was how to give the caller access to the captured text"? What's wrong with `text.getvalue()` or `str(text)`. I still don't understand the actual problem you have with the code you posted. It works, doesn't it?
S.Lott
I was just looking for a more elegant way than what I had. What the user would want is the text, not an object that needs some kind of massaging to get to the text. A small matter, but my curiosity was piqued, so I decided to see just how far I could go.
kindall
@kindall: All objects require "messaging" to do stuff. Why should this be any different? I still don't get the "problem". Please **update** the question with some specific use case that shows something you cannot actually do. Some "problem" or "bug" with the solution you've proposed.
S.Lott