Index: Doc/library/filecmp.rst =================================================================== --- Doc/library/filecmp.rst (revision 62466) +++ Doc/library/filecmp.rst (working copy) @@ -59,13 +59,29 @@ :class:`dircmp` instances are built using this constructor: -.. class:: dircmp(a, b[, ignore[, hide]]) +.. class:: dircmp(a, b [, ignore][, hide][, match]) - Construct a new directory comparison object, to compare the directories *a* and - *b*. *ignore* is a list of names to ignore, and defaults to ``['RCS', 'CVS', - 'tags']``. *hide* is a list of names to hide, and defaults to ``[os.curdir, - os.pardir]``. + Construct a new directory comparison object, to compare the directories *a* + and *b*. + *ignore* is an iterable of patterns for names to ignore, and defaults to + ``['RCS', 'CVS', 'tags']``. + *hide* is an iterable of patterns for names to hide, + and defaults to ``[os.curdir, os.pardir]``. + *match* is a function used to compare the names of directories and files to + the patterns provided in `ignore` and `hide`. match(name, pattern) is called + for each sub-directory and each file in *a* and *b* and each pattern given + in *ignore* and *hide*, after each name and pattern being "normcased" by + os.path.normcase. It defaults to string comparison. When it returns True, + the corresponding directory or file is excluded from the comparison. +Example:: + +To exclude all sub-directories / files matching the pattern '*.tmp':: + + >> import filecmp, fnmatch + >> dc = filecmp.dircmp('dir1', dir2', ignore='*.tmp', match=fnmatch.fnmatch) + + The :class:`dircmp` class provides the following methods: @@ -150,4 +166,3 @@ .. attribute:: dircmp.subdirs A dictionary mapping names in :attr:`common_dirs` to :class:`dircmp` objects. - Index: Lib/filecmp.py =================================================================== --- Lib/filecmp.py (revision 62466) +++ Lib/filecmp.py (working copy) @@ -12,7 +12,7 @@ import os import stat import warnings -from itertools import ifilter, ifilterfalse, imap, izip +from itertools import ifilter, ifilterfalse, imap, izip, chain __all__ = ["cmp","dircmp","cmpfiles"] @@ -78,12 +78,20 @@ class dircmp: """A class that manages the comparison of 2 directories. - dircmp(a,b,ignore=None,hide=None) + dircmp(a,b,ignore=None,hide=None,match=None) A and B are directories. - IGNORE is a list of names to ignore, + IGNORE is an iterable of patterns for names to ignore, defaults to ['RCS', 'CVS', 'tags']. - HIDE is a list of names to hide, + HIDE is an iterable of patterns for names to hide, defaults to [os.curdir, os.pardir]. + MATCH is a function used to compare the names of directories and files + to the patterns provided in IGNORE and HIDE. match(name, pattern) is + called for each sub-directory and each file in A and B and each + pattern given in IGNORE and HIDE, after each name and pattern being + "normcased" by os.path.normcase. + It defaults to string comparison. + When it returns True, the corresponding directory or file is excluded + from the comparison. High level usage: x = dircmp(dir1, dir2) @@ -109,7 +117,7 @@ subdirs: a dictionary of dircmp objects, keyed by names in common_dirs. """ - def __init__(self, a, b, ignore=None, hide=None): # Initialize + def __init__(self, a, b, ignore=None, hide=None, match=None): self.left = a self.right = b if hide is None: @@ -120,12 +128,17 @@ self.ignore = ['RCS', 'CVS', 'tags'] # Names ignored in comparison else: self.ignore = ignore + if match is None: + self.match = str.__eq__ + else: + self.match = match def phase0(self): # Compare everything except common subdirectories - self.left_list = _filter(os.listdir(self.left), - self.hide+self.ignore) - self.right_list = _filter(os.listdir(self.right), - self.hide+self.ignore) + patterns = [os.path.normcase(p) + for p in chain(self.hide, self.ignore)] + self.left_list = _filter(os.listdir(self.left), patterns, self.match) + self.right_list = _filter(os.listdir(self.right), patterns, + self.match) self.left_list.sort() self.right_list.sort() @@ -273,10 +286,15 @@ return 2 -# Return a copy with items that occur in skip removed. +# Return a copy with items that match a pattern in skip removed. # -def _filter(flist, skip): - return list(ifilterfalse(skip.__contains__, flist)) +def _filter(flist, skip, match): + def _match_any(name): + for pattern in skip: + if match(os.path.normcase(name), pattern): + return True + return False + return list(ifilterfalse(_match_any, flist)) # Demonstration and testing. Index: Lib/test/test_filecmp.py =================================================================== --- Lib/test/test_filecmp.py (revision 62466) +++ Lib/test/test_filecmp.py (working copy) @@ -1,5 +1,5 @@ -import os, filecmp, shutil, tempfile, shutil +import os, filecmp, shutil, tempfile, fnmatch import unittest from test import test_support @@ -52,10 +52,21 @@ shutil.rmtree(dir, True) os.mkdir(dir) if self.caseinsensitive and dir is self.dir_same: - fn = 'FiLe' # Verify case-insensitive comparison + fn1 = 'FiLe' # Verify case-insensitive comparison + fn2 = fn1 + '.TMP' + dir_ign = os.path.join(dir, 'Dir-Ignore') else: - fn = 'file' - output = open(os.path.join(dir, fn), 'w') + fn1 = 'file' + fn2 = fn1 + '.tmp' + dir_ign = os.path.join(dir, 'dir-ignore') + os.mkdir(dir_ign) + for dn in [dir, dir_ign]: + for fn in [fn1, fn2]: + output = open(os.path.join(dn, fn), 'w') + output.write(data) + output.close() + output = open(os.path.join(dir_ign, os.path.basename(dir) + + '.ign'), 'w') output.write(data) output.close() @@ -95,10 +106,42 @@ (['file'], ['file2'], []), "Comparing mismatched directories fails") + def test_dircmp_default(self): + # Check attributes for comparison of two identical directories + ignore = ['dir-ignore', 'file.tmp'] + d = filecmp.dircmp(self.dir, self.dir_same, ignore=ignore) + if self.caseinsensitive: + self.assertEqual([d.left_list, d.right_list],[['file'], ['FiLe']]) + else: + self.assertEqual([d.left_list, d.right_list],[['file'], ['file']]) + self.failUnless(d.common == ['file']) + self.failUnless(d.left_only == d.right_only == []) + self.failUnless(d.same_files == ['file']) + self.failUnless(d.diff_files == []) - def test_dircmp(self): + # Check attributes for comparison of two different directories + d = filecmp.dircmp(self.dir, self.dir_diff, ignore=ignore) + self.failUnless(d.left_list == ['file']) + self.failUnless(d.right_list == ['file', 'file2']) + self.failUnless(d.common == ['file']) + self.failUnless(d.left_only == []) + self.failUnless(d.right_only == ['file2']) + self.failUnless(d.same_files == ['file']) + self.failUnless(d.diff_files == []) + + # Add different file2 + output = open(os.path.join(self.dir, 'file2'), 'w') + output.write('Different contents.\n') + output.close() + d = filecmp.dircmp(self.dir, self.dir_diff, ignore=ignore) + self.failUnless(d.same_files == ['file']) + self.failUnless(d.diff_files == ['file2']) + + def test_dircmp_fnmatch(self): # Check attributes for comparison of two identical directories - d = filecmp.dircmp(self.dir, self.dir_same) + ignore = ['*ignore*', '*.tmp'] + d = filecmp.dircmp(self.dir, self.dir_same, ignore=ignore, + match=fnmatch.fnmatch) if self.caseinsensitive: self.assertEqual([d.left_list, d.right_list],[['file'], ['FiLe']]) else: @@ -109,7 +152,8 @@ self.failUnless(d.diff_files == []) # Check attributes for comparison of two different directories - d = filecmp.dircmp(self.dir, self.dir_diff) + d = filecmp.dircmp(self.dir, self.dir_diff, ignore=ignore, + match=fnmatch.fnmatch) self.failUnless(d.left_list == ['file']) self.failUnless(d.right_list == ['file', 'file2']) self.failUnless(d.common == ['file']) @@ -122,7 +166,8 @@ output = open(os.path.join(self.dir, 'file2'), 'w') output.write('Different contents.\n') output.close() - d = filecmp.dircmp(self.dir, self.dir_diff) + d = filecmp.dircmp(self.dir, self.dir_diff, ignore=ignore, + match=fnmatch.fnmatch) self.failUnless(d.same_files == ['file']) self.failUnless(d.diff_files == ['file2'])