##// END OF EJS Templates
Rework atomic_writing with tests & docstring
Thomas Kluyver -
Show More
@@ -313,7 +313,7 b' class FileContentsManager(ContentsManager):'
313 bcontent = base64.decodestring(b64_bytes)
313 bcontent = base64.decodestring(b64_bytes)
314 except Exception as e:
314 except Exception as e:
315 raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
315 raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
316 with atomic_writing(os_path, 'wb') as f:
316 with atomic_writing(os_path, text=False) as f:
317 f.write(bcontent)
317 f.write(bcontent)
318
318
319 def _save_directory(self, os_path, model, name='', path=''):
319 def _save_directory(self, os_path, model, name='', path=''):
@@ -17,6 +17,7 b' from __future__ import absolute_import'
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 import codecs
18 import codecs
19 from contextlib import contextmanager
19 from contextlib import contextmanager
20 import io
20 import os
21 import os
21 import sys
22 import sys
22 import tempfile
23 import tempfile
@@ -219,21 +220,55 b" def temp_pyfile(src, ext='.py'):"
219 return fname, f
220 return fname, f
220
221
221 @contextmanager
222 @contextmanager
222 def atomic_writing(path, mode='w', encoding='utf-8', **kwargs):
223 def atomic_writing(path, text=True, encoding='utf-8', **kwargs):
223 tmp_file = path + '.tmp-write'
224 """Context manager to write to a file only if the entire write is successful.
224 if 'b' in mode:
225
225 encoding = None
226 This works by creating a temporary file in the same directory, and renaming
226
227 it over the old file if the context is exited without an error. If the
227 with open(tmp_file, mode, encoding=encoding, **kwargs) as f:
228 target file is a symlink or a hardlink, this will not be preserved: it will
228 yield f
229 be replaced by a new regular file.
229
230
230 # Written successfully, now rename it
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
233 unavoidable.
234
235 Parameters
236 ----------
237 path : str
238 The target file to write to.
239
240 text : bool, optional
241 Whether to open the file in text mode (i.e. to write unicode). Default is
242 True.
243
244 encoding : str, optional
245 The encoding to use for files opened in text mode. Default is UTF-8.
246
247 **kwargs
248 Passed to :func:`io.open`.
249 """
250 dirname, basename = os.path.split(path)
251 handle, tmp_path = tempfile.mkstemp(prefix=basename, dir=dirname, text=text)
252 if text:
253 fileobj = io.open(handle, 'w', encoding=encoding, **kwargs)
254 else:
255 fileobj = io.open(handle, 'wb', **kwargs)
256
257 try:
258 yield fileobj
259 except:
260 fileobj.close()
261 os.remove(tmp_path)
262 raise
263 else:
264 # Written successfully, now rename it
265 fileobj.close()
231
266
232 if os.name == 'nt' and os.path.exists(path):
267 if os.name == 'nt' and os.path.exists(path):
233 # Rename over existing file doesn't work on Windows
268 # Rename over existing file doesn't work on Windows
234 os.remove(path)
269 os.remove(path)
235
270
236 os.rename(tmp_file, path)
271 os.rename(tmp_path, path)
237
272
238
273
239 def raw_print(*args, **kw):
274 def raw_print(*args, **kw):
@@ -15,6 +15,7 b' 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 sys
19 import sys
19
20
20 from subprocess import Popen, PIPE
21 from subprocess import Popen, PIPE
@@ -23,8 +24,11 b' import unittest'
23 import nose.tools as nt
24 import nose.tools as nt
24
25
25 from IPython.testing.decorators import skipif
26 from IPython.testing.decorators import skipif
26 from IPython.utils.io import Tee, capture_output, unicode_std_stream
27 from IPython.utils.io import (Tee, capture_output, unicode_std_stream,
28 atomic_writing,
29 )
27 from IPython.utils.py3compat import doctest_refactor_print, PY3
30 from IPython.utils.py3compat import doctest_refactor_print, PY3
31 from IPython.utils.tempdir import TemporaryDirectory
28
32
29 if PY3:
33 if PY3:
30 from io import StringIO
34 from io import StringIO
@@ -121,4 +125,27 b' def test_UnicodeStdStream_nowrap():'
121 nt.assert_is(unicode_std_stream(), sys.stdout)
125 nt.assert_is(unicode_std_stream(), sys.stdout)
122 assert not sys.stdout.closed
126 assert not sys.stdout.closed
123 finally:
127 finally:
124 sys.stdout = orig_stdout No newline at end of file
128 sys.stdout = orig_stdout
129
130 def test_atomic_writing():
131 class CustomExc(Exception): pass
132
133 with TemporaryDirectory() as td:
134 f1 = os.path.join(td, 'penguin')
135 with stdlib_io.open(f1, 'w') as f:
136 f.write(u'Before')
137
138 with nt.assert_raises(CustomExc):
139 with atomic_writing(f1) as f:
140 f.write(u'Failing write')
141 raise CustomExc
142
143 # Because of the exception, the file should not have been modified
144 with stdlib_io.open(f1, 'r') as f:
145 nt.assert_equal(f.read(), u'Before')
146
147 with atomic_writing(f1) as f:
148 f.write(u'Overwritten')
149
150 with stdlib_io.open(f1, 'r') as f:
151 nt.assert_equal(f.read(), u'Overwritten')
General Comments 0
You need to be logged in to leave comments. Login now