Issue #12715: Add an optional symlinks argument to shutil functions (copyfile, copymode, copystat, copy, copy2).

When that parameter is true, symlinks aren't dereferenced and the operation
instead acts on the symlink itself (or creates one, if relevant).

Patch by Hynek Schlawack.
This commit is contained in:
Antoine Pitrou 2011-12-29 18:54:15 +01:00
parent d2f1db5355
commit 78091e63d6
4 changed files with 333 additions and 38 deletions

View File

@ -45,7 +45,7 @@ Directory and files operations
be copied.
.. function:: copyfile(src, dst)
.. function:: copyfile(src, dst[, symlinks=False])
Copy the contents (no metadata) of the file named *src* to a file named *dst*.
*dst* must be the complete target file name; look at :func:`copy` for a copy that
@ -56,37 +56,56 @@ Directory and files operations
such as character or block devices and pipes cannot be copied with this
function. *src* and *dst* are path names given as strings.
If *symlinks* is true and *src* is a symbolic link, a new symbolic link will
be created instead of copying the file *src* points to.
.. versionchanged:: 3.3
:exc:`IOError` used to be raised instead of :exc:`OSError`.
Added *symlinks* argument.
.. function:: copymode(src, dst)
.. function:: copymode(src, dst[, symlinks=False])
Copy the permission bits from *src* to *dst*. The file contents, owner, and
group are unaffected. *src* and *dst* are path names given as strings.
group are unaffected. *src* and *dst* are path names given as strings. If
*symlinks* is true, *src* a symbolic link and the operating system supports
modes for symbolic links (for example BSD-based ones), the mode of the link
will be copied.
.. versionchanged:: 3.3
Added *symlinks* argument.
.. function:: copystat(src, dst)
.. function:: copystat(src, dst[, symlinks=False])
Copy the permission bits, last access time, last modification time, and flags
from *src* to *dst*. The file contents, owner, and group are unaffected. *src*
and *dst* are path names given as strings.
and *dst* are path names given as strings. If *src* and *dst* are both
symbolic links and *symlinks* true, the stats of the link will be copied as
far as the platform allows.
.. versionchanged:: 3.3
Added *symlinks* argument.
.. function:: copy(src, dst)
.. function:: copy(src, dst[, symlinks=False]))
Copy the file *src* to the file or directory *dst*. If *dst* is a directory, a
file with the same basename as *src* is created (or overwritten) in the
directory specified. Permission bits are copied. *src* and *dst* are path
names given as strings.
names given as strings. If *symlinks* is true, symbolic links won't be
followed but recreated instead -- this resembles GNU's :program:`cp -P`.
.. versionchanged:: 3.3
Added *symlinks* argument.
.. function:: copy2(src, dst)
.. function:: copy2(src, dst[, symlinks=False])
Similar to :func:`copy`, but metadata is copied as well -- in fact, this is just
:func:`copy` followed by :func:`copystat`. This is similar to the
Unix command :program:`cp -p`.
Unix command :program:`cp -p`. If *symlinks* is true, symbolic links won't
be followed but recreated instead -- this resembles GNU's :program:`cp -P`.
.. versionchanged:: 3.3
Added *symlinks* argument.
.. function:: ignore_patterns(\*patterns)
@ -104,9 +123,9 @@ Directory and files operations
:func:`copy2`.
If *symlinks* is true, symbolic links in the source tree are represented as
symbolic links in the new tree, but the metadata of the original links is NOT
copied; if false or omitted, the contents and metadata of the linked files
are copied to the new tree.
symbolic links in the new tree and the metadata of the original links will
be copied as far as the platform allows; if false or omitted, the contents
and metadata of the linked files are copied to the new tree.
When *symlinks* is false, if the file pointed by the symlink doesn't
exist, a exception will be added in the list of errors raised in
@ -140,6 +159,9 @@ Directory and files operations
Added the *ignore_dangling_symlinks* argument to silent dangling symlinks
errors when *symlinks* is false.
.. versionchanged:: 3.3
Copy metadata when *symlinks* is false.
.. function:: rmtree(path, ignore_errors=False, onerror=None)

View File

@ -82,8 +82,13 @@ def _samefile(src, dst):
return (os.path.normcase(os.path.abspath(src)) ==
os.path.normcase(os.path.abspath(dst)))
def copyfile(src, dst):
"""Copy data from src to dst"""
def copyfile(src, dst, symlinks=False):
"""Copy data from src to dst.
If optional flag `symlinks` is set and `src` is a symbolic link, a new
symlink will be created instead of copying the file it points to.
"""
if _samefile(src, dst):
raise Error("`%s` and `%s` are the same file" % (src, dst))
@ -98,54 +103,94 @@ def copyfile(src, dst):
if stat.S_ISFIFO(st.st_mode):
raise SpecialFileError("`%s` is a named pipe" % fn)
with open(src, 'rb') as fsrc:
with open(dst, 'wb') as fdst:
copyfileobj(fsrc, fdst)
if symlinks and os.path.islink(src):
os.symlink(os.readlink(src), dst)
else:
with open(src, 'rb') as fsrc:
with open(dst, 'wb') as fdst:
copyfileobj(fsrc, fdst)
def copymode(src, dst):
"""Copy mode bits from src to dst"""
if hasattr(os, 'chmod'):
st = os.stat(src)
mode = stat.S_IMODE(st.st_mode)
os.chmod(dst, mode)
def copymode(src, dst, symlinks=False):
"""Copy mode bits from src to dst.
def copystat(src, dst):
"""Copy all stat info (mode bits, atime, mtime, flags) from src to dst"""
st = os.stat(src)
If the optional flag `symlinks` is set, symlinks aren't followed if and
only if both `src` and `dst` are symlinks. If `lchmod` isn't available (eg.
Linux), in these cases, this method does nothing.
"""
if symlinks and os.path.islink(src) and os.path.islink(dst):
if hasattr(os, 'lchmod'):
stat_func, chmod_func = os.lstat, os.lchmod
else:
return
elif hasattr(os, 'chmod'):
stat_func, chmod_func = os.stat, os.chmod
else:
return
st = stat_func(src)
chmod_func(dst, stat.S_IMODE(st.st_mode))
def copystat(src, dst, symlinks=False):
"""Copy all stat info (mode bits, atime, mtime, flags) from src to dst.
If the optional flag `symlinks` is set, symlinks aren't followed if and
only if both `src` and `dst` are symlinks.
"""
def _nop(*args):
pass
if symlinks and os.path.islink(src) and os.path.islink(dst):
stat_func = os.lstat
utime_func = os.lutimes if hasattr(os, 'lutimes') else _nop
chmod_func = os.lchmod if hasattr(os, 'lchmod') else _nop
chflags_func = os.lchflags if hasattr(os, 'lchflags') else _nop
else:
stat_func = os.stat
utime_func = os.utime if hasattr(os, 'utime') else _nop
chmod_func = os.chmod if hasattr(os, 'chmod') else _nop
chflags_func = os.chflags if hasattr(os, 'chflags') else _nop
st = stat_func(src)
mode = stat.S_IMODE(st.st_mode)
if hasattr(os, 'utime'):
os.utime(dst, (st.st_atime, st.st_mtime))
if hasattr(os, 'chmod'):
os.chmod(dst, mode)
if hasattr(os, 'chflags') and hasattr(st, 'st_flags'):
utime_func(dst, (st.st_atime, st.st_mtime))
chmod_func(dst, mode)
if hasattr(st, 'st_flags'):
try:
os.chflags(dst, st.st_flags)
chflags_func(dst, st.st_flags)
except OSError as why:
if (not hasattr(errno, 'EOPNOTSUPP') or
why.errno != errno.EOPNOTSUPP):
raise
def copy(src, dst):
def copy(src, dst, symlinks=False):
"""Copy data and mode bits ("cp src dst").
The destination may be a directory.
If the optional flag `symlinks` is set, symlinks won't be followed. This
resembles GNU's "cp -P src dst".
"""
if os.path.isdir(dst):
dst = os.path.join(dst, os.path.basename(src))
copyfile(src, dst)
copymode(src, dst)
copyfile(src, dst, symlinks=symlinks)
copymode(src, dst, symlinks=symlinks)
def copy2(src, dst):
def copy2(src, dst, symlinks=False):
"""Copy data and all stat info ("cp -p src dst").
The destination may be a directory.
If the optional flag `symlinks` is set, symlinks won't be followed. This
resembles GNU's "cp -P src dst".
"""
if os.path.isdir(dst):
dst = os.path.join(dst, os.path.basename(src))
copyfile(src, dst)
copystat(src, dst)
copyfile(src, dst, symlinks=symlinks)
copystat(src, dst, symlinks=symlinks)
def ignore_patterns(*patterns):
"""Function that can be used as copytree() ignore parameter.
@ -212,7 +257,11 @@ def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
if os.path.islink(srcname):
linkto = os.readlink(srcname)
if symlinks:
# We can't just leave it to `copy_function` because legacy
# code with a custom `copy_function` may rely on copytree
# doing the right thing.
os.symlink(linkto, dstname)
copystat(srcname, dstname, symlinks=symlinks)
else:
# ignore dangling symlink if the flag is on
if not os.path.exists(linkto) and ignore_dangling_symlinks:

View File

@ -164,6 +164,197 @@ class TestShutil(unittest.TestCase):
self.assertTrue(issubclass(exc[0], OSError))
self.errorState = 2
@unittest.skipUnless(hasattr(os, 'chmod'), 'requires os.chmod')
@support.skip_unless_symlink
def test_copymode_follow_symlinks(self):
tmp_dir = self.mkdtemp()
src = os.path.join(tmp_dir, 'foo')
dst = os.path.join(tmp_dir, 'bar')
src_link = os.path.join(tmp_dir, 'baz')
dst_link = os.path.join(tmp_dir, 'quux')
write_file(src, 'foo')
write_file(dst, 'foo')
os.symlink(src, src_link)
os.symlink(dst, dst_link)
os.chmod(src, stat.S_IRWXU|stat.S_IRWXG)
# file to file
os.chmod(dst, stat.S_IRWXO)
self.assertNotEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
shutil.copymode(src, dst)
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
# follow src link
os.chmod(dst, stat.S_IRWXO)
shutil.copymode(src_link, dst)
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
# follow dst link
os.chmod(dst, stat.S_IRWXO)
shutil.copymode(src, dst_link)
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
# follow both links
os.chmod(dst, stat.S_IRWXO)
shutil.copymode(src_link, dst)
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
@unittest.skipUnless(hasattr(os, 'lchmod'), 'requires os.lchmod')
@support.skip_unless_symlink
def test_copymode_symlink_to_symlink(self):
tmp_dir = self.mkdtemp()
src = os.path.join(tmp_dir, 'foo')
dst = os.path.join(tmp_dir, 'bar')
src_link = os.path.join(tmp_dir, 'baz')
dst_link = os.path.join(tmp_dir, 'quux')
write_file(src, 'foo')
write_file(dst, 'foo')
os.symlink(src, src_link)
os.symlink(dst, dst_link)
os.chmod(src, stat.S_IRWXU|stat.S_IRWXG)
os.chmod(dst, stat.S_IRWXU)
os.lchmod(src_link, stat.S_IRWXO|stat.S_IRWXG)
# link to link
os.lchmod(dst_link, stat.S_IRWXO)
shutil.copymode(src_link, dst_link, symlinks=True)
self.assertEqual(os.lstat(src_link).st_mode,
os.lstat(dst_link).st_mode)
self.assertNotEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
# src link - use chmod
os.lchmod(dst_link, stat.S_IRWXO)
shutil.copymode(src_link, dst, symlinks=True)
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
# dst link - use chmod
os.lchmod(dst_link, stat.S_IRWXO)
shutil.copymode(src, dst_link, symlinks=True)
self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode)
@unittest.skipIf(hasattr(os, 'lchmod'), 'requires os.lchmod to be missing')
@support.skip_unless_symlink
def test_copymode_symlink_to_symlink_wo_lchmod(self):
tmp_dir = self.mkdtemp()
src = os.path.join(tmp_dir, 'foo')
dst = os.path.join(tmp_dir, 'bar')
src_link = os.path.join(tmp_dir, 'baz')
dst_link = os.path.join(tmp_dir, 'quux')
write_file(src, 'foo')
write_file(dst, 'foo')
os.symlink(src, src_link)
os.symlink(dst, dst_link)
shutil.copymode(src_link, dst_link, symlinks=True) # silent fail
@support.skip_unless_symlink
def test_copystat_symlinks(self):
tmp_dir = self.mkdtemp()
src = os.path.join(tmp_dir, 'foo')
dst = os.path.join(tmp_dir, 'bar')
src_link = os.path.join(tmp_dir, 'baz')
dst_link = os.path.join(tmp_dir, 'qux')
write_file(src, 'foo')
src_stat = os.stat(src)
os.utime(src, (src_stat.st_atime,
src_stat.st_mtime - 42.0)) # ensure different mtimes
write_file(dst, 'bar')
self.assertNotEqual(os.stat(src).st_mtime, os.stat(dst).st_mtime)
os.symlink(src, src_link)
os.symlink(dst, dst_link)
if hasattr(os, 'lchmod'):
os.lchmod(src_link, stat.S_IRWXO)
if hasattr(os, 'lchflags') and hasattr(stat, 'UF_NODUMP'):
os.lchflags(src_link, stat.UF_NODUMP)
src_link_stat = os.lstat(src_link)
# follow
if hasattr(os, 'lchmod'):
shutil.copystat(src_link, dst_link, symlinks=False)
self.assertNotEqual(src_link_stat.st_mode, os.stat(dst).st_mode)
# don't follow
shutil.copystat(src_link, dst_link, symlinks=True)
dst_link_stat = os.lstat(dst_link)
if hasattr(os, 'lutimes'):
for attr in 'st_atime', 'st_mtime':
# The modification times may be truncated in the new file.
self.assertLessEqual(getattr(src_link_stat, attr),
getattr(dst_link_stat, attr) + 1)
if hasattr(os, 'lchmod'):
self.assertEqual(src_link_stat.st_mode, dst_link_stat.st_mode)
if hasattr(os, 'lchflags') and hasattr(src_link_stat, 'st_flags'):
self.assertEqual(src_link_stat.st_flags, dst_link_stat.st_flags)
# tell to follow but dst is not a link
shutil.copystat(src_link, dst, symlinks=True)
self.assertTrue(abs(os.stat(src).st_mtime - os.stat(dst).st_mtime) <
00000.1)
@support.skip_unless_symlink
def test_copy_symlinks(self):
tmp_dir = self.mkdtemp()
src = os.path.join(tmp_dir, 'foo')
dst = os.path.join(tmp_dir, 'bar')
src_link = os.path.join(tmp_dir, 'baz')
write_file(src, 'foo')
os.symlink(src, src_link)
if hasattr(os, 'lchmod'):
os.lchmod(src_link, stat.S_IRWXU | stat.S_IRWXO)
# don't follow
shutil.copy(src_link, dst, symlinks=False)
self.assertFalse(os.path.islink(dst))
self.assertEqual(read_file(src), read_file(dst))
os.remove(dst)
# follow
shutil.copy(src_link, dst, symlinks=True)
self.assertTrue(os.path.islink(dst))
self.assertEqual(os.readlink(dst), os.readlink(src_link))
if hasattr(os, 'lchmod'):
self.assertEqual(os.lstat(src_link).st_mode,
os.lstat(dst).st_mode)
@support.skip_unless_symlink
def test_copy2_symlinks(self):
tmp_dir = self.mkdtemp()
src = os.path.join(tmp_dir, 'foo')
dst = os.path.join(tmp_dir, 'bar')
src_link = os.path.join(tmp_dir, 'baz')
write_file(src, 'foo')
os.symlink(src, src_link)
if hasattr(os, 'lchmod'):
os.lchmod(src_link, stat.S_IRWXU | stat.S_IRWXO)
if hasattr(os, 'lchflags') and hasattr(stat, 'UF_NODUMP'):
os.lchflags(src_link, stat.UF_NODUMP)
src_stat = os.stat(src)
src_link_stat = os.lstat(src_link)
# follow
shutil.copy2(src_link, dst, symlinks=False)
self.assertFalse(os.path.islink(dst))
self.assertEqual(read_file(src), read_file(dst))
os.remove(dst)
# don't follow
shutil.copy2(src_link, dst, symlinks=True)
self.assertTrue(os.path.islink(dst))
self.assertEqual(os.readlink(dst), os.readlink(src_link))
dst_stat = os.lstat(dst)
if hasattr(os, 'lutimes'):
for attr in 'st_atime', 'st_mtime':
# The modification times may be truncated in the new file.
self.assertLessEqual(getattr(src_link_stat, attr),
getattr(dst_stat, attr) + 1)
if hasattr(os, 'lchmod'):
self.assertEqual(src_link_stat.st_mode, dst_stat.st_mode)
self.assertNotEqual(src_stat.st_mode, dst_stat.st_mode)
if hasattr(os, 'lchflags') and hasattr(src_link_stat, 'st_flags'):
self.assertEqual(src_link_stat.st_flags, dst_stat.st_flags)
@support.skip_unless_symlink
def test_copyfile_symlinks(self):
tmp_dir = self.mkdtemp()
src = os.path.join(tmp_dir, 'src')
dst = os.path.join(tmp_dir, 'dst')
dst_link = os.path.join(tmp_dir, 'dst_link')
link = os.path.join(tmp_dir, 'link')
write_file(src, 'foo')
os.symlink(src, link)
# don't follow
shutil.copyfile(link, dst_link, symlinks=True)
self.assertTrue(os.path.islink(dst_link))
self.assertEqual(os.readlink(link), os.readlink(dst_link))
# follow
shutil.copyfile(link, dst)
self.assertFalse(os.path.islink(dst))
def test_rmtree_dont_delete_file(self):
# When called on a file instead of a directory, don't delete it.
handle, path = tempfile.mkstemp()
@ -190,6 +381,34 @@ class TestShutil(unittest.TestCase):
actual = read_file((dst_dir, 'test_dir', 'test.txt'))
self.assertEqual(actual, '456')
@support.skip_unless_symlink
def test_copytree_symlinks(self):
tmp_dir = self.mkdtemp()
src_dir = os.path.join(tmp_dir, 'src')
dst_dir = os.path.join(tmp_dir, 'dst')
sub_dir = os.path.join(src_dir, 'sub')
os.mkdir(src_dir)
os.mkdir(sub_dir)
write_file((src_dir, 'file.txt'), 'foo')
src_link = os.path.join(sub_dir, 'link')
dst_link = os.path.join(dst_dir, 'sub/link')
os.symlink(os.path.join(src_dir, 'file.txt'),
src_link)
if hasattr(os, 'lchmod'):
os.lchmod(src_link, stat.S_IRWXU | stat.S_IRWXO)
if hasattr(os, 'lchflags') and hasattr(stat, 'UF_NODUMP'):
os.lchflags(src_link, stat.UF_NODUMP)
src_stat = os.lstat(src_link)
shutil.copytree(src_dir, dst_dir, symlinks=True)
self.assertTrue(os.path.islink(os.path.join(dst_dir, 'sub', 'link')))
self.assertEqual(os.readlink(os.path.join(dst_dir, 'sub', 'link')),
os.path.join(src_dir, 'file.txt'))
dst_stat = os.lstat(dst_link)
if hasattr(os, 'lchmod'):
self.assertEqual(dst_stat.st_mode, src_stat.st_mode)
if hasattr(os, 'lchflags'):
self.assertEqual(dst_stat.st_flags, src_stat.st_flags)
def test_copytree_with_exclude(self):
# creating data
join = os.path.join

View File

@ -422,6 +422,11 @@ Core and Builtins
Library
-------
- Issue #12715: Add an optional symlinks argument to shutil functions
(copyfile, copymode, copystat, copy, copy2). When that parameter is
true, symlinks aren't dereferenced and the operation instead acts on the
symlink itself (or creates one, if relevant). Patch by Hynek Schlawack.
- Add a flags parameter to select.epoll.
- Issue #12798: Updated the mimetypes documentation.