Index: Misc/ACKS =================================================================== --- Misc/ACKS (revision 62659) +++ Misc/ACKS (working copy) @@ -754,6 +754,7 @@ Milan Zamazal Artur Zaprzala Mike Zarnstorff +Tarek Ziadé Siebren van der Zee Uwe Zessin Amaury Forgeot d'Arc Index: Doc/library/shutil.rst =================================================================== --- Doc/library/shutil.rst (revision 62659) +++ Doc/library/shutil.rst (working copy) @@ -76,16 +76,24 @@ Similar to :func:`copy`, but last access time and last modification time are copied as well. This is similar to the Unix command :program:`cp -p`. +.. function:: ignore_patterns(\*patterns) -.. function:: copytree(src, dst[, symlinks]) + This function can be used as a callable with :func:`copytree`. It allows ignoring + files that matches the glob-style patterns provided. See the example below. +.. function:: copytree(src, dst[, [symlinks, ignore]) + Recursively copy an entire directory tree rooted at *src*. The destination directory, named by *dst*, must not already exist; it will be created as well as missing parent directories. Permissions and times of directories are copied with :func:`copystat`, individual files are copied using :func:`copy2`. If *symlinks* is true, symbolic links in the source tree are represented as symbolic links in the new tree; if false or omitted, the contents of the linked - files are copied to the new tree. If exception(s) occur, an :exc:`Error` is + files are copied to the new tree. If *ignore* is given, it must be a callable + that will receive the path being visited by :func:`copytree`, and a list of its + elements. The callable must return a list of folder and file names relative + to the path, that will be ignored in the copy process. :func:`ignore_patterns` + is an example of such callable. If exception(s) occur, an :exc:`Error` is raised with a list of reasons. The source code for this should be considered an example rather than a tool. @@ -98,6 +106,8 @@ Create intermediate directories needed to create *dst*, rather than raising an error. Copy permissions and times of directories using :func:`copystat`. + .. versionchanged:: 2.6 + Added the *ignore* argument to be able to influence what is being copied. .. function:: rmtree(path[, ignore_errors[, onerror]]) @@ -151,11 +161,18 @@ above, with the docstring omitted. It demonstrates many of the other functions provided by this module. :: - def copytree(src, dst, symlinks=False): + def copytree(src, dst, symlinks=False, ignore=None): names = os.listdir(src) + if ignore is not None: + ignored_names = ignore(src, names) + else: + ignored_names = set() + os.makedirs(dst) errors = [] for name in names: + if name in ignored_names: + continue srcname = os.path.join(src, name) dstname = os.path.join(dst, name) try: @@ -163,7 +180,7 @@ linkto = os.readlink(srcname) os.symlink(linkto, dstname) elif os.path.isdir(srcname): - copytree(srcname, dstname, symlinks) + copytree(srcname, dstname, symlinks, ignore) else: copy2(srcname, dstname) # XXX What about devices, sockets etc.? @@ -182,3 +199,26 @@ errors.extend((src, dst, str(why))) if errors: raise Error, errors + +Another example that uses the `ignore_patterns` helper:: + + from shutil import ignore_patterns + from shutil import copytree + + copytree(source, destination, ignore=ignore_patterns('*.pyc', 'tmp*')) + +This will copy everything except the .pyc files and the files or +folders that starts with `tmp`. + +Another example, that adds a logging call:: + + from shutil import ignore_patterns + from shutil import copytree + import logging + + def _logging(path, names): + logging.info('Working in %s' % path) + return set() # nothing will be ignored + + copytree(source, destination, ignore=_logging) + Index: Lib/shutil.py =================================================================== --- Lib/shutil.py (revision 62659) +++ Lib/shutil.py (working copy) @@ -8,6 +8,7 @@ import sys import stat from os.path import abspath +import fnmatch __all__ = ["copyfileobj","copyfile","copymode","copystat","copy","copy2", "copytree","move","rmtree","Error"] @@ -93,8 +94,19 @@ copyfile(src, dst) copystat(src, dst) +def ignore_patterns(*patterns): + """Function that can be used as copytree() ignore parameter. -def copytree(src, dst, symlinks=False): + Patterns is a sequence of glob-style patterns + that are used to exclude files""" + def _ignore_patterns(path, names): + ignored_names = [] + for pattern in patterns: + ignored_names.extend(fnmatch.filter(names, pattern)) + return set(ignored_names) + return _ignore_patterns + +def copytree(src, dst, symlinks=False, ignore=None): """Recursively copy a directory tree using copy2(). The destination directory must not already exist. @@ -105,13 +117,30 @@ it is false, the contents of the files pointed to by symbolic links are copied. + The optional ignore argument is a callable. If given, it + is called with the `src` parameter, which is the folder + being visited by copytree(), and `names` which is the list + of the elements in `src`. + + callable(src, names) -> ignored_name + + It returns a list of names relative to the src path, of elements + that will not be copied. + XXX Consider this example code rather than the ultimate tool. """ names = os.listdir(src) + if ignore is not None: + ignored_names = ignore(src, names) + else: + ignored_names = set() + os.makedirs(dst) errors = [] for name in names: + if name in ignored_names: + continue srcname = os.path.join(src, name) dstname = os.path.join(dst, name) try: @@ -119,7 +148,7 @@ linkto = os.readlink(srcname) os.symlink(linkto, dstname) elif os.path.isdir(srcname): - copytree(srcname, dstname, symlinks) + copytree(srcname, dstname, symlinks, ignore) else: copy2(srcname, dstname) # XXX What about devices, sockets etc.? @@ -139,7 +168,7 @@ if errors: raise Error, errors -def rmtree(path, ignore_errors=False, onerror=None): +def rmtree(path, ignore_errors=False, onerror=None): """Recursively delete a directory tree. If ignore_errors is set, errors are ignored; otherwise, if onerror Index: Lib/test/test_shutil.py =================================================================== --- Lib/test/test_shutil.py (revision 62659) +++ Lib/test/test_shutil.py (working copy) @@ -108,7 +108,83 @@ if os.path.exists(path): shutil.rmtree(path) + def test_copytree_with_exclude(self): + + def write_data(path, data): + f = open(path, "w") + f.write(data) + f.close() + def read_data(path): + f = open(path) + data = f.read() + f.close() + return data + + # creating data + join = os.path.join + exists = os.path.exists + src_dir = tempfile.mkdtemp() + dst_dir = join(tempfile.mkdtemp(), 'destination') + write_data(join(src_dir, 'test.txt'), '123') + write_data(join(src_dir, 'test.tmp'), '123') + os.mkdir(join(src_dir, 'test_dir')) + write_data(join(src_dir, 'test_dir', 'test.txt'), '456') + os.mkdir(join(src_dir, 'test_dir2')) + write_data(join(src_dir, 'test_dir2', 'test.txt'), '456') + os.mkdir(join(src_dir, 'test_dir2', 'subdir')) + os.mkdir(join(src_dir, 'test_dir2', 'subdir2')) + write_data(join(src_dir, 'test_dir2', 'subdir', 'test.txt'), '456') + write_data(join(src_dir, 'test_dir2', 'subdir2', 'test.py'), '456') + + + # testing glob-like patterns + try: + patterns = shutil.ignore_patterns('*.tmp', 'test_dir2') + shutil.copytree(src_dir, dst_dir, ignore=patterns) + # checking the result: some elements should not be copied + self.assert_(exists(join(dst_dir, 'test.txt'))) + self.assert_(not exists(join(dst_dir, 'test.tmp'))) + self.assert_(not exists(join(dst_dir, 'test_dir2'))) + finally: + if os.path.exists(dst_dir): + shutil.rmtree(dst_dir) + try: + patterns = shutil.ignore_patterns('*.tmp', 'subdir*') + shutil.copytree(src_dir, dst_dir, ignore=patterns) + # checking the result: some elements should not be copied + self.assert_(not exists(join(dst_dir, 'test.tmp'))) + self.assert_(not exists(join(dst_dir, 'test_dir2', 'subdir2'))) + self.assert_(not exists(join(dst_dir, 'test_dir2', 'subdir'))) + finally: + if os.path.exists(dst_dir): + shutil.rmtree(dst_dir) + + # testing callable-style + try: + def _filter(src, names): + res = [] + for name in names: + path = os.path.join(src, name) + + if (os.path.isdir(path) and + path.split()[-1] == 'subdir'): + res.append(name) + elif os.path.splitext(path)[-1] in ('.py'): + res.append(name) + return res + + shutil.copytree(src_dir, dst_dir, ignore=_filter) + + # checking the result: some elements should not be copied + self.assert_(not exists(join(dst_dir, 'test_dir2', 'subdir2', + 'test.py'))) + self.assert_(not exists(join(dst_dir, 'test_dir2', 'subdir'))) + + finally: + if os.path.exists(dst_dir): + shutil.rmtree(dst_dir) + if hasattr(os, "symlink"): def test_dont_copy_file_onto_link_to_itself(self): # bug 851123.