Reputation: 184405
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?
Upvotes: 3
Views: 5272
Reputation: 195
The __new__
method in a class is what initializes the class being constructed and it can be used to get the class to return itself as a string.
class instance_of_str(str):
def __init__(self, string: str):
self._string = string
def __new__(cls, string):
return str.__new__(cls, string)
def __enter__(self):
return self
def __exit__(self, ext_typ, exc_value, traceback):
pass # do stuff
# def __str__(self):
# return self._string
# def __repr__(self):
# return repr(self._string)
def __eq__(self, value):
return value == self._string
with instance_of_str('string') as instance:
if isinstance(instance, str):
print('Is an instance of object type str.')
print(instance, repr(instance))
else:
print('Is not an instance of object type str.')
print(instance, repr(instance))
Output:
>>> Is an instance of object type str.
>>> string 'string'
Upvotes: 0
Reputation: 551
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
Upvotes: 0
Reputation: 184405
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']
Upvotes: 2
Reputation: 5599
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
Upvotes: 4
Reputation: 5599
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 ).
Upvotes: 1
Reputation: 392010
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.
Upvotes: 1
Reputation: 880797
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'
Upvotes: 2