From 247c9af449794ae54e6d0f4ef839ba2035b410c3 Mon Sep 17 00:00:00 2001 From: Nineteendo Date: Sun, 24 Mar 2024 22:01:04 +0100 Subject: [PATCH 01/13] 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/13] 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/13] =?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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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):