Skip to content

Commit a4eb3f6

Browse files
barneygalediegorusso
authored andcommitted
pythonGH-77609: Add recurse_symlinks argument to pathlib.Path.glob() (python#117311)
Replace tri-state `follow_symlinks` with boolean `recurse_symlinks` argument. The new argument controls whether symlinks are followed when expanding recursive `**` wildcards. The possible argument values correspond as follows: follow_symlinks recurse_symlinks =============== ================ False N/A None False True True We therefore drop support for not following symlinks when expanding non-recursive pattern parts; it wasn't requested in the original issue, and it's a feature not found in any shells. This makes the API a easier to grok by eliminating `None` as an option. No news blurb as `follow_symlinks` was new in 3.13.
1 parent 19ea065 commit a4eb3f6

File tree

5 files changed

+34
-95
lines changed

5 files changed

+34
-95
lines changed

Diff for: Doc/library/pathlib.rst

+8-13
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,7 @@ call fails (for example because the path doesn't exist).
985985
.. versionadded:: 3.5
986986

987987

988-
.. method:: Path.glob(pattern, *, case_sensitive=None, follow_symlinks=None)
988+
.. method:: Path.glob(pattern, *, case_sensitive=None, recurse_symlinks=False)
989989

990990
Glob the given relative *pattern* in the directory represented by this path,
991991
yielding all matching files (of any kind)::
@@ -1013,28 +1013,23 @@ call fails (for example because the path doesn't exist).
10131013
typically, case-sensitive on POSIX, and case-insensitive on Windows.
10141014
Set *case_sensitive* to ``True`` or ``False`` to override this behaviour.
10151015

1016-
By default, or when the *follow_symlinks* keyword-only argument is set to
1017-
``None``, this method follows symlinks except when expanding "``**``"
1018-
wildcards. Set *follow_symlinks* to ``True`` to always follow symlinks, or
1019-
``False`` to treat all symlinks as files.
1020-
1021-
.. tip::
1022-
Set *follow_symlinks* to ``True`` or ``False`` to improve performance
1023-
of recursive globbing.
1016+
By default, or when the *recurse_symlinks* keyword-only argument is set to
1017+
``False``, this method follows symlinks except when expanding "``**``"
1018+
wildcards. Set *recurse_symlinks* to ``True`` to always follow symlinks.
10241019

10251020
.. audit-event:: pathlib.Path.glob self,pattern pathlib.Path.glob
10261021

10271022
.. versionchanged:: 3.12
10281023
The *case_sensitive* parameter was added.
10291024

10301025
.. versionchanged:: 3.13
1031-
The *follow_symlinks* parameter was added.
1026+
The *recurse_symlinks* parameter was added.
10321027

10331028
.. versionchanged:: 3.13
10341029
The *pattern* parameter accepts a :term:`path-like object`.
10351030

10361031

1037-
.. method:: Path.rglob(pattern, *, case_sensitive=None, follow_symlinks=None)
1032+
.. method:: Path.rglob(pattern, *, case_sensitive=None, recurse_symlinks=False)
10381033

10391034
Glob the given relative *pattern* recursively. This is like calling
10401035
:func:`Path.glob` with "``**/``" added in front of the *pattern*.
@@ -1048,7 +1043,7 @@ call fails (for example because the path doesn't exist).
10481043
The *case_sensitive* parameter was added.
10491044

10501045
.. versionchanged:: 3.13
1051-
The *follow_symlinks* parameter was added.
1046+
The *recurse_symlinks* parameter was added.
10521047

10531048
.. versionchanged:: 3.13
10541049
The *pattern* parameter accepts a :term:`path-like object`.
@@ -1675,7 +1670,7 @@ The patterns accepted and results generated by :meth:`Path.glob` and
16751670
passing ``recursive=True`` to :func:`glob.glob`.
16761671
3. "``**``" pattern components do not follow symlinks by default in pathlib.
16771672
This behaviour has no equivalent in :func:`glob.glob`, but you can pass
1678-
``follow_symlinks=True`` to :meth:`Path.glob` for compatible behaviour.
1673+
``recurse_symlinks=True`` to :meth:`Path.glob` for compatible behaviour.
16791674
4. Like all :class:`PurePath` and :class:`Path` objects, the values returned
16801675
from :meth:`Path.glob` and :meth:`Path.rglob` don't include trailing
16811676
slashes.

Diff for: Doc/whatsnew/3.13.rst

+7-4
Original file line numberDiff line numberDiff line change
@@ -559,12 +559,15 @@ pathlib
559559
implementation of :mod:`os.path` used for low-level path parsing and
560560
joining: either ``posixpath`` or ``ntpath``.
561561

562-
* Add *follow_symlinks* keyword-only argument to :meth:`pathlib.Path.glob`,
563-
:meth:`~pathlib.Path.rglob`, :meth:`~pathlib.Path.is_file`,
562+
* Add *recurse_symlinks* keyword-only argument to :meth:`pathlib.Path.glob`
563+
and :meth:`~pathlib.Path.rglob`.
564+
(Contributed by Barney Gale in :gh:`77609`).
565+
566+
* Add *follow_symlinks* keyword-only argument to :meth:`~pathlib.Path.is_file`,
564567
:meth:`~pathlib.Path.is_dir`, :meth:`~pathlib.Path.owner`,
565568
:meth:`~pathlib.Path.group`.
566-
(Contributed by Barney Gale in :gh:`77609` and :gh:`105793`, and
567-
Kamil Turek in :gh:`107962`).
569+
(Contributed by Barney Gale in :gh:`105793`, and Kamil Turek in
570+
:gh:`107962`).
568571

569572
* Return files and directories from :meth:`pathlib.Path.glob` and
570573
:meth:`~pathlib.Path.rglob` when given a pattern that ends with "``**``". In

Diff for: Lib/pathlib/__init__.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -619,17 +619,17 @@ def _make_child_relpath(self, name):
619619
path._tail_cached = tail + [name]
620620
return path
621621

622-
def glob(self, pattern, *, case_sensitive=None, follow_symlinks=None):
622+
def glob(self, pattern, *, case_sensitive=None, recurse_symlinks=False):
623623
"""Iterate over this subtree and yield all existing files (of any
624624
kind, including directories) matching the given relative pattern.
625625
"""
626626
sys.audit("pathlib.Path.glob", self, pattern)
627627
if not isinstance(pattern, PurePath):
628628
pattern = self.with_segments(pattern)
629629
return _abc.PathBase.glob(
630-
self, pattern, case_sensitive=case_sensitive, follow_symlinks=follow_symlinks)
630+
self, pattern, case_sensitive=case_sensitive, recurse_symlinks=recurse_symlinks)
631631

632-
def rglob(self, pattern, *, case_sensitive=None, follow_symlinks=None):
632+
def rglob(self, pattern, *, case_sensitive=None, recurse_symlinks=False):
633633
"""Recursively yield all existing files (of any kind, including
634634
directories) matching the given relative pattern, anywhere in
635635
this subtree.
@@ -639,7 +639,7 @@ def rglob(self, pattern, *, case_sensitive=None, follow_symlinks=None):
639639
pattern = self.with_segments(pattern)
640640
pattern = '**' / pattern
641641
return _abc.PathBase.glob(
642-
self, pattern, case_sensitive=case_sensitive, follow_symlinks=follow_symlinks)
642+
self, pattern, case_sensitive=case_sensitive, recurse_symlinks=recurse_symlinks)
643643

644644
def walk(self, top_down=True, on_error=None, follow_symlinks=False):
645645
"""Walk the directory tree from this directory, similar to os.walk()."""

Diff for: Lib/pathlib/_abc.py

+8-12
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,8 @@ def _select_special(paths, part):
6666
yield path._make_child_relpath(part)
6767

6868

69-
def _select_children(parent_paths, dir_only, follow_symlinks, match):
69+
def _select_children(parent_paths, dir_only, match):
7070
"""Yield direct children of given paths, filtering by name and type."""
71-
if follow_symlinks is None:
72-
follow_symlinks = True
7371
for parent_path in parent_paths:
7472
try:
7573
# We must close the scandir() object before proceeding to
@@ -82,7 +80,7 @@ def _select_children(parent_paths, dir_only, follow_symlinks, match):
8280
for entry in entries:
8381
if dir_only:
8482
try:
85-
if not entry.is_dir(follow_symlinks=follow_symlinks):
83+
if not entry.is_dir():
8684
continue
8785
except OSError:
8886
continue
@@ -96,8 +94,6 @@ def _select_recursive(parent_paths, dir_only, follow_symlinks, match):
9694
"""Yield given paths and all their children, recursively, filtering by
9795
string and type.
9896
"""
99-
if follow_symlinks is None:
100-
follow_symlinks = False
10197
for parent_path in parent_paths:
10298
if match is not None:
10399
# If we're filtering paths through a regex, record the length of
@@ -789,7 +785,7 @@ def _make_child_direntry(self, entry):
789785
def _make_child_relpath(self, name):
790786
return self.joinpath(name)
791787

792-
def glob(self, pattern, *, case_sensitive=None, follow_symlinks=True):
788+
def glob(self, pattern, *, case_sensitive=None, recurse_symlinks=True):
793789
"""Iterate over this subtree and yield all existing files (of any
794790
kind, including directories) matching the given relative pattern.
795791
"""
@@ -818,7 +814,7 @@ def glob(self, pattern, *, case_sensitive=None, follow_symlinks=True):
818814
# Consume following non-special components, provided we're
819815
# treating symlinks consistently. Each component is joined
820816
# onto 'part', which is used to generate an re.Pattern object.
821-
if follow_symlinks is not None:
817+
if recurse_symlinks:
822818
while stack and stack[-1] not in specials:
823819
part += sep + stack.pop()
824820

@@ -827,7 +823,7 @@ def glob(self, pattern, *, case_sensitive=None, follow_symlinks=True):
827823
match = _compile_pattern(part, sep, case_sensitive) if part != '**' else None
828824

829825
# Recursively walk directories, filtering by type and regex.
830-
paths = _select_recursive(paths, bool(stack), follow_symlinks, match)
826+
paths = _select_recursive(paths, bool(stack), recurse_symlinks, match)
831827

832828
# De-duplicate if we've already seen a '**' component.
833829
if deduplicate_paths:
@@ -843,18 +839,18 @@ def glob(self, pattern, *, case_sensitive=None, follow_symlinks=True):
843839
match = _compile_pattern(part, sep, case_sensitive) if part != '*' else None
844840

845841
# Iterate over directories' children filtering by type and regex.
846-
paths = _select_children(paths, bool(stack), follow_symlinks, match)
842+
paths = _select_children(paths, bool(stack), match)
847843
return paths
848844

849-
def rglob(self, pattern, *, case_sensitive=None, follow_symlinks=True):
845+
def rglob(self, pattern, *, case_sensitive=None, recurse_symlinks=True):
850846
"""Recursively yield all existing files (of any kind, including
851847
directories) matching the given relative pattern, anywhere in
852848
this subtree.
853849
"""
854850
if not isinstance(pattern, PurePathBase):
855851
pattern = self.with_segments(pattern)
856852
pattern = '**' / pattern
857-
return self.glob(pattern, case_sensitive=case_sensitive, follow_symlinks=follow_symlinks)
853+
return self.glob(pattern, case_sensitive=case_sensitive, recurse_symlinks=recurse_symlinks)
858854

859855
def walk(self, top_down=True, on_error=None, follow_symlinks=False):
860856
"""Walk the directory tree from this directory, similar to os.walk()."""

Diff for: Lib/test/test_pathlib/test_pathlib_abc.py

+7-62
Original file line numberDiff line numberDiff line change
@@ -1776,9 +1776,9 @@ def _check(path, pattern, case_sensitive, expected):
17761776
_check(path, "dirb/file*", False, ["dirB/fileB"])
17771777

17781778
@needs_symlinks
1779-
def test_glob_follow_symlinks_common(self):
1779+
def test_glob_recurse_symlinks_common(self):
17801780
def _check(path, glob, expected):
1781-
actual = {path for path in path.glob(glob, follow_symlinks=True)
1781+
actual = {path for path in path.glob(glob, recurse_symlinks=True)
17821782
if path.parts.count("linkD") <= 1} # exclude symlink loop.
17831783
self.assertEqual(actual, { P(self.base, q) for q in expected })
17841784
P = self.cls
@@ -1812,39 +1812,9 @@ def _check(path, glob, expected):
18121812
_check(p, "*/dirD/**", ["dirC/dirD/", "dirC/dirD/fileD"])
18131813
_check(p, "*/dirD/**/", ["dirC/dirD/"])
18141814

1815-
@needs_symlinks
1816-
def test_glob_no_follow_symlinks_common(self):
1817-
def _check(path, glob, expected):
1818-
actual = {path for path in path.glob(glob, follow_symlinks=False)}
1819-
self.assertEqual(actual, { P(self.base, q) for q in expected })
1820-
P = self.cls
1821-
p = P(self.base)
1822-
_check(p, "fileB", [])
1823-
_check(p, "dir*/file*", ["dirB/fileB", "dirC/fileC"])
1824-
_check(p, "*A", ["dirA", "fileA", "linkA"])
1825-
_check(p, "*B/*", ["dirB/fileB", "dirB/linkD"])
1826-
_check(p, "*/fileB", ["dirB/fileB"])
1827-
_check(p, "*/", ["dirA/", "dirB/", "dirC/", "dirE/"])
1828-
_check(p, "dir*/*/..", ["dirC/dirD/.."])
1829-
_check(p, "dir*/**", [
1830-
"dirA/", "dirA/linkC",
1831-
"dirB/", "dirB/fileB", "dirB/linkD",
1832-
"dirC/", "dirC/fileC", "dirC/dirD", "dirC/dirD/fileD", "dirC/novel.txt",
1833-
"dirE/"])
1834-
_check(p, "dir*/**/", ["dirA/", "dirB/", "dirC/", "dirC/dirD/", "dirE/"])
1835-
_check(p, "dir*/**/..", ["dirA/..", "dirB/..", "dirC/..", "dirC/dirD/..", "dirE/.."])
1836-
_check(p, "dir*/*/**", ["dirC/dirD/", "dirC/dirD/fileD"])
1837-
_check(p, "dir*/*/**/", ["dirC/dirD/"])
1838-
_check(p, "dir*/*/**/..", ["dirC/dirD/.."])
1839-
_check(p, "dir*/**/fileC", ["dirC/fileC"])
1840-
_check(p, "dir*/*/../dirD/**", ["dirC/dirD/../dirD/", "dirC/dirD/../dirD/fileD"])
1841-
_check(p, "dir*/*/../dirD/**/", ["dirC/dirD/../dirD/"])
1842-
_check(p, "*/dirD/**", ["dirC/dirD/", "dirC/dirD/fileD"])
1843-
_check(p, "*/dirD/**/", ["dirC/dirD/"])
1844-
1845-
def test_rglob_follow_symlinks_none(self):
1815+
def test_rglob_recurse_symlinks_false(self):
18461816
def _check(path, glob, expected):
1847-
actual = set(path.rglob(glob, follow_symlinks=None))
1817+
actual = set(path.rglob(glob, recurse_symlinks=False))
18481818
self.assertEqual(actual, { P(self.base, q) for q in expected })
18491819
P = self.cls
18501820
p = P(self.base)
@@ -1901,9 +1871,9 @@ def test_rglob_windows(self):
19011871
self.assertEqual(set(map(str, p.rglob("FILEd"))), {f"{p}\\dirD\\fileD"})
19021872

19031873
@needs_symlinks
1904-
def test_rglob_follow_symlinks_common(self):
1874+
def test_rglob_recurse_symlinks_common(self):
19051875
def _check(path, glob, expected):
1906-
actual = {path for path in path.rglob(glob, follow_symlinks=True)
1876+
actual = {path for path in path.rglob(glob, recurse_symlinks=True)
19071877
if path.parts.count("linkD") <= 1} # exclude symlink loop.
19081878
self.assertEqual(actual, { P(self.base, q) for q in expected })
19091879
P = self.cls
@@ -1932,37 +1902,12 @@ def _check(path, glob, expected):
19321902
_check(p, "*.txt", ["dirC/novel.txt"])
19331903
_check(p, "*.*", ["dirC/novel.txt"])
19341904

1935-
@needs_symlinks
1936-
def test_rglob_no_follow_symlinks_common(self):
1937-
def _check(path, glob, expected):
1938-
actual = {path for path in path.rglob(glob, follow_symlinks=False)}
1939-
self.assertEqual(actual, { P(self.base, q) for q in expected })
1940-
P = self.cls
1941-
p = P(self.base)
1942-
_check(p, "fileB", ["dirB/fileB"])
1943-
_check(p, "*/fileA", [])
1944-
_check(p, "*/fileB", ["dirB/fileB"])
1945-
_check(p, "file*", ["fileA", "dirB/fileB", "dirC/fileC", "dirC/dirD/fileD", ])
1946-
_check(p, "*/", ["dirA/", "dirB/", "dirC/", "dirC/dirD/", "dirE/"])
1947-
_check(p, "", ["", "dirA/", "dirB/", "dirC/", "dirE/", "dirC/dirD/"])
1948-
1949-
p = P(self.base, "dirC")
1950-
_check(p, "*", ["dirC/fileC", "dirC/novel.txt",
1951-
"dirC/dirD", "dirC/dirD/fileD"])
1952-
_check(p, "file*", ["dirC/fileC", "dirC/dirD/fileD"])
1953-
_check(p, "*/*", ["dirC/dirD/fileD"])
1954-
_check(p, "*/", ["dirC/dirD/"])
1955-
_check(p, "", ["dirC/", "dirC/dirD/"])
1956-
# gh-91616, a re module regression
1957-
_check(p, "*.txt", ["dirC/novel.txt"])
1958-
_check(p, "*.*", ["dirC/novel.txt"])
1959-
19601905
@needs_symlinks
19611906
def test_rglob_symlink_loop(self):
19621907
# Don't get fooled by symlink loops (Issue #26012).
19631908
P = self.cls
19641909
p = P(self.base)
1965-
given = set(p.rglob('*', follow_symlinks=None))
1910+
given = set(p.rglob('*', recurse_symlinks=False))
19661911
expect = {'brokenLink',
19671912
'dirA', 'dirA/linkC',
19681913
'dirB', 'dirB/fileB', 'dirB/linkD',

0 commit comments

Comments
 (0)