Skip to content

Commit c9394a0

Browse files
Merge remote-tracking branch 'upstream/main' into gh-89760-realpath-volume-guid
# Conflicts: # Lib/ntpath.py # Lib/test/test_ntpath.py
2 parents 507b732 + 0c38e05 commit c9394a0

3 files changed

Lines changed: 51 additions & 7 deletions

File tree

Lib/ntpath.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -626,12 +626,23 @@ def _getfinalpathname_nonstrict(path, ignored_error=OSError):
626626
allowed_winerror = 1, 2, 3, 5, 21, 32, 50, 53, 65, 67, 87, 123, 161, 1005, 1920, 1921
627627

628628
# Non-strict algorithm is to find as much of the target directory
629-
# as we can and join the rest.
629+
# as we can and join the rest. join() is not used, because the tail
630+
# can contain a colon and be mistaken for a drive (gh-102475).
631+
if isinstance(path, bytes):
632+
sep = b'\\'
633+
else:
634+
sep = '\\'
635+
636+
def join(path, tail):
637+
if path[-1:] == sep or not tail:
638+
return path + tail
639+
return path + sep + tail
640+
630641
tail = path[:0]
631642
while path:
632643
try:
633644
path = _getfinalpathname(path)
634-
return join(path, tail) if tail else path
645+
return join(path, tail)
635646
except ignored_error as ex:
636647
if ex.winerror not in allowed_winerror:
637648
raise
@@ -642,7 +653,7 @@ def _getfinalpathname_nonstrict(path, ignored_error=OSError):
642653
new_path = _readlink_deep(path,
643654
ignored_error=ignored_error)
644655
if new_path != path:
645-
return join(new_path, tail) if tail else new_path
656+
return join(new_path, tail)
646657
except ignored_error:
647658
# If we fail to readlink(), let's keep traversing
648659
pass
@@ -657,7 +668,7 @@ def _getfinalpathname_nonstrict(path, ignored_error=OSError):
657668
path, name = split(path)
658669
if path and not name:
659670
return path + tail
660-
tail = join(name, tail) if tail else name
671+
tail = join(name, tail)
661672
return tail
662673

663674
def realpath(path, /, *, strict=False):
@@ -667,7 +678,6 @@ def realpath(path, /, *, strict=False):
667678
unc_prefix = b'\\\\?\\UNC\\'
668679
new_unc_prefix = b'\\\\'
669680
colon_sep = b':\\'
670-
cwd = os.getcwdb()
671681
# bpo-38081: Special case for realpath(b'nul')
672682
devnull = b'nul'
673683
if normcase(path) == devnull:
@@ -677,7 +687,6 @@ def realpath(path, /, *, strict=False):
677687
unc_prefix = '\\\\?\\UNC\\'
678688
new_unc_prefix = '\\\\'
679689
colon_sep = ':\\'
680-
cwd = os.getcwd()
681690
# bpo-38081: Special case for realpath('nul')
682691
devnull = 'nul'
683692
if normcase(path) == devnull:
@@ -694,7 +703,9 @@ def realpath(path, /, *, strict=False):
694703
ignored_error = OSError
695704

696705
if not had_prefix and not isabs(path):
697-
path = join(cwd, path)
706+
# abspath() is used instead of join(cwd, path), because the path
707+
# can be relative to another drive (gh-102475).
708+
path = abspath(path)
698709
try:
699710
path = _getfinalpathname(path)
700711
initial_winerror = 0

Lib/test/test_ntpath.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,6 +1535,35 @@ def test_isjunction(self):
15351535
self.assertFalse(ntpath.isjunction('tmpdir'))
15361536
self.assertPathEqual(ntpath.realpath('testjunc'), ntpath.realpath('tmpdir'))
15371537

1538+
@unittest.skipIf(sys.platform != 'win32', "Can only test on win32.")
1539+
def test_realpath_drive_like_names(self):
1540+
# gh-102475: the unresolved tail is appended, not joined, so a name
1541+
# which looks like a drive does not reset the path.
1542+
drive = ntpath.splitroot(os.getcwd())[0]
1543+
for path, expected in [
1544+
('C:/spam:eggs', 'C:\\spam:eggs'),
1545+
('C:/nonexistent/spam:eggs', 'C:\\nonexistent\\spam:eggs'),
1546+
('C:/spam:eggs/ham', 'C:\\spam:eggs\\ham'),
1547+
('C:/nonexistent/spam:eggs/ham', 'C:\\nonexistent\\spam:eggs\\ham'),
1548+
]:
1549+
with self.subTest(path=path):
1550+
self.assertEqual(ntpath.realpath(path), expected)
1551+
self.assertEqual(ntpath.realpath(os.fsencode(path)),
1552+
os.fsencode(expected))
1553+
1554+
@unittest.skipIf(sys.platform != 'win32', "Can only test on win32.")
1555+
def test_realpath_drive_relative(self):
1556+
# gh-102475: the working directory of a drive which does not exist
1557+
# is its root directory.
1558+
for drive in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
1559+
if not ntpath.exists(drive + ':'):
1560+
break
1561+
else:
1562+
raise unittest.SkipTest('all drives exist')
1563+
self.assertEqual(ntpath.realpath(drive + ':spam'),
1564+
drive + ':\\spam')
1565+
self.assertEqual(ntpath.realpath(drive + ':'), drive + ':\\')
1566+
15381567
@unittest.skipIf(sys.platform != 'win32', "Can only test junctions with creation on win32.")
15391568
def test_realpath_volume_guid_path(self):
15401569
# gh-89760: the \\?\ prefix cannot be stripped from a volume GUID path.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :func:`os.path.realpath` on Windows: the unresolved part of the path is
2+
now appended, not joined, so a file name which looks like a drive (e.g.
3+
``spam:eggs``) no longer discards the resolved part. A path relative to
4+
another drive is now resolved against the root directory of that drive.

0 commit comments

Comments
 (0)