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