Skip to content

Commit 072011b

Browse files
authored
gh-100809: Fix handling of drive-relative paths in pathlib.Path.absolute() (GH-100812)
Resolving the drive independently uses the OS API, which ensures it starts from the current directory on that drive.
1 parent d401b20 commit 072011b

File tree

4 files changed

+64
-1
lines changed

4 files changed

+64
-1
lines changed

Lib/pathlib.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -816,7 +816,12 @@ def absolute(self):
816816
"""
817817
if self.is_absolute():
818818
return self
819-
return self._from_parts([os.getcwd()] + self._parts)
819+
elif self._drv:
820+
# There is a CWD on each drive-letter drive.
821+
cwd = self._flavour.abspath(self._drv)
822+
else:
823+
cwd = os.getcwd()
824+
return self._from_parts([cwd] + self._parts)
820825

821826
def resolve(self, strict=False):
822827
"""

Lib/test/support/os_helper.py

+35
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
import re
66
import stat
7+
import string
78
import sys
89
import time
910
import unittest
@@ -716,3 +717,37 @@ def __exit__(self, *ignore_exc):
716717
else:
717718
self._environ[k] = v
718719
os.environ = self._environ
720+
721+
722+
try:
723+
import ctypes
724+
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
725+
726+
ERROR_FILE_NOT_FOUND = 2
727+
DDD_REMOVE_DEFINITION = 2
728+
DDD_EXACT_MATCH_ON_REMOVE = 4
729+
DDD_NO_BROADCAST_SYSTEM = 8
730+
except (ImportError, AttributeError):
731+
def subst_drive(path):
732+
raise unittest.SkipTest('ctypes or kernel32 is not available')
733+
else:
734+
@contextlib.contextmanager
735+
def subst_drive(path):
736+
"""Temporarily yield a substitute drive for a given path."""
737+
for c in reversed(string.ascii_uppercase):
738+
drive = f'{c}:'
739+
if (not kernel32.QueryDosDeviceW(drive, None, 0) and
740+
ctypes.get_last_error() == ERROR_FILE_NOT_FOUND):
741+
break
742+
else:
743+
raise unittest.SkipTest('no available logical drive')
744+
if not kernel32.DefineDosDeviceW(
745+
DDD_NO_BROADCAST_SYSTEM, drive, path):
746+
raise ctypes.WinError(ctypes.get_last_error())
747+
try:
748+
yield drive
749+
finally:
750+
if not kernel32.DefineDosDeviceW(
751+
DDD_REMOVE_DEFINITION | DDD_EXACT_MATCH_ON_REMOVE,
752+
drive, path):
753+
raise ctypes.WinError(ctypes.get_last_error())

Lib/test/test_pathlib.py

+20
Original file line numberDiff line numberDiff line change
@@ -2973,6 +2973,26 @@ def test_absolute(self):
29732973
self.assertEqual(str(P('a', 'b', 'c').absolute()),
29742974
os.path.join(share, 'a', 'b', 'c'))
29752975

2976+
drive = os.path.splitdrive(BASE)[0]
2977+
with os_helper.change_cwd(BASE):
2978+
# Relative path with root
2979+
self.assertEqual(str(P('\\').absolute()), drive + '\\')
2980+
self.assertEqual(str(P('\\foo').absolute()), drive + '\\foo')
2981+
2982+
# Relative path on current drive
2983+
self.assertEqual(str(P(drive).absolute()), BASE)
2984+
self.assertEqual(str(P(drive + 'foo').absolute()), os.path.join(BASE, 'foo'))
2985+
2986+
with os_helper.subst_drive(BASE) as other_drive:
2987+
# Set the working directory on the substitute drive
2988+
saved_cwd = os.getcwd()
2989+
other_cwd = f'{other_drive}\\dirA'
2990+
os.chdir(other_cwd)
2991+
os.chdir(saved_cwd)
2992+
2993+
# Relative path on another drive
2994+
self.assertEqual(str(P(other_drive).absolute()), other_cwd)
2995+
self.assertEqual(str(P(other_drive + 'foo').absolute()), other_cwd + '\\foo')
29762996

29772997
def test_glob(self):
29782998
P = self.cls
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix handling of drive-relative paths (like 'C:' and 'C:foo') in
2+
:meth:`pathlib.Path.absolute`. This method now uses the OS API
3+
to retrieve the correct current working directory for the drive.

0 commit comments

Comments
 (0)