From 19e90fb3a336fd63f556289770ad1e9a1799e32f 2014-09-11 23:47:10 From: Thomas Kluyver Date: 2014-09-11 23:47:10 Subject: [PATCH] Copy file metadata in atomic save Closes gh-6405 --- diff --git a/IPython/utils/io.py b/IPython/utils/io.py index 7c28ca3..656b956 100644 --- a/IPython/utils/io.py +++ b/IPython/utils/io.py @@ -19,6 +19,8 @@ import codecs from contextlib import contextmanager import io import os +import shutil +import stat import sys import tempfile from .capture import CapturedIO, capture_output @@ -219,6 +221,17 @@ def temp_pyfile(src, ext='.py'): f.flush() return fname, f +def _copy_metadata(src, dst): + """Copy the set of metadata we want for atomic_writing. + + Permission bits and flags. We'd like to copy file ownership as well, but we + can't do that. + """ + shutil.copymode(src, dst) + st = os.stat(src) + if hasattr(os, 'chflags') and hasattr(st, 'st_flags'): + os.chflags(st.st_flags) + @contextmanager def atomic_writing(path, text=True, encoding='utf-8', **kwargs): """Context manager to write to a file only if the entire write is successful. @@ -268,6 +281,13 @@ def atomic_writing(path, text=True, encoding='utf-8', **kwargs): # Written successfully, now rename it fileobj.close() + # Copy permission bits, access time, etc. + try: + _copy_metadata(path, tmp_path) + except OSError: + # e.g. the file didn't already exist. Ignore any failure to copy metadata + pass + if os.name == 'nt' and os.path.exists(path): # Rename over existing file doesn't work on Windows os.remove(path) diff --git a/IPython/utils/tests/test_io.py b/IPython/utils/tests/test_io.py index bf4ed7d..87ccc36 100644 --- a/IPython/utils/tests/test_io.py +++ b/IPython/utils/tests/test_io.py @@ -16,6 +16,7 @@ from __future__ import absolute_import import io as stdlib_io import os.path +import stat import sys from subprocess import Popen, PIPE @@ -134,6 +135,10 @@ def test_atomic_writing(): f1 = os.path.join(td, 'penguin') with stdlib_io.open(f1, 'w') as f: f.write(u'Before') + + if os.name != 'nt': + os.chmod(f1, 0o701) + orig_mode = stat.S_IMODE(os.stat(f1).st_mode) with nt.assert_raises(CustomExc): with atomic_writing(f1) as f: @@ -149,3 +154,7 @@ def test_atomic_writing(): with stdlib_io.open(f1, 'r') as f: nt.assert_equal(f.read(), u'Overwritten') + + if os.name != 'nt': + mode = stat.S_IMODE(os.stat(f1).st_mode) + nt.assert_equal(mode, orig_mode)