From 247c9af449794ae54e6d0f4ef839ba2035b410c3 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Sun, 24 Mar 2024 22:01:04 +0100 Subject: [PATCH 01/29] Handle leading `//` for `posixpath.commonpath` --- Lib/posixpath.py | 46 ++++++++++++++++++-------------------- Lib/test/test_posixpath.py | 3 +++ 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 33943b4403636a..38ee5a1c180ab4 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -545,38 +545,36 @@ def relpath(path, start=None): def commonpath(paths): """Given a sequence 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') - if isinstance(paths[0], bytes): + try: + _, roots, tails = zip(*map(splitroot, paths)) + prefix = min(roots) + except (TypeError, AttributeError): + genericpath._check_arg_types('commonpath', *paths) + raise + + if not prefix and max(roots): + raise ValueError("Can't mix absolute and relative paths") + + if isinstance(prefix, bytes): sep = b'/' curdir = b'.' else: sep = '/' curdir = '.' - try: - 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 - - 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) + 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]: + common = s1[:i] + break + else: 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 prefix + sep.join(common) \ No newline at end of file diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index cbb7c4c52d9697..cf491482752086 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: `posixpath.commonpath` doesn't handle leading `//` properly + check(['//foo/bar', '//foo/baz'], '//foo') + class PosixCommonTest(test_genericpath.CommonTest, unittest.TestCase): pathmodule = posixpath From 0c38276ed948199ee0695d48ef5d93e0b25091cc Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Sun, 24 Mar 2024 22:47:41 +0100 Subject: [PATCH 02/29] raise TypeError for non-sequences --- Lib/posixpath.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 38ee5a1c180ab4..20f9825facbc59 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -545,11 +545,11 @@ def relpath(path, start=None): def commonpath(paths): """Given a sequence of path names, returns the longest common sub-path.""" - if not paths: + _, roots, tails = zip(*map(splitroot, paths)) + if not roots: raise ValueError('commonpath() arg is an empty sequence') try: - _, roots, tails = zip(*map(splitroot, paths)) prefix = min(roots) except (TypeError, AttributeError): genericpath._check_arg_types('commonpath', *paths) @@ -577,4 +577,4 @@ def commonpath(paths): else: common = s1 - return prefix + sep.join(common) \ No newline at end of file + return prefix + sep.join(common) From a9b0832cdd5cb4bdd2ad7a65f7b46d7d299a3312 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 20:20:37 +0000 Subject: [PATCH 03/29] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2024-03-26-20-20-36.gh-issue-117201.qaS7em.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2024-03-26-20-20-36.gh-issue-117201.qaS7em.rst 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`. From a5212827b3485c7a52b0f2a9af99ccc2ad7f4afd Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Tue, 26 Mar 2024 22:05:10 +0100 Subject: [PATCH 04/29] Remove `break` & `else` --- Lib/posixpath.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 20f9825facbc59..d94150e425ad26 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -572,9 +572,6 @@ def commonpath(paths): s2 = max(split_paths) for i, c in enumerate(s1): if c != s2[i]: - common = s1[:i] - break - else: - common = s1 + return prefix + sep.join(s1[:i]) - return prefix + sep.join(common) + return prefix + sep.join(s1) From 26eac213196480b81551c654b6b2fd9e84888932 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Tue, 26 Mar 2024 23:24:05 +0100 Subject: [PATCH 05/29] Handle errors for iterable --- Lib/posixpath.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index d94150e425ad26..c707f1f68da8f2 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -538,21 +538,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 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 a iterable of path names, returns the longest common sub-path.""" _, roots, tails = zip(*map(splitroot, paths)) if not roots: - raise ValueError('commonpath() arg is an empty sequence') + raise ValueError('commonpath() arg is an empty iterable') try: prefix = min(roots) except (TypeError, AttributeError): - genericpath._check_arg_types('commonpath', *paths) + 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 prefix and max(roots): From d83743d05cad06c6ebda2d748806142f163a5e14 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Wed, 27 Mar 2024 07:44:45 +0100 Subject: [PATCH 06/29] Remove redundant check --- Lib/posixpath.py | 18 ++++++++++-------- Lib/test/test_posixpath.py | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index c707f1f68da8f2..b446cda8eb4195 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -545,12 +545,14 @@ def relpath(path, start=None): def commonpath(paths): """Given a iterable of path names, returns the longest common sub-path.""" - _, roots, tails = zip(*map(splitroot, paths)) - if not roots: - raise ValueError('commonpath() arg is an empty iterable') + 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 try: - prefix = min(roots) + root = min(roots) except (TypeError, AttributeError): genericpath._check_arg_types('commonpath', *( # Can't use paths, can be an iterable @@ -558,10 +560,10 @@ def commonpath(paths): )) raise - if not prefix and max(roots): + if not root and max(roots): raise ValueError("Can't mix absolute and relative paths") - if isinstance(prefix, bytes): + if isinstance(root, bytes): sep = b'/' curdir = b'.' else: @@ -575,6 +577,6 @@ def commonpath(paths): s2 = max(split_paths) for i, c in enumerate(s1): if c != s2[i]: - return prefix + sep.join(s1[:i]) + return root + sep.join(s1[:i]) - return prefix + sep.join(s1) + return root + sep.join(s1) diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index cf491482752086..996157a7d867e4 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -749,7 +749,7 @@ def check_error(exc, paths): self.assertRaises(TypeError, posixpath.commonpath, ['usr/lib/', b'/usr/lib/python3']) - # gh-117201: `posixpath.commonpath` doesn't handle leading `//` properly + # gh-117201: Handle leading `//` for `posixpath.commonpath` check(['//foo/bar', '//foo/baz'], '//foo') From 1ae0c43910bbddd4ecd421dcac7e33ecd6b34703 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Thu, 28 Mar 2024 11:05:59 +0100 Subject: [PATCH 07/29] raise TypeError for non-sequences --- Lib/ntpath.py | 65 ++++++++++++++++++++--------------------- Lib/test/test_ntpath.py | 3 ++ 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index e7cbfe17ecb3c8..fda549c4385c2e 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 a 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/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'], From e6de234aa4194d552feb175d74e86ce8c5333076 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Thu, 28 Mar 2024 11:13:48 +0100 Subject: [PATCH 08/29] Update NEWS.d --- .../2024-03-26-20-20-36.gh-issue-117201.qaS7em.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index e0fc6db6b941b8..7141fe5f4b38c5 100644 --- 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 @@ -1 +1,2 @@ -Handle leading ``//`` for :func:`posixpath.commonpath` using :func:`posixpath.splitroot`. +* Handle leading ``//`` for :func:`posixpath.commonpath` using :func:`posixpath.splitroot`. +* Raise TypeError for non-sequences for :func:`ntpath.commonpath`. \ No newline at end of file From 012c12ee43a8ef8efd8042a8a8dce1d4510b9245 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Thu, 28 Mar 2024 11:15:22 +0100 Subject: [PATCH 09/29] Add newline --- .../2024-03-26-20-20-36.gh-issue-117201.qaS7em.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 7141fe5f4b38c5..cb232bec3a593d 100644 --- 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 @@ -1,2 +1,2 @@ * Handle leading ``//`` for :func:`posixpath.commonpath` using :func:`posixpath.splitroot`. -* Raise TypeError for non-sequences for :func:`ntpath.commonpath`. \ No newline at end of file +* Raise TypeError for non-sequences for :func:`ntpath.commonpath`. From fb1c91aa228c00d66c8b59a886d1804c20d1dd9f Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Thu, 28 Mar 2024 15:38:53 +0100 Subject: [PATCH 10/29] Speed up `posixpath.ismount` --- Lib/posixpath.py | 44 ++++++++++++++++---------------------------- 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index b446cda8eb4195..2b3b4062f37887 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -217,31 +217,19 @@ def ismount(path): except (OSError, ValueError): # It doesn't exist -- so not a mount point. :-) return False - else: - # A symlink can never be a mount point - if stat.S_ISLNK(s1.st_mode): - return False - path = os.fspath(path) - if isinstance(path, bytes): - parent = join(path, b'..') - else: - parent = join(path, '..') - parent = realpath(parent) + # A symlink can never be a mount point + if stat.S_ISLNK(s1.st_mode): + return False + + parent = realpath(dirname(path)) try: s2 = os.lstat(parent) except (OSError, ValueError): return False - dev1 = s1.st_dev - dev2 = s2.st_dev - if dev1 != dev2: - return True # path/.. on a different device as path - ino1 = s1.st_ino - ino2 = s2.st_ino - if ino1 == ino2: - return True # path/.. is the same i-node as path - return False + # path/.. on a different device as path or the same i-node as path + return s1.st_dev != s2.st_dev or s1.st_ino == s2.st_ino # Expand paths beginning with '~' or '~user'. @@ -380,29 +368,29 @@ def normpath(path): if isinstance(path, bytes): sep = b'/' empty = b'' - dot = b'.' - dotdot = b'..' + curdir = b'.' + pardir = b'..' else: sep = '/' empty = '' - dot = '.' - dotdot = '..' + curdir = '.' + pardir = '..' if path == empty: - return dot + return curdir _, initial_slashes, path = splitroot(path) comps = path.split(sep) new_comps = [] for comp in comps: - if comp in (empty, dot): + if comp in (empty, curdir): continue - if (comp != dotdot or (not initial_slashes and not new_comps) or - (new_comps and new_comps[-1] == dotdot)): + if (comp != pardir or (not initial_slashes and not new_comps) or + (new_comps and new_comps[-1] == pardir)): new_comps.append(comp) elif new_comps: new_comps.pop() comps = new_comps path = initial_slashes + sep.join(comps) - return path or dot + return path or curdir else: def normpath(path): From ca90fde42531efb86ab035ecafd0c0d54ff8cef0 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Thu, 28 Mar 2024 16:22:21 +0100 Subject: [PATCH 11/29] unnest `posixpath.expanduser` --- Lib/posixpath.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 2b3b4062f37887..8bd931ef9d5e8c 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -255,22 +255,7 @@ def expanduser(path): i = path.find(sep, 1) if i < 0: i = len(path) - if i == 1: - if 'HOME' not in os.environ: - try: - import pwd - except ImportError: - # pwd module unavailable, return path unchanged - return path - try: - userhome = pwd.getpwuid(os.getuid()).pw_dir - except KeyError: - # bpo-10496: if the current user identifier doesn't exist in the - # password database, return the path unchanged - return path - else: - userhome = os.environ['HOME'] - else: + if i != 1: try: import pwd except ImportError: @@ -278,7 +263,7 @@ def expanduser(path): return path name = path[1:i] if isinstance(name, bytes): - name = str(name, 'ASCII') + name = name.decode('ascii') try: pwent = pwd.getpwnam(name) except KeyError: @@ -286,6 +271,21 @@ def expanduser(path): # password database, return the path unchanged return path userhome = pwent.pw_dir + elif 'HOME' in os.environ: + userhome = os.environ['HOME'] + else: + try: + import pwd + except ImportError: + # pwd module unavailable, return path unchanged + return path + try: + userhome = pwd.getpwuid(os.getuid()).pw_dir + except KeyError: + # bpo-10496: if the current user identifier doesn't exist in the + # password database, return the path unchanged + return path + # if no user home, return the path unchanged on VxWorks if userhome is None and sys.platform == "vxworks": return path From 758e0d6b85404b26c903c389466296144afb885a Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Thu, 28 Mar 2024 17:28:48 +0100 Subject: [PATCH 12/29] Speed up `posixpath.expanduser` & `posixpath.normpath` --- Lib/ntpath.py | 2 +- Lib/posixpath.py | 71 ++++++++++++++++++++---------------------------- 2 files changed, 31 insertions(+), 42 deletions(-) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index fda549c4385c2e..21a56cf47d9f79 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -853,7 +853,7 @@ def relpath(path, start=None): # stripped from the returned path. def commonpath(paths): - """Given a iterable 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 iterable') diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 8bd931ef9d5e8c..4ff34ad5930f72 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -258,30 +258,24 @@ def expanduser(path): if i != 1: try: import pwd - except ImportError: + name = path[1:i] + if isinstance(name, bytes): + name = name.decode('ascii') + + userhome = pwd.getpwnam(name).pw_dir + except (ImportError, KeyError): # pwd module unavailable, return path unchanged - return path - name = path[1:i] - if isinstance(name, bytes): - name = name.decode('ascii') - try: - pwent = pwd.getpwnam(name) - except KeyError: # bpo-10496: if the user name from the path doesn't exist in the # password database, return the path unchanged return path - userhome = pwent.pw_dir elif 'HOME' in os.environ: userhome = os.environ['HOME'] else: try: import pwd - except ImportError: - # pwd module unavailable, return path unchanged - return path - try: userhome = pwd.getpwuid(os.getuid()).pw_dir - except KeyError: + except (ImportError, KeyError): + # pwd module unavailable, return path unchanged # bpo-10496: if the current user identifier doesn't exist in the # password database, return the path unchanged return path @@ -360,45 +354,40 @@ def expandvars(path): try: from posix import _path_normpath - + def normpath(path): + """Normalize path, eliminating double slashes, etc.""" + path = os.fspath(path) + if isinstance(path, bytes): + return os.fsencode(_path_normpath(os.fsdecode(path))) or b"." + return _path_normpath(path) or "." except ImportError: def normpath(path): """Normalize path, eliminating double slashes, etc.""" path = os.fspath(path) if isinstance(path, bytes): sep = b'/' - empty = b'' curdir = b'.' pardir = b'..' else: sep = '/' - empty = '' curdir = '.' pardir = '..' - if path == empty: + if not path: return curdir - _, initial_slashes, path = splitroot(path) - comps = path.split(sep) - new_comps = [] - for comp in comps: - if comp in (empty, curdir): + _, root, tail = splitroot(path) + comps = [] + for comp in tail.split(sep): + if not comp or comp == curdir: continue - if (comp != pardir or (not initial_slashes and not new_comps) or - (new_comps and new_comps[-1] == pardir)): - new_comps.append(comp) - elif new_comps: - new_comps.pop() - comps = new_comps - path = initial_slashes + sep.join(comps) - return path or curdir - -else: - def normpath(path): - """Normalize path, eliminating double slashes, etc.""" - path = os.fspath(path) - if isinstance(path, bytes): - return os.fsencode(_path_normpath(os.fsdecode(path))) or b"." - return _path_normpath(path) or "." + if ( + comp != pardir + or (not root and not comps) + or (comps and comps[-1] == pardir) + ): + comps.append(comp) + elif comps: + comps.pop() + return (root + sep.join(comps)) or curdir def abspath(path): @@ -420,7 +409,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 @@ -532,7 +521,7 @@ def relpath(path, start=None): # returned path. def commonpath(paths): - """Given a iterable 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)) From 218e43e75130eb0f15446374765cd307c58ab198 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Thu, 28 Mar 2024 19:34:38 +0100 Subject: [PATCH 13/29] Move refactoring to new branch --- Lib/posixpath.py | 123 ++++++++++++++++++++++++++++------------------- 1 file changed, 73 insertions(+), 50 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 4ff34ad5930f72..f1eb7b1d895872 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -217,19 +217,31 @@ def ismount(path): except (OSError, ValueError): # It doesn't exist -- so not a mount point. :-) return False + else: + # A symlink can never be a mount point + if stat.S_ISLNK(s1.st_mode): + return False - # A symlink can never be a mount point - if stat.S_ISLNK(s1.st_mode): - return False - - parent = realpath(dirname(path)) + path = os.fspath(path) + if isinstance(path, bytes): + parent = join(path, b'..') + else: + parent = join(path, '..') + parent = realpath(parent) try: s2 = os.lstat(parent) except (OSError, ValueError): return False - # path/.. on a different device as path or the same i-node as path - return s1.st_dev != s2.st_dev or s1.st_ino == s2.st_ino + dev1 = s1.st_dev + dev2 = s2.st_dev + if dev1 != dev2: + return True # path/.. on a different device as path + ino1 = s1.st_ino + ino2 = s2.st_ino + if ino1 == ino2: + return True # path/.. is the same i-node as path + return False # Expand paths beginning with '~' or '~user'. @@ -255,31 +267,37 @@ def expanduser(path): i = path.find(sep, 1) if i < 0: i = len(path) - if i != 1: + if i == 1: + if 'HOME' not in os.environ: + try: + import pwd + except ImportError: + # pwd module unavailable, return path unchanged + return path + try: + userhome = pwd.getpwuid(os.getuid()).pw_dir + except KeyError: + # bpo-10496: if the current user identifier doesn't exist in the + # password database, return the path unchanged + return path + else: + userhome = os.environ['HOME'] + else: try: import pwd - name = path[1:i] - if isinstance(name, bytes): - name = name.decode('ascii') - - userhome = pwd.getpwnam(name).pw_dir - except (ImportError, KeyError): + except ImportError: # pwd module unavailable, return path unchanged - # bpo-10496: if the user name from the path doesn't exist in the - # password database, return the path unchanged return path - elif 'HOME' in os.environ: - userhome = os.environ['HOME'] - else: + name = path[1:i] + if isinstance(name, bytes): + name = str(name, 'ASCII') try: - import pwd - userhome = pwd.getpwuid(os.getuid()).pw_dir - except (ImportError, KeyError): - # pwd module unavailable, return path unchanged - # bpo-10496: if the current user identifier doesn't exist in the + pwent = pwd.getpwnam(name) + except KeyError: + # bpo-10496: if the user name from the path doesn't exist in the # password database, return the path unchanged return path - + userhome = pwent.pw_dir # if no user home, return the path unchanged on VxWorks if userhome is None and sys.platform == "vxworks": return path @@ -354,40 +372,45 @@ def expandvars(path): try: from posix import _path_normpath - def normpath(path): - """Normalize path, eliminating double slashes, etc.""" - path = os.fspath(path) - if isinstance(path, bytes): - return os.fsencode(_path_normpath(os.fsdecode(path))) or b"." - return _path_normpath(path) or "." + except ImportError: def normpath(path): """Normalize path, eliminating double slashes, etc.""" path = os.fspath(path) if isinstance(path, bytes): sep = b'/' - curdir = b'.' - pardir = b'..' + empty = b'' + dot = b'.' + dotdot = b'..' else: sep = '/' - curdir = '.' - pardir = '..' - if not path: - return curdir - _, root, tail = splitroot(path) - comps = [] - for comp in tail.split(sep): - if not comp or comp == curdir: + empty = '' + dot = '.' + dotdot = '..' + if path == empty: + return dot + _, initial_slashes, path = splitroot(path) + comps = path.split(sep) + new_comps = [] + for comp in comps: + if comp in (empty, dot): continue - if ( - comp != pardir - or (not root and not comps) - or (comps and comps[-1] == pardir) - ): - comps.append(comp) - elif comps: - comps.pop() - return (root + sep.join(comps)) or curdir + if (comp != dotdot or (not initial_slashes and not new_comps) or + (new_comps and new_comps[-1] == dotdot)): + new_comps.append(comp) + elif new_comps: + new_comps.pop() + comps = new_comps + path = initial_slashes + sep.join(comps) + return path or dot + +else: + def normpath(path): + """Normalize path, eliminating double slashes, etc.""" + path = os.fspath(path) + if isinstance(path, bytes): + return os.fsencode(_path_normpath(os.fsdecode(path))) or b"." + return _path_normpath(path) or "." def abspath(path): From 6df0703c78a5f7b23d04122ea3b6053989c39c53 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Thu, 28 Mar 2024 19:57:17 +0100 Subject: [PATCH 14/29] Move bug fix to new branch --- Lib/ntpath.py | 65 +++++++++++++++++++++-------------------- Lib/test/test_ntpath.py | 3 -- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 21a56cf47d9f79..e7cbfe17ecb3c8 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 iterable of paths given as input. +# Return the longest common sub-path of the sequence 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 iterable. Additionally, any trailing separator is +# first path given in the sequence. Additionally, any trailing separator is # stripped from the returned path. def commonpath(paths): - """Given an iterable of path names, returns the longest common sub-path.""" - paths = tuple(map(os.fspath, paths)) + """Given a sequence of path names, returns the longest common sub-path.""" + if not paths: - raise ValueError('commonpath() arg is an empty iterable') + raise ValueError('commonpath() arg is an empty sequence') - path = paths[0] - if isinstance(path, bytes): + paths = tuple(map(os.fspath, paths)) + if isinstance(paths[0], bytes): sep = b'\\' altsep = b'/' curdir = b'.' @@ -869,34 +869,37 @@ def commonpath(paths): curdir = '.' try: - rootsplits = [splitroot(p.replace(altsep, sep).lower()) for p in paths] + 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) 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/test/test_ntpath.py b/Lib/test/test_ntpath.py index c816f99e7e9f1b..9cb03e3cd5de8d 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -871,14 +871,11 @@ 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'], From df5225b237a64eec58ed446d306b23df58a375f6 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Thu, 28 Mar 2024 19:59:24 +0100 Subject: [PATCH 15/29] Remove leftovers --- Lib/posixpath.py | 2 +- .../2024-03-26-20-20-36.gh-issue-117201.qaS7em.rst | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index f1eb7b1d895872..82269e14dff35b 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, _ = _joinrealpath(filename[:0], filename, strict, {}) + path, ok = _joinrealpath(filename[:0], filename, strict, {}) return abspath(path) # Join two paths, normalizing and eliminating any symbolic links 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 index cb232bec3a593d..e0fc6db6b941b8 100644 --- 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 @@ -1,2 +1 @@ -* Handle leading ``//`` for :func:`posixpath.commonpath` using :func:`posixpath.splitroot`. -* Raise TypeError for non-sequences for :func:`ntpath.commonpath`. +Handle leading ``//`` for :func:`posixpath.commonpath` using :func:`posixpath.splitroot`. From 5cf500b31f3d7d69b4e9633fa0739c1922336a93 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Fri, 29 Mar 2024 20:06:36 +0100 Subject: [PATCH 16/29] Add additional test cases --- Lib/test/test_posixpath.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index 996157a7d867e4..8a958ea900b3ce 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -708,6 +708,8 @@ def check_error(exc, paths): 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', 'usr']) check(['/usr/local'], '/usr/local') check(['/usr/local', '/usr/local'], '/usr/local') From c1cffda8cf8a0acd9f8bed29cee0af0667bc77be Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Fri, 29 Mar 2024 20:08:08 +0100 Subject: [PATCH 17/29] Move in try except block --- Lib/posixpath.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 82269e14dff35b..20da01fe51dd23 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -553,30 +553,29 @@ def commonpath(paths): try: root = min(roots) + if not root and max(roots): + raise ValueError("Can't mix absolute and relative paths") + + if isinstance(root, bytes): + sep = b'/' + curdir = b'.' + else: + sep = '/' + curdir = '.' + + 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]) + + return root + sep.join(s1) 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 root and max(roots): - raise ValueError("Can't mix absolute and relative paths") - - if isinstance(root, bytes): - sep = b'/' - curdir = b'.' - else: - sep = '/' - curdir = '.' - - 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]) - - return root + sep.join(s1) From 53f414f622ac6b3f645ffd3735b1965193c75e3c Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Fri, 29 Mar 2024 20:12:07 +0100 Subject: [PATCH 18/29] Move out try except block --- Lib/posixpath.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 20da01fe51dd23..20e66e73ae8904 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -551,18 +551,18 @@ def commonpath(paths): except ValueError: raise ValueError('commonpath() arg is an empty iterable') from None + if isinstance(roots[0], bytes): + sep = b'/' + curdir = b'.' + else: + sep = '/' + curdir = '.' + try: root = min(roots) if not root and max(roots): raise ValueError("Can't mix absolute and relative paths") - if isinstance(root, bytes): - sep = b'/' - curdir = b'.' - else: - sep = '/' - curdir = '.' - split_paths = [ [c for c in tail.split(sep) if c and c != curdir] for tail in tails ] From fef266ef6aaa7bb369e276faa91160cb24d3323d Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Fri, 29 Mar 2024 20:14:53 +0100 Subject: [PATCH 19/29] Revert renaming --- Lib/posixpath.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 20e66e73ae8904..4bb5194c509fbe 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -559,8 +559,8 @@ def commonpath(paths): curdir = '.' try: - root = min(roots) - if not root and max(roots): + prefix = min(roots) + if not prefix and max(roots): raise ValueError("Can't mix absolute and relative paths") split_paths = [ @@ -570,9 +570,9 @@ def commonpath(paths): s2 = max(split_paths) for i, c in enumerate(s1): if c != s2[i]: - return root + sep.join(s1[:i]) + return prefix + sep.join(s1[:i]) - return root + sep.join(s1) + return prefix + sep.join(s1) except (TypeError, AttributeError): genericpath._check_arg_types('commonpath', *( # Can't use paths, can be an iterable From 34c927577529313c53e3dd09056cf1625f014836 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Sat, 30 Mar 2024 22:25:04 +0100 Subject: [PATCH 20/29] Revert unnecessary changes --- Lib/posixpath.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 4bb5194c509fbe..29f19c4c8dacd3 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -545,12 +545,13 @@ def relpath(path, start=None): def commonpath(paths): """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(paths) + + if not paths: + raise ValueError('commonpath() arg is an empty iterable') + + _, roots, tails = zip(*map(splitroot, paths)) if isinstance(roots[0], bytes): sep = b'/' curdir = b'.' @@ -560,6 +561,7 @@ def commonpath(paths): try: prefix = min(roots) + if not prefix and max(roots): raise ValueError("Can't mix absolute and relative paths") @@ -568,11 +570,13 @@ def commonpath(paths): ] s1 = min(split_paths) s2 = max(split_paths) + common = s1 for i, c in enumerate(s1): if c != s2[i]: - return prefix + sep.join(s1[:i]) + common = s1[:i] + break - return prefix + sep.join(s1) + return prefix + sep.join(common) except (TypeError, AttributeError): genericpath._check_arg_types('commonpath', *( # Can't use paths, can be an iterable From ad9c461f2930d23694ac1736605e3262d8e6275f Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Sun, 31 Mar 2024 09:26:11 +0200 Subject: [PATCH 21/29] Speedup mix check --- Lib/posixpath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 29f19c4c8dacd3..f1a800afffe0ac 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -562,7 +562,7 @@ def commonpath(paths): try: prefix = min(roots) - if not prefix and max(roots): + if not prefix and any(roots): raise ValueError("Can't mix absolute and relative paths") split_paths = [ From 7565559af38ef171aa9357225bb038a8613320af Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Mon, 1 Apr 2024 20:18:07 +0200 Subject: [PATCH 22/29] Revert list comprehensions --- Lib/posixpath.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 29f19c4c8dacd3..ade22b5ef3835e 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -561,13 +561,12 @@ def commonpath(paths): try: prefix = min(roots) + split_paths = [path.split(sep) for path in tails] if not prefix and max(roots): raise ValueError("Can't mix absolute and relative paths") - split_paths = [ - [c for c in tail.split(sep) if c and c != curdir] for tail in tails - ] + 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 From a0ecc76904d4c35f7747dd1670b10c159f32f489 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Mon, 1 Apr 2024 20:19:53 +0200 Subject: [PATCH 23/29] Rename `tails` --- Lib/posixpath.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index dd7ea997fea463..40f9ad248465d4 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -551,8 +551,8 @@ def commonpath(paths): if not paths: raise ValueError('commonpath() arg is an empty iterable') - _, roots, tails = zip(*map(splitroot, paths)) - if isinstance(roots[0], bytes): + _, roots, paths = zip(*map(splitroot, paths)) + if isinstance(paths[0], bytes): sep = b'/' curdir = b'.' else: @@ -561,7 +561,7 @@ def commonpath(paths): try: prefix = min(roots) - split_paths = [path.split(sep) for path in tails] + split_paths = [path.split(sep) for path in paths] if not prefix and any(roots): raise ValueError("Can't mix absolute and relative paths") @@ -579,6 +579,6 @@ def commonpath(paths): 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) + root + tail for root, tail in zip(roots, paths) )) raise From 1d5b4197a9bd21372e271760f01076390a7997f5 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Wed, 3 Apr 2024 20:07:58 +0200 Subject: [PATCH 24/29] pass *paths Co-authored-by: Serhiy Storchaka --- Lib/posixpath.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index d6c4255a762dcd..b196e0067e56a3 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -554,8 +554,5 @@ def commonpath(paths): return prefix + sep.join(common) except (TypeError, AttributeError): - genericpath._check_arg_types('commonpath', *( - # Can't use paths, can be an iterable - root + tail for root, tail in zip(roots, paths) - )) + genericpath._check_arg_types('commonpath', *paths) raise From 54fff2e47de9f792ab4a8f0014ff449599fd0e68 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Wed, 3 Apr 2024 20:56:08 +0200 Subject: [PATCH 25/29] Add additional tests --- Lib/test/test_posixpath.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index 8a958ea900b3ce..e18d0637c04a33 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -700,22 +700,32 @@ 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(['/', '/dev'], '/') check(['/usr', '/dev'], '/') @@ -738,21 +748,9 @@ def check_error(exc, paths): check(['', 'spam/alot'], '') check_error(ValueError, ['', '/spam/alot']) - self.assertRaises(TypeError, posixpath.commonpath, - [b'/usr/lib/', '/usr/lib/python3']) - self.assertRaises(TypeError, posixpath.commonpath, - [b'/usr/lib/', 'usr/lib/python3']) - self.assertRaises(TypeError, posixpath.commonpath, - [b'usr/lib/', '/usr/lib/python3']) - self.assertRaises(TypeError, posixpath.commonpath, - ['/usr/lib/', b'/usr/lib/python3']) - self.assertRaises(TypeError, posixpath.commonpath, - ['/usr/lib/', b'usr/lib/python3']) - self.assertRaises(TypeError, posixpath.commonpath, - ['usr/lib/', b'/usr/lib/python3']) - - # gh-117201: Handle leading `//` for `posixpath.commonpath` - check(['//foo/bar', '//foo/baz'], '//foo') + check_error(TypeError, posixpath.commonpath, [b'/usr/lib/', '/usr/lib64']) + check_error(TypeError, posixpath.commonpath, [b'/usr/lib/', 'usr/lib64']) + check_error(TypeError, posixpath.commonpath, [b'usr/lib/', '/usr/lib64']) class PosixCommonTest(test_genericpath.CommonTest, unittest.TestCase): From 598f8a17227049c3de058a72b78ba0cee94ab80f Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Wed, 3 Apr 2024 21:06:53 +0200 Subject: [PATCH 26/29] Fix tests --- Lib/test/test_posixpath.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index e18d0637c04a33..b3a08a0a0a86f2 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -748,9 +748,9 @@ def check_error(exc, paths): check(['', 'spam/alot'], '') check_error(ValueError, ['', '/spam/alot']) - check_error(TypeError, posixpath.commonpath, [b'/usr/lib/', '/usr/lib64']) - check_error(TypeError, posixpath.commonpath, [b'/usr/lib/', 'usr/lib64']) - check_error(TypeError, posixpath.commonpath, [b'usr/lib/', '/usr/lib64']) + check_error(TypeError, [b'/usr/lib/', '/usr/lib/python3']) + check_error(TypeError, [b'/usr/lib/', 'usr/lib/python3']) + check_error(TypeError, [b'usr/lib/', '/usr/lib/python3']) class PosixCommonTest(test_genericpath.CommonTest, unittest.TestCase): From bb354319472da748a1bfdadac2d4d12e816b7bc2 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Wed, 3 Apr 2024 22:10:13 +0200 Subject: [PATCH 27/29] Add test & fix failing tests --- Lib/test/test_posixpath.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index b3a08a0a0a86f2..386aea178ead3d 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -718,11 +718,13 @@ def check_error(exc, paths): 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') @@ -748,9 +750,18 @@ def check_error(exc, paths): check(['', 'spam/alot'], '') check_error(ValueError, ['', '/spam/alot']) - check_error(TypeError, [b'/usr/lib/', '/usr/lib/python3']) - check_error(TypeError, [b'/usr/lib/', 'usr/lib/python3']) - check_error(TypeError, [b'usr/lib/', '/usr/lib/python3']) + self.assertRaises(TypeError, posixpath.commonpath, + [b'/usr/lib/', '/usr/lib/python3']) + self.assertRaises(TypeError, posixpath.commonpath, + [b'/usr/lib/', 'usr/lib/python3']) + self.assertRaises(TypeError, posixpath.commonpath, + [b'usr/lib/', '/usr/lib/python3']) + self.assertRaises(TypeError, posixpath.commonpath, + ['/usr/lib/', b'/usr/lib/python3']) + self.assertRaises(TypeError, posixpath.commonpath, + ['/usr/lib/', b'usr/lib/python3']) + self.assertRaises(TypeError, posixpath.commonpath, + ['usr/lib/', b'/usr/lib/python3']) class PosixCommonTest(test_genericpath.CommonTest, unittest.TestCase): From 93a19e85edcb2b29d693754c83b1c5ba44a0143f Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Wed, 3 Apr 2024 22:22:47 +0200 Subject: [PATCH 28/29] Remove `tuple()` call --- Lib/posixpath.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index b196e0067e56a3..1d1727ddfe0e16 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -523,12 +523,11 @@ def relpath(path, start=None): def commonpath(paths): """Given an iterable of path names, returns the longest common sub-path.""" - paths = tuple(paths) - - if not paths: - raise ValueError('commonpath() arg is an empty iterable') + try: + _, roots, paths = zip(*map(splitroot, paths)) + except ValueError: + raise ValueError('commonpath() arg is an empty iterable') from None - _, roots, paths = zip(*map(splitroot, paths)) if isinstance(paths[0], bytes): sep = b'/' curdir = b'.' From 88e0e6d3e72db7b2619ce40bcebd052d95094ea8 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Thu, 4 Apr 2024 08:55:05 +0200 Subject: [PATCH 29/29] remove try-except --- Lib/posixpath.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 1d1727ddfe0e16..ad356b0d9e67d8 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -523,11 +523,12 @@ def relpath(path, start=None): def commonpath(paths): """Given an iterable of path names, returns the longest common sub-path.""" - try: - _, roots, paths = zip(*map(splitroot, paths)) - except ValueError: - raise ValueError('commonpath() arg is an empty iterable') from None + rootsplits = map(splitroot, paths) + + if not rootsplits: + raise ValueError('commonpath() arg is an empty iterable') + _, roots, paths = zip(*rootsplits) if isinstance(paths[0], bytes): sep = b'/' curdir = b'.'