##// END OF EJS Templates
Backport PR #6269: Implement atomic save...
MinRK -
Show More
@@ -26,6 +26,7 b' from tornado import web'
26
26
27 from .nbmanager import NotebookManager
27 from .nbmanager import NotebookManager
28 from IPython.nbformat import current
28 from IPython.nbformat import current
29 from IPython.utils.io import atomic_writing
29 from IPython.utils.traitlets import Unicode, Bool, TraitError
30 from IPython.utils.traitlets import Unicode, Bool, TraitError
30 from IPython.utils.py3compat import getcwd
31 from IPython.utils.py3compat import getcwd
31 from IPython.utils import tz
32 from IPython.utils import tz
@@ -304,7 +305,7 b' class FileNotebookManager(NotebookManager):'
304 nb['metadata']['name'] = u''
305 nb['metadata']['name'] = u''
305 try:
306 try:
306 self.log.debug("Autosaving notebook %s", os_path)
307 self.log.debug("Autosaving notebook %s", os_path)
307 with io.open(os_path, 'w', encoding='utf-8') as f:
308 with atomic_writing(os_path, encoding='utf-8') as f:
308 current.write(nb, f, u'json')
309 current.write(nb, f, u'json')
309 except Exception as e:
310 except Exception as e:
310 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
311 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
@@ -314,7 +315,7 b' class FileNotebookManager(NotebookManager):'
314 py_path = os.path.splitext(os_path)[0] + '.py'
315 py_path = os.path.splitext(os_path)[0] + '.py'
315 self.log.debug("Writing script %s", py_path)
316 self.log.debug("Writing script %s", py_path)
316 try:
317 try:
317 with io.open(py_path, 'w', encoding='utf-8') as f:
318 with atomic_writing(py_path, encoding='utf-8') as f:
318 current.write(nb, f, u'py')
319 current.write(nb, f, u'py')
319 except Exception as e:
320 except Exception as e:
320 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
321 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
@@ -16,6 +16,8 b' from __future__ import absolute_import'
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 import codecs
18 import codecs
19 from contextlib import contextmanager
20 import io
19 import os
21 import os
20 import sys
22 import sys
21 import tempfile
23 import tempfile
@@ -217,6 +219,61 b" def temp_pyfile(src, ext='.py'):"
217 f.flush()
219 f.flush()
218 return fname, f
220 return fname, f
219
221
222 @contextmanager
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
264 # Flush to disk
265 fileobj.flush()
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)
274
275 os.rename(tmp_path, path)
276
220
277
221 def raw_print(*args, **kw):
278 def raw_print(*args, **kw):
222 """Raw print to sys.__stdout__, otherwise identical interface to print()."""
279 """Raw print to sys.__stdout__, otherwise identical interface to print()."""
@@ -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