From b93d2a87c36ecd7eaf3f0cb71772b6318a2396d1 2014-10-01 19:15:26 From: MinRK Date: 2014-10-01 19:15:26 Subject: [PATCH] Backport PR #6453: Copy file metadata in atomic save Closes gh-6405 --- diff --git a/IPython/utils/io.py b/IPython/utils/io.py index f9f22cb..1211b63 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. @@ -247,6 +260,7 @@ def atomic_writing(path, text=True, encoding='utf-8', **kwargs): **kwargs Passed to :func:`io.open`. """ + path = os.path.realpath(path) # Dereference symlinks dirname, basename = os.path.split(path) handle, tmp_path = tempfile.mkstemp(prefix=basename, dir=dirname, text=text) if text: @@ -268,6 +282,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..9374fa6 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 @@ -135,6 +136,17 @@ def test_atomic_writing(): 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) + + f2 = os.path.join(td, 'flamingo') + try: + os.symlink(f1, f2) + have_symlink = True + except (AttributeError, NotImplementedError): + have_symlink = False + with nt.assert_raises(CustomExc): with atomic_writing(f1) as f: f.write(u'Failing write') @@ -149,3 +161,15 @@ 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) + + if have_symlink: + # Check that writing over a file preserves a symlink + with atomic_writing(f2) as f: + f.write(u'written from symlink') + + with stdlib_io.open(f1, 'r') as f: + nt.assert_equal(f.read(), u'written from symlink') \ No newline at end of file