Skip to content

gh-117201: Handle leading // for posixpath.commonpath #117202

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed
65 changes: 31 additions & 34 deletions Lib/ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'.'
Expand All @@ -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
Expand Down
56 changes: 28 additions & 28 deletions Lib/posixpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
3 changes: 3 additions & 0 deletions Lib/test/test_ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
3 changes: 3 additions & 0 deletions Lib/test/test_posixpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* Handle leading ``//`` for :func:`posixpath.commonpath` using :func:`posixpath.splitroot`.
* Raise TypeError for non-sequences for :func:`ntpath.commonpath`.
Loading