Skip to content

Commit 929338d

Browse files
committed
gh-155935: Stop FileFinder._find_children retrying a failing directory scan
Split the `OSError` handling: an `OSError` from `next()` ends the listing, while one from `is_dir()` or `is_file()` skips just that entry.
1 parent e391052 commit 929338d

3 files changed

Lines changed: 72 additions & 4 deletions

File tree

Lib/importlib/_bootstrap_external.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,6 +1454,13 @@ def _find_children(self):
14541454
while True:
14551455
try:
14561456
entry = next(scan_iterator)
1457+
except StopIteration:
1458+
break
1459+
except OSError:
1460+
# A failing scan cannot make progress; end the listing
1461+
# like _fill_cache() treats an unreadable directory.
1462+
break
1463+
try:
14571464
if entry.name == _PYCACHE:
14581465
continue
14591466
# packages
@@ -1467,9 +1474,7 @@ def _find_children(self):
14671474
if entry.name.endswith(suffix)
14681475
}
14691476
except OSError:
1470-
pass # ignore exceptions from next(scan_iterator) and os.DirEntry
1471-
except StopIteration:
1472-
break
1477+
pass # skip entries whose os.DirEntry methods fail
14731478

14741479
def discover(self, parent=None):
14751480
if parent and parent.submodule_search_locations is None:

Lib/test/test_importlib/test_discover.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from unittest.mock import Mock
1+
from unittest.mock import Mock, patch
22

33
from test.test_importlib import util
44

@@ -114,6 +114,68 @@ def test_invalid_parent(self):
114114
with self.assertRaises(ValueError):
115115
list(finder.discover(example))
116116

117+
def _patch_scandir(self, scandir):
118+
module_os = self.machinery.FileFinder._fill_cache.__globals__['_os']
119+
return patch.object(module_os, 'scandir', scandir)
120+
121+
def test_discover_persistently_failing_scan(self):
122+
# gh-155935: an iterator that raises OSError on every next() call
123+
# must end the listing instead of looping forever.
124+
class FailingScandirIterator:
125+
calls = 0
126+
127+
def __enter__(self):
128+
return self
129+
130+
def __exit__(self, *args):
131+
return False
132+
133+
def __next__(self):
134+
self.calls += 1
135+
if self.calls > 100:
136+
# Safety net so regressed code fails fast on the call
137+
# count below instead of hanging the test forever.
138+
raise StopIteration
139+
raise OSError('persistently failing directory scan')
140+
141+
scan_iterator = FailingScandirIterator()
142+
with self._patch_scandir(lambda path: scan_iterator):
143+
finder = self.get_finder('dummy')
144+
discovered = list(finder.discover())
145+
self.assertEqual(discovered, [])
146+
# A failed scan must not be retried.
147+
self.assertEqual(scan_iterator.calls, 1)
148+
149+
def test_find_children_failing_direntry(self):
150+
# An entry whose DirEntry methods raise OSError is skipped; the
151+
# remaining entries are still listed.
152+
failing = Mock()
153+
failing.name = 'failing'
154+
failing.is_dir.side_effect = OSError('stat failed')
155+
good = Mock()
156+
good.name = 'example.py'
157+
good.is_dir.return_value = False
158+
good.is_file.return_value = True
159+
160+
class FakeScandirIterator:
161+
def __init__(self, entries):
162+
self._iterator = iter(entries)
163+
164+
def __enter__(self):
165+
return self
166+
167+
def __exit__(self, *args):
168+
return False
169+
170+
def __next__(self):
171+
return next(self._iterator)
172+
173+
with self._patch_scandir(
174+
lambda path: FakeScandirIterator([failing, good])):
175+
finder = self.get_finder('dummy')
176+
children = list(finder._find_children())
177+
self.assertEqual(children, ['example'])
178+
117179

118180
(
119181
Frozen_TestFileFinder,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix :meth:`!importlib.machinery.FileFinder.discover` looping forever when the underlying directory scan keeps failing with :exc:`OSError`. A failing scan now ends the listing instead of being retried.

0 commit comments

Comments
 (0)