Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions Lib/ntpath.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -626,12 +626,23 @@ def _getfinalpathname_nonstrict(path, ignored_error=OSError):
allowed_winerror = 1, 2, 3, 5, 21, 32, 50, 53, 65, 67, 87, 123, 161, 1005, 1920, 1921

# Non-strict algorithm is to find as much of the target directory
# as we can and join the rest.
# as we can and join the rest. join() is not used, because the tail
# can contain a colon and be mistaken for a drive (gh-102475).
if isinstance(path, bytes):
sep = b'\\'
else:
sep = '\\'

def join(path, tail):
if path[-1:] == sep or not tail:
return path + tail
return path + sep + tail

tail = path[:0]
while path:
try:
path = _getfinalpathname(path)
return join(path, tail) if tail else path
return join(path, tail)
except ignored_error as ex:
if ex.winerror not in allowed_winerror:
raise
Expand All@@ -642,7 +653,7 @@ def _getfinalpathname_nonstrict(path, ignored_error=OSError):
new_path = _readlink_deep(path,
ignored_error=ignored_error)
if new_path != path:
return join(new_path, tail) if tail else new_path
return join(new_path, tail)
except ignored_error:
# If we fail to readlink(), let's keep traversing
pass
Expand All@@ -657,7 +668,7 @@ def _getfinalpathname_nonstrict(path, ignored_error=OSError):
path, name = split(path)
if path and not name:
return path + tail
tail = join(name, tail) if tail else name
tail = join(name, tail)
return tail

def realpath(path, /, *, strict=False):
Expand All@@ -666,7 +677,6 @@ def realpath(path, /, *, strict=False):
prefix = b'\\\\?\\'
unc_prefix = b'\\\\?\\UNC\\'
new_unc_prefix = b'\\\\'
cwd = os.getcwdb()
# bpo-38081: Special case for realpath(b'nul')
devnull = b'nul'
if normcase(path) == devnull:
Expand All@@ -675,7 +685,6 @@ def realpath(path, /, *, strict=False):
prefix = '\\\\?\\'
unc_prefix = '\\\\?\\UNC\\'
new_unc_prefix = '\\\\'
cwd = os.getcwd()
# bpo-38081: Special case for realpath('nul')
devnull = 'nul'
if normcase(path) == devnull:
Expand All@@ -692,7 +701,9 @@ def realpath(path, /, *, strict=False):
ignored_error = OSError

if not had_prefix and not isabs(path):
path = join(cwd, path)
# abspath() is used instead of join(cwd, path), because the path
# can be relative to another drive (gh-102475).
path = abspath(path)
try:
path = _getfinalpathname(path)
initial_winerror = 0
Expand Down
29 changes: 29 additions & 0 deletions Lib/test/test_ntpath.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1535,6 +1535,35 @@ def test_isjunction(self):
self.assertFalse(ntpath.isjunction('tmpdir'))
self.assertPathEqual(ntpath.realpath('testjunc'), ntpath.realpath('tmpdir'))

@unittest.skipIf(sys.platform != 'win32', "Can only test on win32.")
def test_realpath_drive_like_names(self):
# gh-102475: the unresolved tail is appended, not joined, so a name
# which looks like a drive does not reset the path.
drive = ntpath.splitroot(os.getcwd())[0]
for path, expected in [
('C:/spam:eggs', 'C:\\spam:eggs'),
('C:/nonexistent/spam:eggs', 'C:\\nonexistent\\spam:eggs'),
('C:/spam:eggs/ham', 'C:\\spam:eggs\\ham'),
('C:/nonexistent/spam:eggs/ham', 'C:\\nonexistent\\spam:eggs\\ham'),
]:
with self.subTest(path=path):
self.assertEqual(ntpath.realpath(path), expected)
self.assertEqual(ntpath.realpath(os.fsencode(path)),
os.fsencode(expected))

@unittest.skipIf(sys.platform != 'win32', "Can only test on win32.")
def test_realpath_drive_relative(self):
# gh-102475: the working directory of a drive which does not exist
# is its root directory.
for drive in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
if not ntpath.exists(drive + ':'):
break
else:
raise unittest.SkipTest('all drives exist')
self.assertEqual(ntpath.realpath(drive + ':spam'),
drive + ':\\spam')
self.assertEqual(ntpath.realpath(drive + ':'), drive + ':\\')

def test_isfile_invalid_paths(self):
isfile = ntpath.isfile
self.assertIs(isfile('/tmp\udfffabcds'), False)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
Fix :func:`os.path.realpath` on Windows: the unresolved part of the path is
now appended, not joined, so a file name which looks like a drive (e.g.
``spam:eggs``) no longer discards the resolved part. A path relative to
another drive is now resolved against the root directory of that drive.
Loading