##// END OF EJS Templates
Rework atomic_writing with tests & docstring
Thomas Kluyver -
Show More
@@ -313,7 +313,7 b' class FileContentsManager(ContentsManager):'
313 313 bcontent = base64.decodestring(b64_bytes)
314 314 except Exception as e:
315 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 317 f.write(bcontent)
318 318
319 319 def _save_directory(self, os_path, model, name='', path=''):
@@ -17,6 +17,7 b' from __future__ import absolute_import'
17 17 #-----------------------------------------------------------------------------
18 18 import codecs
19 19 from contextlib import contextmanager
20 import io
20 21 import os
21 22 import sys
22 23 import tempfile
@@ -219,21 +220,55 b" def temp_pyfile(src, ext='.py'):"
219 220 return fname, f
220 221
221 222 @contextmanager
222 def atomic_writing(path, mode='w', encoding='utf-8', **kwargs):
223 tmp_file = path + '.tmp-write'
224 if 'b' in mode:
225 encoding = None
226
227 with open(tmp_file, mode, encoding=encoding, **kwargs) as f:
228 yield f
229
230 # Written successfully, now rename it
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.
225
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
228 target file is a symlink or a hardlink, this will not be preserved: it will
229 be replaced by a new regular file.
230
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):
233 # Rename over existing file doesn't work on Windows
234 os.remove(path)
267 if os.name == 'nt' and os.path.exists(path):
268 # Rename over existing file doesn't work on Windows
269 os.remove(path)
235 270
236 os.rename(tmp_file, path)
271 os.rename(tmp_path, path)
237 272
238 273
239 274 def raw_print(*args, **kw):
@@ -15,6 +15,7 b' from __future__ import print_function'
15 15 from __future__ import absolute_import
16 16
17 17 import io as stdlib_io
18 import os.path
18 19 import sys
19 20
20 21 from subprocess import Popen, PIPE
@@ -23,8 +24,11 b' import unittest'
23 24 import nose.tools as nt
24 25
25 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 30 from IPython.utils.py3compat import doctest_refactor_print, PY3
31 from IPython.utils.tempdir import TemporaryDirectory
28 32
29 33 if PY3:
30 34 from io import StringIO
@@ -121,4 +125,27 b' def test_UnicodeStdStream_nowrap():'
121 125 nt.assert_is(unicode_std_stream(), sys.stdout)
122 126 assert not sys.stdout.closed
123 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