diff --git a/Lib/ntpath.py b/Lib/ntpath.py index e7cbfe17ecb3c8..21a56cf47d9f79 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -842,24 +842,24 @@ 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 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 +# first path given in the iterable. 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.""" - + """Given an iterable of path names, returns the longest common sub-path.""" + paths = tuple(map(os.fspath, paths)) if not paths: - raise ValueError('commonpath() arg is an empty sequence') + raise ValueError('commonpath() arg is an empty iterable') - paths = tuple(map(os.fspath, paths)) - if isinstance(paths[0], bytes): + path = paths[0] + if isinstance(path, bytes): sep = b'\\' altsep = b'/' curdir = b'.' @@ -869,37 +869,34 @@ def commonpath(paths): curdir = '.' try: - drivesplits = [splitroot(p.replace(altsep, sep).lower()) for p in paths] - split_paths = [p.split(sep) for d, r, p in drivesplits] - - if len({r for d, r, p in drivesplits}) != 1: - raise ValueError("Can't mix absolute and relative paths") - - # 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. - if len({d for d, r, p in drivesplits}) != 1: - raise ValueError("Paths don't have the same drive") - - drive, root, path = splitroot(paths[0].replace(altsep, sep)) - common = path.split(sep) - common = [c for c in common if c and c != curdir] - - split_paths = [[c for c in s if c and c != curdir] for s in split_paths] - 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)] - - return drive + root + sep.join(common) + rootsplits = [splitroot(p.replace(altsep, sep).lower()) for p in paths] except (TypeError, AttributeError): genericpath._check_arg_types('commonpath', *paths) 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. + if len({drt[0] for drt in rootsplits}) != 1: + raise ValueError("Paths don't have the same drive") + + if len({drt[1] for drt in rootsplits}) != 1: + raise ValueError("Can't mix absolute and relative paths") + + drive, root, tail = splitroot(path.replace(altsep, sep)) + common = [c for c in tail.split(sep) if c and c != curdir] + split_paths = [ + [c for c in drt[2].split(sep) if c and c != curdir] + for drt in rootsplits + ] + s1 = min(split_paths) + s2 = max(split_paths) + for i, c in enumerate(s1): + if c != s2[i]: + return drive + root + sep.join(common[:i]) + + return drive + root + sep.join(common[:len(s1)]) + try: # The isdir(), isfile(), islink() and exists() implementations in diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 33943b4403636a..f1eb7b1d895872 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -432,7 +432,7 @@ def realpath(filename, *, strict=False): """Return the canonical path of the specified filename, eliminating any symbolic links encountered in the path.""" filename = os.fspath(filename) - path, ok = _joinrealpath(filename[:0], filename, strict, {}) + path, _ = _joinrealpath(filename[:0], filename, strict, {}) return abspath(path) # Join two paths, normalizing and eliminating any symbolic links @@ -538,45 +538,45 @@ 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.""" + try: + # Raises TypeError if paths is not iterable + _, roots, tails = zip(*map(splitroot, paths)) + except ValueError: + raise ValueError('commonpath() arg is an empty iterable') from None - paths = tuple(map(os.fspath, paths)) + try: + root = min(roots) + except (TypeError, AttributeError): + genericpath._check_arg_types('commonpath', *( + # Can't use paths, can be an iterable + root + tail for root, tail in zip(roots, tails) + )) + raise - if not paths: - raise ValueError('commonpath() arg is an empty sequence') + if not root and max(roots): + raise ValueError("Can't mix absolute and relative paths") - if isinstance(paths[0], bytes): + if isinstance(root, bytes): sep = b'/' curdir = b'.' else: sep = '/' curdir = '.' - try: - split_paths = [path.split(sep) for path in paths] + split_paths = [ + [c for c in tail.split(sep) if c and c != curdir] for tail in tails + ] + s1 = min(split_paths) + s2 = max(split_paths) + for i, c in enumerate(s1): + if c != s2[i]: + return root + sep.join(s1[:i]) - try: - isabs, = set(p[:1] == sep for p in paths) - except ValueError: - raise ValueError("Can't mix absolute and relative paths") from None - - split_paths = [[c for c in s if c and c != curdir] for s in split_paths] - s1 = min(split_paths) - s2 = max(split_paths) - common = s1 - for i, c in enumerate(s1): - if c != s2[i]: - 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) - raise + return root + sep.join(s1) diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 9cb03e3cd5de8d..c816f99e7e9f1b 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -871,11 +871,14 @@ def check_error(exc, paths): self.assertRaises(exc, ntpath.commonpath, [os.fsencode(p) for p in paths]) + self.assertRaises(TypeError, ntpath.commonpath, None) self.assertRaises(ValueError, ntpath.commonpath, []) + self.assertRaises(ValueError, ntpath.commonpath, iter([])) check_error(ValueError, ['C:\\Program Files', 'Program Files']) check_error(ValueError, ['C:\\Program Files', 'C:Program Files']) check_error(ValueError, ['\\Program Files', 'Program Files']) check_error(ValueError, ['Program Files', 'C:\\Program Files']) + check(['C:\\Program Files'], 'C:\\Program Files') check(['C:\\Program Files', 'C:\\Program Files'], 'C:\\Program Files') check(['C:\\Program Files\\', 'C:\\Program Files'], diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index cbb7c4c52d9697..996157a7d867e4 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -749,6 +749,9 @@ def check_error(exc, paths): self.assertRaises(TypeError, posixpath.commonpath, ['usr/lib/', b'/usr/lib/python3']) + # gh-117201: Handle leading `//` for `posixpath.commonpath` + check(['//foo/bar', '//foo/baz'], '//foo') + class PosixCommonTest(test_genericpath.CommonTest, unittest.TestCase): pathmodule = posixpath 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..cb232bec3a593d --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-03-26-20-20-36.gh-issue-117201.qaS7em.rst @@ -0,0 +1,2 @@ +* Handle leading ``//`` for :func:`posixpath.commonpath` using :func:`posixpath.splitroot`. +* Raise TypeError for non-sequences for :func:`ntpath.commonpath`.