#2663: support an *ignore* argument to shutil.copytree(). Patch by Tarek Ziade.

This is a new feature, but Barry authorized adding it in the beta period.
diff --git a/Lib/shutil.py b/Lib/shutil.py
index 6ce4023..3af280d 100644
--- a/Lib/shutil.py
+++ b/Lib/shutil.py
@@ -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,32 @@
     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 directory
+    being visited by copytree(), and `names` which is the list of
+    `src` contents, as returned by os.listdir():
+
+        callable(src, names) -> ignored_names
+
+    Since copytree() is called recursively, the callable will be
+    called once for each directory that is copied. It returns a
+    list of names relative to the `src` directory that should
+    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 +150,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.?
diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py
index dfdec0d..fa5bbb1 100644
--- a/Lib/test/test_shutil.py
+++ b/Lib/test/test_shutil.py
@@ -108,6 +108,82 @@
                 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):