From 59dcdb124bc78ea56dc033b3c6e17bbc2ca13e07 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sun, 12 Mar 2023 03:24:15 +0000 Subject: [PATCH 01/12] GH-77609: Support following symlinks in `pathlib.Path.glob()` Add a keyword-only *follow_symlinks* parameter to `pathlib.Path.glob()` and `rglob()`, defaulting to false. When set to true, symlinks to directories are followed as if they were directories. Previously these methods followed symlinks except when evaluating "`**`" wildcards; on Windows they returned paths in filesystem casing except when evaluating non-wildcard tokens. Both these problems are solved here. This will allow us to address GH-102613 and GH-81079 in future commits. --- Doc/library/pathlib.rst | 18 ++++- Lib/pathlib.py | 64 +++++---------- Lib/test/test_pathlib.py | 80 ++++++++++++------- ...3-03-12-03-37-03.gh-issue-77609.aOQttm.rst | 2 + 4 files changed, 85 insertions(+), 79 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-03-12-03-37-03.gh-issue-77609.aOQttm.rst diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 8e91936680fab8..d282cd86841655 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -852,7 +852,7 @@ call fails (for example because the path doesn't exist). .. versionadded:: 3.5 -.. method:: Path.glob(pattern) +.. method:: Path.glob(pattern, *, follow_symlinks=False) Glob the given relative *pattern* in the directory represented by this path, yielding all matching files (of any kind):: @@ -873,6 +873,9 @@ call fails (for example because the path doesn't exist). PosixPath('setup.py'), PosixPath('test_pathlib.py')] + By default, :meth:`Path.glob` does not follow symlinks. Set + *follow_symlinks* to true to visit symlinks to directories. + .. note:: Using the "``**``" pattern in large directory trees may consume an inordinate amount of time. @@ -883,6 +886,10 @@ call fails (for example because the path doesn't exist). Return only directories if *pattern* ends with a pathname components separator (:data:`~os.sep` or :data:`~os.altsep`). + .. versionchanged:: 3.12 + The *follow_symlinks* parameter was added. In previous versions, + symlinks were followed except when expanding "``**``" wildcards. + .. method:: Path.group() Return the name of the group owning the file. :exc:`KeyError` is raised @@ -1268,7 +1275,7 @@ call fails (for example because the path doesn't exist). .. versionadded:: 3.6 The *strict* argument (pre-3.6 behavior is strict). -.. method:: Path.rglob(pattern) +.. method:: Path.rglob(pattern, *, follow_symlinks=False) Glob the given relative *pattern* recursively. This is like calling :func:`Path.glob` with "``**/``" added in front of the *pattern*, where @@ -1281,12 +1288,19 @@ call fails (for example because the path doesn't exist). PosixPath('setup.py'), PosixPath('test_pathlib.py')] + By default, :meth:`Path.rglob` does not follow symlinks. Set + *follow_symlinks* to true to visit symlinks to directories. + .. audit-event:: pathlib.Path.rglob self,pattern pathlib.Path.rglob .. versionchanged:: 3.11 Return only directories if *pattern* ends with a pathname components separator (:data:`~os.sep` or :data:`~os.altsep`). + .. versionchanged:: 3.12 + The *follow_symlinks* parameter was added. In previous versions, + symlinks were followed except when expanding "``**``" wildcards. + .. method:: Path.rmdir() Remove this directory. The directory must be empty. diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 55c44f12e5a2fb..db8f8e0235b876 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -54,12 +54,6 @@ def _ignore_error(exception): return (getattr(exception, 'errno', None) in _IGNORED_ERRNOS or getattr(exception, 'winerror', None) in _IGNORED_WINERRORS) - -def _is_wildcard_pattern(pat): - # Whether this pattern needs actual matching using fnmatch, or can - # be looked up directly as a file. - return "*" in pat or "?" in pat or "[" in pat - # # Globbing helpers # @@ -74,10 +68,8 @@ def _make_selector(pattern_parts, flavour): cls = _RecursiveWildcardSelector elif '**' in pat: raise ValueError("Invalid pattern: '**' can only be an entire path component") - elif _is_wildcard_pattern(pat): - cls = _WildcardSelector else: - cls = _PreciseSelector + cls = _WildcardSelector return cls(pat, child_parts, flavour) @@ -94,48 +86,28 @@ def __init__(self, child_parts, flavour): self.successor = _TerminatingSelector() self.dironly = False - def select_from(self, parent_path): + def select_from(self, parent_path, follow_symlinks): """Iterate over all child paths of `parent_path` matched by this selector. This can contain parent_path itself.""" path_cls = type(parent_path) - is_dir = path_cls.is_dir - exists = path_cls.exists scandir = path_cls._scandir normcase = path_cls._flavour.normcase - if not is_dir(parent_path): - return iter([]) - return self._select_from(parent_path, is_dir, exists, scandir, normcase) + return self._select_from(parent_path, follow_symlinks, scandir, normcase) class _TerminatingSelector: - def _select_from(self, parent_path, is_dir, exists, scandir, normcase): + def _select_from(self, parent_path, follow_symlinks, scandir, normcase): yield parent_path -class _PreciseSelector(_Selector): - - def __init__(self, name, child_parts, flavour): - self.name = name - _Selector.__init__(self, child_parts, flavour) - - def _select_from(self, parent_path, is_dir, exists, scandir, normcase): - try: - path = parent_path._make_child_relpath(self.name) - if (is_dir if self.dironly else exists)(path): - for p in self.successor._select_from(path, is_dir, exists, scandir, normcase): - yield p - except PermissionError: - return - - class _WildcardSelector(_Selector): def __init__(self, pat, child_parts, flavour): self.match = re.compile(fnmatch.translate(flavour.normcase(pat))).fullmatch _Selector.__init__(self, child_parts, flavour) - def _select_from(self, parent_path, is_dir, exists, scandir, normcase): + def _select_from(self, parent_path, follow_symlinks, scandir, normcase): try: # We must close the scandir() object before proceeding to # avoid exhausting file descriptors when globbing deep trees. @@ -147,7 +119,7 @@ def _select_from(self, parent_path, is_dir, exists, scandir, normcase): # "entry.is_dir()" can raise PermissionError # in some cases (see bpo-38894), which is not # among the errors ignored by _ignore_error() - if not entry.is_dir(): + if not entry.is_dir(follow_symlinks=follow_symlinks): continue except OSError as e: if not _ignore_error(e): @@ -156,7 +128,7 @@ def _select_from(self, parent_path, is_dir, exists, scandir, normcase): name = entry.name if self.match(normcase(name)): path = parent_path._make_child_relpath(name) - for p in self.successor._select_from(path, is_dir, exists, scandir, normcase): + for p in self.successor._select_from(path, follow_symlinks, scandir, normcase): yield p except PermissionError: return @@ -167,7 +139,7 @@ class _RecursiveWildcardSelector(_Selector): def __init__(self, pat, child_parts, flavour): _Selector.__init__(self, child_parts, flavour) - def _iterate_directories(self, parent_path, is_dir, scandir): + def _iterate_directories(self, parent_path, follow_symlinks, scandir): yield parent_path try: # We must close the scandir() object before proceeding to @@ -177,24 +149,24 @@ def _iterate_directories(self, parent_path, is_dir, scandir): for entry in entries: entry_is_dir = False try: - entry_is_dir = entry.is_dir() + entry_is_dir = entry.is_dir(follow_symlinks=follow_symlinks) except OSError as e: if not _ignore_error(e): raise - if entry_is_dir and not entry.is_symlink(): + if entry_is_dir: path = parent_path._make_child_relpath(entry.name) - for p in self._iterate_directories(path, is_dir, scandir): + for p in self._iterate_directories(path, follow_symlinks, scandir): yield p except PermissionError: return - def _select_from(self, parent_path, is_dir, exists, scandir, normcase): + def _select_from(self, parent_path, follow_symlinks, scandir, normcase): try: yielded = set() try: successor_select = self.successor._select_from - for starting_point in self._iterate_directories(parent_path, is_dir, scandir): - for p in successor_select(starting_point, is_dir, exists, scandir, normcase): + for starting_point in self._iterate_directories(parent_path, follow_symlinks, scandir): + for p in successor_select(starting_point, follow_symlinks, scandir, normcase): if p not in yielded: yield p yielded.add(p) @@ -763,7 +735,7 @@ def _scandir(self): # includes scandir(), which is used to implement glob(). return os.scandir(self) - def glob(self, pattern): + def glob(self, pattern, *, follow_symlinks=False): """Iterate over this subtree and yield all existing files (of any kind, including directories) matching the given relative pattern. """ @@ -776,10 +748,10 @@ def glob(self, pattern): if pattern[-1] in (self._flavour.sep, self._flavour.altsep): pattern_parts.append('') selector = _make_selector(tuple(pattern_parts), self._flavour) - for p in selector.select_from(self): + for p in selector.select_from(self, follow_symlinks): yield p - def rglob(self, pattern): + def rglob(self, pattern, *, follow_symlinks=False): """Recursively yield all existing files (of any kind, including directories) matching the given relative pattern, anywhere in this subtree. @@ -791,7 +763,7 @@ def rglob(self, pattern): if pattern and pattern[-1] in (self._flavour.sep, self._flavour.altsep): pattern_parts.append('') selector = _make_selector(("**",) + tuple(pattern_parts), self._flavour) - for p in selector.select_from(self): + for p in selector.select_from(self, follow_symlinks): yield p def absolute(self): diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index f05dead5886743..d8d34377ec9d29 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -1760,22 +1760,25 @@ def _check(glob, expected): _check(p.glob("dir*/file*"), ["dirB/fileB", "dirC/fileC"]) if not os_helper.can_symlink(): _check(p.glob("*A"), ['dirA', 'fileA']) - else: - _check(p.glob("*A"), ['dirA', 'fileA', 'linkA']) - if not os_helper.can_symlink(): _check(p.glob("*B/*"), ['dirB/fileB']) else: - _check(p.glob("*B/*"), ['dirB/fileB', 'dirB/linkD', - 'linkB/fileB', 'linkB/linkD']) - if not os_helper.can_symlink(): - _check(p.glob("*/fileB"), ['dirB/fileB']) - else: - _check(p.glob("*/fileB"), ['dirB/fileB', 'linkB/fileB']) + _check(p.glob("*A"), ['dirA', 'fileA', 'linkA']) + _check(p.glob("*B/*"), ['dirB/fileB', 'dirB/linkD']) + _check(p.glob("*/fileB"), ['dirB/fileB']) + _check(p.glob("*/"), ["dirA", "dirB", "dirC", "dirE"]) - if not os_helper.can_symlink(): - _check(p.glob("*/"), ["dirA", "dirB", "dirC", "dirE"]) - else: - _check(p.glob("*/"), ["dirA", "dirB", "dirC", "dirE", "linkB"]) + @os_helper.skip_unless_symlink + def test_glob_follow_symlinks_common(self): + def _check(path, glob, expected): + self.assertEqual(set(path.glob(glob, follow_symlinks=True)), { P(BASE, q) for q in expected }) + P = self.cls + p = P(BASE) + _check(p, "fileB", []) + _check(p, "dir*/file*", ["dirB/fileB", "dirC/fileC"]) + _check(p, "*A", ['dirA', 'fileA', 'linkA']) + _check(p, "*B/*", ['dirB/fileB', 'dirB/linkD', 'linkB/fileB', 'linkB/linkD']) + _check(p, "*/fileB", ['dirB/fileB', 'linkB/fileB']) + _check(p, "*/", ["dirA", "dirB", "dirC", "dirE", "linkB"]) def test_rglob_common(self): def _check(glob, expected): @@ -1787,22 +1790,10 @@ def _check(glob, expected): _check(it, ["fileA"]) _check(p.rglob("fileB"), ["dirB/fileB"]) _check(p.rglob("*/fileA"), []) - if not os_helper.can_symlink(): - _check(p.rglob("*/fileB"), ["dirB/fileB"]) - else: - _check(p.rglob("*/fileB"), ["dirB/fileB", "dirB/linkD/fileB", - "linkB/fileB", "dirA/linkC/fileB"]) + _check(p.rglob("*/fileB"), ["dirB/fileB"]) _check(p.rglob("file*"), ["fileA", "dirB/fileB", "dirC/fileC", "dirC/dirD/fileD"]) - if not os_helper.can_symlink(): - _check(p.rglob("*/"), [ - "dirA", "dirB", "dirC", "dirC/dirD", "dirE", - ]) - else: - _check(p.rglob("*/"), [ - "dirA", "dirA/linkC", "dirB", "dirB/linkD", "dirC", - "dirC/dirD", "dirE", "linkB", - ]) + _check(p.rglob("*/"), ["dirA", "dirB", "dirC", "dirC/dirD", "dirE"]) _check(p.rglob(""), ["", "dirA", "dirB", "dirC", "dirE", "dirC/dirD"]) p = P(BASE, "dirC") @@ -1816,6 +1807,33 @@ def _check(glob, expected): _check(p.rglob("*.txt"), ["dirC/novel.txt"]) _check(p.rglob("*.*"), ["dirC/novel.txt"]) + @os_helper.skip_unless_symlink + def test_rglob_follow_symlinks_common(self): + def _check(path, glob, expected): + actual = {path for path in path.rglob(glob, follow_symlinks=True) + if 'linkD' not in path.parts} # exclude symlink loop. + self.assertEqual(actual, { P(BASE, q) for q in expected }) + P = self.cls + p = P(BASE) + _check(p, "fileB", ["dirB/fileB", "dirA/linkC/fileB", "linkB/fileB"]) + _check(p, "*/fileA", []) + _check(p, "*/fileB", ["dirB/fileB", "dirA/linkC/fileB", "linkB/fileB"]) + _check(p, "file*", ["fileA", "dirA/linkC/fileB", "dirB/fileB", + "dirC/fileC", "dirC/dirD/fileD", "linkB/fileB"]) + _check(p, "*/", ["dirA", "dirA/linkC", "dirB", "dirC", "dirC/dirD", "dirE", "linkB"]) + _check(p, "", ["", "dirA", "dirA/linkC", "dirB", "dirC", "dirE", "dirC/dirD", "linkB"]) + + p = P(BASE, "dirC") + _check(p, "*", ["dirC/fileC", "dirC/novel.txt", + "dirC/dirD", "dirC/dirD/fileD"]) + _check(p, "file*", ["dirC/fileC", "dirC/dirD/fileD"]) + _check(p, "*/*", ["dirC/dirD/fileD"]) + _check(p, "*/", ["dirC/dirD"]) + _check(p, "", ["dirC", "dirC/dirD"]) + # gh-91616, a re module regression + _check(p, "*.txt", ["dirC/novel.txt"]) + _check(p, "*.*", ["dirC/novel.txt"]) + @os_helper.skip_unless_symlink def test_rglob_symlink_loop(self): # Don't get fooled by symlink loops (Issue #26012). @@ -1856,8 +1874,8 @@ def test_glob_dotdot(self): # ".." is not special in globs. P = self.cls p = P(BASE) - self.assertEqual(set(p.glob("..")), { P(BASE, "..") }) - self.assertEqual(set(p.glob("dirA/../file*")), { P(BASE, "dirA/../fileA") }) + self.assertEqual(set(p.glob("..")), set()) + self.assertEqual(set(p.glob("dirA/../file*")), set()) self.assertEqual(set(p.glob("../xyzzy")), set()) @os_helper.skip_unless_symlink @@ -3053,7 +3071,7 @@ def test_glob(self): self.assertEqual(set(p.glob("FILEa")), { P(BASE, "fileA") }) self.assertEqual(set(p.glob("*a\\")), { P(BASE, "dirA") }) self.assertEqual(set(p.glob("F*a")), { P(BASE, "fileA") }) - self.assertEqual(set(map(str, p.glob("FILEa"))), {f"{p}\\FILEa"}) + self.assertEqual(set(map(str, p.glob("FILEa"))), {f"{p}\\fileA"}) self.assertEqual(set(map(str, p.glob("F*a"))), {f"{p}\\fileA"}) def test_rglob(self): @@ -3061,7 +3079,7 @@ def test_rglob(self): p = P(BASE, "dirC") self.assertEqual(set(p.rglob("FILEd")), { P(BASE, "dirC/dirD/fileD") }) self.assertEqual(set(p.rglob("*\\")), { P(BASE, "dirC/dirD") }) - self.assertEqual(set(map(str, p.rglob("FILEd"))), {f"{p}\\dirD\\FILEd"}) + self.assertEqual(set(map(str, p.rglob("FILEd"))), {f"{p}\\dirD\\fileD"}) def test_expanduser(self): P = self.cls diff --git a/Misc/NEWS.d/next/Library/2023-03-12-03-37-03.gh-issue-77609.aOQttm.rst b/Misc/NEWS.d/next/Library/2023-03-12-03-37-03.gh-issue-77609.aOQttm.rst new file mode 100644 index 00000000000000..35e61088de58a6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-03-12-03-37-03.gh-issue-77609.aOQttm.rst @@ -0,0 +1,2 @@ +Add *follow_symlinks* argument to :meth:`pathlib.Path.glob` and +:meth:`~pathlib.Path.rglob`, defaulting to false. From e850bde979b7ffb311f5f27b06154e98cb450c3f Mon Sep 17 00:00:00 2001 From: barneygale Date: Sun, 12 Mar 2023 04:13:54 +0000 Subject: [PATCH 02/12] Fix top-level error handling. --- Lib/pathlib.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index db8f8e0235b876..e7f4edce034776 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -92,6 +92,10 @@ def select_from(self, parent_path, follow_symlinks): path_cls = type(parent_path) scandir = path_cls._scandir normcase = path_cls._flavour.normcase + if not follow_symlinks and parent_path.is_symlink(): + return iter([]) + if not parent_path.is_dir(): + return iter([]) return self._select_from(parent_path, follow_symlinks, scandir, normcase) From 4f4ffd3969e74156845f0e0c28e9b9430127a7b1 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sun, 12 Mar 2023 04:19:33 +0000 Subject: [PATCH 03/12] Always follow top-level symlink --- Lib/pathlib.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index e7f4edce034776..7112134777dc39 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -92,8 +92,6 @@ def select_from(self, parent_path, follow_symlinks): path_cls = type(parent_path) scandir = path_cls._scandir normcase = path_cls._flavour.normcase - if not follow_symlinks and parent_path.is_symlink(): - return iter([]) if not parent_path.is_dir(): return iter([]) return self._select_from(parent_path, follow_symlinks, scandir, normcase) From b6c019eebde58197e1028ce9749e3ab9282e5fe2 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sun, 12 Mar 2023 04:56:34 +0000 Subject: [PATCH 04/12] Simplify test diff slightly --- Lib/test/test_pathlib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index d8d34377ec9d29..c3b55304ab8890 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -1760,9 +1760,11 @@ def _check(glob, expected): _check(p.glob("dir*/file*"), ["dirB/fileB", "dirC/fileC"]) if not os_helper.can_symlink(): _check(p.glob("*A"), ['dirA', 'fileA']) - _check(p.glob("*B/*"), ['dirB/fileB']) else: _check(p.glob("*A"), ['dirA', 'fileA', 'linkA']) + if not os_helper.can_symlink(): + _check(p.glob("*B/*"), ['dirB/fileB']) + else: _check(p.glob("*B/*"), ['dirB/fileB', 'dirB/linkD']) _check(p.glob("*/fileB"), ['dirB/fileB']) _check(p.glob("*/"), ["dirA", "dirB", "dirC", "dirE"]) From 8d657eeaf421e3defee08fe5ff585bbd11a7cf65 Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 14 Mar 2023 00:55:30 +0000 Subject: [PATCH 05/12] Clarify docs for *follow_symlinks* argument --- Doc/library/pathlib.rst | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index d282cd86841655..3ff95ec344be01 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -873,8 +873,8 @@ call fails (for example because the path doesn't exist). PosixPath('setup.py'), PosixPath('test_pathlib.py')] - By default, :meth:`Path.glob` does not follow symlinks. Set - *follow_symlinks* to true to visit symlinks to directories. + By default, :meth:`Path.glob` does not follow symlinks when expanding a + pattern. Set *follow_symlinks* to true to visit symlinks to directories. .. note:: Using the "``**``" pattern in large directory trees may consume @@ -888,7 +888,8 @@ call fails (for example because the path doesn't exist). .. versionchanged:: 3.12 The *follow_symlinks* parameter was added. In previous versions, - symlinks were followed except when expanding "``**``" wildcards. + symlinks were followed when expanding patterns, except when expanding + "``**``" wildcards. .. method:: Path.group() @@ -1288,8 +1289,8 @@ call fails (for example because the path doesn't exist). PosixPath('setup.py'), PosixPath('test_pathlib.py')] - By default, :meth:`Path.rglob` does not follow symlinks. Set - *follow_symlinks* to true to visit symlinks to directories. + By default, :meth:`Path.rglob` does not follow symlinks when expanding a + pattern. Set *follow_symlinks* to true to visit symlinks to directories. .. audit-event:: pathlib.Path.rglob self,pattern pathlib.Path.rglob @@ -1299,7 +1300,8 @@ call fails (for example because the path doesn't exist). .. versionchanged:: 3.12 The *follow_symlinks* parameter was added. In previous versions, - symlinks were followed except when expanding "``**``" wildcards. + symlinks were followed when expanding patterns, except when expanding + "``**``" wildcards. .. method:: Path.rmdir() From b6003638c6a2befdd2448113b56fcc113f4f9e59 Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 14 Mar 2023 19:31:25 +0000 Subject: [PATCH 06/12] Backwards compatibility --- Doc/library/pathlib.rst | 22 +++++----- Lib/pathlib.py | 10 +++-- Lib/test/test_pathlib.py | 87 ++++++++++++++++++++++++++++++++++------ 3 files changed, 91 insertions(+), 28 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 3ff95ec344be01..7ced50411ce9c6 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -852,7 +852,7 @@ call fails (for example because the path doesn't exist). .. versionadded:: 3.5 -.. method:: Path.glob(pattern, *, follow_symlinks=False) +.. method:: Path.glob(pattern, *, follow_symlinks=None) Glob the given relative *pattern* in the directory represented by this path, yielding all matching files (of any kind):: @@ -873,8 +873,9 @@ call fails (for example because the path doesn't exist). PosixPath('setup.py'), PosixPath('test_pathlib.py')] - By default, :meth:`Path.glob` does not follow symlinks when expanding a - pattern. Set *follow_symlinks* to true to visit symlinks to directories. + By default, :meth:`Path.glob` follows symlinks except when expanding + "``**``" wildcards. Set *follow_symlinks* to true to always follow + symlinks, or false to treat all symlinks as files. .. note:: Using the "``**``" pattern in large directory trees may consume @@ -887,9 +888,7 @@ call fails (for example because the path doesn't exist). separator (:data:`~os.sep` or :data:`~os.altsep`). .. versionchanged:: 3.12 - The *follow_symlinks* parameter was added. In previous versions, - symlinks were followed when expanding patterns, except when expanding - "``**``" wildcards. + The *follow_symlinks* parameter was added. .. method:: Path.group() @@ -1276,7 +1275,7 @@ call fails (for example because the path doesn't exist). .. versionadded:: 3.6 The *strict* argument (pre-3.6 behavior is strict). -.. method:: Path.rglob(pattern, *, follow_symlinks=False) +.. method:: Path.rglob(pattern, *, follow_symlinks=None) Glob the given relative *pattern* recursively. This is like calling :func:`Path.glob` with "``**/``" added in front of the *pattern*, where @@ -1289,8 +1288,9 @@ call fails (for example because the path doesn't exist). PosixPath('setup.py'), PosixPath('test_pathlib.py')] - By default, :meth:`Path.rglob` does not follow symlinks when expanding a - pattern. Set *follow_symlinks* to true to visit symlinks to directories. + By default, :meth:`Path.rglob` follows symlinks except when expanding + "``**``" wildcards. Set *follow_symlinks* to true to always follow + symlinks, or false to treat all symlinks as files. .. audit-event:: pathlib.Path.rglob self,pattern pathlib.Path.rglob @@ -1299,9 +1299,7 @@ call fails (for example because the path doesn't exist). separator (:data:`~os.sep` or :data:`~os.altsep`). .. versionchanged:: 3.12 - The *follow_symlinks* parameter was added. In previous versions, - symlinks were followed when expanding patterns, except when expanding - "``**``" wildcards. + The *follow_symlinks* parameter was added. .. method:: Path.rmdir() diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 7112134777dc39..f0db92e4cc3da9 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -110,6 +110,7 @@ def __init__(self, pat, child_parts, flavour): _Selector.__init__(self, child_parts, flavour) def _select_from(self, parent_path, follow_symlinks, scandir, normcase): + follow_dirlinks = True if follow_symlinks is None else follow_symlinks try: # We must close the scandir() object before proceeding to # avoid exhausting file descriptors when globbing deep trees. @@ -121,7 +122,7 @@ def _select_from(self, parent_path, follow_symlinks, scandir, normcase): # "entry.is_dir()" can raise PermissionError # in some cases (see bpo-38894), which is not # among the errors ignored by _ignore_error() - if not entry.is_dir(follow_symlinks=follow_symlinks): + if not entry.is_dir(follow_symlinks=follow_dirlinks): continue except OSError as e: if not _ignore_error(e): @@ -142,6 +143,7 @@ def __init__(self, pat, child_parts, flavour): _Selector.__init__(self, child_parts, flavour) def _iterate_directories(self, parent_path, follow_symlinks, scandir): + follow_dirlinks = False if follow_symlinks is None else follow_symlinks yield parent_path try: # We must close the scandir() object before proceeding to @@ -151,7 +153,7 @@ def _iterate_directories(self, parent_path, follow_symlinks, scandir): for entry in entries: entry_is_dir = False try: - entry_is_dir = entry.is_dir(follow_symlinks=follow_symlinks) + entry_is_dir = entry.is_dir(follow_symlinks=follow_dirlinks) except OSError as e: if not _ignore_error(e): raise @@ -737,7 +739,7 @@ def _scandir(self): # includes scandir(), which is used to implement glob(). return os.scandir(self) - def glob(self, pattern, *, follow_symlinks=False): + def glob(self, pattern, *, follow_symlinks=None): """Iterate over this subtree and yield all existing files (of any kind, including directories) matching the given relative pattern. """ @@ -753,7 +755,7 @@ def glob(self, pattern, *, follow_symlinks=False): for p in selector.select_from(self, follow_symlinks): yield p - def rglob(self, pattern, *, follow_symlinks=False): + def rglob(self, pattern, *, follow_symlinks=None): """Recursively yield all existing files (of any kind, including directories) matching the given relative pattern, anywhere in this subtree. diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index c3b55304ab8890..ab4cdabfc7cc07 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -1765,23 +1765,47 @@ def _check(glob, expected): if not os_helper.can_symlink(): _check(p.glob("*B/*"), ['dirB/fileB']) else: - _check(p.glob("*B/*"), ['dirB/fileB', 'dirB/linkD']) - _check(p.glob("*/fileB"), ['dirB/fileB']) - _check(p.glob("*/"), ["dirA", "dirB", "dirC", "dirE"]) + _check(p.glob("*B/*"), ['dirB/fileB', 'dirB/linkD', + 'linkB/fileB', 'linkB/linkD']) + if not os_helper.can_symlink(): + _check(p.glob("*/fileB"), ['dirB/fileB']) + else: + _check(p.glob("*/fileB"), ['dirB/fileB', 'linkB/fileB']) + + if not os_helper.can_symlink(): + _check(p.glob("*/"), ["dirA", "dirB", "dirC", "dirE"]) + else: + _check(p.glob("*/"), ["dirA", "dirB", "dirC", "dirE", "linkB"]) @os_helper.skip_unless_symlink def test_glob_follow_symlinks_common(self): def _check(path, glob, expected): - self.assertEqual(set(path.glob(glob, follow_symlinks=True)), { P(BASE, q) for q in expected }) + actual = {path for path in path.glob(glob, follow_symlinks=True) + if "linkD" not in path.parent.parts} # exclude symlink loop. + self.assertEqual(actual, { P(BASE, q) for q in expected }) P = self.cls p = P(BASE) _check(p, "fileB", []) _check(p, "dir*/file*", ["dirB/fileB", "dirC/fileC"]) - _check(p, "*A", ['dirA', 'fileA', 'linkA']) - _check(p, "*B/*", ['dirB/fileB', 'dirB/linkD', 'linkB/fileB', 'linkB/linkD']) - _check(p, "*/fileB", ['dirB/fileB', 'linkB/fileB']) + _check(p, "*A", ["dirA", "fileA", "linkA"]) + _check(p, "*B/*", ["dirB/fileB", "dirB/linkD", "linkB/fileB", "linkB/linkD"]) + _check(p, "*/fileB", ["dirB/fileB", "linkB/fileB"]) _check(p, "*/", ["dirA", "dirB", "dirC", "dirE", "linkB"]) + @os_helper.skip_unless_symlink + def test_glob_no_follow_symlinks_common(self): + def _check(path, glob, expected): + actual = {path for path in path.glob(glob, follow_symlinks=False)} + self.assertEqual(actual, { P(BASE, q) for q in expected }) + P = self.cls + p = P(BASE) + _check(p, "fileB", []) + _check(p, "dir*/file*", ["dirB/fileB", "dirC/fileC"]) + _check(p, "*A", ["dirA", "fileA", "linkA"]) + _check(p, "*B/*", ["dirB/fileB", "dirB/linkD"]) + _check(p, "*/fileB", ["dirB/fileB"]) + _check(p, "*/", ["dirA", "dirB", "dirC", "dirE"]) + def test_rglob_common(self): def _check(glob, expected): self.assertEqual(set(glob), { P(BASE, q) for q in expected }) @@ -1792,10 +1816,22 @@ def _check(glob, expected): _check(it, ["fileA"]) _check(p.rglob("fileB"), ["dirB/fileB"]) _check(p.rglob("*/fileA"), []) - _check(p.rglob("*/fileB"), ["dirB/fileB"]) + if not os_helper.can_symlink(): + _check(p.rglob("*/fileB"), ["dirB/fileB"]) + else: + _check(p.rglob("*/fileB"), ["dirB/fileB", "dirB/linkD/fileB", + "linkB/fileB", "dirA/linkC/fileB"]) _check(p.rglob("file*"), ["fileA", "dirB/fileB", "dirC/fileC", "dirC/dirD/fileD"]) - _check(p.rglob("*/"), ["dirA", "dirB", "dirC", "dirC/dirD", "dirE"]) + if not os_helper.can_symlink(): + _check(p.rglob("*/"), [ + "dirA", "dirB", "dirC", "dirC/dirD", "dirE", + ]) + else: + _check(p.rglob("*/"), [ + "dirA", "dirA/linkC", "dirB", "dirB/linkD", "dirC", + "dirC/dirD", "dirE", "linkB", + ]) _check(p.rglob(""), ["", "dirA", "dirB", "dirC", "dirE", "dirC/dirD"]) p = P(BASE, "dirC") @@ -1813,7 +1849,7 @@ def _check(glob, expected): def test_rglob_follow_symlinks_common(self): def _check(path, glob, expected): actual = {path for path in path.rglob(glob, follow_symlinks=True) - if 'linkD' not in path.parts} # exclude symlink loop. + if 'linkD' not in path.parent.parts} # exclude symlink loop. self.assertEqual(actual, { P(BASE, q) for q in expected }) P = self.cls p = P(BASE) @@ -1822,8 +1858,35 @@ def _check(path, glob, expected): _check(p, "*/fileB", ["dirB/fileB", "dirA/linkC/fileB", "linkB/fileB"]) _check(p, "file*", ["fileA", "dirA/linkC/fileB", "dirB/fileB", "dirC/fileC", "dirC/dirD/fileD", "linkB/fileB"]) - _check(p, "*/", ["dirA", "dirA/linkC", "dirB", "dirC", "dirC/dirD", "dirE", "linkB"]) - _check(p, "", ["", "dirA", "dirA/linkC", "dirB", "dirC", "dirE", "dirC/dirD", "linkB"]) + _check(p, "*/", ["dirA", "dirA/linkC", "dirA/linkC/linkD", "dirB", "dirB/linkD", + "dirC", "dirC/dirD", "dirE", "linkB", "linkB/linkD"]) + _check(p, "", ["", "dirA", "dirA/linkC", "dirA/linkC/linkD", "dirB", "dirB/linkD", + "dirC", "dirE", "dirC/dirD", "linkB", "linkB/linkD"]) + + p = P(BASE, "dirC") + _check(p, "*", ["dirC/fileC", "dirC/novel.txt", + "dirC/dirD", "dirC/dirD/fileD"]) + _check(p, "file*", ["dirC/fileC", "dirC/dirD/fileD"]) + _check(p, "*/*", ["dirC/dirD/fileD"]) + _check(p, "*/", ["dirC/dirD"]) + _check(p, "", ["dirC", "dirC/dirD"]) + # gh-91616, a re module regression + _check(p, "*.txt", ["dirC/novel.txt"]) + _check(p, "*.*", ["dirC/novel.txt"]) + + @os_helper.skip_unless_symlink + def test_rglob_no_follow_symlinks_common(self): + def _check(path, glob, expected): + actual = {path for path in path.rglob(glob, follow_symlinks=False)} + self.assertEqual(actual, { P(BASE, q) for q in expected }) + P = self.cls + p = P(BASE) + _check(p, "fileB", ["dirB/fileB"]) + _check(p, "*/fileA", []) + _check(p, "*/fileB", ["dirB/fileB"]) + _check(p, "file*", ["fileA", "dirB/fileB", "dirC/fileC", "dirC/dirD/fileD", ]) + _check(p, "*/", ["dirA", "dirB", "dirC", "dirC/dirD", "dirE"]) + _check(p, "", ["", "dirA", "dirB", "dirC", "dirE", "dirC/dirD"]) p = P(BASE, "dirC") _check(p, "*", ["dirC/fileC", "dirC/novel.txt", From b2766f8b2237bea8d335f792a8e160ffc10b13e2 Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 14 Mar 2023 21:39:44 +0000 Subject: [PATCH 07/12] Fix handling of '..' in patterns. --- Lib/pathlib.py | 11 +++++++++++ Lib/test/test_pathlib.py | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index f0db92e4cc3da9..9f1e183bf057c2 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -68,6 +68,8 @@ def _make_selector(pattern_parts, flavour): cls = _RecursiveWildcardSelector elif '**' in pat: raise ValueError("Invalid pattern: '**' can only be an entire path component") + elif pat == '..': + cls = _ParentSelector else: cls = _WildcardSelector return cls(pat, child_parts, flavour) @@ -103,6 +105,15 @@ def _select_from(self, parent_path, follow_symlinks, scandir, normcase): yield parent_path +class _ParentSelector(_Selector): + def __init__(self, pat, child_parts, flavour): + _Selector.__init__(self, child_parts, flavour) + + def _select_from(self, parent_path, follow_symlinks, scandir, normcase): + path = parent_path._make_child_relpath('..') + return self.successor._select_from(path, follow_symlinks, scandir, normcase) + + class _WildcardSelector(_Selector): def __init__(self, pat, child_parts, flavour): diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index ab4cdabfc7cc07..7512b3c268287c 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -1939,8 +1939,8 @@ def test_glob_dotdot(self): # ".." is not special in globs. P = self.cls p = P(BASE) - self.assertEqual(set(p.glob("..")), set()) - self.assertEqual(set(p.glob("dirA/../file*")), set()) + self.assertEqual(set(p.glob("..")), { P(BASE, "..") }) + self.assertEqual(set(p.glob("dirA/../file*")), { P(BASE, "dirA/../fileA") }) self.assertEqual(set(p.glob("../xyzzy")), set()) @os_helper.skip_unless_symlink From 8704d33e0d36ba895cf540a3a49c1984c49ac133 Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 14 Mar 2023 23:44:27 +0000 Subject: [PATCH 08/12] Deprecate follow_symlinks=None --- Doc/library/pathlib.rst | 20 ++++-- Lib/pathlib.py | 14 +++++ Lib/test/test_pathlib.py | 129 +++++++++++++++++++++------------------ 3 files changed, 96 insertions(+), 67 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 7ced50411ce9c6..cf21276862feeb 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -873,9 +873,9 @@ call fails (for example because the path doesn't exist). PosixPath('setup.py'), PosixPath('test_pathlib.py')] - By default, :meth:`Path.glob` follows symlinks except when expanding - "``**``" wildcards. Set *follow_symlinks* to true to always follow - symlinks, or false to treat all symlinks as files. + By default, :meth:`Path.glob` emits a deprecation warning and follows + symlinks except when expanding "``**``" wildcards. Set *follow_symlinks* + to true to always follow symlinks, or false to treat all symlinks as files. .. note:: Using the "``**``" pattern in large directory trees may consume @@ -890,6 +890,10 @@ call fails (for example because the path doesn't exist). .. versionchanged:: 3.12 The *follow_symlinks* parameter was added. + .. deprecated-removed:: 3.12 3.14 + + Setting *follow_symlinks* to ``None`` (e.g. by omitting it) is deprecated. + .. method:: Path.group() Return the name of the group owning the file. :exc:`KeyError` is raised @@ -1288,9 +1292,9 @@ call fails (for example because the path doesn't exist). PosixPath('setup.py'), PosixPath('test_pathlib.py')] - By default, :meth:`Path.rglob` follows symlinks except when expanding - "``**``" wildcards. Set *follow_symlinks* to true to always follow - symlinks, or false to treat all symlinks as files. + By default, :meth:`Path.rglob` emits a deprecation warning and follows + symlinks except when expanding "``**``" wildcards. Set *follow_symlinks* + to true to always follow symlinks, or false to treat all symlinks as files. .. audit-event:: pathlib.Path.rglob self,pattern pathlib.Path.rglob @@ -1301,6 +1305,10 @@ call fails (for example because the path doesn't exist). .. versionchanged:: 3.12 The *follow_symlinks* parameter was added. + .. deprecated-removed:: 3.12 3.14 + + Setting *follow_symlinks* to ``None`` (e.g. by omitting it) is deprecated. + .. method:: Path.rmdir() Remove this directory. The directory must be empty. diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 9f1e183bf057c2..07b6415a211b01 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -755,6 +755,13 @@ def glob(self, pattern, *, follow_symlinks=None): kind, including directories) matching the given relative pattern. """ sys.audit("pathlib.Path.glob", self, pattern) + if follow_symlinks is None: + msg = ("pathlib.Path.glob(pattern, follow_symlinks=None) is " + "deprecated and scheduled for removal in Python {remove}. " + "The follow_symlinks keyword-only argument should be set " + "to either True or False.") + warnings._deprecated("pathlib.Path.glob(pattern, follow_symlinks=None)", + msg, remove=(3, 14)) if not pattern: raise ValueError("Unacceptable pattern: {!r}".format(pattern)) drv, root, pattern_parts = self._parse_parts((pattern,)) @@ -772,6 +779,13 @@ def rglob(self, pattern, *, follow_symlinks=None): this subtree. """ sys.audit("pathlib.Path.rglob", self, pattern) + if follow_symlinks is None: + msg = ("pathlib.Path.rglob(pattern, follow_symlinks=None) is " + "deprecated and scheduled for removal in Python {remove}. " + "The follow_symlinks keyword-only argument should be set " + "to either True or False.") + warnings._deprecated("pathlib.Path.rglob(pattern, follow_symlinks=None)", msg, + remove=(3, 14)) drv, root, pattern_parts = self._parse_parts((pattern,)) if drv or root: raise NotImplementedError("Non-relative patterns are unsupported") diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 7512b3c268287c..6dc87e030301c6 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -11,6 +11,7 @@ import tempfile import unittest from unittest import mock +import warnings from test.support import import_helper from test.support import is_emscripten, is_wasi @@ -1749,39 +1750,42 @@ def test_iterdir_nodir(self): errno.ENOENT, errno.EINVAL)) def test_glob_common(self): - def _check(glob, expected): - self.assertEqual(set(glob), { P(BASE, q) for q in expected }) + def _check(path, glob, expected): + with self.assertWarns(DeprecationWarning): + actual = {q for q in path.glob(glob)} + self.assertEqual(actual, { P(BASE, q) for q in expected }) P = self.cls p = P(BASE) - it = p.glob("fileA") - self.assertIsInstance(it, collections.abc.Iterator) - _check(it, ["fileA"]) - _check(p.glob("fileB"), []) - _check(p.glob("dir*/file*"), ["dirB/fileB", "dirC/fileC"]) + with self.assertWarns(DeprecationWarning): + it = p.glob("fileA") + self.assertIsInstance(it, collections.abc.Iterator) + self.assertEqual(set(it), { P(BASE, "fileA") }) + _check(p, "fileB", []) + _check(p, "dir*/file*", ["dirB/fileB", "dirC/fileC"]) if not os_helper.can_symlink(): - _check(p.glob("*A"), ['dirA', 'fileA']) + _check(p, "*A", ['dirA', 'fileA']) else: - _check(p.glob("*A"), ['dirA', 'fileA', 'linkA']) + _check(p, "*A", ['dirA', 'fileA', 'linkA']) if not os_helper.can_symlink(): - _check(p.glob("*B/*"), ['dirB/fileB']) + _check(p, "*B/*", ['dirB/fileB']) else: - _check(p.glob("*B/*"), ['dirB/fileB', 'dirB/linkD', + _check(p, "*B/*", ['dirB/fileB', 'dirB/linkD', 'linkB/fileB', 'linkB/linkD']) if not os_helper.can_symlink(): - _check(p.glob("*/fileB"), ['dirB/fileB']) + _check(p, "*/fileB", ['dirB/fileB']) else: - _check(p.glob("*/fileB"), ['dirB/fileB', 'linkB/fileB']) + _check(p, "*/fileB", ['dirB/fileB', 'linkB/fileB']) if not os_helper.can_symlink(): - _check(p.glob("*/"), ["dirA", "dirB", "dirC", "dirE"]) + _check(p, "*/", ["dirA", "dirB", "dirC", "dirE"]) else: - _check(p.glob("*/"), ["dirA", "dirB", "dirC", "dirE", "linkB"]) + _check(p, "*/", ["dirA", "dirB", "dirC", "dirE", "linkB"]) @os_helper.skip_unless_symlink def test_glob_follow_symlinks_common(self): def _check(path, glob, expected): - actual = {path for path in path.glob(glob, follow_symlinks=True) - if "linkD" not in path.parent.parts} # exclude symlink loop. + actual = {q for q in path.glob(glob, follow_symlinks=True) + if "linkD" not in q.parent.parts} # exclude symlink loop. self.assertEqual(actual, { P(BASE, q) for q in expected }) P = self.cls p = P(BASE) @@ -1795,7 +1799,7 @@ def _check(path, glob, expected): @os_helper.skip_unless_symlink def test_glob_no_follow_symlinks_common(self): def _check(path, glob, expected): - actual = {path for path in path.glob(glob, follow_symlinks=False)} + actual = {q for q in path.glob(glob, follow_symlinks=False)} self.assertEqual(actual, { P(BASE, q) for q in expected }) P = self.cls p = P(BASE) @@ -1807,43 +1811,46 @@ def _check(path, glob, expected): _check(p, "*/", ["dirA", "dirB", "dirC", "dirE"]) def test_rglob_common(self): - def _check(glob, expected): - self.assertEqual(set(glob), { P(BASE, q) for q in expected }) + def _check(path, glob, expected): + with self.assertWarns(DeprecationWarning): + actual = {q for q in path.rglob(glob)} + self.assertEqual(actual, { P(BASE, q) for q in expected }) P = self.cls p = P(BASE) - it = p.rglob("fileA") - self.assertIsInstance(it, collections.abc.Iterator) - _check(it, ["fileA"]) - _check(p.rglob("fileB"), ["dirB/fileB"]) - _check(p.rglob("*/fileA"), []) + with self.assertWarns(DeprecationWarning): + it = p.rglob("fileA") + self.assertIsInstance(it, collections.abc.Iterator) + self.assertEqual(set(it), { P(BASE, "fileA") }) + _check(p, "fileB", ["dirB/fileB"]) + _check(p, "*/fileA", []) if not os_helper.can_symlink(): - _check(p.rglob("*/fileB"), ["dirB/fileB"]) + _check(p, "*/fileB", ["dirB/fileB"]) else: - _check(p.rglob("*/fileB"), ["dirB/fileB", "dirB/linkD/fileB", + _check(p, "*/fileB", ["dirB/fileB", "dirB/linkD/fileB", "linkB/fileB", "dirA/linkC/fileB"]) - _check(p.rglob("file*"), ["fileA", "dirB/fileB", + _check(p, "file*", ["fileA", "dirB/fileB", "dirC/fileC", "dirC/dirD/fileD"]) if not os_helper.can_symlink(): - _check(p.rglob("*/"), [ + _check(p, "*/", [ "dirA", "dirB", "dirC", "dirC/dirD", "dirE", ]) else: - _check(p.rglob("*/"), [ + _check(p, "*/", [ "dirA", "dirA/linkC", "dirB", "dirB/linkD", "dirC", "dirC/dirD", "dirE", "linkB", ]) - _check(p.rglob(""), ["", "dirA", "dirB", "dirC", "dirE", "dirC/dirD"]) + _check(p, "", ["", "dirA", "dirB", "dirC", "dirE", "dirC/dirD"]) p = P(BASE, "dirC") - _check(p.rglob("*"), ["dirC/fileC", "dirC/novel.txt", + _check(p, "*", ["dirC/fileC", "dirC/novel.txt", "dirC/dirD", "dirC/dirD/fileD"]) - _check(p.rglob("file*"), ["dirC/fileC", "dirC/dirD/fileD"]) - _check(p.rglob("*/*"), ["dirC/dirD/fileD"]) - _check(p.rglob("*/"), ["dirC/dirD"]) - _check(p.rglob(""), ["dirC", "dirC/dirD"]) + _check(p, "file*", ["dirC/fileC", "dirC/dirD/fileD"]) + _check(p, "*/*", ["dirC/dirD/fileD"]) + _check(p, "*/", ["dirC/dirD"]) + _check(p, "", ["dirC", "dirC/dirD"]) # gh-91616, a re module regression - _check(p.rglob("*.txt"), ["dirC/novel.txt"]) - _check(p.rglob("*.*"), ["dirC/novel.txt"]) + _check(p, "*.txt", ["dirC/novel.txt"]) + _check(p, "*.*", ["dirC/novel.txt"]) @os_helper.skip_unless_symlink def test_rglob_follow_symlinks_common(self): @@ -1904,7 +1911,7 @@ def test_rglob_symlink_loop(self): # Don't get fooled by symlink loops (Issue #26012). P = self.cls p = P(BASE) - given = set(p.rglob('*')) + given = set(p.rglob('*', follow_symlinks=False)) expect = {'brokenLink', 'dirA', 'dirA/linkC', 'dirB', 'dirB/fileB', 'dirB/linkD', @@ -1925,10 +1932,10 @@ def test_glob_many_open_files(self): p = P(base, *(['d']*depth)) p.mkdir(parents=True) pattern = '/'.join(['*'] * depth) - iters = [base.glob(pattern) for j in range(100)] + iters = [base.glob(pattern, follow_symlinks=True) for j in range(100)] for it in iters: self.assertEqual(next(it), p) - iters = [base.rglob('d') for j in range(100)] + iters = [base.rglob('d', follow_symlinks=True) for j in range(100)] p = base for i in range(depth): p = p / 'd' @@ -1939,9 +1946,9 @@ def test_glob_dotdot(self): # ".." is not special in globs. P = self.cls p = P(BASE) - self.assertEqual(set(p.glob("..")), { P(BASE, "..") }) - self.assertEqual(set(p.glob("dirA/../file*")), { P(BASE, "dirA/../fileA") }) - self.assertEqual(set(p.glob("../xyzzy")), set()) + self.assertEqual(set(p.glob("..", follow_symlinks=True)), { P(BASE, "..") }) + self.assertEqual(set(p.glob("dirA/../file*", follow_symlinks=True)), { P(BASE, "dirA/../fileA") }) + self.assertEqual(set(p.glob("../xyzzy", follow_symlinks=True)), set()) @os_helper.skip_unless_symlink def test_glob_permissions(self): @@ -1971,11 +1978,11 @@ def my_scandir(path): return contextlib.nullcontext(entries) with mock.patch("os.scandir", my_scandir): - self.assertEqual(len(set(base.glob("*"))), 3) + self.assertEqual(len(set(base.glob("*", follow_symlinks=True))), 3) subdir.mkdir() - self.assertEqual(len(set(base.glob("*"))), 4) + self.assertEqual(len(set(base.glob("*", follow_symlinks=True))), 4) subdir.chmod(000) - self.assertEqual(len(set(base.glob("*"))), 4) + self.assertEqual(len(set(base.glob("*", follow_symlinks=True))), 4) def _check_resolve(self, p, expected, strict=True): q = p.resolve(strict) @@ -2894,7 +2901,7 @@ def test_unsupported_flavour(self): def test_glob_empty_pattern(self): p = self.cls() with self.assertRaisesRegex(ValueError, 'Unacceptable pattern'): - list(p.glob('')) + list(p.glob('', follow_symlinks=True)) @only_posix @@ -2987,18 +2994,18 @@ def test_resolve_loop(self): def test_glob(self): P = self.cls p = P(BASE) - given = set(p.glob("FILEa")) + given = set(p.glob("FILEa", follow_symlinks=True)) expect = set() if not os_helper.fs_is_case_insensitive(BASE) else given self.assertEqual(given, expect) - self.assertEqual(set(p.glob("FILEa*")), set()) + self.assertEqual(set(p.glob("FILEa*", follow_symlinks=True)), set()) def test_rglob(self): P = self.cls p = P(BASE, "dirC") - given = set(p.rglob("FILEd")) + given = set(p.rglob("FILEd", follow_symlinks=True)) expect = set() if not os_helper.fs_is_case_insensitive(BASE) else given self.assertEqual(given, expect) - self.assertEqual(set(p.rglob("FILEd*")), set()) + self.assertEqual(set(p.rglob("FILEd*", follow_symlinks=True)), set()) @unittest.skipUnless(hasattr(pwd, 'getpwall'), 'pwd module does not expose getpwall()') @@ -3061,7 +3068,7 @@ def test_expanduser(self): "Bad file descriptor in /dev/fd affects only macOS") def test_handling_bad_descriptor(self): try: - file_descriptors = list(pathlib.Path('/dev/fd').rglob("*"))[3:] + file_descriptors = list(pathlib.Path('/dev/fd').rglob("*", follow_symlinks=True))[3:] if not file_descriptors: self.skipTest("no file descriptors - issue was not reproduced") # Checking all file descriptors because there is no guarantee @@ -3133,18 +3140,18 @@ def test_absolute(self): def test_glob(self): P = self.cls p = P(BASE) - self.assertEqual(set(p.glob("FILEa")), { P(BASE, "fileA") }) - self.assertEqual(set(p.glob("*a\\")), { P(BASE, "dirA") }) - self.assertEqual(set(p.glob("F*a")), { P(BASE, "fileA") }) - self.assertEqual(set(map(str, p.glob("FILEa"))), {f"{p}\\fileA"}) - self.assertEqual(set(map(str, p.glob("F*a"))), {f"{p}\\fileA"}) + self.assertEqual(set(p.glob("FILEa", follow_symlinks=True)), { P(BASE, "fileA") }) + self.assertEqual(set(p.glob("*a\\", follow_symlinks=True)), { P(BASE, "dirA") }) + self.assertEqual(set(p.glob("F*a", follow_symlinks=True)), { P(BASE, "fileA") }) + self.assertEqual(set(map(str, p.glob("FILEa", follow_symlinks=True))), {f"{p}\\fileA"}) + self.assertEqual(set(map(str, p.glob("F*a", follow_symlinks=True))), {f"{p}\\fileA"}) def test_rglob(self): P = self.cls p = P(BASE, "dirC") - self.assertEqual(set(p.rglob("FILEd")), { P(BASE, "dirC/dirD/fileD") }) - self.assertEqual(set(p.rglob("*\\")), { P(BASE, "dirC/dirD") }) - self.assertEqual(set(map(str, p.rglob("FILEd"))), {f"{p}\\dirD\\fileD"}) + self.assertEqual(set(p.rglob("FILEd", follow_symlinks=True)), { P(BASE, "dirC/dirD/fileD") }) + self.assertEqual(set(p.rglob("*\\", follow_symlinks=True)), { P(BASE, "dirC/dirD") }) + self.assertEqual(set(map(str, p.rglob("FILEd", follow_symlinks=True))), {f"{p}\\dirD\\fileD"}) def test_expanduser(self): P = self.cls From 6f7a83afac0abb94b703c8f5facbb7f9bff253d6 Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 14 Mar 2023 23:45:00 +0000 Subject: [PATCH 09/12] Fix news --- .../next/Library/2023-03-12-03-37-03.gh-issue-77609.aOQttm.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2023-03-12-03-37-03.gh-issue-77609.aOQttm.rst b/Misc/NEWS.d/next/Library/2023-03-12-03-37-03.gh-issue-77609.aOQttm.rst index 35e61088de58a6..a653a0bee3b7fb 100644 --- a/Misc/NEWS.d/next/Library/2023-03-12-03-37-03.gh-issue-77609.aOQttm.rst +++ b/Misc/NEWS.d/next/Library/2023-03-12-03-37-03.gh-issue-77609.aOQttm.rst @@ -1,2 +1,3 @@ Add *follow_symlinks* argument to :meth:`pathlib.Path.glob` and -:meth:`~pathlib.Path.rglob`, defaulting to false. +:meth:`~pathlib.Path.rglob`. The default value, ``None``, causes +a deprecation warning to be emitted. From ce7973fc60715d91ec651cbce3fff597a39d84ce Mon Sep 17 00:00:00 2001 From: barneygale Date: Wed, 10 May 2023 19:37:45 +0100 Subject: [PATCH 10/12] Revert "Fix news" This reverts commit 6f7a83afac0abb94b703c8f5facbb7f9bff253d6. --- .../next/Library/2023-03-12-03-37-03.gh-issue-77609.aOQttm.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2023-03-12-03-37-03.gh-issue-77609.aOQttm.rst b/Misc/NEWS.d/next/Library/2023-03-12-03-37-03.gh-issue-77609.aOQttm.rst index a653a0bee3b7fb..35e61088de58a6 100644 --- a/Misc/NEWS.d/next/Library/2023-03-12-03-37-03.gh-issue-77609.aOQttm.rst +++ b/Misc/NEWS.d/next/Library/2023-03-12-03-37-03.gh-issue-77609.aOQttm.rst @@ -1,3 +1,2 @@ Add *follow_symlinks* argument to :meth:`pathlib.Path.glob` and -:meth:`~pathlib.Path.rglob`. The default value, ``None``, causes -a deprecation warning to be emitted. +:meth:`~pathlib.Path.rglob`, defaulting to false. From 2db28d924c26539030c2ccdcee90893bb6daae00 Mon Sep 17 00:00:00 2001 From: barneygale Date: Wed, 10 May 2023 19:40:29 +0100 Subject: [PATCH 11/12] Revert "Deprecate follow_symlinks=None" This reverts commit 8704d33e0d36ba895cf540a3a49c1984c49ac133. --- Doc/library/pathlib.rst | 20 ++---- Lib/pathlib.py | 14 ---- Lib/test/test_pathlib.py | 141 +++++++++++++++++++-------------------- 3 files changed, 73 insertions(+), 102 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 31edacbce62981..d6f59ba27e3714 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -876,9 +876,9 @@ call fails (for example because the path doesn't exist). PosixPath('setup.py'), PosixPath('test_pathlib.py')] - By default, :meth:`Path.glob` emits a deprecation warning and follows - symlinks except when expanding "``**``" wildcards. Set *follow_symlinks* - to true to always follow symlinks, or false to treat all symlinks as files. + By default, :meth:`Path.glob` follows symlinks except when expanding + "``**``" wildcards. Set *follow_symlinks* to true to always follow + symlinks, or false to treat all symlinks as files. .. note:: Using the "``**``" pattern in large directory trees may consume @@ -893,10 +893,6 @@ call fails (for example because the path doesn't exist). .. versionchanged:: 3.12 The *follow_symlinks* parameter was added. - .. deprecated-removed:: 3.12 3.14 - - Setting *follow_symlinks* to ``None`` (e.g. by omitting it) is deprecated. - .. method:: Path.group() Return the name of the group owning the file. :exc:`KeyError` is raised @@ -1295,9 +1291,9 @@ call fails (for example because the path doesn't exist). PosixPath('setup.py'), PosixPath('test_pathlib.py')] - By default, :meth:`Path.rglob` emits a deprecation warning and follows - symlinks except when expanding "``**``" wildcards. Set *follow_symlinks* - to true to always follow symlinks, or false to treat all symlinks as files. + By default, :meth:`Path.rglob` follows symlinks except when expanding + "``**``" wildcards. Set *follow_symlinks* to true to always follow + symlinks, or false to treat all symlinks as files. .. audit-event:: pathlib.Path.rglob self,pattern pathlib.Path.rglob @@ -1308,10 +1304,6 @@ call fails (for example because the path doesn't exist). .. versionchanged:: 3.12 The *follow_symlinks* parameter was added. - .. deprecated-removed:: 3.12 3.14 - - Setting *follow_symlinks* to ``None`` (e.g. by omitting it) is deprecated. - .. method:: Path.rmdir() Remove this directory. The directory must be empty. diff --git a/Lib/pathlib.py b/Lib/pathlib.py index b3c50fdea72975..2a82c8f12c5a49 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -826,13 +826,6 @@ def glob(self, pattern, *, follow_symlinks=None): kind, including directories) matching the given relative pattern. """ sys.audit("pathlib.Path.glob", self, pattern) - if follow_symlinks is None: - msg = ("pathlib.Path.glob(pattern, follow_symlinks=None) is " - "deprecated and scheduled for removal in Python {remove}. " - "The follow_symlinks keyword-only argument should be set " - "to either True or False.") - warnings._deprecated("pathlib.Path.glob(pattern, follow_symlinks=None)", - msg, remove=(3, 14)) if not pattern: raise ValueError("Unacceptable pattern: {!r}".format(pattern)) drv, root, pattern_parts = self._parse_path(pattern) @@ -850,13 +843,6 @@ def rglob(self, pattern, *, follow_symlinks=None): this subtree. """ sys.audit("pathlib.Path.rglob", self, pattern) - if follow_symlinks is None: - msg = ("pathlib.Path.rglob(pattern, follow_symlinks=None) is " - "deprecated and scheduled for removal in Python {remove}. " - "The follow_symlinks keyword-only argument should be set " - "to either True or False.") - warnings._deprecated("pathlib.Path.rglob(pattern, follow_symlinks=None)", msg, - remove=(3, 14)) drv, root, pattern_parts = self._parse_path(pattern) if drv or root: raise NotImplementedError("Non-relative patterns are unsupported") diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index da503ed0e65370..2edf48d96f9582 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -11,7 +11,6 @@ import tempfile import unittest from unittest import mock -import warnings from test.support import import_helper from test.support import set_recursion_limit @@ -1787,44 +1786,41 @@ def test_iterdir_nodir(self): errno.ENOENT, errno.EINVAL)) def test_glob_common(self): - def _check(path, glob, expected): - with self.assertWarns(DeprecationWarning): - actual = {q for q in path.glob(glob)} - self.assertEqual(actual, { P(BASE, q) for q in expected }) + def _check(glob, expected): + self.assertEqual(set(glob), { P(BASE, q) for q in expected }) P = self.cls p = P(BASE) - with self.assertWarns(DeprecationWarning): - it = p.glob("fileA") - self.assertIsInstance(it, collections.abc.Iterator) - self.assertEqual(set(it), { P(BASE, "fileA") }) - _check(p, "fileB", []) - _check(p, "dir*/file*", ["dirB/fileB", "dirC/fileC"]) + it = p.glob("fileA") + self.assertIsInstance(it, collections.abc.Iterator) + _check(it, ["fileA"]) + _check(p.glob("fileB"), []) + _check(p.glob("dir*/file*"), ["dirB/fileB", "dirC/fileC"]) if not os_helper.can_symlink(): - _check(p, "*A", ['dirA', 'fileA']) + _check(p.glob("*A"), ['dirA', 'fileA']) else: - _check(p, "*A", ['dirA', 'fileA', 'linkA']) + _check(p.glob("*A"), ['dirA', 'fileA', 'linkA']) if not os_helper.can_symlink(): - _check(p, "*B/*", ['dirB/fileB']) + _check(p.glob("*B/*"), ['dirB/fileB']) else: - _check(p, "*B/*", ['dirB/fileB', 'dirB/linkD', + _check(p.glob("*B/*"), ['dirB/fileB', 'dirB/linkD', 'linkB/fileB', 'linkB/linkD']) if not os_helper.can_symlink(): - _check(p, "*/fileB", ['dirB/fileB']) + _check(p.glob("*/fileB"), ['dirB/fileB']) else: - _check(p, "*/fileB", ['dirB/fileB', 'linkB/fileB']) + _check(p.glob("*/fileB"), ['dirB/fileB', 'linkB/fileB']) if os_helper.can_symlink(): - _check(p, "brokenLink", ['brokenLink']) + _check(p.glob("brokenLink"), ['brokenLink']) if not os_helper.can_symlink(): - _check(p, "*/", ["dirA", "dirB", "dirC", "dirE"]) + _check(p.glob("*/"), ["dirA", "dirB", "dirC", "dirE"]) else: - _check(p, "*/", ["dirA", "dirB", "dirC", "dirE", "linkB"]) + _check(p.glob("*/"), ["dirA", "dirB", "dirC", "dirE", "linkB"]) @os_helper.skip_unless_symlink def test_glob_follow_symlinks_common(self): def _check(path, glob, expected): - actual = {q for q in path.glob(glob, follow_symlinks=True) - if "linkD" not in q.parent.parts} # exclude symlink loop. + actual = {path for path in path.glob(glob, follow_symlinks=True) + if "linkD" not in path.parent.parts} # exclude symlink loop. self.assertEqual(actual, { P(BASE, q) for q in expected }) P = self.cls p = P(BASE) @@ -1838,7 +1834,7 @@ def _check(path, glob, expected): @os_helper.skip_unless_symlink def test_glob_no_follow_symlinks_common(self): def _check(path, glob, expected): - actual = {q for q in path.glob(glob, follow_symlinks=False)} + actual = {path for path in path.glob(glob, follow_symlinks=False)} self.assertEqual(actual, { P(BASE, q) for q in expected }) P = self.cls p = P(BASE) @@ -1850,46 +1846,43 @@ def _check(path, glob, expected): _check(p, "*/", ["dirA", "dirB", "dirC", "dirE"]) def test_rglob_common(self): - def _check(path, glob, expected): - with self.assertWarns(DeprecationWarning): - actual = {q for q in path.rglob(glob)} - self.assertEqual(actual, { P(BASE, q) for q in expected }) + def _check(glob, expected): + self.assertEqual(set(glob), { P(BASE, q) for q in expected }) P = self.cls p = P(BASE) - with self.assertWarns(DeprecationWarning): - it = p.rglob("fileA") - self.assertIsInstance(it, collections.abc.Iterator) - self.assertEqual(set(it), { P(BASE, "fileA") }) - _check(p, "fileB", ["dirB/fileB"]) - _check(p, "*/fileA", []) + it = p.rglob("fileA") + self.assertIsInstance(it, collections.abc.Iterator) + _check(it, ["fileA"]) + _check(p.rglob("fileB"), ["dirB/fileB"]) + _check(p.rglob("*/fileA"), []) if not os_helper.can_symlink(): - _check(p, "*/fileB", ["dirB/fileB"]) + _check(p.rglob("*/fileB"), ["dirB/fileB"]) else: - _check(p, "*/fileB", ["dirB/fileB", "dirB/linkD/fileB", + _check(p.rglob("*/fileB"), ["dirB/fileB", "dirB/linkD/fileB", "linkB/fileB", "dirA/linkC/fileB"]) - _check(p, "file*", ["fileA", "dirB/fileB", + _check(p.rglob("file*"), ["fileA", "dirB/fileB", "dirC/fileC", "dirC/dirD/fileD"]) if not os_helper.can_symlink(): - _check(p, "*/", [ + _check(p.rglob("*/"), [ "dirA", "dirB", "dirC", "dirC/dirD", "dirE", ]) else: - _check(p, "*/", [ + _check(p.rglob("*/"), [ "dirA", "dirA/linkC", "dirB", "dirB/linkD", "dirC", "dirC/dirD", "dirE", "linkB", ]) - _check(p, "", ["", "dirA", "dirB", "dirC", "dirE", "dirC/dirD"]) + _check(p.rglob(""), ["", "dirA", "dirB", "dirC", "dirE", "dirC/dirD"]) p = P(BASE, "dirC") - _check(p, "*", ["dirC/fileC", "dirC/novel.txt", + _check(p.rglob("*"), ["dirC/fileC", "dirC/novel.txt", "dirC/dirD", "dirC/dirD/fileD"]) - _check(p, "file*", ["dirC/fileC", "dirC/dirD/fileD"]) - _check(p, "*/*", ["dirC/dirD/fileD"]) - _check(p, "*/", ["dirC/dirD"]) - _check(p, "", ["dirC", "dirC/dirD"]) + _check(p.rglob("file*"), ["dirC/fileC", "dirC/dirD/fileD"]) + _check(p.rglob("*/*"), ["dirC/dirD/fileD"]) + _check(p.rglob("*/"), ["dirC/dirD"]) + _check(p.rglob(""), ["dirC", "dirC/dirD"]) # gh-91616, a re module regression - _check(p, "*.txt", ["dirC/novel.txt"]) - _check(p, "*.*", ["dirC/novel.txt"]) + _check(p.rglob("*.txt"), ["dirC/novel.txt"]) + _check(p.rglob("*.*"), ["dirC/novel.txt"]) @os_helper.skip_unless_symlink def test_rglob_follow_symlinks_common(self): @@ -1950,7 +1943,7 @@ def test_rglob_symlink_loop(self): # Don't get fooled by symlink loops (Issue #26012). P = self.cls p = P(BASE) - given = set(p.rglob('*', follow_symlinks=False)) + given = set(p.rglob('*')) expect = {'brokenLink', 'dirA', 'dirA/linkC', 'dirB', 'dirB/fileB', 'dirB/linkD', @@ -1971,10 +1964,10 @@ def test_glob_many_open_files(self): p = P(base, *(['d']*depth)) p.mkdir(parents=True) pattern = '/'.join(['*'] * depth) - iters = [base.glob(pattern, follow_symlinks=True) for j in range(100)] + iters = [base.glob(pattern) for j in range(100)] for it in iters: self.assertEqual(next(it), p) - iters = [base.rglob('d', follow_symlinks=True) for j in range(100)] + iters = [base.rglob('d') for j in range(100)] p = base for i in range(depth): p = p / 'd' @@ -1985,14 +1978,14 @@ def test_glob_dotdot(self): # ".." is not special in globs. P = self.cls p = P(BASE) - self.assertEqual(set(p.glob("..", follow_symlinks=True)), { P(BASE, "..") }) - self.assertEqual(set(p.glob("../..", follow_symlinks=True)), { P(BASE, "..", "..") }) - self.assertEqual(set(p.glob("dirA/..", follow_symlinks=True)), { P(BASE, "dirA", "..") }) - self.assertEqual(set(p.glob("dirA/../file*", follow_symlinks=True)), { P(BASE, "dirA/../fileA") }) - self.assertEqual(set(p.glob("dirA/../file*/..", follow_symlinks=True)), set()) - self.assertEqual(set(p.glob("../xyzzy", follow_symlinks=True)), set()) - self.assertEqual(set(p.glob("xyzzy/..", follow_symlinks=True)), set()) - self.assertEqual(set(p.glob("/".join([".."] * 50), follow_symlinks=True)), { P(BASE, *[".."] * 50)}) + self.assertEqual(set(p.glob("..")), { P(BASE, "..") }) + self.assertEqual(set(p.glob("../..")), { P(BASE, "..", "..") }) + self.assertEqual(set(p.glob("dirA/..")), { P(BASE, "dirA", "..") }) + self.assertEqual(set(p.glob("dirA/../file*")), { P(BASE, "dirA/../fileA") }) + self.assertEqual(set(p.glob("dirA/../file*/..")), set()) + self.assertEqual(set(p.glob("../xyzzy")), set()) + self.assertEqual(set(p.glob("xyzzy/..")), set()) + self.assertEqual(set(p.glob("/".join([".."] * 50))), { P(BASE, *[".."] * 50)}) @os_helper.skip_unless_symlink def test_glob_permissions(self): @@ -2022,11 +2015,11 @@ def my_scandir(path): return contextlib.nullcontext(entries) with mock.patch("os.scandir", my_scandir): - self.assertEqual(len(set(base.glob("*", follow_symlinks=True))), 3) + self.assertEqual(len(set(base.glob("*"))), 3) subdir.mkdir() - self.assertEqual(len(set(base.glob("*", follow_symlinks=True))), 4) + self.assertEqual(len(set(base.glob("*"))), 4) subdir.chmod(000) - self.assertEqual(len(set(base.glob("*", follow_symlinks=True))), 4) + self.assertEqual(len(set(base.glob("*"))), 4) def _check_resolve(self, p, expected, strict=True): q = p.resolve(strict) @@ -2970,7 +2963,7 @@ def test_unsupported_flavour(self): def test_glob_empty_pattern(self): p = self.cls() with self.assertRaisesRegex(ValueError, 'Unacceptable pattern'): - list(p.glob('', follow_symlinks=True)) + list(p.glob('')) @only_posix @@ -3063,18 +3056,18 @@ def test_resolve_loop(self): def test_glob(self): P = self.cls p = P(BASE) - given = set(p.glob("FILEa", follow_symlinks=True)) + given = set(p.glob("FILEa")) expect = set() if not os_helper.fs_is_case_insensitive(BASE) else given self.assertEqual(given, expect) - self.assertEqual(set(p.glob("FILEa*", follow_symlinks=True)), set()) + self.assertEqual(set(p.glob("FILEa*")), set()) def test_rglob(self): P = self.cls p = P(BASE, "dirC") - given = set(p.rglob("FILEd", follow_symlinks=True)) + given = set(p.rglob("FILEd")) expect = set() if not os_helper.fs_is_case_insensitive(BASE) else given self.assertEqual(given, expect) - self.assertEqual(set(p.rglob("FILEd*", follow_symlinks=True)), set()) + self.assertEqual(set(p.rglob("FILEd*")), set()) @unittest.skipUnless(hasattr(pwd, 'getpwall'), 'pwd module does not expose getpwall()') @@ -3137,7 +3130,7 @@ def test_expanduser(self): "Bad file descriptor in /dev/fd affects only macOS") def test_handling_bad_descriptor(self): try: - file_descriptors = list(pathlib.Path('/dev/fd').rglob("*", follow_symlinks=True))[3:] + file_descriptors = list(pathlib.Path('/dev/fd').rglob("*"))[3:] if not file_descriptors: self.skipTest("no file descriptors - issue was not reproduced") # Checking all file descriptors because there is no guarantee @@ -3209,18 +3202,18 @@ def test_absolute(self): def test_glob(self): P = self.cls p = P(BASE) - self.assertEqual(set(p.glob("FILEa", follow_symlinks=True)), { P(BASE, "fileA") }) - self.assertEqual(set(p.glob("*a\\", follow_symlinks=True)), { P(BASE, "dirA") }) - self.assertEqual(set(p.glob("F*a", follow_symlinks=True)), { P(BASE, "fileA") }) - self.assertEqual(set(map(str, p.glob("FILEa", follow_symlinks=True))), {f"{p}\\fileA"}) - self.assertEqual(set(map(str, p.glob("F*a", follow_symlinks=True))), {f"{p}\\fileA"}) + self.assertEqual(set(p.glob("FILEa")), { P(BASE, "fileA") }) + self.assertEqual(set(p.glob("*a\\")), { P(BASE, "dirA") }) + self.assertEqual(set(p.glob("F*a")), { P(BASE, "fileA") }) + self.assertEqual(set(map(str, p.glob("FILEa"))), {f"{p}\\fileA"}) + self.assertEqual(set(map(str, p.glob("F*a"))), {f"{p}\\fileA"}) def test_rglob(self): P = self.cls p = P(BASE, "dirC") - self.assertEqual(set(p.rglob("FILEd", follow_symlinks=True)), { P(BASE, "dirC/dirD/fileD") }) - self.assertEqual(set(p.rglob("*\\", follow_symlinks=True)), { P(BASE, "dirC/dirD") }) - self.assertEqual(set(map(str, p.rglob("FILEd", follow_symlinks=True))), {f"{p}\\dirD\\fileD"}) + self.assertEqual(set(p.rglob("FILEd")), { P(BASE, "dirC/dirD/fileD") }) + self.assertEqual(set(p.rglob("*\\")), { P(BASE, "dirC/dirD") }) + self.assertEqual(set(map(str, p.rglob("FILEd"))), {f"{p}\\dirD\\fileD"}) def test_expanduser(self): P = self.cls From 4f2a49290dfedb609946e1bcd0dc5656cd5bca63 Mon Sep 17 00:00:00 2001 From: barneygale Date: Wed, 24 May 2023 00:07:05 +0100 Subject: [PATCH 12/12] Re-target to 3.13 --- Doc/library/pathlib.rst | 4 ++-- Doc/whatsnew/3.13.rst | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 0083dc5e62f1c3..ee3330f44f47d0 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -929,7 +929,7 @@ call fails (for example because the path doesn't exist). .. versionadded:: 3.12 The *case_sensitive* argument. - .. versionadded:: 3.12 + .. versionadded:: 3.13 The *follow_symlinks* argument. .. method:: Path.group() @@ -1349,7 +1349,7 @@ call fails (for example because the path doesn't exist). .. versionadded:: 3.12 The *case_sensitive* argument. - .. versionadded:: 3.12 + .. versionadded:: 3.13 The *follow_symlinks* argument. .. method:: Path.rmdir() diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index e0c3c2a3592ec7..a13dbf864a86e7 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -87,6 +87,12 @@ New Modules Improved Modules ================ +pathlib +------- + +* Add *follow_symlinks* keyword-only argument to :meth:`pathlib.Path.glob` and + :meth:`~pathlib.Path.rglob`. + (Contributed by Barney Gale in :gh:`77609`.) Optimizations =============