diff --git a/Doc/library/os.rst b/Doc/library/os.rst --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -2208,18 +2208,21 @@ os.rmdir(os.path.join(root, name)) -.. function:: fwalk(top, topdown=True, onerror=None, followlinks=False) +.. function:: fwalk(top='.', topdown=True, onerror=None, followlinks=False, *, dir_fd=None) .. index:: single: directory; walking single: directory; traversal This behaves exactly like :func:`walk`, except that it yields a 4-tuple - ``(dirpath, dirnames, filenames, dirfd)``. + ``(dirpath, dirnames, filenames, dirfd)``, and it supports ``dir_fd``. *dirpath*, *dirnames* and *filenames* are identical to :func:`walk` output, and *dirfd* is a file descriptor referring to the directory *dirpath*. + This function always supports :ref:`paths relative to directory descriptors + `. + .. note:: Since :func:`fwalk` yields file descriptors, those are only valid until diff --git a/Lib/os.py b/Lib/os.py --- a/Lib/os.py +++ b/Lib/os.py @@ -422,9 +422,9 @@ __all__.append("walk") -if open in supports_dir_fd: +if {open, stat} <= supports_dir_fd and {listdir, stat} <= supports_fd: - def fwalk(top, topdown=True, onerror=None, followlinks=False): + def fwalk(top=".", topdown=True, onerror=None, followlinks=False, *, dir_fd=None): """Directory tree generator. This behaves exactly like walk(), except that it yields a 4-tuple @@ -434,9 +434,13 @@ `dirpath`, `dirnames` and `filenames` are identical to walk() output, and `dirfd` is a file descriptor referring to the directory `dirpath`. - The advantage of walkfd() over walk() is that it's safe against symlink + The advantage of fwalk() over walk() is that it's safe against symlink races (when followlinks is False). + If dir_fd is not None, it should be a file descriptor open to a directory, + and top should be relative; top will then be relative to that directory. + (dir_fd is always supported for fwalk.) + Caution: Since fwalk() yields file descriptors, those are only valid until the next iteration step, so you should dup() them if you want to keep them @@ -455,11 +459,11 @@ """ # Note: To guard against symlink races, we use the standard # lstat()/open()/fstat() trick. - orig_st = lstat(top) - topfd = open(top, O_RDONLY) + orig_st = stat(top, follow_symlinks=False, dir_fd=dir_fd) + topfd = open(top, O_RDONLY, dir_fd=dir_fd) try: if (followlinks or (st.S_ISDIR(orig_st.st_mode) and - path.samestat(orig_st, fstat(topfd)))): + path.samestat(orig_st, stat(topfd)))): yield from _fwalk(topfd, top, topdown, onerror, followlinks) finally: close(topfd) @@ -502,7 +506,7 @@ onerror(err) return try: - if followlinks or path.samestat(orig_st, fstat(dirfd)): + if followlinks or path.samestat(orig_st, stat(dirfd)): dirpath = path.join(toppath, name) yield from _fwalk(dirfd, dirpath, topdown, onerror, followlinks) finally: diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -741,19 +741,38 @@ class FwalkTests(WalkTests): """Tests for os.fwalk().""" - def test_compare_to_walk(self): - # compare with walk() results + def _compare_to_walk(self, walk_kwargs, fwalk_kwargs): + """ + compare with walk() results. + """ for topdown, followlinks in itertools.product((True, False), repeat=2): - args = support.TESTFN, topdown, None, followlinks + d = {'topdown': topdown, 'followlinks': followlinks} + walk_kwargs.update(d) + fwalk_kwargs.update(d) + expected = {} - for root, dirs, files in os.walk(*args): + for root, dirs, files in os.walk(**walk_kwargs): expected[root] = (set(dirs), set(files)) - for root, dirs, files, rootfd in os.fwalk(*args): + for root, dirs, files, rootfd in os.fwalk(**fwalk_kwargs): self.assertIn(root, expected) self.assertEqual(expected[root], (set(dirs), set(files))) + def test_compare_to_walk(self): + kwargs = {'top': support.TESTFN} + self._compare_to_walk(kwargs, kwargs) + def test_dir_fd(self): + try: + fd = os.open(".", os.O_RDONLY) + walk_kwargs = {'top': support.TESTFN} + fwalk_kwargs = walk_kwargs.copy() + fwalk_kwargs['dir_fd'] = fd + self._compare_to_walk(walk_kwargs, fwalk_kwargs) + finally: + os.close(fd) + + def test_yields_correct_dir_fd(self): # check returned file descriptors for topdown, followlinks in itertools.product((True, False), repeat=2): args = support.TESTFN, topdown, None, followlinks