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')