diff -r 47943fe516ec Doc/library/os.path.rst --- a/Doc/library/os.path.rst Mon Nov 05 21:33:22 2012 +1000 +++ b/Doc/library/os.path.rst Mon Nov 05 15:22:38 2012 -0500 @@ -54,6 +54,16 @@ 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. + + .. versionadded:: 3.4 + + .. function:: commonprefix(list) Return the longest path prefix (taken character-by-character) that is a prefix diff -r 47943fe516ec Lib/ntpath.py --- a/Lib/ntpath.py Mon Nov 05 21:33:22 2012 +1000 +++ b/Lib/ntpath.py Mon Nov 05 15:22:38 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,50 @@ return join(*rel_list) +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 + + drivespecs = [splitdrive(p)[0] for p in paths] + pathspecs = [splitdrive(normcase(p))[1] for p in paths] + + # Check that all drive letters or UNC paths match. + drive_or_unc = drivespecs[0] + try: + if not all(d == drive_or_unc for d in drivespecs): + return None + 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 + + + sep = _get_sep(pathspecs[0]) + split_paths = [path.split(sep) for path in pathspecs] + 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 drive_or_unc + else: + return drive_or_unc + 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 47943fe516ec Lib/posixpath.py --- a/Lib/posixpath.py Mon Nov 05 21:33:22 2012 +1000 +++ b/Lib/posixpath.py Mon Nov 05 15:22:38 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,41 @@ if not rel_list: return curdir return join(*rel_list) + + +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 47943fe516ec Lib/test/test_posixpath.py --- a/Lib/test/test_posixpath.py Mon Nov 05 21:33:22 2012 +1000 +++ b/Lib/test/test_posixpath.py Mon Nov 05 15:22:38 2012 -0500 @@ -523,6 +523,40 @@ 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(['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.assertRaises(TypeError, posixpath.commonpath, + [b'/usr/lib/', '/usr/lib/python3']) + class PosixCommonTest(test_genericpath.CommonTest): pathmodule = posixpath