Skip to content

Commit 635ce29

Browse files
gh-104803: Implement ntpath.isdevdrive for checking whether a path is on a Windows Dev Drive (GH-104805)
(cherry picked from commit bfd20d2) Co-authored-by: Steve Dower <[email protected]>
1 parent 5dc6b18 commit 635ce29

File tree

6 files changed

+216
-1
lines changed

6 files changed

+216
-1
lines changed

Doc/library/os.path.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,24 @@ the :mod:`glob` module.)
304304
Accepts a :term:`path-like object`.
305305

306306

307+
.. function:: isdevdrive(path)
308+
309+
Return ``True`` if pathname *path* is located on a Windows Dev Drive.
310+
A Dev Drive is optimized for developer scenarios, and offers faster
311+
performance for reading and writing files. It is recommended for use for
312+
source code, temporary build directories, package caches, and other
313+
IO-intensive operations.
314+
315+
May raise an error for an invalid path, for example, one without a
316+
recognizable drive, but returns ``False`` on platforms that do not support
317+
Dev Drives. See `the Windows documentation <https://learn.microsoft.com/windows/dev-drive/>`_
318+
for information on enabling and creating Dev Drives.
319+
320+
.. availability:: Windows.
321+
322+
.. versionadded:: 3.12
323+
324+
307325
.. function:: join(path, *paths)
308326

309327
Join one or more path segments intelligently. The return value is the

Lib/ntpath.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,3 +867,19 @@ def commonpath(paths):
867867
except ImportError:
868868
# Use genericpath.* as imported above
869869
pass
870+
871+
872+
try:
873+
from nt import _path_isdevdrive
874+
except ImportError:
875+
def isdevdrive(path):
876+
"""Determines whether the specified path is on a Windows Dev Drive."""
877+
# Never a Dev Drive
878+
return False
879+
else:
880+
def isdevdrive(path):
881+
"""Determines whether the specified path is on a Windows Dev Drive."""
882+
try:
883+
return _path_isdevdrive(abspath(path))
884+
except OSError:
885+
return False

Lib/test/test_ntpath.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,26 @@ def test_fast_paths_in_use(self):
992992
self.assertTrue(os.path.exists is nt._path_exists)
993993
self.assertFalse(inspect.isfunction(os.path.exists))
994994

995+
@unittest.skipIf(os.name != 'nt', "Dev Drives only exist on Win32")
996+
def test_isdevdrive(self):
997+
# Result may be True or False, but shouldn't raise
998+
self.assertIn(ntpath.isdevdrive(os_helper.TESTFN), (True, False))
999+
# ntpath.isdevdrive can handle relative paths
1000+
self.assertIn(ntpath.isdevdrive("."), (True, False))
1001+
self.assertIn(ntpath.isdevdrive(b"."), (True, False))
1002+
# Volume syntax is supported
1003+
self.assertIn(ntpath.isdevdrive(os.listvolumes()[0]), (True, False))
1004+
# Invalid volume returns False from os.path method
1005+
self.assertFalse(ntpath.isdevdrive(r"\\?\Volume{00000000-0000-0000-0000-000000000000}\\"))
1006+
# Invalid volume raises from underlying helper
1007+
with self.assertRaises(OSError):
1008+
nt._path_isdevdrive(r"\\?\Volume{00000000-0000-0000-0000-000000000000}\\")
1009+
1010+
@unittest.skipIf(os.name == 'nt', "isdevdrive fallback only used off Win32")
1011+
def test_isdevdrive_fallback(self):
1012+
# Fallback always returns False
1013+
self.assertFalse(ntpath.isdevdrive(os_helper.TESTFN))
1014+
9951015

9961016
class NtCommonTest(test_genericpath.CommonTest, unittest.TestCase):
9971017
pathmodule = ntpath
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add :func:`os.path.isdevdrive` to detect whether a path is on a Windows Dev
2+
Drive. Returns ``False`` on platforms that do not support Dev Drive, and is
3+
absent on non-Windows platforms.

Modules/clinic/posixmodule.c.h

Lines changed: 69 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/posixmodule.c

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4530,6 +4530,95 @@ os_listmounts_impl(PyObject *module, path_t *volume)
45304530
}
45314531

45324532

4533+
/*[clinic input]
4534+
os._path_isdevdrive
4535+
4536+
path: path_t
4537+
4538+
Determines whether the specified path is on a Windows Dev Drive.
4539+
4540+
[clinic start generated code]*/
4541+
4542+
static PyObject *
4543+
os__path_isdevdrive_impl(PyObject *module, path_t *path)
4544+
/*[clinic end generated code: output=1f437ea6677433a2 input=ee83e4996a48e23d]*/
4545+
{
4546+
#ifndef PERSISTENT_VOLUME_STATE_DEV_VOLUME
4547+
/* This flag will be documented at
4548+
https://learn.microsoft.com/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_fs_persistent_volume_information
4549+
after release, and will be available in the latest WinSDK.
4550+
We include the flag to avoid a specific version dependency
4551+
on the latest WinSDK. */
4552+
const int PERSISTENT_VOLUME_STATE_DEV_VOLUME = 0x00002000;
4553+
#endif
4554+
int err = 0;
4555+
PyObject *r = NULL;
4556+
wchar_t volume[MAX_PATH];
4557+
4558+
Py_BEGIN_ALLOW_THREADS
4559+
if (!GetVolumePathNameW(path->wide, volume, MAX_PATH)) {
4560+
/* invalid path of some kind */
4561+
/* Note that this also includes the case where a volume is mounted
4562+
in a path longer than 260 characters. This is likely to be rare
4563+
and problematic for other reasons, so a (soft) failure in this
4564+
check seems okay. */
4565+
err = GetLastError();
4566+
} else if (GetDriveTypeW(volume) != DRIVE_FIXED) {
4567+
/* only care about local dev drives */
4568+
r = Py_False;
4569+
} else {
4570+
HANDLE hVolume = CreateFileW(
4571+
volume,
4572+
FILE_READ_ATTRIBUTES,
4573+
FILE_SHARE_READ | FILE_SHARE_WRITE,
4574+
NULL,
4575+
OPEN_EXISTING,
4576+
FILE_FLAG_BACKUP_SEMANTICS,
4577+
NULL
4578+
);
4579+
if (hVolume == INVALID_HANDLE_VALUE) {
4580+
err = GetLastError();
4581+
} else {
4582+
FILE_FS_PERSISTENT_VOLUME_INFORMATION volumeState = {0};
4583+
volumeState.Version = 1;
4584+
volumeState.FlagMask = PERSISTENT_VOLUME_STATE_DEV_VOLUME;
4585+
if (!DeviceIoControl(
4586+
hVolume,
4587+
FSCTL_QUERY_PERSISTENT_VOLUME_STATE,
4588+
&volumeState,
4589+
sizeof(volumeState),
4590+
&volumeState,
4591+
sizeof(volumeState),
4592+
NULL,
4593+
NULL
4594+
)) {
4595+
err = GetLastError();
4596+
}
4597+
CloseHandle(hVolume);
4598+
if (err == ERROR_INVALID_PARAMETER) {
4599+
/* not supported on this platform */
4600+
r = Py_False;
4601+
} else if (!err) {
4602+
r = (volumeState.VolumeFlags & PERSISTENT_VOLUME_STATE_DEV_VOLUME)
4603+
? Py_True : Py_False;
4604+
}
4605+
}
4606+
}
4607+
Py_END_ALLOW_THREADS
4608+
4609+
if (err) {
4610+
PyErr_SetFromWindowsErr(err);
4611+
return NULL;
4612+
}
4613+
4614+
if (r) {
4615+
return Py_NewRef(r);
4616+
}
4617+
4618+
return NULL;
4619+
}
4620+
4621+
45334622
int
45344623
_PyOS_getfullpathname(const wchar_t *path, wchar_t **abspath_p)
45354624
{
@@ -15797,6 +15886,7 @@ static PyMethodDef posix_methods[] = {
1579715886
OS_SETNS_METHODDEF
1579815887
OS_UNSHARE_METHODDEF
1579915888

15889+
OS__PATH_ISDEVDRIVE_METHODDEF
1580015890
OS__PATH_ISDIR_METHODDEF
1580115891
OS__PATH_ISFILE_METHODDEF
1580215892
OS__PATH_ISLINK_METHODDEF

0 commit comments

Comments
 (0)