From 4e80705a18da8bfa8e83c3047166423463622136 Mon Sep 17 00:00:00 2001 From: barneygale Date: Fri, 12 Jan 2024 17:32:43 +0000 Subject: [PATCH 1/6] GH-79634: Accept path-like objects as pathlib glob patterns. Allow `os.PathLike` objects to be passed as patterns to `pathlib.Path.glob()` and `rglob()`. --- Doc/library/pathlib.rst | 6 +++ Lib/pathlib/__init__.py | 39 +++++++++++-------- Lib/pathlib/_abc.py | 30 +++++++------- Lib/test/test_pathlib/test_pathlib.py | 16 ++++++++ ...4-01-12-17-32-36.gh-issue-79634.uTSTRI.rst | 2 + 5 files changed, 63 insertions(+), 30 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-01-12-17-32-36.gh-issue-79634.uTSTRI.rst diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 60791725c2323d..3c60ec342935c7 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1020,6 +1020,9 @@ call fails (for example because the path doesn't exist). future Python release, patterns with this ending will match both files and directories. Add a trailing slash to match only directories. + .. versionchanged:: 3.13 + The *pattern* parameter accepts a :term:`path-like object`. + .. method:: Path.group(*, follow_symlinks=True) Return the name of the group owning the file. :exc:`KeyError` is raised @@ -1482,6 +1485,9 @@ call fails (for example because the path doesn't exist). .. versionchanged:: 3.13 The *follow_symlinks* parameter was added. + .. versionchanged:: 3.13 + The *pattern* parameter accepts a :term:`path-like object`. + .. method:: Path.rmdir() Remove this directory. The directory must be empty. diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index e70cfe91d322bc..139887b8e1f222 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -407,6 +407,23 @@ def as_uri(self): from urllib.parse import quote_from_bytes return prefix + quote_from_bytes(os.fsencode(path)) + @property + def _pattern_parts(self): + """List of path components, to be used with patterns in glob().""" + parts = self._tail.copy() + if self._raw_path.endswith('**'): + # GH-70303: '**' only matches directories. Add trailing slash. + warnings.warn( + "Pattern ending '**' will match files and directories in a " + "future Python release. Add a trailing slash to match only " + "directories and remove this warning.", + FutureWarning, 4) + parts.append('') + elif self._raw_path[-1] in (self.pathmod.sep, self.pathmod.altsep): + # GH-65238: pathlib doesn't preserve trailing slash. Add it back. + parts.append('') + return parts + # Subclassing os.PathLike makes isinstance() checks slower, # which in turn makes Path construction slower. Register instead! @@ -551,14 +568,8 @@ def glob(self, pattern, *, case_sensitive=None, follow_symlinks=None): kind, including directories) matching the given relative pattern. """ sys.audit("pathlib.Path.glob", self, pattern) - if pattern.endswith('**'): - # GH-70303: '**' only matches directories. Add trailing slash. - warnings.warn( - "Pattern ending '**' will match files and directories in a " - "future Python release. Add a trailing slash to match only " - "directories and remove this warning.", - FutureWarning, 2) - pattern = f'{pattern}/' + if not isinstance(pattern, PurePath): + pattern = self.with_segments(pattern) return _abc.PathBase.glob( self, pattern, case_sensitive=case_sensitive, follow_symlinks=follow_symlinks) @@ -568,15 +579,9 @@ def rglob(self, pattern, *, case_sensitive=None, follow_symlinks=None): this subtree. """ sys.audit("pathlib.Path.rglob", self, pattern) - if pattern.endswith('**'): - # GH-70303: '**' only matches directories. Add trailing slash. - warnings.warn( - "Pattern ending '**' will match files and directories in a " - "future Python release. Add a trailing slash to match only " - "directories and remove this warning.", - FutureWarning, 2) - pattern = f'{pattern}/' - pattern = f'**/{pattern}' + if not isinstance(pattern, PurePath): + pattern = self.with_segments(pattern) + pattern = '**' / pattern return _abc.PathBase.glob( self, pattern, case_sensitive=case_sensitive, follow_symlinks=follow_symlinks) diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index e53921edaa2cae..52c1cee66e39ba 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -406,6 +406,11 @@ def is_reserved(self): name = self.name.partition('.')[0].partition(':')[0].rstrip(' ') return name.upper() in _WIN_RESERVED_NAMES + @property + def _pattern_parts(self): + """List of path components, to be used with patterns in glob().""" + return list(self.parts) + def match(self, path_pattern, *, case_sensitive=None): """ Return True if this path matches the given pattern. @@ -415,11 +420,10 @@ def match(self, path_pattern, *, case_sensitive=None): if case_sensitive is None: case_sensitive = _is_case_sensitive(self.pathmod) sep = path_pattern.pathmod.sep - pattern_str = str(path_pattern) if path_pattern.anchor: - pass + pattern_str = str(path_pattern) elif path_pattern.parts: - pattern_str = f'**{sep}{pattern_str}' + pattern_str = str('**' / path_pattern) else: raise ValueError("empty pattern") match = _compile_pattern(pattern_str, sep, case_sensitive) @@ -706,16 +710,14 @@ def glob(self, pattern, *, case_sensitive=None, follow_symlinks=None): """Iterate over this subtree and yield all existing files (of any kind, including directories) matching the given relative pattern. """ - path_pattern = self.with_segments(pattern) - if path_pattern.anchor: + if not isinstance(pattern, PurePathBase): + pattern = self.with_segments(pattern) + if pattern.anchor: raise NotImplementedError("Non-relative patterns are unsupported") - elif not path_pattern.parts: + elif not pattern.parts: raise ValueError("Unacceptable pattern: {!r}".format(pattern)) - pattern_parts = list(path_pattern.parts) - if not self.pathmod.basename(pattern): - # GH-65238: pathlib doesn't preserve trailing slash. Add it back. - pattern_parts.append('') + pattern_parts = pattern._pattern_parts if case_sensitive is None: # TODO: evaluate case-sensitivity of each directory in _select_children(). @@ -752,7 +754,7 @@ def glob(self, pattern, *, case_sensitive=None, follow_symlinks=None): # Filter out paths that don't match pattern. prefix_len = len(str(self._make_child_relpath('_'))) - 1 - match = _compile_pattern(str(path_pattern), sep, case_sensitive) + match = _compile_pattern(str(pattern), sep, case_sensitive) paths = (path for path in paths if match(str(path), prefix_len)) return paths @@ -775,8 +777,10 @@ def rglob(self, pattern, *, case_sensitive=None, follow_symlinks=None): directories) matching the given relative pattern, anywhere in this subtree. """ - return self.glob( - f'**/{pattern}', case_sensitive=case_sensitive, follow_symlinks=follow_symlinks) + if not isinstance(pattern, PurePathBase): + pattern = self.with_segments(pattern) + pattern = '**' / pattern + return self.glob(pattern, case_sensitive=case_sensitive, follow_symlinks=follow_symlinks) def walk(self, top_down=True, on_error=None, follow_symlinks=False): """Walk the directory tree from this directory, similar to os.walk().""" diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 1b560adfc3b57a..2d0a5d8eddf950 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -1859,6 +1859,22 @@ def test_glob_recursive_no_trailing_slash(self): with self.assertWarns(FutureWarning): p.rglob('*/**') + def test_glob_pathlike(self): + P = self.cls + p = P(self.base) + pattern = "dir*/file*" + expect = {p / "dirB/fileB", p / "dirC/fileC"} + self.assertEqual(expect, set(p.glob(P(pattern)))) + self.assertEqual(expect, set(p.glob(FakePath(pattern)))) + + def test_rglob_pathlike(self): + P = self.cls + p = P(self.base, "dirC") + pattern = "**/file*" + expect = {p / "fileC", p / "dirD/fileD"} + self.assertEqual(expect, set(p.rglob(P(pattern)))) + self.assertEqual(expect, set(p.rglob(FakePath(pattern)))) + @only_posix class PosixPathTest(PathTest, PurePosixPathTest): diff --git a/Misc/NEWS.d/next/Library/2024-01-12-17-32-36.gh-issue-79634.uTSTRI.rst b/Misc/NEWS.d/next/Library/2024-01-12-17-32-36.gh-issue-79634.uTSTRI.rst new file mode 100644 index 00000000000000..ba19b5209e648e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-01-12-17-32-36.gh-issue-79634.uTSTRI.rst @@ -0,0 +1,2 @@ +Accept :term:`path-like objects ` as patterns in +:meth:`pathlib.Path.glob` and :meth:`~pathlib.Path.rglob`. From 3eb53bc4d446f0bb52a4ecb88d448902df4538b4 Mon Sep 17 00:00:00 2001 From: barneygale Date: Fri, 19 Jan 2024 22:10:04 +0000 Subject: [PATCH 2/6] Tiny speedup --- Lib/pathlib/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index fa8d1899433f67..eaac1136201e64 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -471,7 +471,8 @@ def as_uri(self): def _pattern_parts(self): """List of path components, to be used with patterns in glob().""" parts = self._tail.copy() - if self._raw_path.endswith('**'): + pattern = self._raw_path + if pattern.endswith('**'): # GH-70303: '**' only matches directories. Add trailing slash. warnings.warn( "Pattern ending '**' will match files and directories in a " @@ -479,7 +480,7 @@ def _pattern_parts(self): "directories and remove this warning.", FutureWarning, 4) parts.append('') - elif self._raw_path[-1] in (self.pathmod.sep, self.pathmod.altsep): + elif pattern[-1] in (self.pathmod.sep, self.pathmod.altsep): # GH-65238: pathlib doesn't preserve trailing slash. Add it back. parts.append('') return parts From 980e4f3f87e8937351d0f16ee2a6af5b1991be6a Mon Sep 17 00:00:00 2001 From: barneygale Date: Sat, 20 Jan 2024 00:27:56 +0000 Subject: [PATCH 3/6] Process pattern as a stack. --- Lib/pathlib/__init__.py | 5 +++-- Lib/pathlib/_abc.py | 29 +++++++++++++---------------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index eaac1136201e64..7c57aa9387f918 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -468,8 +468,8 @@ def as_uri(self): return prefix + quote_from_bytes(os.fsencode(path)) @property - def _pattern_parts(self): - """List of path components, to be used with patterns in glob().""" + def _pattern_stack(self): + """Stack of path components, to be used with patterns in glob().""" parts = self._tail.copy() pattern = self._raw_path if pattern.endswith('**'): @@ -483,6 +483,7 @@ def _pattern_parts(self): elif pattern[-1] in (self.pathmod.sep, self.pathmod.altsep): # GH-65238: pathlib doesn't preserve trailing slash. Add it back. parts.append('') + parts.reverse() return parts diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index fd17bc3fc4f019..810ab54b2bf94e 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -428,9 +428,9 @@ def is_absolute(self): return self.pathmod.isabs(self._raw_path) @property - def _pattern_parts(self): - """List of path components, to be used with patterns in glob().""" - return list(self.parts) + def _pattern_stack(self): + """Stack of path components, to be used with patterns in glob().""" + return self._stack[1] def match(self, path_pattern, *, case_sensitive=None): """ @@ -738,8 +738,6 @@ def glob(self, pattern, *, case_sensitive=None, follow_symlinks=None): elif not pattern.parts: raise ValueError("Unacceptable pattern: {!r}".format(pattern)) - pattern_parts = pattern._pattern_parts - if case_sensitive is None: # TODO: evaluate case-sensitivity of each directory in _select_children(). case_sensitive = _is_case_sensitive(self.pathmod) @@ -751,14 +749,13 @@ def glob(self, pattern, *, case_sensitive=None, follow_symlinks=None): # build a `re.Pattern` object. This pattern is used to filter the # recursive walk. As a result, pattern parts following a '**' wildcard # do not perform any filesystem access, which can be much faster! - filter_paths = follow_symlinks is not None and '..' not in pattern_parts + stack = pattern._pattern_stack + filter_paths = follow_symlinks is not None and '..' not in stack deduplicate_paths = False sep = self.pathmod.sep paths = iter([self.joinpath('')] if self.is_dir() else []) - part_idx = 0 - while part_idx < len(pattern_parts): - part = pattern_parts[part_idx] - part_idx += 1 + while stack: + part = stack.pop() if part == '': # Trailing slash. pass @@ -766,11 +763,11 @@ def glob(self, pattern, *, case_sensitive=None, follow_symlinks=None): paths = (path._make_child_relpath('..') for path in paths) elif part == '**': # Consume adjacent '**' components. - while part_idx < len(pattern_parts) and pattern_parts[part_idx] == '**': - part_idx += 1 + while stack and stack[-1] == '**': + stack.pop() - if filter_paths and part_idx < len(pattern_parts) and pattern_parts[part_idx] != '': - dir_only = pattern_parts[-1] == '' + if filter_paths and stack and stack[-1] != '': + dir_only = stack[0] == '' paths = _select_recursive(paths, dir_only, follow_symlinks) # Filter out paths that don't match pattern. @@ -779,7 +776,7 @@ def glob(self, pattern, *, case_sensitive=None, follow_symlinks=None): paths = (path for path in paths if match(str(path), prefix_len)) return paths - dir_only = part_idx < len(pattern_parts) + dir_only = bool(stack) paths = _select_recursive(paths, dir_only, follow_symlinks) if deduplicate_paths: # De-duplicate if we've already seen a '**' component. @@ -788,7 +785,7 @@ def glob(self, pattern, *, case_sensitive=None, follow_symlinks=None): elif '**' in part: raise ValueError("Invalid pattern: '**' can only be an entire path component") else: - dir_only = part_idx < len(pattern_parts) + dir_only = bool(stack) match = _compile_pattern(part, sep, case_sensitive) paths = _select_children(paths, dir_only, follow_symlinks, match) return paths From 3e71a3e715c33841102f13b8d0615ae2ea6a833d Mon Sep 17 00:00:00 2001 From: barneygale Date: Sat, 20 Jan 2024 00:40:06 +0000 Subject: [PATCH 4/6] Move validation into _pattern_stack --- Lib/pathlib/__init__.py | 12 ++++++++---- Lib/pathlib/_abc.py | 10 ++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index 7c57aa9387f918..37470ed4e5ff3c 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -472,7 +472,14 @@ def _pattern_stack(self): """Stack of path components, to be used with patterns in glob().""" parts = self._tail.copy() pattern = self._raw_path - if pattern.endswith('**'): + if self.anchor: + raise NotImplementedError("Non-relative patterns are unsupported") + elif not parts: + raise ValueError("Unacceptable pattern: {!r}".format(pattern)) + elif pattern[-1] in (self.pathmod.sep, self.pathmod.altsep): + # GH-65238: pathlib doesn't preserve trailing slash. Add it back. + parts.append('') + elif parts[-1] == '**': # GH-70303: '**' only matches directories. Add trailing slash. warnings.warn( "Pattern ending '**' will match files and directories in a " @@ -480,9 +487,6 @@ def _pattern_stack(self): "directories and remove this warning.", FutureWarning, 4) parts.append('') - elif pattern[-1] in (self.pathmod.sep, self.pathmod.altsep): - # GH-65238: pathlib doesn't preserve trailing slash. Add it back. - parts.append('') parts.reverse() return parts diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index 810ab54b2bf94e..1a5571332020df 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -430,7 +430,10 @@ def is_absolute(self): @property def _pattern_stack(self): """Stack of path components, to be used with patterns in glob().""" - return self._stack[1] + anchor, parts = self._stack + if anchor: + raise NotImplementedError("Non-relative patterns are unsupported") + return parts def match(self, path_pattern, *, case_sensitive=None): """ @@ -733,11 +736,6 @@ def glob(self, pattern, *, case_sensitive=None, follow_symlinks=None): """ if not isinstance(pattern, PurePathBase): pattern = self.with_segments(pattern) - if pattern.anchor: - raise NotImplementedError("Non-relative patterns are unsupported") - elif not pattern.parts: - raise ValueError("Unacceptable pattern: {!r}".format(pattern)) - if case_sensitive is None: # TODO: evaluate case-sensitivity of each directory in _select_children(). case_sensitive = _is_case_sensitive(self.pathmod) From 0c20f70a7e02c0b790409553cee58c12a5fff240 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sat, 20 Jan 2024 01:03:30 +0000 Subject: [PATCH 5/6] Simplify handling of '', '.' and '..' segments in patterns. --- Lib/pathlib/__init__.py | 4 +++- Lib/pathlib/_abc.py | 51 +++++++++++++++++++---------------------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index 37470ed4e5ff3c..b043aed12b3849 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -603,7 +603,7 @@ def iterdir(self): def _scandir(self): return os.scandir(self) - def _make_child_entry(self, entry, is_dir=False): + def _make_child_entry(self, entry): # Transform an entry yielded from _scandir() into a path object. path_str = entry.name if str(self) == '.' else entry.path path = self.with_segments(path_str) @@ -614,6 +614,8 @@ def _make_child_entry(self, entry, is_dir=False): return path def _make_child_relpath(self, name): + if not name: + return self path_str = str(self) tail = self._tail if tail: diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index 1a5571332020df..e5eeb4afce2ea9 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -63,6 +63,12 @@ def _compile_pattern(pat, sep, case_sensitive): return re.compile(regex, flags=flags).match +def _select_special(paths, part): + """Yield special literal children of the given paths.""" + for path in paths: + yield path._make_child_relpath(part) + + def _select_children(parent_paths, dir_only, follow_symlinks, match): """Yield direct children of given paths, filtering by name and type.""" if follow_symlinks is None: @@ -84,7 +90,7 @@ def _select_children(parent_paths, dir_only, follow_symlinks, match): except OSError: continue if match(entry.name): - yield parent_path._make_child_entry(entry, dir_only) + yield parent_path._make_child_entry(entry) def _select_recursive(parent_paths, dir_only, follow_symlinks): @@ -107,7 +113,7 @@ def _select_recursive(parent_paths, dir_only, follow_symlinks): for entry in entries: try: if entry.is_dir(follow_symlinks=follow_symlinks): - paths.append(path._make_child_entry(entry, dir_only)) + paths.append(path._make_child_entry(entry)) continue except OSError: pass @@ -721,10 +727,8 @@ def _scandir(self): from contextlib import nullcontext return nullcontext(self.iterdir()) - def _make_child_entry(self, entry, is_dir=False): + def _make_child_entry(self, entry): # Transform an entry yielded from _scandir() into a path object. - if is_dir: - return entry.joinpath('') return entry def _make_child_relpath(self, name): @@ -740,39 +744,27 @@ def glob(self, pattern, *, case_sensitive=None, follow_symlinks=None): # TODO: evaluate case-sensitivity of each directory in _select_children(). case_sensitive = _is_case_sensitive(self.pathmod) - # If symlinks are handled consistently, and the pattern does not - # contain '..' components, then we can use a 'walk-and-match' strategy - # when expanding '**' wildcards. When a '**' wildcard is encountered, - # all following pattern parts are immediately consumed and used to - # build a `re.Pattern` object. This pattern is used to filter the - # recursive walk. As a result, pattern parts following a '**' wildcard - # do not perform any filesystem access, which can be much faster! stack = pattern._pattern_stack - filter_paths = follow_symlinks is not None and '..' not in stack + specials = ('', '.', '..') + filter_paths = False deduplicate_paths = False sep = self.pathmod.sep paths = iter([self.joinpath('')] if self.is_dir() else []) while stack: part = stack.pop() - if part == '': - # Trailing slash. - pass - elif part == '..': - paths = (path._make_child_relpath('..') for path in paths) + if part in specials: + paths = _select_special(paths, part) elif part == '**': # Consume adjacent '**' components. while stack and stack[-1] == '**': stack.pop() - if filter_paths and stack and stack[-1] != '': - dir_only = stack[0] == '' - paths = _select_recursive(paths, dir_only, follow_symlinks) - - # Filter out paths that don't match pattern. - prefix_len = len(str(self._make_child_relpath('_'))) - 1 - match = _compile_pattern(str(pattern), sep, case_sensitive) - paths = (path for path in paths if match(str(path), prefix_len)) - return paths + # Consume adjacent non-special components and enable post-walk + # regex filtering, provided we're treating symlinks consistently. + if follow_symlinks is not None: + while stack and stack[-1] not in specials: + filter_paths = True + stack.pop() dir_only = bool(stack) paths = _select_recursive(paths, dir_only, follow_symlinks) @@ -786,6 +778,11 @@ def glob(self, pattern, *, case_sensitive=None, follow_symlinks=None): dir_only = bool(stack) match = _compile_pattern(part, sep, case_sensitive) paths = _select_children(paths, dir_only, follow_symlinks, match) + if filter_paths: + # Filter out paths that don't match pattern. + prefix_len = len(str(self._make_child_relpath('_'))) - 1 + match = _compile_pattern(str(pattern), sep, case_sensitive) + paths = (path for path in paths if match(str(path), prefix_len)) return paths def rglob(self, pattern, *, case_sensitive=None, follow_symlinks=None): From b500051826c125e0b0bc197360ff624b485c1a84 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sat, 20 Jan 2024 01:28:54 +0000 Subject: [PATCH 6/6] Fix tests --- Lib/test/test_pathlib/test_pathlib.py | 7 +++++++ Lib/test/test_pathlib/test_pathlib_abc.py | 9 ++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 89fed9b556662b..bdbe92369639ef 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -1818,6 +1818,13 @@ def test_walk_above_recursion_limit(self): list(base.walk()) list(base.walk(top_down=False)) + def test_glob_empty_pattern(self): + p = self.cls('') + with self.assertRaisesRegex(ValueError, 'Unacceptable pattern'): + list(p.glob('')) + with self.assertRaisesRegex(ValueError, 'Unacceptable pattern'): + list(p.glob('.')) + def test_glob_many_open_files(self): depth = 30 P = self.cls diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index f877c98b7678f4..199718a8a69c5a 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -1045,9 +1045,12 @@ def _check(glob, expected): _check(p.glob("*/"), ["dirA/", "dirB/", "dirC/", "dirE/", "linkB/"]) def test_glob_empty_pattern(self): - p = self.cls('') - with self.assertRaisesRegex(ValueError, 'Unacceptable pattern'): - list(p.glob('')) + def _check(glob, expected): + self.assertEqual(set(glob), { P(self.base, q) for q in expected }) + P = self.cls + p = P(self.base) + _check(p.glob(""), [""]) + _check(p.glob("."), ["."]) def test_glob_case_sensitive(self): P = self.cls