3

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?

kindall
  • 178,883
  • 35
  • 278
  • 309
  • Regardless of whether or not there is an answer to this question, I know I'd prefer you return a class that implemented `__str__`. I'm not sure how it's a drag that you have to, at some point, explicitly say, "right here is where I lock this in as a string, no further changes" by calling `str()`. So what's the gain? – Mike DeSimone Oct 12 '10 at 23:07
  • @Mike: Mainly that a user wants the string, not an object that has to be converted to string. – kindall Oct 12 '10 at 23:14
  • It looks like UserString (well, MutableString, but but that's going away) is basically what I want, so I'm going to vote to close this. Edit: whoops, guess I can't really do that as none of the reasons really apply. – kindall Oct 12 '10 at 23:18
  • 2
    @kindall: Then you put that in an answer, mark it as the correct one, and carry on. Bonus points for linking to UserString and MutableString's pages on docs.python.org. – Mike DeSimone Oct 13 '10 at 03:58
  • "Mainly that a user wants the string". So? What's wrong with giving them a plain-old string? They can concatenate to it in the standard Python way: `string += string_data`. What's wrong with that? – S.Lott Oct 13 '10 at 14:25
  • You can't give them a plain old string in the `with` statement, sadly, because the data they're interested (the output captured inside the body of the `with` statement) isn't available, and the string can't be changed after you give it to them. Hence, you have to return a mutable object that contains a string, and fill in the string in the context manager's `__exit__` (or after the `yield` if you're using `@contetmanager`). – kindall Oct 13 '10 at 14:53
  • "and the string can't be changed after you give it to them" How is this a problem? This is standard Python. Strings are immutable. You can create a new string and add data to it. What problem do you actually have? I don't understand what problem you're trying to solve with all of this. Please explain why a string is a problem. – S.Lott Oct 13 '10 at 15:40
  • I'm trying to write a context manager that captures the output of whatever is under the `with` statement into a string, using StringIO. I would like to let the user of this context manager specify the variable that receives the captured output *in the `with` statement* because it belongs there. But at the point I have to provide the value will end up in the user's variable, I don't *have* the captured output yet. If I gave them a string, it would be empty! So I have to give them something I can change when I do have the captured output. If I want to do it as a context manager, that is. :-) – kindall Oct 13 '10 at 16:05
  • 1
    Check out http://pypi.python.org/pypi/stringlike – Elliot Cameron Jun 11 '12 at 17:15

6 Answers6

5

How to make a class that acts like a string? Subclass str

import os
class LikeAStr(str):
    '''Making a class like a str object; or more precisely
    making a str subclass with added contextmanager functionality.'''

    def __init__(self, diff_directory):
        self._iwd = os.getcwd()
        self._cwd = diff_directory

    def __enter__(self):
        return self

    def __exit__(self, ext_typ, exc_value, traceback):
        try: os.chdir(self._iwd) # might get deleted within the "with" statement
        except: pass

    def __str__(self):
        return self._cwd

    def __repr__(self):
        return repr(self._cwd)


astr = LikeAStr('C:\\')

with LikeAStr('C:\\') as astr:
    print 1, os.getcwd()
    os.chdir( astr ) # expects str() or unicode() not some other class
    print 2, os.getcwd()
    #

# out of with block
print 3, os.getcwd()
print 4, astr == 'C:\\'

Output:

1 D:\Projects\Python\
2 C:\
3 D:\Projects\Python\
4 True
DevPlayer
  • 5,393
  • 1
  • 25
  • 20
2

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
  • 842,883
  • 184
  • 1,785
  • 1,677
  • 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 Oct 12 '10 at 23:20
2

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 actually ended up using a bytearray because it works enough like a string for most purposes, but is mutable. I also wrote a separate version that splitlines() the text into a list. This works great 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
  • 178,883
  • 35
  • 278
  • 309
1

How to make a class that acts like a string?

If you don't want to subclass str for whatever reason:

class StrBuiltin(object):
    def __init__(self, astr=''):
        self._str = astr

    def __enter__(self):
        return self

    def __exit__(self, ext_typ, exc_value, traceback):
        pass # do stuff

    def __str__(self):
        return self._str

    def __repr__(self):
        return repr(self._str)

    def __eq__(self, lvalue):
        return lvalue == self._str

    def str(self):
        '''pretend to "convert to a str"'''
        return self._str

astr = StrBuiltin('Eggs&spam')

if isinstance( astr.str(), str):
    print 'Is like a str.'
else:
    print 'Is not like a str.'

I know you didn't want to do str(MyClass) but MyClass.str() kind of implies, to me, that this class is expected to expose itself as a str to functions which expect a str as part of the object. Instead of some unexpected result of "who know's what would be returned by str( SomeObject ).

DevPlayer
  • 5,393
  • 1
  • 25
  • 20
1

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
  • 384,516
  • 81
  • 508
  • 779
  • The `as` variable is **always** available after the with statement, actually. :-) – kindall Oct 13 '10 at 22:50
  • @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 Oct 13 '10 at 22:52
  • 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 Oct 13 '10 at 23:18
  • "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 Oct 13 '10 at 23:19
  • 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 Oct 13 '10 at 23:50
  • "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 Oct 14 '10 at 00:09
  • 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 Oct 14 '10 at 01:56
  • 1
    @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 Oct 14 '10 at 02:12
0

This is an old question but is an interesting one. Using the idea from @S.Lott you can use contextmanagers to create a more robust and reusable tool:

@contextmanager
def redefine_print(stream):
    global print
    from functools import partial, wraps
    old_print = print
    try:
        print = wraps(print)(partial(print, file=stream))
        yield print
    finally:
        print = old_print

sample use with file-like objects:

with open('file', 'a+') as stream:
    print('a')       # print in the interface
    with redefine_print(stream):
        print('b')   # print in the file 
    print('c')       # print in the interface
    stream.seek(0)
    print(stream.readlines())

sample use with StringIO objects

import io
stream = io.StringIO()
with redefine_print(stream) as xprint:
    print('b')   # add to the ioStream
    xprint('x')   # same as print, just to see how the object works

print(stream.getvalue())   # print the intercepted value
print(xprint.__doc__)      # see how @wraps helps to keep print() signature
PyGuy
  • 434
  • 5
  • 15