diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 76ee721bfb5e33..ad356b0d9e67d8 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -515,19 +515,20 @@ def relpath(path, start=None): raise -# Return the longest common sub-path of the sequence of paths given as input. +# Return the longest common sub-path of the iterable 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.""" + """Given an iterable of path names, returns the longest common sub-path.""" - paths = tuple(map(os.fspath, paths)) + rootsplits = map(splitroot, paths) - if not paths: - raise ValueError('commonpath() arg is an empty sequence') + if not rootsplits: + raise ValueError('commonpath() arg is an empty iterable') + _, roots, paths = zip(*rootsplits) if isinstance(paths[0], bytes): sep = b'/' curdir = b'.' @@ -536,12 +537,11 @@ def commonpath(paths): curdir = '.' try: + prefix = min(roots) split_paths = [path.split(sep) for path in paths] - try: - isabs, = set(p[:1] == sep for p in paths) - except ValueError: - raise ValueError("Can't mix absolute and relative paths") from None + if not prefix and any(roots): + raise ValueError("Can't mix absolute and relative paths") split_paths = [[c for c in s if c and c != curdir] for s in split_paths] s1 = min(split_paths) @@ -552,7 +552,6 @@ def commonpath(paths): common = s1[:i] break - prefix = sep if isabs else sep[:0] return prefix + sep.join(common) except (TypeError, AttributeError): genericpath._check_arg_types('commonpath', *paths) diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index cbb7c4c52d9697..386aea178ead3d 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -700,20 +700,34 @@ def check(paths, expected): os.fsencode(expected)) def check_error(exc, paths): self.assertRaises(exc, posixpath.commonpath, paths) + self.assertRaises(exc, posixpath.commonpath, paths[::-1]) self.assertRaises(exc, posixpath.commonpath, [os.fsencode(p) for p in paths]) + self.assertRaises(exc, posixpath.commonpath, + [os.fsencode(p) for p in paths[::-1]]) self.assertRaises(TypeError, posixpath.commonpath, None) self.assertRaises(ValueError, posixpath.commonpath, []) self.assertRaises(ValueError, posixpath.commonpath, iter([])) check_error(ValueError, ['/usr', 'usr']) - check_error(ValueError, ['usr', '/usr']) + check_error(ValueError, ['//usr', 'usr']) + check_error(ValueError, ['///usr', 'usr']) + check_error(ValueError, ['//usr', '/usr', 'usr']) + # gh-117201: Handle leading slashes check(['/usr/local'], '/usr/local') + check(['//usr/local'], '//usr/local') + check(['///usr/local'], '/usr/local') check(['/usr/local', '/usr/local'], '/usr/local') + check(['/usr/local', '//usr/local'], '/usr/local') + check(['/usr/local', '///usr/local'], '/usr/local') + check(['//usr/local', '//usr/local'], '//usr/local') + check(['//usr/local', '///usr/local'], '/usr/local') + check(['///usr/local', '///usr/local'], '/usr/local') + check(['/usr/local/', '/usr/local'], '/usr/local') check(['/usr/local/', '/usr/local/'], '/usr/local') - check(['/usr//local', '//usr/local'], '/usr/local') + check(['/usr//local', '/usr/local'], '/usr/local') check(['/usr/./local', '/./usr/local'], '/usr/local') check(['/', '/dev'], '/') check(['/usr', '/dev'], '/') diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-03-26-20-20-36.gh-issue-117201.qaS7em.rst b/Misc/NEWS.d/next/Core and Builtins/2024-03-26-20-20-36.gh-issue-117201.qaS7em.rst new file mode 100644 index 00000000000000..e0fc6db6b941b8 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-03-26-20-20-36.gh-issue-117201.qaS7em.rst @@ -0,0 +1 @@ +Handle leading ``//`` for :func:`posixpath.commonpath` using :func:`posixpath.splitroot`.