mirror of https://github.com/python/cpython
GH-89727: Fix `os.fwalk()` recursion error on deep trees (#119638)
Implement `os.fwalk()` using a list as a stack to avoid emitting recursion errors on deeply nested trees.
This commit is contained in:
parent
2cc3502f98
commit
3c890b503c
89
Lib/os.py
89
Lib/os.py
|
@ -478,24 +478,52 @@ if {open, stat} <= supports_dir_fd and {scandir, stat} <= supports_fd:
|
||||||
"""
|
"""
|
||||||
sys.audit("os.fwalk", top, topdown, onerror, follow_symlinks, dir_fd)
|
sys.audit("os.fwalk", top, topdown, onerror, follow_symlinks, dir_fd)
|
||||||
top = fspath(top)
|
top = fspath(top)
|
||||||
# Note: To guard against symlink races, we use the standard
|
stack = [(_fwalk_walk, (True, dir_fd, top, top, None))]
|
||||||
# lstat()/open()/fstat() trick.
|
isbytes = isinstance(top, bytes)
|
||||||
if not follow_symlinks:
|
while stack:
|
||||||
orig_st = stat(top, follow_symlinks=False, dir_fd=dir_fd)
|
yield from _fwalk(stack, isbytes, topdown, onerror, follow_symlinks)
|
||||||
topfd = open(top, O_RDONLY | O_NONBLOCK, dir_fd=dir_fd)
|
|
||||||
try:
|
|
||||||
if (follow_symlinks or (st.S_ISDIR(orig_st.st_mode) and
|
|
||||||
path.samestat(orig_st, stat(topfd)))):
|
|
||||||
yield from _fwalk(topfd, top, isinstance(top, bytes),
|
|
||||||
topdown, onerror, follow_symlinks)
|
|
||||||
finally:
|
|
||||||
close(topfd)
|
|
||||||
|
|
||||||
def _fwalk(topfd, toppath, isbytes, topdown, onerror, follow_symlinks):
|
# Each item in the _fwalk() stack is a pair (action, args).
|
||||||
|
_fwalk_walk = 0 # args: (isroot, dirfd, toppath, topname, entry)
|
||||||
|
_fwalk_yield = 1 # args: (toppath, dirnames, filenames, topfd)
|
||||||
|
_fwalk_close = 2 # args: dirfd
|
||||||
|
|
||||||
|
def _fwalk(stack, isbytes, topdown, onerror, follow_symlinks):
|
||||||
# Note: This uses O(depth of the directory tree) file descriptors: if
|
# Note: This uses O(depth of the directory tree) file descriptors: if
|
||||||
# necessary, it can be adapted to only require O(1) FDs, see issue
|
# necessary, it can be adapted to only require O(1) FDs, see issue
|
||||||
# #13734.
|
# #13734.
|
||||||
|
|
||||||
|
action, value = stack.pop()
|
||||||
|
if action == _fwalk_close:
|
||||||
|
close(value)
|
||||||
|
return
|
||||||
|
elif action == _fwalk_yield:
|
||||||
|
yield value
|
||||||
|
return
|
||||||
|
assert action == _fwalk_walk
|
||||||
|
isroot, dirfd, toppath, topname, entry = value
|
||||||
|
try:
|
||||||
|
if not follow_symlinks:
|
||||||
|
# Note: To guard against symlink races, we use the standard
|
||||||
|
# lstat()/open()/fstat() trick.
|
||||||
|
if entry is None:
|
||||||
|
orig_st = stat(topname, follow_symlinks=False, dir_fd=dirfd)
|
||||||
|
else:
|
||||||
|
orig_st = entry.stat(follow_symlinks=False)
|
||||||
|
topfd = open(topname, O_RDONLY | O_NONBLOCK, dir_fd=dirfd)
|
||||||
|
except OSError as err:
|
||||||
|
if isroot:
|
||||||
|
raise
|
||||||
|
if onerror is not None:
|
||||||
|
onerror(err)
|
||||||
|
return
|
||||||
|
stack.append((_fwalk_close, topfd))
|
||||||
|
if not follow_symlinks:
|
||||||
|
if isroot and not st.S_ISDIR(orig_st.st_mode):
|
||||||
|
return
|
||||||
|
if not path.samestat(orig_st, stat(topfd)):
|
||||||
|
return
|
||||||
|
|
||||||
scandir_it = scandir(topfd)
|
scandir_it = scandir(topfd)
|
||||||
dirs = []
|
dirs = []
|
||||||
nondirs = []
|
nondirs = []
|
||||||
|
@ -521,31 +549,18 @@ if {open, stat} <= supports_dir_fd and {scandir, stat} <= supports_fd:
|
||||||
|
|
||||||
if topdown:
|
if topdown:
|
||||||
yield toppath, dirs, nondirs, topfd
|
yield toppath, dirs, nondirs, topfd
|
||||||
|
else:
|
||||||
|
stack.append((_fwalk_yield, (toppath, dirs, nondirs, topfd)))
|
||||||
|
|
||||||
for name in dirs if entries is None else zip(dirs, entries):
|
toppath = path.join(toppath, toppath[:0]) # Add trailing slash.
|
||||||
try:
|
if entries is None:
|
||||||
if not follow_symlinks:
|
stack.extend(
|
||||||
if topdown:
|
(_fwalk_walk, (False, topfd, toppath + name, name, None))
|
||||||
orig_st = stat(name, dir_fd=topfd, follow_symlinks=False)
|
for name in dirs[::-1])
|
||||||
else:
|
else:
|
||||||
assert entries is not None
|
stack.extend(
|
||||||
name, entry = name
|
(_fwalk_walk, (False, topfd, toppath + name, name, entry))
|
||||||
orig_st = entry.stat(follow_symlinks=False)
|
for name, entry in zip(dirs[::-1], entries[::-1]))
|
||||||
dirfd = open(name, O_RDONLY | O_NONBLOCK, dir_fd=topfd)
|
|
||||||
except OSError as err:
|
|
||||||
if onerror is not None:
|
|
||||||
onerror(err)
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
if follow_symlinks or path.samestat(orig_st, stat(dirfd)):
|
|
||||||
dirpath = path.join(toppath, name)
|
|
||||||
yield from _fwalk(dirfd, dirpath, isbytes,
|
|
||||||
topdown, onerror, follow_symlinks)
|
|
||||||
finally:
|
|
||||||
close(dirfd)
|
|
||||||
|
|
||||||
if not topdown:
|
|
||||||
yield toppath, dirs, nondirs, topfd
|
|
||||||
|
|
||||||
__all__.append("fwalk")
|
__all__.append("fwalk")
|
||||||
|
|
||||||
|
|
|
@ -1687,8 +1687,6 @@ class FwalkTests(WalkTests):
|
||||||
|
|
||||||
# fwalk() keeps file descriptors open
|
# fwalk() keeps file descriptors open
|
||||||
test_walk_many_open_files = None
|
test_walk_many_open_files = None
|
||||||
# fwalk() still uses recursion
|
|
||||||
test_walk_above_recursion_limit = None
|
|
||||||
|
|
||||||
|
|
||||||
class BytesWalkTests(WalkTests):
|
class BytesWalkTests(WalkTests):
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
Fix issue with :func:`os.fwalk` where a :exc:`RecursionError` was raised on
|
||||||
|
deep directory trees by adjusting the implementation to be iterative instead
|
||||||
|
of recursive.
|
Loading…
Reference in New Issue