Skip to content

Commit 86ebd5c

Browse files
mdboomeryksun
andauthored
gh-101196: Make isdir/isfile/exists faster on Windows (GH-101324)
Co-authored-by: Eryk Sun <eryksun@gmail.com>
1 parent 3a88de7 commit 86ebd5c

File tree

9 files changed

+624
-34
lines changed

9 files changed

+624
-34
lines changed

Include/pyport.h

+4
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@ typedef Py_ssize_t Py_ssize_clean_t;
247247
#define S_ISCHR(x) (((x) & S_IFMT) == S_IFCHR)
248248
#endif
249249

250+
#ifndef S_ISLNK
251+
#define S_ISLNK(x) (((x) & S_IFMT) == S_IFLNK)
252+
#endif
253+
250254
#ifdef __cplusplus
251255
/* Move this down here since some C++ #include's don't like to be included
252256
inside an extern "C" */

Lib/genericpath.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import stat
88

99
__all__ = ['commonprefix', 'exists', 'getatime', 'getctime', 'getmtime',
10-
'getsize', 'isdir', 'isfile', 'samefile', 'sameopenfile',
10+
'getsize', 'isdir', 'isfile', 'islink', 'samefile', 'sameopenfile',
1111
'samestat']
1212

1313

@@ -45,6 +45,18 @@ def isdir(s):
4545
return stat.S_ISDIR(st.st_mode)
4646

4747

48+
# Is a path a symbolic link?
49+
# This will always return false on systems where os.lstat doesn't exist.
50+
51+
def islink(path):
52+
"""Test whether a path is a symbolic link"""
53+
try:
54+
st = os.lstat(path)
55+
except (OSError, ValueError, AttributeError):
56+
return False
57+
return stat.S_ISLNK(st.st_mode)
58+
59+
4860
def getsize(filename):
4961
"""Return the size of a file, reported by os.stat()."""
5062
return os.stat(filename).st_size

Lib/ntpath.py

+8-19
Original file line numberDiff line numberDiff line change
@@ -276,19 +276,6 @@ def dirname(p):
276276
"""Returns the directory component of a pathname"""
277277
return split(p)[0]
278278

279-
# Is a path a symbolic link?
280-
# This will always return false on systems where os.lstat doesn't exist.
281-
282-
def islink(path):
283-
"""Test whether a path is a symbolic link.
284-
This will always return false for Windows prior to 6.0.
285-
"""
286-
try:
287-
st = os.lstat(path)
288-
except (OSError, ValueError, AttributeError):
289-
return False
290-
return stat.S_ISLNK(st.st_mode)
291-
292279

293280
# Is a path a junction?
294281

@@ -870,11 +857,13 @@ def commonpath(paths):
870857

871858

872859
try:
873-
# The genericpath.isdir implementation uses os.stat and checks the mode
874-
# attribute to tell whether or not the path is a directory.
875-
# This is overkill on Windows - just pass the path to GetFileAttributes
876-
# and check the attribute from there.
877-
from nt import _isdir as isdir
860+
# The isdir(), isfile(), islink() and exists() implementations in
861+
# genericpath use os.stat(). This is overkill on Windows. Use simpler
862+
# builtin functions if they are available.
863+
from nt import _path_isdir as isdir
864+
from nt import _path_isfile as isfile
865+
from nt import _path_islink as islink
866+
from nt import _path_exists as exists
878867
except ImportError:
879-
# Use genericpath.isdir as imported above.
868+
# Use genericpath.* as imported above
880869
pass

Lib/posixpath.py

-12
Original file line numberDiff line numberDiff line change
@@ -187,18 +187,6 @@ def dirname(p):
187187
return head
188188

189189

190-
# Is a path a symbolic link?
191-
# This will always return false on systems where os.lstat doesn't exist.
192-
193-
def islink(path):
194-
"""Test whether a path is a symbolic link"""
195-
try:
196-
st = os.lstat(path)
197-
except (OSError, ValueError, AttributeError):
198-
return False
199-
return stat.S_ISLNK(st.st_mode)
200-
201-
202190
# Is a path a junction?
203191

204192
def isjunction(path):

Lib/test/test_ntpath.py

+31-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import inspect
12
import ntpath
23
import os
34
import sys
45
import unittest
56
import warnings
6-
from test.support import os_helper
7+
from test.support import cpython_only, os_helper
78
from test.support import TestFailed, is_emscripten
89
from test.support.os_helper import FakePath
910
from test import test_genericpath
@@ -938,6 +939,35 @@ def test_isjunction(self):
938939
self.assertFalse(ntpath.isjunction('tmpdir'))
939940
self.assertPathEqual(ntpath.realpath('testjunc'), ntpath.realpath('tmpdir'))
940941

942+
@unittest.skipIf(sys.platform != 'win32', "drive letters are a windows concept")
943+
def test_isfile_driveletter(self):
944+
drive = os.environ.get('SystemDrive')
945+
if drive is None or len(drive) != 2 or drive[1] != ':':
946+
raise unittest.SkipTest('SystemDrive is not defined or malformed')
947+
self.assertFalse(os.path.isfile('\\\\.\\' + drive))
948+
949+
@unittest.skipIf(sys.platform != 'win32', "windows only")
950+
def test_con_device(self):
951+
self.assertFalse(os.path.isfile(r"\\.\CON"))
952+
self.assertFalse(os.path.isdir(r"\\.\CON"))
953+
self.assertFalse(os.path.islink(r"\\.\CON"))
954+
self.assertTrue(os.path.exists(r"\\.\CON"))
955+
956+
@unittest.skipIf(sys.platform != 'win32', "Fast paths are only for win32")
957+
@cpython_only
958+
def test_fast_paths_in_use(self):
959+
# There are fast paths of these functions implemented in posixmodule.c.
960+
# Confirm that they are being used, and not the Python fallbacks in
961+
# genericpath.py.
962+
self.assertTrue(os.path.isdir is nt._path_isdir)
963+
self.assertFalse(inspect.isfunction(os.path.isdir))
964+
self.assertTrue(os.path.isfile is nt._path_isfile)
965+
self.assertFalse(inspect.isfunction(os.path.isfile))
966+
self.assertTrue(os.path.islink is nt._path_islink)
967+
self.assertFalse(inspect.isfunction(os.path.islink))
968+
self.assertTrue(os.path.exists is nt._path_exists)
969+
self.assertFalse(inspect.isfunction(os.path.exists))
970+
941971

942972
class NtCommonTest(test_genericpath.CommonTest, unittest.TestCase):
943973
pathmodule = ntpath

Lib/test/test_os.py

+2
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,7 @@ def test_access_denied(self):
742742
)
743743
result = os.stat(fname)
744744
self.assertNotEqual(result.st_size, 0)
745+
self.assertTrue(os.path.isfile(fname))
745746

746747
@unittest.skipUnless(sys.platform == "win32", "Win32 specific tests")
747748
def test_stat_block_device(self):
@@ -2860,6 +2861,7 @@ def test_appexeclink(self):
28602861
self.assertEqual(st, os.stat(alias))
28612862
self.assertFalse(stat.S_ISLNK(st.st_mode))
28622863
self.assertEqual(st.st_reparse_tag, stat.IO_REPARSE_TAG_APPEXECLINK)
2864+
self.assertTrue(os.path.isfile(alias))
28632865
# testing the first one we see is sufficient
28642866
break
28652867
else:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The functions ``os.path.isdir``, ``os.path.isfile``, ``os.path.islink`` and
2+
``os.path.exists`` are now 13% to 28% faster on Windows, by making fewer Win32
3+
API calls.

0 commit comments

Comments
 (0)