Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

gh-118345: Fix os.path.abspath() & posixpath.abspath() for relative paths on the other platform #118346

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 29 additions & 19 deletions Lib/ntpath.py
Expand Up @@ -601,42 +601,52 @@ 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):
"""Return the absolute version of a 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."""
return abspath(path)
nineteendo marked this conversation as resolved.
Show resolved Hide resolved
else:
def _readlink_deep(path):
# These error codes indicate that we should stop reading links and
Expand Down
195 changes: 105 additions & 90 deletions Lib/posixpath.py
Expand Up @@ -406,106 +406,121 @@ 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."""
return abspath(path)
nineteendo marked this conversation as resolved.
Show resolved Hide resolved
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')
Expand Down
7 changes: 5 additions & 2 deletions Lib/test/test_ntpath.py
Expand Up @@ -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)))
Expand Down Expand Up @@ -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')
Expand Down