diff -r 4a9dbd9b152e Doc/library/os.path.rst --- a/Doc/library/os.path.rst Mon Nov 05 00:56:02 2012 +1000 +++ b/Doc/library/os.path.rst Sun Nov 04 11:05:17 2012 -0500 @@ -60,6 +60,18 @@ 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. +.. function:: commonpath(paths) + + Return the longest common sub-path of each pathname in the sequence + *paths*. The paths must be absolute, otherwise a ValueError is raised. + If *paths* is empty, return ``None``. If each path ends with a path + separator, the returned sub-path will also end with a separator, otherwise + it will not. Unlike :func:`commonprefix`, this returns a valid path. + + Availability: Unix. + + .. versionadded:: 3.4 + .. function:: dirname(path) diff -r 4a9dbd9b152e Lib/posixpath.py --- a/Lib/posixpath.py Mon Nov 05 00:56:02 2012 +1000 +++ b/Lib/posixpath.py Sun Nov 04 11:05:17 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,36 @@ if not rel_list: return curdir return join(*rel_list) + + +def commonpath(paths): + """Given a sequence of absolute path names, returns the longest common + sub-path.""" + + if not paths: + return None + + if not all(isabs(p) for p in paths): + raise ValueError('All paths must be absolute') + + sep = _get_sep(paths[0]) + + 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 + + zipped_components = list(zip(*split_paths)) + for i, cs in enumerate(zipped_components): + first = cs[0] + if not all(c == first for c in cs): + i = i - 1 # last matching index + break + common = [cs[0] for cs in zipped_components[:i+1]] + return sep + join(*common) diff -r 4a9dbd9b152e Lib/test/test_posixpath.py --- a/Lib/test/test_posixpath.py Mon Nov 05 00:56:02 2012 +1000 +++ b/Lib/test/test_posixpath.py Sun Nov 04 11:05:17 2012 -0500 @@ -523,6 +523,31 @@ 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.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/lib/', '/usr/lib/python3']), + '/usr/lib') + self.assertEqual( + posixpath.commonpath(['/usr/lib/', '/usr/lib/', '/dev/']), '/') + self.assertEqual( + posixpath.commonpath([b'/usr/lib/', b'/usr/lib/python3']), + b'/usr/lib') + self.assertEqual(posixpath.commonpath(['/usr/lib/', '/usr/lib64/']), + '/usr') + self.assertRaises(TypeError, posixpath.commonpath, + [b'/usr/lib/', '/usr/lib/python3']) + self.assertRaises(ValueError, posixpath.commonpath, ['spam', 'alot']) + class PosixCommonTest(test_genericpath.CommonTest): pathmodule = posixpath