Skip to content

Commit ec181bb

Browse files
committed
pythonGH-77609: Support following symlinks in pathlib.Path.glob()
1 parent da1980a commit ec181bb

File tree

4 files changed

+50
-5
lines changed

4 files changed

+50
-5
lines changed

Diff for: Doc/library/pathlib.rst

+9-2
Original file line numberDiff line numberDiff line change
@@ -866,8 +866,9 @@ call fails (for example because the path doesn't exist).
866866
[PosixPath('docs/conf.py')]
867867

868868
Patterns are the same as for :mod:`fnmatch`, with the addition of "``**``"
869-
which means "this directory and all subdirectories, recursively". In other
870-
words, it enables recursive globbing::
869+
which means "this directory and all subdirectories, recursively", and "``***``"
870+
which additionally follows symlinks to directories. These wildcards enable
871+
recursive globbing::
871872

872873
>>> sorted(Path('.').glob('**/*.py'))
873874
[PosixPath('build/lib/pathlib.py'),
@@ -886,6 +887,9 @@ call fails (for example because the path doesn't exist).
886887
Return only directories if *pattern* ends with a pathname components
887888
separator (:data:`~os.sep` or :data:`~os.altsep`).
888889

890+
.. versionchanged:: 3.12
891+
Support for the "``***``" wildcard was added.
892+
889893
.. method:: Path.group()
890894

891895
Return the name of the group owning the file. :exc:`KeyError` is raised
@@ -1290,6 +1294,9 @@ call fails (for example because the path doesn't exist).
12901294
Return only directories if *pattern* ends with a pathname components
12911295
separator (:data:`~os.sep` or :data:`~os.altsep`).
12921296

1297+
.. versionchanged:: 3.12
1298+
Support for the "``***``" wildcard was added.
1299+
12931300
.. method:: Path.rmdir()
12941301

12951302
Remove this directory. The directory must be empty.

Diff for: Lib/pathlib.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def _make_selector(pattern_parts, flavour):
6767
child_parts = pattern_parts[1:]
6868
if not pat:
6969
return _TerminatingSelector()
70-
if pat == '**':
70+
if pat == '**' or pat == '***':
7171
cls = _RecursiveWildcardSelector
7272
elif pat == '..':
7373
cls = _ParentSelector
@@ -154,6 +154,7 @@ def _select_from(self, parent_path, scandir):
154154
class _RecursiveWildcardSelector(_Selector):
155155

156156
def __init__(self, pat, child_parts, flavour):
157+
self.follow_symlinks = pat == '***'
157158
_Selector.__init__(self, child_parts, flavour)
158159

159160
def _iterate_directories(self, parent_path, scandir):
@@ -166,11 +167,11 @@ def _iterate_directories(self, parent_path, scandir):
166167
for entry in entries:
167168
entry_is_dir = False
168169
try:
169-
entry_is_dir = entry.is_dir()
170+
entry_is_dir = entry.is_dir(follow_symlinks=self.follow_symlinks)
170171
except OSError as e:
171172
if not _ignore_error(e):
172173
raise
173-
if entry_is_dir and not entry.is_symlink():
174+
if entry_is_dir:
174175
path = parent_path._make_child_relpath(entry.name)
175176
for p in self._iterate_directories(path, scandir):
176177
yield p

Diff for: Lib/test/test_pathlib.py

+34
Original file line numberDiff line numberDiff line change
@@ -1938,6 +1938,40 @@ def my_scandir(path):
19381938
subdir.chmod(000)
19391939
self.assertEqual(len(set(base.glob("*"))), 4)
19401940

1941+
def test_glob_recurse_symlinks(self):
1942+
def _check(glob, expected):
1943+
glob = {path for path in glob if "linkD" not in path.parts}
1944+
self.assertEqual(glob, { P(BASE, q) for q in expected })
1945+
P = self.cls
1946+
1947+
p = P(BASE)
1948+
if os_helper.can_symlink():
1949+
_check(p.glob("***/fileB"), ["dirB/fileB", "dirA/linkC/fileB", "linkB/fileB"])
1950+
_check(p.glob("***/*/fileA"), [])
1951+
_check(p.glob("***/*/fileB"), ["dirB/fileB", "linkB/fileB", "dirA/linkC/fileB"])
1952+
_check(p.glob("***/file*"), ["fileA", "dirA/linkC/fileB", "dirB/fileB", "dirC/fileC",
1953+
"dirC/dirD/fileD", "linkB/fileB"])
1954+
_check(p.glob("***/*/"), ["dirA", "dirA/linkC", "dirB", "dirC",
1955+
"dirC/dirD", "dirE", "linkB",])
1956+
_check(p.glob("***"), ["", "dirA", "dirA/linkC", "dirB", "dirC", "dirE", "dirC/dirD",
1957+
"linkB"])
1958+
else:
1959+
_check(p.glob("***/fileB"), ["dirB/fileB"])
1960+
_check(p.glob("***/*/fileA"), [])
1961+
_check(p.glob("***/*/fileB"), ["dirB/fileB"])
1962+
_check(p.glob("***/file*"), ["fileA", "dirB/fileB", "dirC/fileC", "dirC/dirD/fileD"])
1963+
_check(p.glob("***/*/"), ["dirA", "dirB", "dirC", "dirC/dirD", "dirE"])
1964+
_check(p.glob("***"), ["", "dirA", "dirB", "dirC", "dirE", "dirC/dirD"])
1965+
1966+
p = P(BASE, "dirC")
1967+
_check(p.glob("***/*"), ["dirC/fileC", "dirC/novel.txt", "dirC/dirD", "dirC/dirD/fileD"])
1968+
_check(p.glob("***/file*"), ["dirC/fileC", "dirC/dirD/fileD"])
1969+
_check(p.glob("***/*/*"), ["dirC/dirD/fileD"])
1970+
_check(p.glob("***/*/"), ["dirC/dirD"])
1971+
_check(p.glob("***"), ["dirC", "dirC/dirD"])
1972+
_check(p.glob("***/*.txt"), ["dirC/novel.txt"])
1973+
_check(p.glob("***/*.*"), ["dirC/novel.txt"])
1974+
19411975
def _check_resolve(self, p, expected, strict=True):
19421976
q = p.resolve(strict)
19431977
self.assertEqual(q, expected)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add support for "``***``" wildcard in :meth:`pathlib.Path.glob` and
2+
:meth:`~pathlib.Path.rglob`. This wildcard works like "``**``", except that
3+
it also recurses into symlinks.

0 commit comments

Comments
 (0)