mirror of
https://github.com/python/cpython.git
synced 2025-09-01 06:28:36 +00:00
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:
parent
d2f1db5355
commit
78091e63d6
4 changed files with 333 additions and 38 deletions
|
@ -45,7 +45,7 @@ Directory and files operations
|
||||||
be copied.
|
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*.
|
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
|
*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
|
such as character or block devices and pipes cannot be copied with this
|
||||||
function. *src* and *dst* are path names given as strings.
|
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
|
.. versionchanged:: 3.3
|
||||||
:exc:`IOError` used to be raised instead of :exc:`OSError`.
|
: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
|
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
|
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*
|
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
|
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
|
file with the same basename as *src* is created (or overwritten) in the
|
||||||
directory specified. Permission bits are copied. *src* and *dst* are path
|
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
|
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
|
: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)
|
.. function:: ignore_patterns(\*patterns)
|
||||||
|
|
||||||
|
@ -104,9 +123,9 @@ Directory and files operations
|
||||||
:func:`copy2`.
|
:func:`copy2`.
|
||||||
|
|
||||||
If *symlinks* is true, symbolic links in the source tree are represented as
|
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
|
symbolic links in the new tree and the metadata of the original links will
|
||||||
copied; if false or omitted, the contents and metadata of the linked files
|
be copied as far as the platform allows; if false or omitted, the contents
|
||||||
are copied to the new tree.
|
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
|
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
|
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
|
Added the *ignore_dangling_symlinks* argument to silent dangling symlinks
|
||||||
errors when *symlinks* is false.
|
errors when *symlinks* is false.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.3
|
||||||
|
Copy metadata when *symlinks* is false.
|
||||||
|
|
||||||
|
|
||||||
.. function:: rmtree(path, ignore_errors=False, onerror=None)
|
.. function:: rmtree(path, ignore_errors=False, onerror=None)
|
||||||
|
|
||||||
|
|
|
@ -82,8 +82,13 @@ def _samefile(src, dst):
|
||||||
return (os.path.normcase(os.path.abspath(src)) ==
|
return (os.path.normcase(os.path.abspath(src)) ==
|
||||||
os.path.normcase(os.path.abspath(dst)))
|
os.path.normcase(os.path.abspath(dst)))
|
||||||
|
|
||||||
def copyfile(src, dst):
|
def copyfile(src, dst, symlinks=False):
|
||||||
"""Copy data from src to dst"""
|
"""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):
|
if _samefile(src, dst):
|
||||||
raise Error("`%s` and `%s` are the same file" % (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):
|
if stat.S_ISFIFO(st.st_mode):
|
||||||
raise SpecialFileError("`%s` is a named pipe" % fn)
|
raise SpecialFileError("`%s` is a named pipe" % fn)
|
||||||
|
|
||||||
|
if symlinks and os.path.islink(src):
|
||||||
|
os.symlink(os.readlink(src), dst)
|
||||||
|
else:
|
||||||
with open(src, 'rb') as fsrc:
|
with open(src, 'rb') as fsrc:
|
||||||
with open(dst, 'wb') as fdst:
|
with open(dst, 'wb') as fdst:
|
||||||
copyfileobj(fsrc, fdst)
|
copyfileobj(fsrc, fdst)
|
||||||
|
|
||||||
def copymode(src, dst):
|
def copymode(src, dst, symlinks=False):
|
||||||
"""Copy mode bits from src to 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 copystat(src, dst):
|
If the optional flag `symlinks` is set, symlinks aren't followed if and
|
||||||
"""Copy all stat info (mode bits, atime, mtime, flags) from src to dst"""
|
only if both `src` and `dst` are symlinks. If `lchmod` isn't available (eg.
|
||||||
st = os.stat(src)
|
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)
|
mode = stat.S_IMODE(st.st_mode)
|
||||||
if hasattr(os, 'utime'):
|
utime_func(dst, (st.st_atime, st.st_mtime))
|
||||||
os.utime(dst, (st.st_atime, st.st_mtime))
|
chmod_func(dst, mode)
|
||||||
if hasattr(os, 'chmod'):
|
if hasattr(st, 'st_flags'):
|
||||||
os.chmod(dst, mode)
|
|
||||||
if hasattr(os, 'chflags') and hasattr(st, 'st_flags'):
|
|
||||||
try:
|
try:
|
||||||
os.chflags(dst, st.st_flags)
|
chflags_func(dst, st.st_flags)
|
||||||
except OSError as why:
|
except OSError as why:
|
||||||
if (not hasattr(errno, 'EOPNOTSUPP') or
|
if (not hasattr(errno, 'EOPNOTSUPP') or
|
||||||
why.errno != errno.EOPNOTSUPP):
|
why.errno != errno.EOPNOTSUPP):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def copy(src, dst):
|
def copy(src, dst, symlinks=False):
|
||||||
"""Copy data and mode bits ("cp src dst").
|
"""Copy data and mode bits ("cp src dst").
|
||||||
|
|
||||||
The destination may be a directory.
|
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):
|
if os.path.isdir(dst):
|
||||||
dst = os.path.join(dst, os.path.basename(src))
|
dst = os.path.join(dst, os.path.basename(src))
|
||||||
copyfile(src, dst)
|
copyfile(src, dst, symlinks=symlinks)
|
||||||
copymode(src, dst)
|
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").
|
"""Copy data and all stat info ("cp -p src dst").
|
||||||
|
|
||||||
The destination may be a directory.
|
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):
|
if os.path.isdir(dst):
|
||||||
dst = os.path.join(dst, os.path.basename(src))
|
dst = os.path.join(dst, os.path.basename(src))
|
||||||
copyfile(src, dst)
|
copyfile(src, dst, symlinks=symlinks)
|
||||||
copystat(src, dst)
|
copystat(src, dst, symlinks=symlinks)
|
||||||
|
|
||||||
def ignore_patterns(*patterns):
|
def ignore_patterns(*patterns):
|
||||||
"""Function that can be used as copytree() ignore parameter.
|
"""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):
|
if os.path.islink(srcname):
|
||||||
linkto = os.readlink(srcname)
|
linkto = os.readlink(srcname)
|
||||||
if symlinks:
|
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)
|
os.symlink(linkto, dstname)
|
||||||
|
copystat(srcname, dstname, symlinks=symlinks)
|
||||||
else:
|
else:
|
||||||
# ignore dangling symlink if the flag is on
|
# ignore dangling symlink if the flag is on
|
||||||
if not os.path.exists(linkto) and ignore_dangling_symlinks:
|
if not os.path.exists(linkto) and ignore_dangling_symlinks:
|
||||||
|
|
|
@ -164,6 +164,197 @@ class TestShutil(unittest.TestCase):
|
||||||
self.assertTrue(issubclass(exc[0], OSError))
|
self.assertTrue(issubclass(exc[0], OSError))
|
||||||
self.errorState = 2
|
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):
|
def test_rmtree_dont_delete_file(self):
|
||||||
# When called on a file instead of a directory, don't delete it.
|
# When called on a file instead of a directory, don't delete it.
|
||||||
handle, path = tempfile.mkstemp()
|
handle, path = tempfile.mkstemp()
|
||||||
|
@ -190,6 +381,34 @@ class TestShutil(unittest.TestCase):
|
||||||
actual = read_file((dst_dir, 'test_dir', 'test.txt'))
|
actual = read_file((dst_dir, 'test_dir', 'test.txt'))
|
||||||
self.assertEqual(actual, '456')
|
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):
|
def test_copytree_with_exclude(self):
|
||||||
# creating data
|
# creating data
|
||||||
join = os.path.join
|
join = os.path.join
|
||||||
|
|
|
@ -422,6 +422,11 @@ Core and Builtins
|
||||||
Library
|
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.
|
- Add a flags parameter to select.epoll.
|
||||||
|
|
||||||
- Issue #12798: Updated the mimetypes documentation.
|
- Issue #12798: Updated the mimetypes documentation.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue