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-100809: Fix handling of drive-relative paths in pathlib.Path.absolute() #100812

Merged
merged 11 commits into from
Feb 17, 2023
7 changes: 6 additions & 1 deletion Lib/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -821,7 +821,12 @@ def absolute(self):
"""
if self.is_absolute():
return self
return self._from_parts([os.getcwd()] + self._parts)
elif self._drv:
# There is a CWD on each drive-letter drive.
cwd = self._flavour.abspath(self._drv)
else:
cwd = os.getcwd()
return self._from_parts([cwd] + self._parts)

def resolve(self, strict=False):
"""
Expand Down
35 changes: 35 additions & 0 deletions Lib/test/support/os_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import re
import stat
import string
import sys
import time
import unittest
Expand Down Expand Up @@ -716,3 +717,37 @@ def __exit__(self, *ignore_exc):
else:
self._environ[k] = v
os.environ = self._environ


try:
import ctypes
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)

ERROR_FILE_NOT_FOUND = 2
DDD_REMOVE_DEFINITION = 2
DDD_EXACT_MATCH_ON_REMOVE = 4
DDD_NO_BROADCAST_SYSTEM = 8
barneygale marked this conversation as resolved.
Show resolved Hide resolved
except (ImportError, AttributeError):
def subst_drive(path):
raise unittest.SkipTest('ctypes or kernel32 is not available')
else:
@contextlib.contextmanager
def subst_drive(path):
"""Temporarily yield a substitute drive for a given path."""
for c in reversed(string.ascii_uppercase):
drive = f'{c}:'
if (not kernel32.QueryDosDeviceW(drive, None, 0) and
ctypes.get_last_error() == ERROR_FILE_NOT_FOUND):
break
else:
raise unittest.SkipTest('no available logical drive')
if not kernel32.DefineDosDeviceW(
DDD_NO_BROADCAST_SYSTEM, drive, path):
raise ctypes.WinError(ctypes.get_last_error())
try:
yield drive
finally:
if not kernel32.DefineDosDeviceW(
DDD_REMOVE_DEFINITION | DDD_EXACT_MATCH_ON_REMOVE,
barneygale marked this conversation as resolved.
Show resolved Hide resolved
drive, path):
raise ctypes.WinError(ctypes.get_last_error())
20 changes: 20 additions & 0 deletions Lib/test/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -2970,6 +2970,26 @@ def test_absolute(self):
self.assertEqual(str(P('a', 'b', 'c').absolute()),
os.path.join(share, 'a', 'b', 'c'))

drive = os.path.splitdrive(BASE)[0]
with os_helper.change_cwd(BASE):
# Relative path with root
self.assertEqual(str(P('\\').absolute()), drive + '\\')
self.assertEqual(str(P('\\foo').absolute()), drive + '\\foo')

# Relative path on current drive
self.assertEqual(str(P(drive).absolute()), BASE)
self.assertEqual(str(P(drive + 'foo').absolute()), os.path.join(BASE, 'foo'))

with os_helper.subst_drive(BASE) as other_drive:
# Set the working directory on the substitute drive
saved_cwd = os.getcwd()
other_cwd = f'{other_drive}\\dirA'
os.chdir(other_cwd)
os.chdir(saved_cwd)

# Relative path on another drive
self.assertEqual(str(P(other_drive).absolute()), other_cwd)
self.assertEqual(str(P(other_drive + 'foo').absolute()), other_cwd + '\\foo')

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