Skip to content

bpo-36602: Allow pathlib.Path.iterdir to list recursively #12785

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
553a5a8
bpo-36602: pathlib can list recursively
EpicWink Apr 11, 2019
72bd104
📜🤖 Added by blurb_it.
blurb-it[bot] Apr 11, 2019
3220289
bpo-36602: Check for symlink recurse (GH-12785)
EpicWink Apr 13, 2019
660e616
bpo-36602: Test cyclic symlinks (GH-12785)
EpicWink Apr 13, 2019
82b0978
bpo-36602: Implement cycle detection (GH-12785)
EpicWink Apr 29, 2019
516d41b
bpo-36602: Indicate keyword-only (GH-12785)
EpicWink May 1, 2019
331ff3b
bpo-36602: Add version-changed (GH-12785)
EpicWink May 1, 2019
02ff5cf
Document recursive listing added in Python 3.9
EpicWink Jun 5, 2019
7976603
Don't write text to new files in sumlink test
EpicWink Jun 19, 2019
43c00c3
Add comment on created directory structure
EpicWink Jun 19, 2019
738ef90
Remove unwanted assertion
EpicWink Jun 19, 2019
4bb3b29
Use 'pathlib.Path.touch' to create files in test
EpicWink Jul 4, 2019
1c2969e
Merge branch 'master' into feat/pathlib-iterdir
EpicWink Apr 1, 2020
eca48b9
Merge branch 'master' into feat/pathlib-iterdir
EpicWink Apr 20, 2020
8d90fa4
Merge branch 'master' into feat/pathlib-iterdir
EpicWink Aug 30, 2020
c29c04a
Merge branch 'master' into feat/pathlib-iterdir
EpicWink Aug 30, 2020
7b56d3a
Merge branch 'master' into feat/pathlib-iterdir
EpicWink Oct 5, 2020
4de68c3
Merge branch 'master' into feat/pathlib-iterdir
EpicWink Apr 7, 2021
e2f09a4
Fix symlink-cycle test
EpicWink Apr 7, 2021
7ad402b
Merge branch 'master' into feat/pathlib-iterdir
EpicWink Apr 27, 2021
d65b4a9
Merge branch 'master' into feat/pathlib-iterdir
EpicWink Apr 28, 2021
501e40b
Merge branch 'master' into feat/pathlib-iterdir
EpicWink May 2, 2021
3a1ae8b
Merge branch 'main' into feat/pathlib-iterdir
EpicWink May 14, 2021
143e824
Merge branch 'main' into feat/pathlib-iterdir
EpicWink May 24, 2021
521b530
Merge branch 'main' into feat/pathlib-iterdir
EpicWink Jul 29, 2021
7619a60
Merge branch 'main' into feat/pathlib-iterdir
EpicWink Jul 30, 2021
63beb2e
Merge branch 'main' into feat/pathlib-iterdir
EpicWink Dec 30, 2021
e417896
Merge branch 'main' into feat/pathlib-iterdir
EpicWink Jan 21, 2022
61271ec
Merge branch 'main' into feat/pathlib-iterdir
EpicWink Jan 25, 2022
d569f8e
Merge branch 'main' into feat/pathlib-iterdir
EpicWink Jan 31, 2022
c78c662
Merge branch 'main' into feat/pathlib-iterdir
EpicWink Feb 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,7 @@ call fails (for example because the path doesn't exist).
other errors (such as permission errors) are propagated.


.. method:: Path.iterdir()
.. method:: Path.iterdir(*, recursive=False)

When the path points to a directory, yield path objects of the directory
contents::
Expand All @@ -917,6 +917,19 @@ call fails (for example because the path doesn't exist).
to the directory after creating the iterator, whether an path object for
that file be included is unspecified.

Also supports recursing into subdirectories::

>>> p = Path('foo')
>>> for child in p.iterdir(recursive=True): child
...
PosixPath('foo/a.txt')
PosixPath('foo/b.py')
PosixPath('foo/bar/c.txt')
PosixPath('foo/bar/spam/d.rst')

.. versionchanged:: 3.9
The *recursive* parameter was added.

.. method:: Path.lchmod(mode)

Like :meth:`Path.chmod` but, if the path points to a symbolic link, the
Expand Down
24 changes: 22 additions & 2 deletions Lib/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -920,10 +920,30 @@ def samefile(self, other_path):
other_st = self.__class__(other_path).stat()
return os.path.samestat(st, other_st)

def iterdir(self):
def _iterdir_recursive(self):
"""Recursive directory listing."""
to_list = [self]
listed = set()
while len(to_list) > 0:
dirpath = to_list.pop(0)
for path in dirpath.iterdir(recursive=False):
if path.is_dir():
resolved = path.resolve()
if resolved not in listed:
to_list.append(path)
listed.add(resolved)
else:
yield path

def iterdir(self, *, recursive=False):
"""Iterate over the files in this directory. Does not yield any
result for the special paths '.' and '..'.
result for the special paths '.' and '..'. Yields from
subdirectories if recursive=True (but then doesn't yield any
directories).
"""
if recursive:
yield from self._iterdir_recursive()
return
for name in os.listdir(self):
yield self._make_child_relpath(name)

Expand Down
55 changes: 55 additions & 0 deletions Lib/test/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1612,6 +1612,61 @@ def test_iterdir(self):
expected += ['linkA', 'linkB', 'brokenLink', 'brokenLinkLoop']
self.assertEqual(paths, { P(BASE, q) for q in expected })

def test_iterdir_recursive(self):
P = self.cls
p = P(join('dirC'))
it = p.iterdir(recursive=True)
paths = set(it)
expected = [('dirD', 'fileD'), ('fileC',)]
self.assertEqual(paths, { P(join('dirC'), *q) for q in expected })

@os_helper.skip_unless_symlink
def test_iterdir_recursive_symlink(self):
P = self.cls
p = P(join('dirB'))
it = p.iterdir(recursive=True)
paths = set(it)
expected = [('fileB',), ('linkD', 'fileB')]
self.assertEqual(paths, { P(join('dirB'), *q) for q in expected })

@os_helper.skip_unless_symlink
def test_iterdir_recursive_symlink_cycle(self):
P = self.cls
os.mkdir(join('dirF'))
os.mkdir(join('dirF', 'dirG'))
os.mkdir(join('dirF', 'dirH'))
P(join('dirF', 'fileF')).touch()
P(join('dirF', 'dirG', 'fileG')).touch()
P(join('dirF', 'dirH', 'fileH')).touch()
os.symlink(os.path.join('..', 'dirG'), join('dirF', 'dirH', 'linkG'))
os.symlink(os.path.join('..', 'dirH'), join('dirF', 'dirG', 'linkH'))
# Now have structure
# (BASE)
# |
# ...
# |
# |-- dirF
# | |-- dirG
# | | |-- fileG
# | | `-- linkH -> ../dirH
# | |-- dirH
# | | |-- fileH
# | | `-- linkG -> ../dirG
# | `-- fileF
# |
# ...
try:
p = P(join('dirF'))
it = p.iterdir(recursive=True)
paths = set(it)
expected = [
('fileF',),
('dirG', 'fileG'),
('dirH', 'fileH')]
self.assertEqual(paths, { P(join('dirF'), *q) for q in expected })
finally:
os_helper.rmtree(join('dirF'))

@os_helper.skip_unless_symlink
def test_iterdir_symlink(self):
# __iter__ on a symlink to a directory.
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,7 @@ Ethan Onstott
Ken Jin Ooi
Piet van Oostrum
Tomas Oppelstrup
Laurie Opperman
Jason Orendorff
Bastien Orivel
orlnub123
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``pathlib.Path.iterdir`` can now list items in subdirectories with the optional argument ``recursive=True``. Note that directories will not be yielded in recursive listing