Skip to content

Commit bfd20d2

Browse files
authored
gh-104803: Implement ntpath.isdevdrive for checking whether a path is on a Windows Dev Drive (GH-104805)
1 parent e92ac0a commit bfd20d2

File tree

6 files changed

+216
-1
lines changed

6 files changed

+216
-1
lines changed

Doc/library/os.path.rst

+18
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

+16
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

+20
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
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

+69-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/posixmodule.c

+90
Original file line numberDiff line numberDiff line change
@@ -4534,6 +4534,95 @@ os_listmounts_impl(PyObject *module, path_t *volume)
45344534
}
45354535

45364536

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

15897+
OS__PATH_ISDEVDRIVE_METHODDEF
1580815898
OS__PATH_ISDIR_METHODDEF
1580915899
OS__PATH_ISFILE_METHODDEF
1581015900
OS__PATH_ISLINK_METHODDEF

0 commit comments

Comments
 (0)