##// END OF EJS Templates
Copy file metadata in atomic save...
Thomas Kluyver -
Show More
@@ -1,319 +1,339 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 from __future__ import absolute_import
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 import codecs
18 import codecs
19 from contextlib import contextmanager
19 from contextlib import contextmanager
20 import io
20 import io
21 import os
21 import os
22 import shutil
23 import stat
22 import sys
24 import sys
23 import tempfile
25 import tempfile
24 from .capture import CapturedIO, capture_output
26 from .capture import CapturedIO, capture_output
25 from .py3compat import string_types, input, PY3
27 from .py3compat import string_types, input, PY3
26
28
27 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
28 # Code
30 # Code
29 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
30
32
31
33
32 class IOStream:
34 class IOStream:
33
35
34 def __init__(self,stream, fallback=None):
36 def __init__(self,stream, fallback=None):
35 if not hasattr(stream,'write') or not hasattr(stream,'flush'):
37 if not hasattr(stream,'write') or not hasattr(stream,'flush'):
36 if fallback is not None:
38 if fallback is not None:
37 stream = fallback
39 stream = fallback
38 else:
40 else:
39 raise ValueError("fallback required, but not specified")
41 raise ValueError("fallback required, but not specified")
40 self.stream = stream
42 self.stream = stream
41 self._swrite = stream.write
43 self._swrite = stream.write
42
44
43 # clone all methods not overridden:
45 # clone all methods not overridden:
44 def clone(meth):
46 def clone(meth):
45 return not hasattr(self, meth) and not meth.startswith('_')
47 return not hasattr(self, meth) and not meth.startswith('_')
46 for meth in filter(clone, dir(stream)):
48 for meth in filter(clone, dir(stream)):
47 setattr(self, meth, getattr(stream, meth))
49 setattr(self, meth, getattr(stream, meth))
48
50
49 def __repr__(self):
51 def __repr__(self):
50 cls = self.__class__
52 cls = self.__class__
51 tpl = '{mod}.{cls}({args})'
53 tpl = '{mod}.{cls}({args})'
52 return tpl.format(mod=cls.__module__, cls=cls.__name__, args=self.stream)
54 return tpl.format(mod=cls.__module__, cls=cls.__name__, args=self.stream)
53
55
54 def write(self,data):
56 def write(self,data):
55 try:
57 try:
56 self._swrite(data)
58 self._swrite(data)
57 except:
59 except:
58 try:
60 try:
59 # print handles some unicode issues which may trip a plain
61 # print handles some unicode issues which may trip a plain
60 # write() call. Emulate write() by using an empty end
62 # write() call. Emulate write() by using an empty end
61 # argument.
63 # argument.
62 print(data, end='', file=self.stream)
64 print(data, end='', file=self.stream)
63 except:
65 except:
64 # if we get here, something is seriously broken.
66 # if we get here, something is seriously broken.
65 print('ERROR - failed to write data to stream:', self.stream,
67 print('ERROR - failed to write data to stream:', self.stream,
66 file=sys.stderr)
68 file=sys.stderr)
67
69
68 def writelines(self, lines):
70 def writelines(self, lines):
69 if isinstance(lines, string_types):
71 if isinstance(lines, string_types):
70 lines = [lines]
72 lines = [lines]
71 for line in lines:
73 for line in lines:
72 self.write(line)
74 self.write(line)
73
75
74 # This class used to have a writeln method, but regular files and streams
76 # This class used to have a writeln method, but regular files and streams
75 # in Python don't have this method. We need to keep this completely
77 # in Python don't have this method. We need to keep this completely
76 # compatible so we removed it.
78 # compatible so we removed it.
77
79
78 @property
80 @property
79 def closed(self):
81 def closed(self):
80 return self.stream.closed
82 return self.stream.closed
81
83
82 def close(self):
84 def close(self):
83 pass
85 pass
84
86
85 # setup stdin/stdout/stderr to sys.stdin/sys.stdout/sys.stderr
87 # setup stdin/stdout/stderr to sys.stdin/sys.stdout/sys.stderr
86 devnull = open(os.devnull, 'w')
88 devnull = open(os.devnull, 'w')
87 stdin = IOStream(sys.stdin, fallback=devnull)
89 stdin = IOStream(sys.stdin, fallback=devnull)
88 stdout = IOStream(sys.stdout, fallback=devnull)
90 stdout = IOStream(sys.stdout, fallback=devnull)
89 stderr = IOStream(sys.stderr, fallback=devnull)
91 stderr = IOStream(sys.stderr, fallback=devnull)
90
92
91 class IOTerm:
93 class IOTerm:
92 """ Term holds the file or file-like objects for handling I/O operations.
94 """ Term holds the file or file-like objects for handling I/O operations.
93
95
94 These are normally just sys.stdin, sys.stdout and sys.stderr but for
96 These are normally just sys.stdin, sys.stdout and sys.stderr but for
95 Windows they can can replaced to allow editing the strings before they are
97 Windows they can can replaced to allow editing the strings before they are
96 displayed."""
98 displayed."""
97
99
98 # In the future, having IPython channel all its I/O operations through
100 # In the future, having IPython channel all its I/O operations through
99 # this class will make it easier to embed it into other environments which
101 # this class will make it easier to embed it into other environments which
100 # are not a normal terminal (such as a GUI-based shell)
102 # are not a normal terminal (such as a GUI-based shell)
101 def __init__(self, stdin=None, stdout=None, stderr=None):
103 def __init__(self, stdin=None, stdout=None, stderr=None):
102 mymodule = sys.modules[__name__]
104 mymodule = sys.modules[__name__]
103 self.stdin = IOStream(stdin, mymodule.stdin)
105 self.stdin = IOStream(stdin, mymodule.stdin)
104 self.stdout = IOStream(stdout, mymodule.stdout)
106 self.stdout = IOStream(stdout, mymodule.stdout)
105 self.stderr = IOStream(stderr, mymodule.stderr)
107 self.stderr = IOStream(stderr, mymodule.stderr)
106
108
107
109
108 class Tee(object):
110 class Tee(object):
109 """A class to duplicate an output stream to stdout/err.
111 """A class to duplicate an output stream to stdout/err.
110
112
111 This works in a manner very similar to the Unix 'tee' command.
113 This works in a manner very similar to the Unix 'tee' command.
112
114
113 When the object is closed or deleted, it closes the original file given to
115 When the object is closed or deleted, it closes the original file given to
114 it for duplication.
116 it for duplication.
115 """
117 """
116 # Inspired by:
118 # Inspired by:
117 # http://mail.python.org/pipermail/python-list/2007-May/442737.html
119 # http://mail.python.org/pipermail/python-list/2007-May/442737.html
118
120
119 def __init__(self, file_or_name, mode="w", channel='stdout'):
121 def __init__(self, file_or_name, mode="w", channel='stdout'):
120 """Construct a new Tee object.
122 """Construct a new Tee object.
121
123
122 Parameters
124 Parameters
123 ----------
125 ----------
124 file_or_name : filename or open filehandle (writable)
126 file_or_name : filename or open filehandle (writable)
125 File that will be duplicated
127 File that will be duplicated
126
128
127 mode : optional, valid mode for open().
129 mode : optional, valid mode for open().
128 If a filename was give, open with this mode.
130 If a filename was give, open with this mode.
129
131
130 channel : str, one of ['stdout', 'stderr']
132 channel : str, one of ['stdout', 'stderr']
131 """
133 """
132 if channel not in ['stdout', 'stderr']:
134 if channel not in ['stdout', 'stderr']:
133 raise ValueError('Invalid channel spec %s' % channel)
135 raise ValueError('Invalid channel spec %s' % channel)
134
136
135 if hasattr(file_or_name, 'write') and hasattr(file_or_name, 'seek'):
137 if hasattr(file_or_name, 'write') and hasattr(file_or_name, 'seek'):
136 self.file = file_or_name
138 self.file = file_or_name
137 else:
139 else:
138 self.file = open(file_or_name, mode)
140 self.file = open(file_or_name, mode)
139 self.channel = channel
141 self.channel = channel
140 self.ostream = getattr(sys, channel)
142 self.ostream = getattr(sys, channel)
141 setattr(sys, channel, self)
143 setattr(sys, channel, self)
142 self._closed = False
144 self._closed = False
143
145
144 def close(self):
146 def close(self):
145 """Close the file and restore the channel."""
147 """Close the file and restore the channel."""
146 self.flush()
148 self.flush()
147 setattr(sys, self.channel, self.ostream)
149 setattr(sys, self.channel, self.ostream)
148 self.file.close()
150 self.file.close()
149 self._closed = True
151 self._closed = True
150
152
151 def write(self, data):
153 def write(self, data):
152 """Write data to both channels."""
154 """Write data to both channels."""
153 self.file.write(data)
155 self.file.write(data)
154 self.ostream.write(data)
156 self.ostream.write(data)
155 self.ostream.flush()
157 self.ostream.flush()
156
158
157 def flush(self):
159 def flush(self):
158 """Flush both channels."""
160 """Flush both channels."""
159 self.file.flush()
161 self.file.flush()
160 self.ostream.flush()
162 self.ostream.flush()
161
163
162 def __del__(self):
164 def __del__(self):
163 if not self._closed:
165 if not self._closed:
164 self.close()
166 self.close()
165
167
166
168
167 def ask_yes_no(prompt, default=None, interrupt=None):
169 def ask_yes_no(prompt, default=None, interrupt=None):
168 """Asks a question and returns a boolean (y/n) answer.
170 """Asks a question and returns a boolean (y/n) answer.
169
171
170 If default is given (one of 'y','n'), it is used if the user input is
172 If default is given (one of 'y','n'), it is used if the user input is
171 empty. If interrupt is given (one of 'y','n'), it is used if the user
173 empty. If interrupt is given (one of 'y','n'), it is used if the user
172 presses Ctrl-C. Otherwise the question is repeated until an answer is
174 presses Ctrl-C. Otherwise the question is repeated until an answer is
173 given.
175 given.
174
176
175 An EOF is treated as the default answer. If there is no default, an
177 An EOF is treated as the default answer. If there is no default, an
176 exception is raised to prevent infinite loops.
178 exception is raised to prevent infinite loops.
177
179
178 Valid answers are: y/yes/n/no (match is not case sensitive)."""
180 Valid answers are: y/yes/n/no (match is not case sensitive)."""
179
181
180 answers = {'y':True,'n':False,'yes':True,'no':False}
182 answers = {'y':True,'n':False,'yes':True,'no':False}
181 ans = None
183 ans = None
182 while ans not in answers.keys():
184 while ans not in answers.keys():
183 try:
185 try:
184 ans = input(prompt+' ').lower()
186 ans = input(prompt+' ').lower()
185 if not ans: # response was an empty string
187 if not ans: # response was an empty string
186 ans = default
188 ans = default
187 except KeyboardInterrupt:
189 except KeyboardInterrupt:
188 if interrupt:
190 if interrupt:
189 ans = interrupt
191 ans = interrupt
190 except EOFError:
192 except EOFError:
191 if default in answers.keys():
193 if default in answers.keys():
192 ans = default
194 ans = default
193 print()
195 print()
194 else:
196 else:
195 raise
197 raise
196
198
197 return answers[ans]
199 return answers[ans]
198
200
199
201
200 def temp_pyfile(src, ext='.py'):
202 def temp_pyfile(src, ext='.py'):
201 """Make a temporary python file, return filename and filehandle.
203 """Make a temporary python file, return filename and filehandle.
202
204
203 Parameters
205 Parameters
204 ----------
206 ----------
205 src : string or list of strings (no need for ending newlines if list)
207 src : string or list of strings (no need for ending newlines if list)
206 Source code to be written to the file.
208 Source code to be written to the file.
207
209
208 ext : optional, string
210 ext : optional, string
209 Extension for the generated file.
211 Extension for the generated file.
210
212
211 Returns
213 Returns
212 -------
214 -------
213 (filename, open filehandle)
215 (filename, open filehandle)
214 It is the caller's responsibility to close the open file and unlink it.
216 It is the caller's responsibility to close the open file and unlink it.
215 """
217 """
216 fname = tempfile.mkstemp(ext)[1]
218 fname = tempfile.mkstemp(ext)[1]
217 f = open(fname,'w')
219 f = open(fname,'w')
218 f.write(src)
220 f.write(src)
219 f.flush()
221 f.flush()
220 return fname, f
222 return fname, f
221
223
224 def _copy_metadata(src, dst):
225 """Copy the set of metadata we want for atomic_writing.
226
227 Permission bits and flags. We'd like to copy file ownership as well, but we
228 can't do that.
229 """
230 shutil.copymode(src, dst)
231 st = os.stat(src)
232 if hasattr(os, 'chflags') and hasattr(st, 'st_flags'):
233 os.chflags(st.st_flags)
234
222 @contextmanager
235 @contextmanager
223 def atomic_writing(path, text=True, encoding='utf-8', **kwargs):
236 def atomic_writing(path, text=True, encoding='utf-8', **kwargs):
224 """Context manager to write to a file only if the entire write is successful.
237 """Context manager to write to a file only if the entire write is successful.
225
238
226 This works by creating a temporary file in the same directory, and renaming
239 This works by creating a temporary file in the same directory, and renaming
227 it over the old file if the context is exited without an error. If the
240 it over the old file if the context is exited without an error. If the
228 target file is a symlink or a hardlink, this will not be preserved: it will
241 target file is a symlink or a hardlink, this will not be preserved: it will
229 be replaced by a new regular file.
242 be replaced by a new regular file.
230
243
231 On Windows, there is a small chink in the atomicity: the target file is
244 On Windows, there is a small chink in the atomicity: the target file is
232 deleted before renaming the temporary file over it. This appears to be
245 deleted before renaming the temporary file over it. This appears to be
233 unavoidable.
246 unavoidable.
234
247
235 Parameters
248 Parameters
236 ----------
249 ----------
237 path : str
250 path : str
238 The target file to write to.
251 The target file to write to.
239
252
240 text : bool, optional
253 text : bool, optional
241 Whether to open the file in text mode (i.e. to write unicode). Default is
254 Whether to open the file in text mode (i.e. to write unicode). Default is
242 True.
255 True.
243
256
244 encoding : str, optional
257 encoding : str, optional
245 The encoding to use for files opened in text mode. Default is UTF-8.
258 The encoding to use for files opened in text mode. Default is UTF-8.
246
259
247 **kwargs
260 **kwargs
248 Passed to :func:`io.open`.
261 Passed to :func:`io.open`.
249 """
262 """
250 dirname, basename = os.path.split(path)
263 dirname, basename = os.path.split(path)
251 handle, tmp_path = tempfile.mkstemp(prefix=basename, dir=dirname, text=text)
264 handle, tmp_path = tempfile.mkstemp(prefix=basename, dir=dirname, text=text)
252 if text:
265 if text:
253 fileobj = io.open(handle, 'w', encoding=encoding, **kwargs)
266 fileobj = io.open(handle, 'w', encoding=encoding, **kwargs)
254 else:
267 else:
255 fileobj = io.open(handle, 'wb', **kwargs)
268 fileobj = io.open(handle, 'wb', **kwargs)
256
269
257 try:
270 try:
258 yield fileobj
271 yield fileobj
259 except:
272 except:
260 fileobj.close()
273 fileobj.close()
261 os.remove(tmp_path)
274 os.remove(tmp_path)
262 raise
275 raise
263
276
264 # Flush to disk
277 # Flush to disk
265 fileobj.flush()
278 fileobj.flush()
266 os.fsync(fileobj.fileno())
279 os.fsync(fileobj.fileno())
267
280
268 # Written successfully, now rename it
281 # Written successfully, now rename it
269 fileobj.close()
282 fileobj.close()
270
283
284 # Copy permission bits, access time, etc.
285 try:
286 _copy_metadata(path, tmp_path)
287 except OSError:
288 # e.g. the file didn't already exist. Ignore any failure to copy metadata
289 pass
290
271 if os.name == 'nt' and os.path.exists(path):
291 if os.name == 'nt' and os.path.exists(path):
272 # Rename over existing file doesn't work on Windows
292 # Rename over existing file doesn't work on Windows
273 os.remove(path)
293 os.remove(path)
274
294
275 os.rename(tmp_path, path)
295 os.rename(tmp_path, path)
276
296
277
297
278 def raw_print(*args, **kw):
298 def raw_print(*args, **kw):
279 """Raw print to sys.__stdout__, otherwise identical interface to print()."""
299 """Raw print to sys.__stdout__, otherwise identical interface to print()."""
280
300
281 print(*args, sep=kw.get('sep', ' '), end=kw.get('end', '\n'),
301 print(*args, sep=kw.get('sep', ' '), end=kw.get('end', '\n'),
282 file=sys.__stdout__)
302 file=sys.__stdout__)
283 sys.__stdout__.flush()
303 sys.__stdout__.flush()
284
304
285
305
286 def raw_print_err(*args, **kw):
306 def raw_print_err(*args, **kw):
287 """Raw print to sys.__stderr__, otherwise identical interface to print()."""
307 """Raw print to sys.__stderr__, otherwise identical interface to print()."""
288
308
289 print(*args, sep=kw.get('sep', ' '), end=kw.get('end', '\n'),
309 print(*args, sep=kw.get('sep', ' '), end=kw.get('end', '\n'),
290 file=sys.__stderr__)
310 file=sys.__stderr__)
291 sys.__stderr__.flush()
311 sys.__stderr__.flush()
292
312
293
313
294 # Short aliases for quick debugging, do NOT use these in production code.
314 # Short aliases for quick debugging, do NOT use these in production code.
295 rprint = raw_print
315 rprint = raw_print
296 rprinte = raw_print_err
316 rprinte = raw_print_err
297
317
298 def unicode_std_stream(stream='stdout'):
318 def unicode_std_stream(stream='stdout'):
299 u"""Get a wrapper to write unicode to stdout/stderr as UTF-8.
319 u"""Get a wrapper to write unicode to stdout/stderr as UTF-8.
300
320
301 This ignores environment variables and default encodings, to reliably write
321 This ignores environment variables and default encodings, to reliably write
302 unicode to stdout or stderr.
322 unicode to stdout or stderr.
303
323
304 ::
324 ::
305
325
306 unicode_std_stream().write(u'Ε‚@e¢ŧ←')
326 unicode_std_stream().write(u'Ε‚@e¢ŧ←')
307 """
327 """
308 assert stream in ('stdout', 'stderr')
328 assert stream in ('stdout', 'stderr')
309 stream = getattr(sys, stream)
329 stream = getattr(sys, stream)
310 if PY3:
330 if PY3:
311 try:
331 try:
312 stream_b = stream.buffer
332 stream_b = stream.buffer
313 except AttributeError:
333 except AttributeError:
314 # sys.stdout has been replaced - use it directly
334 # sys.stdout has been replaced - use it directly
315 return stream
335 return stream
316 else:
336 else:
317 stream_b = stream
337 stream_b = stream
318
338
319 return codecs.getwriter('utf-8')(stream_b)
339 return codecs.getwriter('utf-8')(stream_b)
@@ -1,151 +1,160 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 from __future__ import absolute_import
16
16
17 import io as stdlib_io
17 import io as stdlib_io
18 import os.path
18 import os.path
19 import stat
19 import sys
20 import sys
20
21
21 from subprocess import Popen, PIPE
22 from subprocess import Popen, PIPE
22 import unittest
23 import unittest
23
24
24 import nose.tools as nt
25 import nose.tools as nt
25
26
26 from IPython.testing.decorators import skipif
27 from IPython.testing.decorators import skipif
27 from IPython.utils.io import (Tee, capture_output, unicode_std_stream,
28 from IPython.utils.io import (Tee, capture_output, unicode_std_stream,
28 atomic_writing,
29 atomic_writing,
29 )
30 )
30 from IPython.utils.py3compat import doctest_refactor_print, PY3
31 from IPython.utils.py3compat import doctest_refactor_print, PY3
31 from IPython.utils.tempdir import TemporaryDirectory
32 from IPython.utils.tempdir import TemporaryDirectory
32
33
33 if PY3:
34 if PY3:
34 from io import StringIO
35 from io import StringIO
35 else:
36 else:
36 from StringIO import StringIO
37 from StringIO import StringIO
37
38
38 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
39 # Tests
40 # Tests
40 #-----------------------------------------------------------------------------
41 #-----------------------------------------------------------------------------
41
42
42
43
43 def test_tee_simple():
44 def test_tee_simple():
44 "Very simple check with stdout only"
45 "Very simple check with stdout only"
45 chan = StringIO()
46 chan = StringIO()
46 text = 'Hello'
47 text = 'Hello'
47 tee = Tee(chan, channel='stdout')
48 tee = Tee(chan, channel='stdout')
48 print(text, file=chan)
49 print(text, file=chan)
49 nt.assert_equal(chan.getvalue(), text+"\n")
50 nt.assert_equal(chan.getvalue(), text+"\n")
50
51
51
52
52 class TeeTestCase(unittest.TestCase):
53 class TeeTestCase(unittest.TestCase):
53
54
54 def tchan(self, channel, check='close'):
55 def tchan(self, channel, check='close'):
55 trap = StringIO()
56 trap = StringIO()
56 chan = StringIO()
57 chan = StringIO()
57 text = 'Hello'
58 text = 'Hello'
58
59
59 std_ori = getattr(sys, channel)
60 std_ori = getattr(sys, channel)
60 setattr(sys, channel, trap)
61 setattr(sys, channel, trap)
61
62
62 tee = Tee(chan, channel=channel)
63 tee = Tee(chan, channel=channel)
63 print(text, end='', file=chan)
64 print(text, end='', file=chan)
64 setattr(sys, channel, std_ori)
65 setattr(sys, channel, std_ori)
65 trap_val = trap.getvalue()
66 trap_val = trap.getvalue()
66 nt.assert_equal(chan.getvalue(), text)
67 nt.assert_equal(chan.getvalue(), text)
67 if check=='close':
68 if check=='close':
68 tee.close()
69 tee.close()
69 else:
70 else:
70 del tee
71 del tee
71
72
72 def test(self):
73 def test(self):
73 for chan in ['stdout', 'stderr']:
74 for chan in ['stdout', 'stderr']:
74 for check in ['close', 'del']:
75 for check in ['close', 'del']:
75 self.tchan(chan, check)
76 self.tchan(chan, check)
76
77
77 def test_io_init():
78 def test_io_init():
78 """Test that io.stdin/out/err exist at startup"""
79 """Test that io.stdin/out/err exist at startup"""
79 for name in ('stdin', 'stdout', 'stderr'):
80 for name in ('stdin', 'stdout', 'stderr'):
80 cmd = doctest_refactor_print("from IPython.utils import io;print io.%s.__class__"%name)
81 cmd = doctest_refactor_print("from IPython.utils import io;print io.%s.__class__"%name)
81 p = Popen([sys.executable, '-c', cmd],
82 p = Popen([sys.executable, '-c', cmd],
82 stdout=PIPE)
83 stdout=PIPE)
83 p.wait()
84 p.wait()
84 classname = p.stdout.read().strip().decode('ascii')
85 classname = p.stdout.read().strip().decode('ascii')
85 # __class__ is a reference to the class object in Python 3, so we can't
86 # __class__ is a reference to the class object in Python 3, so we can't
86 # just test for string equality.
87 # just test for string equality.
87 assert 'IPython.utils.io.IOStream' in classname, classname
88 assert 'IPython.utils.io.IOStream' in classname, classname
88
89
89 def test_capture_output():
90 def test_capture_output():
90 """capture_output() context works"""
91 """capture_output() context works"""
91
92
92 with capture_output() as io:
93 with capture_output() as io:
93 print('hi, stdout')
94 print('hi, stdout')
94 print('hi, stderr', file=sys.stderr)
95 print('hi, stderr', file=sys.stderr)
95
96
96 nt.assert_equal(io.stdout, 'hi, stdout\n')
97 nt.assert_equal(io.stdout, 'hi, stdout\n')
97 nt.assert_equal(io.stderr, 'hi, stderr\n')
98 nt.assert_equal(io.stderr, 'hi, stderr\n')
98
99
99 def test_UnicodeStdStream():
100 def test_UnicodeStdStream():
100 # Test wrapping a bytes-level stdout
101 # Test wrapping a bytes-level stdout
101 if PY3:
102 if PY3:
102 stdoutb = stdlib_io.BytesIO()
103 stdoutb = stdlib_io.BytesIO()
103 stdout = stdlib_io.TextIOWrapper(stdoutb, encoding='ascii')
104 stdout = stdlib_io.TextIOWrapper(stdoutb, encoding='ascii')
104 else:
105 else:
105 stdout = stdoutb = stdlib_io.BytesIO()
106 stdout = stdoutb = stdlib_io.BytesIO()
106
107
107 orig_stdout = sys.stdout
108 orig_stdout = sys.stdout
108 sys.stdout = stdout
109 sys.stdout = stdout
109 try:
110 try:
110 sample = u"@Ε‚e¢ŧ←"
111 sample = u"@Ε‚e¢ŧ←"
111 unicode_std_stream().write(sample)
112 unicode_std_stream().write(sample)
112
113
113 output = stdoutb.getvalue().decode('utf-8')
114 output = stdoutb.getvalue().decode('utf-8')
114 nt.assert_equal(output, sample)
115 nt.assert_equal(output, sample)
115 assert not stdout.closed
116 assert not stdout.closed
116 finally:
117 finally:
117 sys.stdout = orig_stdout
118 sys.stdout = orig_stdout
118
119
119 @skipif(not PY3, "Not applicable on Python 2")
120 @skipif(not PY3, "Not applicable on Python 2")
120 def test_UnicodeStdStream_nowrap():
121 def test_UnicodeStdStream_nowrap():
121 # If we replace stdout with a StringIO, it shouldn't get wrapped.
122 # If we replace stdout with a StringIO, it shouldn't get wrapped.
122 orig_stdout = sys.stdout
123 orig_stdout = sys.stdout
123 sys.stdout = StringIO()
124 sys.stdout = StringIO()
124 try:
125 try:
125 nt.assert_is(unicode_std_stream(), sys.stdout)
126 nt.assert_is(unicode_std_stream(), sys.stdout)
126 assert not sys.stdout.closed
127 assert not sys.stdout.closed
127 finally:
128 finally:
128 sys.stdout = orig_stdout
129 sys.stdout = orig_stdout
129
130
130 def test_atomic_writing():
131 def test_atomic_writing():
131 class CustomExc(Exception): pass
132 class CustomExc(Exception): pass
132
133
133 with TemporaryDirectory() as td:
134 with TemporaryDirectory() as td:
134 f1 = os.path.join(td, 'penguin')
135 f1 = os.path.join(td, 'penguin')
135 with stdlib_io.open(f1, 'w') as f:
136 with stdlib_io.open(f1, 'w') as f:
136 f.write(u'Before')
137 f.write(u'Before')
138
139 if os.name != 'nt':
140 os.chmod(f1, 0o701)
141 orig_mode = stat.S_IMODE(os.stat(f1).st_mode)
137
142
138 with nt.assert_raises(CustomExc):
143 with nt.assert_raises(CustomExc):
139 with atomic_writing(f1) as f:
144 with atomic_writing(f1) as f:
140 f.write(u'Failing write')
145 f.write(u'Failing write')
141 raise CustomExc
146 raise CustomExc
142
147
143 # Because of the exception, the file should not have been modified
148 # Because of the exception, the file should not have been modified
144 with stdlib_io.open(f1, 'r') as f:
149 with stdlib_io.open(f1, 'r') as f:
145 nt.assert_equal(f.read(), u'Before')
150 nt.assert_equal(f.read(), u'Before')
146
151
147 with atomic_writing(f1) as f:
152 with atomic_writing(f1) as f:
148 f.write(u'Overwritten')
153 f.write(u'Overwritten')
149
154
150 with stdlib_io.open(f1, 'r') as f:
155 with stdlib_io.open(f1, 'r') as f:
151 nt.assert_equal(f.read(), u'Overwritten')
156 nt.assert_equal(f.read(), u'Overwritten')
157
158 if os.name != 'nt':
159 mode = stat.S_IMODE(os.stat(f1).st_mode)
160 nt.assert_equal(mode, orig_mode)
General Comments 0
You need to be logged in to leave comments. Login now