diff --git a/Lib/ntpath.py b/Lib/ntpath.py index e810b655e5ac85..c49ec848403c72 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -601,28 +601,21 @@ def normpath(path): return _path_normpath(path) or "." -def _abspath_fallback(path): - """Return the absolute version of a path as a fallback function in case - `nt._getfullpathname` is not available or raises OSError. See bpo-31047 for - more. - - """ - - path = os.fspath(path) - if not isabs(path): - if isinstance(path, bytes): - cwd = os.getcwdb() - else: - cwd = os.getcwd() - path = join(cwd, path) - return normpath(path) - # Return an absolute path. try: from nt import _getfullpathname except ImportError: # not running on Windows - mock up something sensible - abspath = _abspath_fallback + def abspath(path): + """Return the absolute version of a path.""" + path = os.fspath(path) + if not isabs(path): + drive, _, path = splitroot(path) + if isinstance(path, bytes): + path = join(drive or b'C:', b'\\' + path) + else: + path = join(drive or 'C:', '\\' + path) + return normpath(path) else: # use native Windows method on Windows def abspath(path): @@ -630,13 +623,32 @@ def abspath(path): try: return _getfullpathname(normpath(path)) except (OSError, ValueError): - return _abspath_fallback(path) + path = os.fspath(path) + if not isabs(path): + if isinstance(path, bytes): + sep = b'/' + cwd = os.getcwdb() + else: + sep = '/' + cwd = os.getcwd() + drive, _, path = splitroot(path) + if drive and drive != splitroot(cwd)[0]: + try: + cwd = _getfullpathname(drive) + except (OSError, ValueError): + cwd = join(drive, sep) + path = join(cwd, path) + return normpath(path) try: from nt import _findfirstfile, _getfinalpathname, readlink as _nt_readlink except ImportError: # realpath is a no-op on systems without _getfinalpathname support. - realpath = abspath + def realpath(path, *, strict=False): + """Return an absolute path.""" + if strict: + raise NotImplementedError('realpath: strict unavailable on this platform') + return abspath(path) else: def _readlink_deep(path): # These error codes indicate that we should stop reading links and diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 56b7915826daf4..d95389691beb6a 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -406,106 +406,123 @@ def normpath(path): return _path_normpath(path) or "." -def abspath(path): - """Return an absolute path.""" - path = os.fspath(path) - if isinstance(path, bytes): - if not path.startswith(b'/'): - path = join(os.getcwdb(), path) - else: - if not path.startswith('/'): - path = join(os.getcwd(), path) - return normpath(path) +if os.name == "nt": # not running on Unix - mock up something sensible + def abspath(path): + """Return an absolute path.""" + path = os.fspath(path) + sep = _get_sep(path) + if not path.startswith(sep): + path = sep + path + return normpath(path) +else: # use native Unix method on Unix + def abspath(path): + """Return an absolute path.""" + path = os.fspath(path) + if isinstance(path, bytes): + if not path.startswith(b'/'): + path = join(os.getcwdb(), path) + else: + if not path.startswith('/'): + path = join(os.getcwd(), path) + return normpath(path) # Return a canonical path (i.e. the absolute location of a file on the # filesystem). -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) - if isinstance(filename, bytes): - sep = b'/' - curdir = b'.' - pardir = b'..' - getcwd = os.getcwdb - else: - sep = '/' - curdir = '.' - pardir = '..' - getcwd = os.getcwd - - # The stack of unresolved path parts. When popped, a special value of None - # indicates that a symlink target has been resolved, and that the original - # symlink path can be retrieved by popping again. The [::-1] slice is a - # very fast way of spelling list(reversed(...)). - rest = filename.split(sep)[::-1] - - # The resolved path, which is absolute throughout this function. - # Note: getcwd() returns a normalized and symlink-free path. - path = sep if filename.startswith(sep) else getcwd() - - # Mapping from symlink paths to *fully resolved* symlink targets. If a - # symlink is encountered but not yet resolved, the value is None. This is - # used both to detect symlink loops and to speed up repeated traversals of - # the same links. - seen = {} - - while rest: - name = rest.pop() - if name is None: - # resolved symlink target - seen[rest.pop()] = path - continue - if not name or name == curdir: - # current dir - continue - if name == pardir: - # parent dir - path = path[:path.rindex(sep)] or sep - continue - if path == sep: - newpath = path + name +if os.name == "nt": + # realpath is a no-op on Windows. + def realpath(path, *, strict=False): + """Return an absolute path.""" + if strict: + raise NotImplementedError('realpath: strict unavailable on this platform') + return abspath(path) +else: + 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) + if isinstance(filename, bytes): + sep = b'/' + curdir = b'.' + pardir = b'..' + getcwd = os.getcwdb else: - newpath = path + sep + name - try: - st = os.lstat(newpath) - if not stat.S_ISLNK(st.st_mode): + sep = '/' + curdir = '.' + pardir = '..' + getcwd = os.getcwd + + # The stack of unresolved path parts. When popped, a special value of + # None indicates that a symlink target has been resolved, and that the + # original symlink path can be retrieved by popping again. The [::-1] + # slice is a very fast way of spelling list(reversed(...)). + rest = filename.split(sep)[::-1] + + # The resolved path, which is absolute throughout this function. + # Note: getcwd() returns a normalized and symlink-free path. + path = sep if filename.startswith(sep) else getcwd() + + # Mapping from symlink paths to *fully resolved* symlink targets. If a + # symlink is encountered but not yet resolved, the value is None. This + # is used both to detect symlink loops and to speed up repeated + # traversals of the same links. + seen = {} + + while rest: + name = rest.pop() + if name is None: + # resolved symlink target + seen[rest.pop()] = path + continue + if not name or name == curdir: + # current dir + continue + if name == pardir: + # parent dir + path = path[:path.rindex(sep)] or sep + continue + if path == sep: + newpath = path + name + else: + newpath = path + sep + name + try: + st = os.lstat(newpath) + if not stat.S_ISLNK(st.st_mode): + path = newpath + continue + except OSError: + if strict: + raise path = newpath continue - except OSError: - if strict: - raise - path = newpath - continue - # Resolve the symbolic link - if newpath in seen: - # Already seen this path - path = seen[newpath] - if path is not None: - # use cached value + # Resolve the symbolic link + if newpath in seen: + # Already seen this path + path = seen[newpath] + if path is not None: + # use cached value + continue + # The symlink is not resolved, so we must have a symlink loop. + if strict: + # Raise OSError(errno.ELOOP) + os.stat(newpath) + path = newpath continue - # The symlink is not resolved, so we must have a symlink loop. - if strict: - # Raise OSError(errno.ELOOP) - os.stat(newpath) - path = newpath - continue - seen[newpath] = None # not resolved symlink - target = os.readlink(newpath) - if target.startswith(sep): - # Symlink target is absolute; reset resolved path. - path = sep - # Push the symlink path onto the stack, and signal its specialness by - # also pushing None. When these entries are popped, we'll record the - # fully-resolved symlink target in the 'seen' mapping. - rest.append(newpath) - rest.append(None) - # Push the unresolved symlink target parts onto the stack. - rest.extend(target.split(sep)[::-1]) + seen[newpath] = None # not resolved symlink + target = os.readlink(newpath) + if target.startswith(sep): + # Symlink target is absolute; reset resolved path. + path = sep + # Push the symlink path onto the stack, and signal its specialness + # by also pushing None. When these entries are popped, we'll record + # the fully-resolved symlink target in the 'seen' mapping. + rest.append(newpath) + rest.append(None) + # Push the unresolved symlink target parts onto the stack. + rest.extend(target.split(sep)[::-1]) - return path + return path supports_unicode_filenames = (sys.platform == 'darwin') diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 7f91bf1c2b837a..04eaca40674a21 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -376,16 +376,18 @@ def test_normpath(self): tester("ntpath.normpath('\\\\')", '\\\\') tester("ntpath.normpath('//?/UNC/server/share/..')", '\\\\?\\UNC\\server\\share\\') + @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') def test_realpath_curdir(self): - expected = ntpath.normpath(os.getcwd()) + expected = os.getcwd() tester("ntpath.realpath('.')", expected) tester("ntpath.realpath('./.')", expected) tester("ntpath.realpath('/'.join(['.'] * 100))", expected) tester("ntpath.realpath('.\\.')", expected) tester("ntpath.realpath('\\'.join(['.'] * 100))", expected) + @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') def test_realpath_pardir(self): - expected = ntpath.normpath(os.getcwd()) + expected = os.getcwd() tester("ntpath.realpath('..')", ntpath.dirname(expected)) tester("ntpath.realpath('../..')", ntpath.dirname(ntpath.dirname(expected))) @@ -839,6 +841,7 @@ def test_abspath(self): drive, _ = ntpath.splitdrive(cwd_dir) tester('ntpath.abspath("/abc/")', drive + "\\abc") + @unittest.skipUnless(nt, "relpath requires 'nt' module") def test_relpath(self): tester('ntpath.relpath("a")', 'a') tester('ntpath.relpath(ntpath.abspath("a"))', 'a') diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index 32a20efbb64e1d..43fa95f2a9a055 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -20,16 +20,6 @@ ABSTFN = abspath(os_helper.TESTFN) -def skip_if_ABSTFN_contains_backslash(test): - """ - On Windows, posixpath.abspath still returns paths with backslashes - instead of posix forward slashes. If this is the case, several tests - fail, so skip them. - """ - found_backslash = '\\' in ABSTFN - msg = "ABSTFN is not a posix path - tests fail" - return [test, unittest.skip(msg)(test)][found_backslash] - def safe_rmdir(dirname): try: os.rmdir(dirname) @@ -419,7 +409,7 @@ def test_normpath(self): result = posixpath.normpath(path) self.assertEqual(result, expected) - @skip_if_ABSTFN_contains_backslash + @unittest.skipUnless(posix, "realpath requires 'posix' module") def test_realpath_curdir(self): self.assertEqual(realpath('.'), os.getcwd()) self.assertEqual(realpath('./.'), os.getcwd()) @@ -429,7 +419,7 @@ def test_realpath_curdir(self): self.assertEqual(realpath(b'./.'), os.getcwdb()) self.assertEqual(realpath(b'/'.join([b'.'] * 100)), os.getcwdb()) - @skip_if_ABSTFN_contains_backslash + @unittest.skipUnless(posix, "realpath requires 'posix' module") def test_realpath_pardir(self): self.assertEqual(realpath('..'), dirname(os.getcwd())) self.assertEqual(realpath('../..'), dirname(dirname(os.getcwd()))) @@ -440,7 +430,7 @@ def test_realpath_pardir(self): self.assertEqual(realpath(b'/'.join([b'..'] * 100)), b'/') @os_helper.skip_unless_symlink - @skip_if_ABSTFN_contains_backslash + @unittest.skipUnless(posix, "realpath requires 'posix' module") def test_realpath_basic(self): # Basic operation. try: @@ -450,7 +440,7 @@ def test_realpath_basic(self): os_helper.unlink(ABSTFN) @os_helper.skip_unless_symlink - @skip_if_ABSTFN_contains_backslash + @unittest.skipUnless(posix, "realpath requires 'posix' module") def test_realpath_strict(self): # Bug #43757: raise FileNotFoundError in strict mode if we encounter # a path that does not exist. @@ -462,7 +452,7 @@ def test_realpath_strict(self): os_helper.unlink(ABSTFN) @os_helper.skip_unless_symlink - @skip_if_ABSTFN_contains_backslash + @unittest.skipUnless(posix, "realpath requires 'posix' module") def test_realpath_relative(self): try: os.symlink(posixpath.relpath(ABSTFN+"1"), ABSTFN) @@ -471,7 +461,7 @@ def test_realpath_relative(self): os_helper.unlink(ABSTFN) @os_helper.skip_unless_symlink - @skip_if_ABSTFN_contains_backslash + @unittest.skipUnless(posix, "realpath requires 'posix' module") def test_realpath_missing_pardir(self): try: os.symlink(os_helper.TESTFN + "1", os_helper.TESTFN) @@ -480,7 +470,7 @@ def test_realpath_missing_pardir(self): os_helper.unlink(os_helper.TESTFN) @os_helper.skip_unless_symlink - @skip_if_ABSTFN_contains_backslash + @unittest.skipUnless(posix, "realpath requires 'posix' module") def test_realpath_symlink_loops(self): # Bug #930024, return the path unchanged if we get into an infinite # symlink loop in non-strict mode (default). @@ -521,7 +511,7 @@ def test_realpath_symlink_loops(self): os_helper.unlink(ABSTFN+"a") @os_helper.skip_unless_symlink - @skip_if_ABSTFN_contains_backslash + @unittest.skipUnless(posix, "realpath requires 'posix' module") def test_realpath_symlink_loops_strict(self): # Bug #43757, raise OSError if we get into an infinite symlink loop in # strict mode. @@ -562,7 +552,7 @@ def test_realpath_symlink_loops_strict(self): os_helper.unlink(ABSTFN+"a") @os_helper.skip_unless_symlink - @skip_if_ABSTFN_contains_backslash + @unittest.skipUnless(posix, "realpath requires 'posix' module") def test_realpath_repeated_indirect_symlinks(self): # Issue #6975. try: @@ -576,7 +566,7 @@ def test_realpath_repeated_indirect_symlinks(self): safe_rmdir(ABSTFN) @os_helper.skip_unless_symlink - @skip_if_ABSTFN_contains_backslash + @unittest.skipUnless(posix, "realpath requires 'posix' module") def test_realpath_deep_recursion(self): depth = 10 try: @@ -595,7 +585,7 @@ def test_realpath_deep_recursion(self): safe_rmdir(ABSTFN) @os_helper.skip_unless_symlink - @skip_if_ABSTFN_contains_backslash + @unittest.skipUnless(posix, "realpath requires 'posix' module") def test_realpath_resolve_parents(self): # We also need to resolve any symlinks in the parents of a relative # path passed to realpath. E.g.: current working directory is @@ -614,7 +604,7 @@ def test_realpath_resolve_parents(self): safe_rmdir(ABSTFN) @os_helper.skip_unless_symlink - @skip_if_ABSTFN_contains_backslash + @unittest.skipUnless(posix, "realpath requires 'posix' module") def test_realpath_resolve_before_normalizing(self): # Bug #990669: Symbolic links should be resolved before we # normalize the path. E.g.: if we have directories 'a', 'k' and 'y' @@ -642,7 +632,7 @@ def test_realpath_resolve_before_normalizing(self): safe_rmdir(ABSTFN) @os_helper.skip_unless_symlink - @skip_if_ABSTFN_contains_backslash + @unittest.skipUnless(posix, "realpath requires 'posix' module") def test_realpath_resolve_first(self): # Bug #1213894: The first component of the path, if not absolute, # must be resolved too. @@ -660,6 +650,7 @@ def test_realpath_resolve_first(self): safe_rmdir(ABSTFN + "/k") safe_rmdir(ABSTFN) + @unittest.skipUnless(posix, "relpath requires 'posix' module") def test_relpath(self): (real_getcwd, os.getcwd) = (os.getcwd, lambda: r"/home/user/bar") try: @@ -687,6 +678,7 @@ def test_relpath(self): finally: os.getcwd = real_getcwd + @unittest.skipUnless(posix, "relpath requires 'posix' module") def test_relpath_bytes(self): (real_getcwdb, os.getcwdb) = (os.getcwdb, lambda: br"/home/user/bar") try: diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-04-27-14-39-02.gh-issue-118345.-TITAI.rst b/Misc/NEWS.d/next/Core and Builtins/2024-04-27-14-39-02.gh-issue-118345.-TITAI.rst new file mode 100644 index 00000000000000..56f43a069ea368 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-04-27-14-39-02.gh-issue-118345.-TITAI.rst @@ -0,0 +1 @@ +Fix :func:`ntpath.abspath`, :func:`posixpath.abspath` & :func:`posixpath.realpath` for relative paths on the other platform`.