diff --git a/IPython/html/services/contents/filemanager.py b/IPython/html/services/contents/filemanager.py index 59da30917..ef6cbdf78 100644 --- a/IPython/html/services/contents/filemanager.py +++ b/IPython/html/services/contents/filemanager.py @@ -13,6 +13,7 @@ from tornado import web from .manager import ContentsManager from IPython.nbformat import current +from IPython.utils.io import atomic_writing from IPython.utils.path import ensure_dir_exists from IPython.utils.traitlets import Unicode, Bool, TraitError from IPython.utils.py3compat import getcwd @@ -295,7 +296,7 @@ class FileContentsManager(ContentsManager): if 'name' in nb['metadata']: nb['metadata']['name'] = u'' - with io.open(os_path, 'w', encoding='utf-8') as f: + with atomic_writing(os_path, encoding='utf-8') as f: current.write(nb, f, u'json') def _save_file(self, os_path, model, name='', path=''): @@ -312,7 +313,7 @@ class FileContentsManager(ContentsManager): bcontent = base64.decodestring(b64_bytes) except Exception as e: raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e)) - with io.open(os_path, 'wb') as f: + with atomic_writing(os_path, text=False) as f: f.write(bcontent) def _save_directory(self, os_path, model, name='', path=''): diff --git a/IPython/utils/tests/test_io.py b/IPython/utils/tests/test_io.py index 9f28ba2cd..bf4ed7d72 100644 --- a/IPython/utils/tests/test_io.py +++ b/IPython/utils/tests/test_io.py @@ -15,6 +15,7 @@ from __future__ import print_function from __future__ import absolute_import import io as stdlib_io +import os.path import sys from subprocess import Popen, PIPE @@ -23,8 +24,11 @@ import unittest import nose.tools as nt from IPython.testing.decorators import skipif -from IPython.utils.io import Tee, capture_output, unicode_std_stream +from IPython.utils.io import (Tee, capture_output, unicode_std_stream, + atomic_writing, + ) from IPython.utils.py3compat import doctest_refactor_print, PY3 +from IPython.utils.tempdir import TemporaryDirectory if PY3: from io import StringIO @@ -121,4 +125,27 @@ def test_UnicodeStdStream_nowrap(): nt.assert_is(unicode_std_stream(), sys.stdout) assert not sys.stdout.closed finally: - sys.stdout = orig_stdout \ No newline at end of file + sys.stdout = orig_stdout + +def test_atomic_writing(): + class CustomExc(Exception): pass + + with TemporaryDirectory() as td: + f1 = os.path.join(td, 'penguin') + with stdlib_io.open(f1, 'w') as f: + f.write(u'Before') + + with nt.assert_raises(CustomExc): + with atomic_writing(f1) as f: + f.write(u'Failing write') + raise CustomExc + + # Because of the exception, the file should not have been modified + with stdlib_io.open(f1, 'r') as f: + nt.assert_equal(f.read(), u'Before') + + with atomic_writing(f1) as f: + f.write(u'Overwritten') + + with stdlib_io.open(f1, 'r') as f: + nt.assert_equal(f.read(), u'Overwritten')