##// END OF EJS Templates
Add unicode_std_stream function to write UTF-8 to stdout/stderr
Thomas Kluyver -
Show More
@@ -1,229 +1,254 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 IO related utilities.
3 IO related utilities.
4 """
4 """
5
5
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7 # Copyright (C) 2008-2011 The IPython Development Team
7 # Copyright (C) 2008-2011 The IPython Development Team
8 #
8 #
9 # Distributed under the terms of the BSD License. The full license is in
9 # Distributed under the terms of the BSD License. The full license is in
10 # the file COPYING, distributed as part of this software.
10 # the file COPYING, distributed as part of this software.
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 from __future__ import print_function
12 from __future__ import print_function
13 from __future__ import absolute_import
13
14
14 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
15 # Imports
16 # Imports
16 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 import codecs
17 import os
19 import os
18 import sys
20 import sys
19 import tempfile
21 import tempfile
20 from .capture import CapturedIO, capture_output
22 from .capture import CapturedIO, capture_output
21 from .py3compat import string_types, input
23 from .py3compat import string_types, input, PY3
22
24
23 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
24 # Code
26 # Code
25 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
26
28
27
29
28 class IOStream:
30 class IOStream:
29
31
30 def __init__(self,stream, fallback=None):
32 def __init__(self,stream, fallback=None):
31 if not hasattr(stream,'write') or not hasattr(stream,'flush'):
33 if not hasattr(stream,'write') or not hasattr(stream,'flush'):
32 if fallback is not None:
34 if fallback is not None:
33 stream = fallback
35 stream = fallback
34 else:
36 else:
35 raise ValueError("fallback required, but not specified")
37 raise ValueError("fallback required, but not specified")
36 self.stream = stream
38 self.stream = stream
37 self._swrite = stream.write
39 self._swrite = stream.write
38
40
39 # clone all methods not overridden:
41 # clone all methods not overridden:
40 def clone(meth):
42 def clone(meth):
41 return not hasattr(self, meth) and not meth.startswith('_')
43 return not hasattr(self, meth) and not meth.startswith('_')
42 for meth in filter(clone, dir(stream)):
44 for meth in filter(clone, dir(stream)):
43 setattr(self, meth, getattr(stream, meth))
45 setattr(self, meth, getattr(stream, meth))
44
46
45 def write(self,data):
47 def write(self,data):
46 try:
48 try:
47 self._swrite(data)
49 self._swrite(data)
48 except:
50 except:
49 try:
51 try:
50 # print handles some unicode issues which may trip a plain
52 # print handles some unicode issues which may trip a plain
51 # write() call. Emulate write() by using an empty end
53 # write() call. Emulate write() by using an empty end
52 # argument.
54 # argument.
53 print(data, end='', file=self.stream)
55 print(data, end='', file=self.stream)
54 except:
56 except:
55 # if we get here, something is seriously broken.
57 # if we get here, something is seriously broken.
56 print('ERROR - failed to write data to stream:', self.stream,
58 print('ERROR - failed to write data to stream:', self.stream,
57 file=sys.stderr)
59 file=sys.stderr)
58
60
59 def writelines(self, lines):
61 def writelines(self, lines):
60 if isinstance(lines, string_types):
62 if isinstance(lines, string_types):
61 lines = [lines]
63 lines = [lines]
62 for line in lines:
64 for line in lines:
63 self.write(line)
65 self.write(line)
64
66
65 # This class used to have a writeln method, but regular files and streams
67 # This class used to have a writeln method, but regular files and streams
66 # in Python don't have this method. We need to keep this completely
68 # in Python don't have this method. We need to keep this completely
67 # compatible so we removed it.
69 # compatible so we removed it.
68
70
69 @property
71 @property
70 def closed(self):
72 def closed(self):
71 return self.stream.closed
73 return self.stream.closed
72
74
73 def close(self):
75 def close(self):
74 pass
76 pass
75
77
76 # setup stdin/stdout/stderr to sys.stdin/sys.stdout/sys.stderr
78 # setup stdin/stdout/stderr to sys.stdin/sys.stdout/sys.stderr
77 devnull = open(os.devnull, 'a')
79 devnull = open(os.devnull, 'a')
78 stdin = IOStream(sys.stdin, fallback=devnull)
80 stdin = IOStream(sys.stdin, fallback=devnull)
79 stdout = IOStream(sys.stdout, fallback=devnull)
81 stdout = IOStream(sys.stdout, fallback=devnull)
80 stderr = IOStream(sys.stderr, fallback=devnull)
82 stderr = IOStream(sys.stderr, fallback=devnull)
81
83
82 class IOTerm:
84 class IOTerm:
83 """ Term holds the file or file-like objects for handling I/O operations.
85 """ Term holds the file or file-like objects for handling I/O operations.
84
86
85 These are normally just sys.stdin, sys.stdout and sys.stderr but for
87 These are normally just sys.stdin, sys.stdout and sys.stderr but for
86 Windows they can can replaced to allow editing the strings before they are
88 Windows they can can replaced to allow editing the strings before they are
87 displayed."""
89 displayed."""
88
90
89 # In the future, having IPython channel all its I/O operations through
91 # In the future, having IPython channel all its I/O operations through
90 # this class will make it easier to embed it into other environments which
92 # this class will make it easier to embed it into other environments which
91 # are not a normal terminal (such as a GUI-based shell)
93 # are not a normal terminal (such as a GUI-based shell)
92 def __init__(self, stdin=None, stdout=None, stderr=None):
94 def __init__(self, stdin=None, stdout=None, stderr=None):
93 mymodule = sys.modules[__name__]
95 mymodule = sys.modules[__name__]
94 self.stdin = IOStream(stdin, mymodule.stdin)
96 self.stdin = IOStream(stdin, mymodule.stdin)
95 self.stdout = IOStream(stdout, mymodule.stdout)
97 self.stdout = IOStream(stdout, mymodule.stdout)
96 self.stderr = IOStream(stderr, mymodule.stderr)
98 self.stderr = IOStream(stderr, mymodule.stderr)
97
99
98
100
99 class Tee(object):
101 class Tee(object):
100 """A class to duplicate an output stream to stdout/err.
102 """A class to duplicate an output stream to stdout/err.
101
103
102 This works in a manner very similar to the Unix 'tee' command.
104 This works in a manner very similar to the Unix 'tee' command.
103
105
104 When the object is closed or deleted, it closes the original file given to
106 When the object is closed or deleted, it closes the original file given to
105 it for duplication.
107 it for duplication.
106 """
108 """
107 # Inspired by:
109 # Inspired by:
108 # http://mail.python.org/pipermail/python-list/2007-May/442737.html
110 # http://mail.python.org/pipermail/python-list/2007-May/442737.html
109
111
110 def __init__(self, file_or_name, mode="w", channel='stdout'):
112 def __init__(self, file_or_name, mode="w", channel='stdout'):
111 """Construct a new Tee object.
113 """Construct a new Tee object.
112
114
113 Parameters
115 Parameters
114 ----------
116 ----------
115 file_or_name : filename or open filehandle (writable)
117 file_or_name : filename or open filehandle (writable)
116 File that will be duplicated
118 File that will be duplicated
117
119
118 mode : optional, valid mode for open().
120 mode : optional, valid mode for open().
119 If a filename was give, open with this mode.
121 If a filename was give, open with this mode.
120
122
121 channel : str, one of ['stdout', 'stderr']
123 channel : str, one of ['stdout', 'stderr']
122 """
124 """
123 if channel not in ['stdout', 'stderr']:
125 if channel not in ['stdout', 'stderr']:
124 raise ValueError('Invalid channel spec %s' % channel)
126 raise ValueError('Invalid channel spec %s' % channel)
125
127
126 if hasattr(file_or_name, 'write') and hasattr(file_or_name, 'seek'):
128 if hasattr(file_or_name, 'write') and hasattr(file_or_name, 'seek'):
127 self.file = file_or_name
129 self.file = file_or_name
128 else:
130 else:
129 self.file = open(file_or_name, mode)
131 self.file = open(file_or_name, mode)
130 self.channel = channel
132 self.channel = channel
131 self.ostream = getattr(sys, channel)
133 self.ostream = getattr(sys, channel)
132 setattr(sys, channel, self)
134 setattr(sys, channel, self)
133 self._closed = False
135 self._closed = False
134
136
135 def close(self):
137 def close(self):
136 """Close the file and restore the channel."""
138 """Close the file and restore the channel."""
137 self.flush()
139 self.flush()
138 setattr(sys, self.channel, self.ostream)
140 setattr(sys, self.channel, self.ostream)
139 self.file.close()
141 self.file.close()
140 self._closed = True
142 self._closed = True
141
143
142 def write(self, data):
144 def write(self, data):
143 """Write data to both channels."""
145 """Write data to both channels."""
144 self.file.write(data)
146 self.file.write(data)
145 self.ostream.write(data)
147 self.ostream.write(data)
146 self.ostream.flush()
148 self.ostream.flush()
147
149
148 def flush(self):
150 def flush(self):
149 """Flush both channels."""
151 """Flush both channels."""
150 self.file.flush()
152 self.file.flush()
151 self.ostream.flush()
153 self.ostream.flush()
152
154
153 def __del__(self):
155 def __del__(self):
154 if not self._closed:
156 if not self._closed:
155 self.close()
157 self.close()
156
158
157
159
158 def ask_yes_no(prompt,default=None):
160 def ask_yes_no(prompt,default=None):
159 """Asks a question and returns a boolean (y/n) answer.
161 """Asks a question and returns a boolean (y/n) answer.
160
162
161 If default is given (one of 'y','n'), it is used if the user input is
163 If default is given (one of 'y','n'), it is used if the user input is
162 empty. Otherwise the question is repeated until an answer is given.
164 empty. Otherwise the question is repeated until an answer is given.
163
165
164 An EOF is treated as the default answer. If there is no default, an
166 An EOF is treated as the default answer. If there is no default, an
165 exception is raised to prevent infinite loops.
167 exception is raised to prevent infinite loops.
166
168
167 Valid answers are: y/yes/n/no (match is not case sensitive)."""
169 Valid answers are: y/yes/n/no (match is not case sensitive)."""
168
170
169 answers = {'y':True,'n':False,'yes':True,'no':False}
171 answers = {'y':True,'n':False,'yes':True,'no':False}
170 ans = None
172 ans = None
171 while ans not in answers.keys():
173 while ans not in answers.keys():
172 try:
174 try:
173 ans = input(prompt+' ').lower()
175 ans = input(prompt+' ').lower()
174 if not ans: # response was an empty string
176 if not ans: # response was an empty string
175 ans = default
177 ans = default
176 except KeyboardInterrupt:
178 except KeyboardInterrupt:
177 pass
179 pass
178 except EOFError:
180 except EOFError:
179 if default in answers.keys():
181 if default in answers.keys():
180 ans = default
182 ans = default
181 print()
183 print()
182 else:
184 else:
183 raise
185 raise
184
186
185 return answers[ans]
187 return answers[ans]
186
188
187
189
188 def temp_pyfile(src, ext='.py'):
190 def temp_pyfile(src, ext='.py'):
189 """Make a temporary python file, return filename and filehandle.
191 """Make a temporary python file, return filename and filehandle.
190
192
191 Parameters
193 Parameters
192 ----------
194 ----------
193 src : string or list of strings (no need for ending newlines if list)
195 src : string or list of strings (no need for ending newlines if list)
194 Source code to be written to the file.
196 Source code to be written to the file.
195
197
196 ext : optional, string
198 ext : optional, string
197 Extension for the generated file.
199 Extension for the generated file.
198
200
199 Returns
201 Returns
200 -------
202 -------
201 (filename, open filehandle)
203 (filename, open filehandle)
202 It is the caller's responsibility to close the open file and unlink it.
204 It is the caller's responsibility to close the open file and unlink it.
203 """
205 """
204 fname = tempfile.mkstemp(ext)[1]
206 fname = tempfile.mkstemp(ext)[1]
205 f = open(fname,'w')
207 f = open(fname,'w')
206 f.write(src)
208 f.write(src)
207 f.flush()
209 f.flush()
208 return fname, f
210 return fname, f
209
211
210
212
211 def raw_print(*args, **kw):
213 def raw_print(*args, **kw):
212 """Raw print to sys.__stdout__, otherwise identical interface to print()."""
214 """Raw print to sys.__stdout__, otherwise identical interface to print()."""
213
215
214 print(*args, sep=kw.get('sep', ' '), end=kw.get('end', '\n'),
216 print(*args, sep=kw.get('sep', ' '), end=kw.get('end', '\n'),
215 file=sys.__stdout__)
217 file=sys.__stdout__)
216 sys.__stdout__.flush()
218 sys.__stdout__.flush()
217
219
218
220
219 def raw_print_err(*args, **kw):
221 def raw_print_err(*args, **kw):
220 """Raw print to sys.__stderr__, otherwise identical interface to print()."""
222 """Raw print to sys.__stderr__, otherwise identical interface to print()."""
221
223
222 print(*args, sep=kw.get('sep', ' '), end=kw.get('end', '\n'),
224 print(*args, sep=kw.get('sep', ' '), end=kw.get('end', '\n'),
223 file=sys.__stderr__)
225 file=sys.__stderr__)
224 sys.__stderr__.flush()
226 sys.__stderr__.flush()
225
227
226
228
227 # Short aliases for quick debugging, do NOT use these in production code.
229 # Short aliases for quick debugging, do NOT use these in production code.
228 rprint = raw_print
230 rprint = raw_print
229 rprinte = raw_print_err
231 rprinte = raw_print_err
232
233 def unicode_std_stream(stream='stdout'):
234 """Get a wrapper to write unicode to stdout/stderr as UTF-8.
235
236 This ignores environment variables and default encodings, to reliably write
237 unicode to stdout or stderr.
238
239 ::
240
241 unicode_std_stream().write(u'ł@e¶ŧ←')
242 """
243 assert stream in ('stdout', 'stderr')
244 stream = getattr(sys, stream)
245 if PY3:
246 try:
247 stream_b = stream.buffer
248 except AttributeError:
249 # sys.stdout has been replaced - use it directly
250 return stream
251 else:
252 stream_b = stream
253
254 return codecs.getwriter('utf-8')(stream_b)
@@ -1,90 +1,124 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """Tests for io.py"""
2 """Tests for io.py"""
3
3
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (C) 2008-2011 The IPython Development Team
5 # Copyright (C) 2008-2011 The IPython Development Team
6 #
6 #
7 # Distributed under the terms of the BSD License. The full license is in
7 # Distributed under the terms of the BSD License. The full license is in
8 # the file COPYING, distributed as part of this software.
8 # the file COPYING, distributed as part of this software.
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Imports
12 # Imports
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 from __future__ import print_function
14 from __future__ import print_function
15 from __future__ import absolute_import
15
16
17 import io as stdlib_io
16 import sys
18 import sys
17
19
18 from subprocess import Popen, PIPE
20 from subprocess import Popen, PIPE
19 import unittest
21 import unittest
20
22
21 import nose.tools as nt
23 import nose.tools as nt
22
24
23 from IPython.utils.io import Tee, capture_output
25 from IPython.testing.decorators import skipif
26 from IPython.utils.io import Tee, capture_output, unicode_std_stream
24 from IPython.utils.py3compat import doctest_refactor_print, PY3
27 from IPython.utils.py3compat import doctest_refactor_print, PY3
25
28
26 if PY3:
29 if PY3:
27 from io import StringIO
30 from io import StringIO
28 else:
31 else:
29 from StringIO import StringIO
32 from StringIO import StringIO
30
33
31 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
32 # Tests
35 # Tests
33 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
34
37
35
38
36 def test_tee_simple():
39 def test_tee_simple():
37 "Very simple check with stdout only"
40 "Very simple check with stdout only"
38 chan = StringIO()
41 chan = StringIO()
39 text = 'Hello'
42 text = 'Hello'
40 tee = Tee(chan, channel='stdout')
43 tee = Tee(chan, channel='stdout')
41 print(text, file=chan)
44 print(text, file=chan)
42 nt.assert_equal(chan.getvalue(), text+"\n")
45 nt.assert_equal(chan.getvalue(), text+"\n")
43
46
44
47
45 class TeeTestCase(unittest.TestCase):
48 class TeeTestCase(unittest.TestCase):
46
49
47 def tchan(self, channel, check='close'):
50 def tchan(self, channel, check='close'):
48 trap = StringIO()
51 trap = StringIO()
49 chan = StringIO()
52 chan = StringIO()
50 text = 'Hello'
53 text = 'Hello'
51
54
52 std_ori = getattr(sys, channel)
55 std_ori = getattr(sys, channel)
53 setattr(sys, channel, trap)
56 setattr(sys, channel, trap)
54
57
55 tee = Tee(chan, channel=channel)
58 tee = Tee(chan, channel=channel)
56 print(text, end='', file=chan)
59 print(text, end='', file=chan)
57 setattr(sys, channel, std_ori)
60 setattr(sys, channel, std_ori)
58 trap_val = trap.getvalue()
61 trap_val = trap.getvalue()
59 nt.assert_equal(chan.getvalue(), text)
62 nt.assert_equal(chan.getvalue(), text)
60 if check=='close':
63 if check=='close':
61 tee.close()
64 tee.close()
62 else:
65 else:
63 del tee
66 del tee
64
67
65 def test(self):
68 def test(self):
66 for chan in ['stdout', 'stderr']:
69 for chan in ['stdout', 'stderr']:
67 for check in ['close', 'del']:
70 for check in ['close', 'del']:
68 self.tchan(chan, check)
71 self.tchan(chan, check)
69
72
70 def test_io_init():
73 def test_io_init():
71 """Test that io.stdin/out/err exist at startup"""
74 """Test that io.stdin/out/err exist at startup"""
72 for name in ('stdin', 'stdout', 'stderr'):
75 for name in ('stdin', 'stdout', 'stderr'):
73 cmd = doctest_refactor_print("from IPython.utils import io;print io.%s.__class__"%name)
76 cmd = doctest_refactor_print("from IPython.utils import io;print io.%s.__class__"%name)
74 p = Popen([sys.executable, '-c', cmd],
77 p = Popen([sys.executable, '-c', cmd],
75 stdout=PIPE)
78 stdout=PIPE)
76 p.wait()
79 p.wait()
77 classname = p.stdout.read().strip().decode('ascii')
80 classname = p.stdout.read().strip().decode('ascii')
78 # __class__ is a reference to the class object in Python 3, so we can't
81 # __class__ is a reference to the class object in Python 3, so we can't
79 # just test for string equality.
82 # just test for string equality.
80 assert 'IPython.utils.io.IOStream' in classname, classname
83 assert 'IPython.utils.io.IOStream' in classname, classname
81
84
82 def test_capture_output():
85 def test_capture_output():
83 """capture_output() context works"""
86 """capture_output() context works"""
84
87
85 with capture_output() as io:
88 with capture_output() as io:
86 print('hi, stdout')
89 print('hi, stdout')
87 print('hi, stderr', file=sys.stderr)
90 print('hi, stderr', file=sys.stderr)
88
91
89 nt.assert_equal(io.stdout, 'hi, stdout\n')
92 nt.assert_equal(io.stdout, 'hi, stdout\n')
90 nt.assert_equal(io.stderr, 'hi, stderr\n')
93 nt.assert_equal(io.stderr, 'hi, stderr\n')
94
95 def test_UnicodeStdStream():
96 # Test wrapping a bytes-level stdout
97 if PY3:
98 stdoutb = stdlib_io.BytesIO()
99 stdout = stdlib_io.TextIOWrapper(stdoutb, encoding='ascii')
100 else:
101 stdout = stdoutb = stdlib_io.BytesIO()
102
103 orig_stdout = sys.stdout
104 sys.stdout = stdout
105 try:
106 sample = u"@łe¶ŧ←"
107 unicode_std_stream().write(sample)
108
109 output = stdoutb.getvalue().decode('utf-8')
110 nt.assert_equal(output, sample)
111 assert not stdout.closed
112 finally:
113 sys.stdout = orig_stdout
114
115 @skipif(not PY3, "Not applicable on Python 2")
116 def test_UnicodeStdStream_nowrap():
117 # If we replace stdout with a StringIO, it shouldn't get wrapped.
118 orig_stdout = sys.stdout
119 sys.stdout = StringIO()
120 try:
121 nt.assert_is(unicode_std_stream(), sys.stdout)
122 assert not sys.stdout.closed
123 finally:
124 sys.stdout = orig_stdout No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now