diff -r ec00f8570c55 Doc/library/os.path.rst --- a/Doc/library/os.path.rst Mon Nov 05 22:23:16 2012 +0200 +++ b/Doc/library/os.path.rst Mon Nov 12 19:10:56 2012 -0500 @@ -54,11 +54,24 @@ empty string (``''``). +.. function:: commonpath(paths) + + Return the longest common sub-path of each pathname in the sequence + *paths*. Return ``None`` if *paths* contains both absolute and relative + pathnames, or if *paths* is empty. Unlike :func:`commonprefix`, this + returns a valid path. + + Availability: Unix, Windows + + .. versionadded:: 3.4 + + .. function:: commonprefix(list) - Return the longest path prefix (taken character-by-character) that is a prefix - of all paths in *list*. If *list* is empty, return the empty string (``''``). - Note that this may return invalid paths because it works a character at a time. + Return the longest path prefix (taken character-by-character) that is a + prefix of all paths in *list*. If *list* is empty, return the empty string + (``''``). Note that this may return invalid paths because it works a + character at a time. To obtain a valid path, see :func:`commonpath`. .. function:: dirname(path) diff -r ec00f8570c55 Lib/ntpath.py --- a/Lib/ntpath.py Mon Nov 05 22:23:16 2012 +0200 +++ b/Lib/ntpath.py Mon Nov 12 19:10:56 2012 -0500 @@ -17,7 +17,7 @@ "ismount", "expanduser","expandvars","normpath","abspath", "splitunc","curdir","pardir","sep","pathsep","defpath","altsep", "extsep","devnull","realpath","supports_unicode_filenames","relpath", - "samefile", "sameopenfile",] + "samefile", "sameopenfile", 'commonpath'] # strings representing various path-related bits and pieces # These are primarily for export; internally, they are hardcoded. @@ -636,6 +636,66 @@ return join(*rel_list) +# Return the longest common sub-path of the sequence of paths given as input. +# The function is case-insensitive and 'separator-insensitive', i.e. if the +# only difference between two paths is the use of '\' versus '/' as separator, +# they are deemed to be equal. +# +# However, the returned path will have the standard '\' separator (even if the +# given paths had the alternative '/' separator) and will have the case of the +# first path given in the sequence. Additionally, any trailing separator is +# stripped from the returned path. + +def commonpath(paths): + """Given a sequence of path names, returns the longest common sub-path.""" + + if not paths: + return None + + if any(isabs(p) for p in paths) and any(not isabs(p) for p in paths): + # There is a mix of absolute and relative pathnames. + return None + + sep, altsep = _get_sep(paths[0]), _get_altsep(paths[0]) + drivesplits = [splitdrive(normcase(p)) for p in paths] + + try: + split_paths = [p.split(sep) for d, p in drivesplits] + except TypeError: + valid_types = all(isinstance(p, (str, bytes, bytearray)) + for p in paths) + if valid_types: + # Must have a mixture of text and binary data. + raise TypeError("Can't mix strings and bytes in path" + "components.") from None + raise + + # Check that all drive letters or UNC paths match. The check is made only + # now otherwise type errors for mixing strings and bytes would not be + # caught. + first_drive = drivesplits[0][0] + if not all(d == first_drive for d, p in drivesplits): + return None + + drive, path = splitdrive(paths[0].replace(altsep, sep)) + common = path.split(sep) + prefix = drive + sep if isabs(paths[0]) else '' + + s1 = min(split_paths) + s2 = max(split_paths) + for i, c in enumerate(s1): + if c != s2[i]: + common = common[:i] + break + else: + common = common[:len(s1)] + + if not common: + return prefix + else: + return prefix + join(*common).rstrip(sep) + + # determine if two files are in fact the same file try: # GetFinalPathNameByHandle is available starting with Windows 6.0. diff -r ec00f8570c55 Lib/posixpath.py --- a/Lib/posixpath.py Mon Nov 05 22:23:16 2012 +0200 +++ b/Lib/posixpath.py Mon Nov 12 19:10:56 2012 -0500 @@ -22,7 +22,8 @@ "ismount", "expanduser","expandvars","normpath","abspath", "samefile","sameopenfile","samestat", "curdir","pardir","sep","pathsep","defpath","altsep","extsep", - "devnull","realpath","supports_unicode_filenames","relpath"] + "devnull","realpath","supports_unicode_filenames","relpath", + "commonpath"] # Strings representing various path-related bits and pieces. # These are primarily for export; internally, they are hardcoded. @@ -467,3 +468,46 @@ if not rel_list: return curdir return join(*rel_list) + + +# Return the longest common sub-path of the sequence of paths given as input. +# The paths are not normalized before comparing them (this is the +# responsibility of the caller). Any trailing separator is stripped from the +# returned path. + +def commonpath(paths): + """Given a sequence of path names, returns the longest common sub-path.""" + + if not paths: + return None + + if any(isabs(p) for p in paths) and any(not isabs(p) for p in paths): + # There is a mix of absolute and relative pathnames. + return None + + sep = _get_sep(paths[0]) + prefix = sep if isabs(paths[0]) else '' + + try: + split_paths = [path.split(sep) for path in paths] + except TypeError: + valid_types = all(isinstance(p, (str, bytes, bytearray)) + for p in paths) + if valid_types: + # Must have a mixture of text and binary data + raise TypeError("Can't mix strings and bytes in path " + "components.") from None + raise + + s1 = min(split_paths) + s2 = max(split_paths) + common = s1 + for i, c in enumerate(s1): + if c != s2[i]: + common = s1[:i] + break + + if not common: + return prefix + else: + return prefix + join(*common).rstrip(sep) diff -r ec00f8570c55 Lib/test/test_ntpath.py --- a/Lib/test/test_ntpath.py Mon Nov 05 22:23:16 2012 +0200 +++ b/Lib/test/test_ntpath.py Mon Nov 12 19:10:56 2012 -0500 @@ -243,6 +243,46 @@ tester('ntpath.relpath("/a/b", "/a/b")', '.') tester('ntpath.relpath("c:/foo", "C:/FOO")', '.') + def test_commonpath(self): + + tester('ntpath.commonpath([])', None) + tester('ntpath.commonpath(["C:\\Program Files", "Documents/"])', None) + tester('ntpath.commonpath(["C:\\Program Files"])', 'C:\\Program Files') + tester('ntpath.commonpath(["C:\\Program Files", "C:\\Program Files"])', 'C:\\Program Files') + tester('ntpath.commonpath(["C:\\Program Files\\", "C:\\Program Files"])', 'C:\\Program Files') + tester('ntpath.commonpath(["C:\\Program Files\\", "C:\\Program Files\\"])', 'C:\\Program Files') + tester('ntpath.commonpath(["C:\\", "C:\\bin"])', 'C:\\') + tester('ntpath.commonpath(["C:\\Program Files", "C:\\bin"])', 'C:\\') + tester('ntpath.commonpath(["C:\\Program Files", "C:\\Program Files\\Bar"])', 'C:\\Program Files') + tester('ntpath.commonpath(["C:\\Program Files\\Foo", "C:\\Program Files\\Bar"])', 'C:\\Program Files') + tester('ntpath.commonpath([b"C:\\Program Files\\Foo", b"C:\\Program Files\\Bar"])', b'C:\\Program Files') + tester('ntpath.commonpath(["C:\\Program Files", "C:\\Projects"])', 'C:\\') + tester('ntpath.commonpath(["C:\\Program Files\\", "C:\\Projects"])', 'C:\\') + + tester('ntpath.commonpath(["C:\\Program Files\\Foo", "C:/Program Files/Bar"])', 'C:\\Program Files') + tester('ntpath.commonpath(["C:\\Program Files\\Foo", "c:/program files/bar"])', 'C:\\Program Files') + tester('ntpath.commonpath(["c:/program files/bar", "C:\\Program Files\\Foo"])', 'c:\\program files') + + tester('ntpath.commonpath(["C:\\Program Files\\Foo", "D:/Program Files/Bar"])', None) + + # tester function won't work with the following tests + self.assertEquals(ntpath.commonpath(['spam']), 'spam') + self.assertEqual(ntpath.commonpath(['spam']), 'spam') + self.assertEqual(ntpath.commonpath(['spam', 'spam']), 'spam') + self.assertEqual(ntpath.commonpath(['spam', 'alot']), '') + self.assertEqual(ntpath.commonpath(['and\\jam', 'and\\spam']), 'and') + self.assertEqual( + ntpath.commonpath(['and\\jam', 'and\\spam', 'alot']), '') + self.assertEqual( + ntpath.commonpath(['and\\jam', 'and\\spam', 'and']), 'and') + + self.assertEqual(ntpath.commonpath(['']), '') + self.assertEqual(ntpath.commonpath(['', 'spam\\alot']), '') + self.assertIsNone(ntpath.commonpath(['', '\\spam\\alot'])) + + self.assertRaises(TypeError, ntpath.commonpath, + [b'C:\\Program Files', 'C:\\Program Files\\Foo']) + def test_sameopenfile(self): with TemporaryFile() as tf1, TemporaryFile() as tf2: # Make sure the same file is really the same diff -r ec00f8570c55 Lib/test/test_posixpath.py --- a/Lib/test/test_posixpath.py Mon Nov 05 22:23:16 2012 +0200 +++ b/Lib/test/test_posixpath.py Mon Nov 12 19:10:56 2012 -0500 @@ -523,6 +523,49 @@ with open(fname, "wb") as a, open(fname, "wb") as b: self.assertTrue(posixpath.sameopenfile(a.fileno(), b.fileno())) + def test_commonpath(self): + + self.assertIsNone(posixpath.commonpath([])) + self.assertIsNone(posixpath.commonpath(['/usr', 'spam'])) + self.assertEqual(posixpath.commonpath(['/usr/local']), '/usr/local') + self.assertEqual(posixpath.commonpath(['/usr/local', '/usr/local']), + '/usr/local') + self.assertEqual(posixpath.commonpath(['/usr/local/', '/usr/local']), + '/usr/local') + self.assertEqual(posixpath.commonpath(['/usr/local/', '/usr/local/']), + '/usr/local') + self.assertEqual(posixpath.commonpath(['/', '/dev']), '/') + self.assertEqual(posixpath.commonpath(['/usr', '/dev']), '/') + self.assertEqual( + posixpath.commonpath(['/usr/lib/', '/usr/lib/python3']), + '/usr/lib') + self.assertEqual( + posixpath.commonpath([b'/usr/lib/', b'/usr/lib/python3']), + b'/usr/lib') + self.assertEqual(posixpath.commonpath(['/usr/lib/', '/usr/lib64/']), + '/usr') + + self.assertEqual(posixpath.commonpath(['/usr/lib', '/usr/lib64']), + '/usr') + self.assertEqual(posixpath.commonpath(['/usr/lib/', '/usr/lib64']), + '/usr') + + self.assertEqual(posixpath.commonpath(['spam']), 'spam') + self.assertEqual(posixpath.commonpath(['spam', 'spam']), 'spam') + self.assertEqual(posixpath.commonpath(['spam', 'alot']), '') + self.assertEqual(posixpath.commonpath(['and/jam', 'and/spam']), 'and') + self.assertEqual( + posixpath.commonpath(['and/jam', 'and/spam', 'alot']), '') + self.assertEqual( + posixpath.commonpath(['and/jam', 'and/spam', 'and']), 'and') + + self.assertEqual(posixpath.commonpath(['']), '') + self.assertEqual(posixpath.commonpath(['', 'spam/alot']), '') + self.assertIsNone(posixpath.commonpath(['', '/spam/alot'])) + + self.assertRaises(TypeError, posixpath.commonpath, + [b'/usr/lib/', '/usr/lib/python3']) + class PosixCommonTest(test_genericpath.CommonTest): pathmodule = posixpath