diff -r b66e82c9f852 Doc/library/shutil.rst --- a/Doc/library/shutil.rst Tue Jun 26 23:05:27 2012 +0200 +++ b/Doc/library/shutil.rst Thu Jun 28 00:00:38 2012 +0300 @@ -47,7 +47,7 @@ be copied. -.. function:: copyfile(src, dst, symlinks=False) +.. function:: copyfile(src, dst, follow_symlinks=True) Copy the contents (no metadata) of the file named *src* to a file named *dst* and return *dst*. *dst* must be the complete target file name; look at @@ -59,63 +59,63 @@ 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 + If *follow_symlinks* is false 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. + Added *follow_symlinks* argument. .. versionchanged:: 3.3 Added return of the *dst*. -.. function:: copymode(src, dst, symlinks=False) +.. function:: copymode(src, dst, follow_symlinks=True) 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. If - *symlinks* is true, *src* a symbolic link and the operating system supports + *follow_symlinks* is false, *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. + Added *follow_symlinks* argument. -.. function:: copystat(src, dst, symlinks=False) +.. function:: copystat(src, dst, follow_symlinks=True) 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. If *src* and *dst* are both - symbolic links and *symlinks* true, the stats of the link will be copied as + symbolic links and *follow_symlinks* false, the stats of the link will be copied as far as the platform allows. .. versionchanged:: 3.3 - Added *symlinks* argument. + Added *follow_symlinks* argument. -.. function:: copy(src, dst, symlinks=False)) +.. function:: copy(src, dst, follow_symlinks=True)) Copy the file *src* to the file or directory *dst* and return the file's destination. 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. If *symlinks* is true, symbolic links won't be + names given as strings. If *follow_symlinks* is false, symbolic links won't be followed but recreated instead -- this resembles GNU's :program:`cp -P`. .. versionchanged:: 3.3 - Added *symlinks* argument. + Added *follow_symlinks* argument. .. versionchanged:: 3.3 Added return of the *dst*. -.. function:: copy2(src, dst, symlinks=False) +.. function:: copy2(src, dst, follow_symlinks=False) Similar to :func:`shutil.copy`, including that the destination is returned, but metadata is copied as well. This is - similar to the Unix command :program:`cp -p`. If *symlinks* is true, + similar to the Unix command :program:`cp -p`. If *follow_symlinks* is false, symbolic links won't be followed but recreated instead -- this resembles GNU's :program:`cp -P`. .. versionchanged:: 3.3 - Added *symlinks* argument, try to copy extended file system attributes + Added *follow_symlinks* argument, try to copy extended file system attributes too (currently Linux only). .. versionchanged:: 3.3 @@ -128,7 +128,9 @@ match one of the glob-style *patterns* provided. See the example below. -.. function:: copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2, ignore_dangling_symlinks=False) +.. function:: copytree(src, dst, symlinks=False, ignore=None, + copy_function=copy2, ignore_dangling_symlinks=False, *, + follow_symlinks=True) Recursively copy an entire directory tree rooted at *src*, returning the destination directory. The destination @@ -137,12 +139,14 @@ are copied with :func:`copystat`, individual files are copied using :func:`shutil.copy2`. - If *symlinks* is true, symbolic links in the source tree are represented as + If *follow_symlinks* is false or *symlinks* is true, + symbolic links in the source tree are represented as 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 + When *follow_symlinks* is true or *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 a :exc:`Error` exception at the end of the copy process. You can set the optional *ignore_dangling_symlinks* flag to true if you @@ -172,10 +176,13 @@ .. versionchanged:: 3.2 Added the *ignore_dangling_symlinks* argument to silent dangling symlinks - errors when *symlinks* is false. + errors when *follow_symlinks* is true or *symlinks* is false. .. versionchanged:: 3.3 - Copy metadata when *symlinks* is false. + Copy metadata when *follow_symlinks* is true or *symlinks* is false. + + .. versionchanged:: 3.3 + Added *follow_symlinks* argument with opposite meaning to *symlinks*. .. versionchanged:: 3.3 Added return of the *dst*. diff -r b66e82c9f852 Lib/shutil.py --- a/Lib/shutil.py Tue Jun 26 23:05:27 2012 +0200 +++ b/Lib/shutil.py Thu Jun 28 00:00:38 2012 +0300 @@ -82,10 +82,10 @@ return (os.path.normcase(os.path.abspath(src)) == os.path.normcase(os.path.abspath(dst))) -def copyfile(src, dst, symlinks=False): +def copyfile(src, dst, follow_symlinks=True): """Copy data from src to dst. - If optional flag `symlinks` is set and `src` is a symbolic link, a new + If optional flag `follow_symlinks` is not set and `src` is a symbolic link, a new symlink will be created instead of copying the file it points to. """ @@ -103,7 +103,7 @@ if stat.S_ISFIFO(st.st_mode): raise SpecialFileError("`%s` is a named pipe" % fn) - if symlinks and os.path.islink(src): + if not follow_symlinks and os.path.islink(src): os.symlink(os.readlink(src), dst) else: with open(src, 'rb') as fsrc: @@ -111,15 +111,15 @@ copyfileobj(fsrc, fdst) return dst -def copymode(src, dst, symlinks=False): +def copymode(src, dst, follow_symlinks=True): """Copy mode bits from src to dst. - If the optional flag `symlinks` is set, symlinks aren't followed if and + If the optional flag `follow_symlinks` is not 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 not follow_symlinks and os.path.islink(src) and os.path.islink(dst): if hasattr(os, 'lchmod'): stat_func, chmod_func = os.lstat, os.lchmod else: @@ -132,10 +132,10 @@ st = stat_func(src) chmod_func(dst, stat.S_IMODE(st.st_mode)) -def copystat(src, dst, symlinks=False): +def copystat(src, dst, follow_symlinks=True): """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 + If the optional flag `follow_symlinks` is not set, symlinks aren't followed if and only if both `src` and `dst` are symlinks. """ @@ -143,7 +143,7 @@ pass # follow symlinks (aka don't not follow symlinks) - follow = not (symlinks and os.path.islink(src) and os.path.islink(dst)) + follow = follow_symlinks or not (os.path.islink(src) and os.path.islink(dst)) if follow: # use the real function if it exists def lookup(name): @@ -186,19 +186,19 @@ raise if hasattr(os, 'listxattr'): - def _copyxattr(src, dst, symlinks=False): + def _copyxattr(src, dst, follow_symlinks=True): """Copy extended filesystem attributes from `src` to `dst`. Overwrite existing attributes. - If the optional flag `symlinks` is set, symlinks won't be followed. + If the optional flag `follow_symlinks` is not set, symlinks won't be followed. """ - for name in os.listxattr(src, follow_symlinks=symlinks): + for name in os.listxattr(src, follow_symlinks=follow_symlinks): try: - value = os.getxattr(src, name, follow_symlinks=symlinks) - os.setxattr(dst, name, value, follow_symlinks=symlinks) + value = os.getxattr(src, name, follow_symlinks=follow_symlinks) + os.setxattr(dst, name, value, follow_symlinks=follow_symlinks) except OSError as e: if e.errno not in (errno.EPERM, errno.ENOTSUP, errno.ENODATA): raise @@ -206,36 +206,36 @@ def _copyxattr(*args, **kwargs): pass -def copy(src, dst, symlinks=False): +def copy(src, dst, follow_symlinks=True): """Copy data and mode bits ("cp src dst"). Return the file's destination. The destination may be a directory. - If the optional flag `symlinks` is set, symlinks won't be followed. This + If the optional flag `follow_symlinks` is not 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, symlinks=symlinks) - copymode(src, dst, symlinks=symlinks) + copyfile(src, dst, follow_symlinks=follow_symlinks) + copymode(src, dst, follow_symlinks=follow_symlinks) return dst -def copy2(src, dst, symlinks=False): +def copy2(src, dst, follow_symlinks=True): """Copy data and all stat info ("cp -p src dst"). Return the file's destination." The destination may be a directory. - If the optional flag `symlinks` is set, symlinks won't be followed. This + If the optional flag `follow_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, symlinks=symlinks) - copystat(src, dst, symlinks=symlinks) - _copyxattr(src, dst, symlinks=symlinks) + copyfile(src, dst, follow_symlinks=follow_symlinks) + copystat(src, dst, follow_symlinks=follow_symlinks) + _copyxattr(src, dst, follow_symlinks=follow_symlinks) return dst def ignore_patterns(*patterns): @@ -250,8 +250,10 @@ return set(ignored_names) return _ignore_patterns -def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2, - ignore_dangling_symlinks=False): +_sentry = object() + +def copytree(src, dst, symlinks=_sentry, ignore=None, copy_function=copy2, + ignore_dangling_symlinks=False, *, follow_symlinks=_sentry): """Recursively copy a directory tree. The destination directory must not already exist. @@ -286,6 +288,15 @@ function that supports the same signature (like copy()) can be used. """ + if symlinks is _sentry: + if follow_symlinks is _sentry: + symlinks = False + else: + symlinks = not follow_symlinks + elif follow_symlinks is not _sentry: + raise ValueError( + "'follow_symlinks' and 'symlinks' arguments are incompatible") + names = os.listdir(src) if ignore is not None: ignored_names = ignore(src, names) @@ -307,7 +318,7 @@ # code with a custom `copy_function` may rely on copytree # doing the right thing. os.symlink(linkto, dstname) - copystat(srcname, dstname, symlinks=symlinks) + copystat(srcname, dstname, follow_symlinks=not symlinks) else: # ignore dangling symlink if the flag is on if not os.path.exists(linkto) and ignore_dangling_symlinks: diff -r b66e82c9f852 Lib/test/test_shutil.py --- a/Lib/test/test_shutil.py Tue Jun 26 23:05:27 2012 +0200 +++ b/Lib/test/test_shutil.py Thu Jun 28 00:00:38 2012 +0300 @@ -245,17 +245,17 @@ 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) + shutil.copymode(src_link, dst_link, follow_symlinks=False) 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) + shutil.copymode(src_link, dst, follow_symlinks=False) 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) + shutil.copymode(src, dst_link, follow_symlinks=False) self.assertEqual(os.stat(src).st_mode, os.stat(dst).st_mode) @unittest.skipIf(hasattr(os, 'lchmod'), 'requires os.lchmod to be missing') @@ -270,7 +270,7 @@ write_file(dst, 'foo') os.symlink(src, src_link) os.symlink(dst, dst_link) - shutil.copymode(src_link, dst_link, symlinks=True) # silent fail + shutil.copymode(src_link, dst_link, follow_symlinks=False) # silent fail @support.skip_unless_symlink def test_copystat_symlinks(self): @@ -294,10 +294,10 @@ src_link_stat = os.lstat(src_link) # follow if hasattr(os, 'lchmod'): - shutil.copystat(src_link, dst_link, symlinks=False) + shutil.copystat(src_link, dst_link, follow_symlinks=True) self.assertNotEqual(src_link_stat.st_mode, os.stat(dst).st_mode) # don't follow - shutil.copystat(src_link, dst_link, symlinks=True) + shutil.copystat(src_link, dst_link, follow_symlinks=False) dst_link_stat = os.lstat(dst_link) if os.utime in os.supports_follow_symlinks: for attr in 'st_atime', 'st_mtime': @@ -309,7 +309,7 @@ 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) + shutil.copystat(src_link, dst, follow_symlinks=False) self.assertTrue(abs(os.stat(src).st_mtime - os.stat(dst).st_mtime) < 00000.1) @@ -397,10 +397,10 @@ dst_link = os.path.join(tmp_dir, 'qux') write_file(dst, 'bar') os.symlink(dst, dst_link) - shutil._copyxattr(src_link, dst_link, symlinks=True) + shutil._copyxattr(src_link, dst_link, follow_symlinks=False) self.assertEqual(os.getxattr(dst_link, 'trusted.foo', follow_symlinks=False), b'43') self.assertRaises(OSError, os.getxattr, dst, 'trusted.foo') - shutil._copyxattr(src_link, dst, symlinks=True) + shutil._copyxattr(src_link, dst, follow_symlinks=False) self.assertEqual(os.getxattr(dst, 'trusted.foo'), b'43') @support.skip_unless_symlink @@ -414,12 +414,12 @@ if hasattr(os, 'lchmod'): os.lchmod(src_link, stat.S_IRWXU | stat.S_IRWXO) # don't follow - shutil.copy(src_link, dst, symlinks=False) + shutil.copy(src_link, dst, follow_symlinks=True) 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) + shutil.copy(src_link, dst, follow_symlinks=False) self.assertTrue(os.path.islink(dst)) self.assertEqual(os.readlink(dst), os.readlink(src_link)) if hasattr(os, 'lchmod'): @@ -441,12 +441,12 @@ src_stat = os.stat(src) src_link_stat = os.lstat(src_link) # follow - shutil.copy2(src_link, dst, symlinks=False) + shutil.copy2(src_link, dst, follow_symlinks=True) 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) + shutil.copy2(src_link, dst, follow_symlinks=False) self.assertTrue(os.path.islink(dst)) self.assertEqual(os.readlink(dst), os.readlink(src_link)) dst_stat = os.lstat(dst) @@ -484,7 +484,7 @@ write_file(src, 'foo') os.symlink(src, link) # don't follow - shutil.copyfile(link, dst_link, symlinks=True) + shutil.copyfile(link, dst_link, follow_symlinks=False) self.assertTrue(os.path.islink(dst_link)) self.assertEqual(os.readlink(link), os.readlink(dst_link)) # follow @@ -555,6 +555,33 @@ 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, follow_symlinks=False) + 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_symlinks_old(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')), @@ -565,6 +592,15 @@ if hasattr(os, 'lchflags'): self.assertEqual(dst_stat.st_flags, src_stat.st_flags) + def test_copytree_incompatible_args(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) + self.assertRaises(ValueError, shutil.copytree, src_dir, dst_dir, symlinks=True, follow_symlinks=False) + def test_copytree_with_exclude(self): # creating data join = os.path.join @@ -748,9 +784,9 @@ shutil.copytree(src_dir, dst_dir, ignore_dangling_symlinks=True) self.assertNotIn('test.txt', os.listdir(dst_dir)) - # a dangling symlink is copied if symlinks=True + # a dangling symlink is copied if follow_symlinks=False dst_dir = os.path.join(self.mkdtemp(), 'destination3') - shutil.copytree(src_dir, dst_dir, symlinks=True) + shutil.copytree(src_dir, dst_dir, follow_symlinks=False) self.assertIn('test.txt', os.listdir(dst_dir)) def _copy_file(self, method):