Skip to content

Commit a74f117

Browse files
GH-115060: Speed up pathlib.Path.glob() by omitting initial stat() (#117831)
Since 6258844, paths that might not exist can be fed into pathlib's globbing implementation, which will call `os.scandir()` / `os.lstat()` only when strictly necessary. This allows us to drop an initial `self.is_dir()` call, which saves a `stat()`. Co-authored-by: Shantanu <[email protected]>
1 parent 3095d02 commit a74f117

File tree

6 files changed

+20
-10
lines changed

6 files changed

+20
-10
lines changed

Doc/library/pathlib.rst

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,10 +1004,6 @@ call fails (for example because the path doesn't exist).
10041004
.. seealso::
10051005
:ref:`pathlib-pattern-language` documentation.
10061006

1007-
This method calls :meth:`Path.is_dir` on the top-level directory and
1008-
propagates any :exc:`OSError` exception that is raised. Subsequent
1009-
:exc:`OSError` exceptions from scanning directories are suppressed.
1010-
10111007
By default, or when the *case_sensitive* keyword-only argument is set to
10121008
``None``, this method matches paths using platform-specific casing rules:
10131009
typically, case-sensitive on POSIX, and case-insensitive on Windows.
@@ -1028,6 +1024,11 @@ call fails (for example because the path doesn't exist).
10281024
.. versionchanged:: 3.13
10291025
The *pattern* parameter accepts a :term:`path-like object`.
10301026

1027+
.. versionchanged:: 3.13
1028+
Any :exc:`OSError` exceptions raised from scanning the filesystem are
1029+
suppressed. In previous versions, such exceptions are suppressed in many
1030+
cases, but not all.
1031+
10311032

10321033
.. method:: Path.rglob(pattern, *, case_sensitive=None, recurse_symlinks=False)
10331034

Lib/pathlib/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -607,11 +607,9 @@ def glob(self, pattern, *, case_sensitive=None, recurse_symlinks=False):
607607
if raw[-1] in (self.parser.sep, self.parser.altsep):
608608
# GH-65238: pathlib doesn't preserve trailing slash. Add it back.
609609
parts.append('')
610-
if not self.is_dir():
611-
return iter([])
612610
select = self._glob_selector(parts[::-1], case_sensitive, recurse_symlinks)
613611
root = str(self)
614-
paths = select(root, exists=True)
612+
paths = select(root)
615613

616614
# Normalize results
617615
if root == '.':

Lib/pathlib/_abc.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -705,10 +705,8 @@ def glob(self, pattern, *, case_sensitive=None, recurse_symlinks=True):
705705
anchor, parts = pattern._stack
706706
if anchor:
707707
raise NotImplementedError("Non-relative patterns are unsupported")
708-
if not self.is_dir():
709-
return iter([])
710708
select = self._glob_selector(parts, case_sensitive, recurse_symlinks)
711-
return select(self, exists=True)
709+
return select(self)
712710

713711
def rglob(self, pattern, *, case_sensitive=None, recurse_symlinks=True):
714712
"""Recursively yield all existing files (of any kind, including

Lib/test/test_pathlib/test_pathlib.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,6 +1263,13 @@ def test_glob_dot(self):
12631263
self.assertEqual(
12641264
set(P('.').glob('**/*/*')), {P("dirD/fileD")})
12651265

1266+
def test_glob_inaccessible(self):
1267+
P = self.cls
1268+
p = P(self.base, "mydir1", "mydir2")
1269+
p.mkdir(parents=True)
1270+
p.parent.chmod(0)
1271+
self.assertEqual(set(p.glob('*')), set())
1272+
12661273
def test_rglob_pathlike(self):
12671274
P = self.cls
12681275
p = P(self.base, "dirC")

Lib/test/test_pathlib/test_pathlib_abc.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from pathlib._abc import UnsupportedOperation, ParserBase, PurePathBase, PathBase
99
import posixpath
1010

11+
from test.support import is_wasi
1112
from test.support.os_helper import TESTFN
1213

1314

@@ -1920,6 +1921,8 @@ def test_rglob_symlink_loop(self):
19201921
}
19211922
self.assertEqual(given, {p / x for x in expect})
19221923

1924+
# See https://github.com/WebAssembly/wasi-filesystem/issues/26
1925+
@unittest.skipIf(is_wasi, "WASI resolution of '..' parts doesn't match POSIX")
19231926
def test_glob_dotdot(self):
19241927
# ".." is not special in globs.
19251928
P = self.cls
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Speed up :meth:`pathlib.Path.glob` by omitting an initial
2+
:meth:`~pathlib.Path.is_dir` call. As a result of this change,
3+
:meth:`~pathlib.Path.glob` can no longer raise :exc:`OSError`.

0 commit comments

Comments
 (0)