Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 35.2k
GH-73435: Implement recursive wildcards in pathlib.PurePath.match()#101398
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
barneygale
merged 40 commits into
python:main
from
barneygale:gh-73435-pathlib-match-recursiveMay 30, 2023
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
Show all changes
40 commits
Select commit
Hold shift + click to select a range
608e917
gh-73435: Implement recursive wildcards in pathlib.PurePath.match()
barneygale 9a43c7f
Simplify code slightly
barneygale a846279
Fix support for newlines
barneygale bbd8cd6
Cache translation of individual components
barneygale b5c002e
Drop 'recursive' argument, make this the only behaviour.
barneygale 0afcd54
Undo modifications to fnmatch.py
barneygale fe32717
Merge branch 'main' into gh-73435-pathlib-match-recursive
barneygale 7b6f850
Fix Windows support
barneygale 037488a
Tidy up code.
barneygale 0741950
Add news blurb.
barneygale e1c9731
Merge branch 'main' into gh-73435-pathlib-match-recursive
barneygale db6f0ad
Merge branch 'main' into gh-73435-pathlib-match-recursive
barneygale 8dff9e2
Merge branch 'main' into gh-73435-pathlib-match-recursive
barneygale 314679f
Simplify patch; prepare for use in `glob()`
barneygale 90eebcc
Make better use of path object caching.
barneygale 4b5fffd
Add performance tip to docs
barneygale 5e8bc28
Skip re-initialisation of PurePath patterns.
barneygale e81ab5a
Merge branch 'main' into gh-73435-pathlib-match-recursive
barneygale afb8047
Merge branch 'main' into gh-73435-pathlib-match-recursive
barneygale 722a1ab
Use `re.IGNORECASE` rather than `os.path.normcase()`
barneygale 0ccf3df
Merge branch 'main' into gh-73435-pathlib-match-recursive
barneygale ccea5e1
Add whats new entry
barneygale dd04294
Update Doc/whatsnew/3.12.rst
barneygale b258641
Apply suggestions from code review
barneygale ced8998
Explain _FNMATCH_SLICE
barneygale a33c7b6
Accidentally a word.
barneygale 4b3bddb
Cache pattern compilation
barneygale 6ad30dd
Remove unneeded `from None` suffix, whoops.
barneygale 052890f
Tiny performance improvement: avoid accessing path.parts
barneygale d789b6d
Typo fix
barneygale 4fe77c6
Avoid hashing path object when compiling pattern.
barneygale 4770c13
More performance tweaks
barneygale 559787d
Merge branch 'main' into gh-73435-pathlib-match-recursive
barneygale 9c09fc4
Merge branch 'main' into gh-73435-pathlib-match-recursive
barneygale eb35dbc
Re-target to 3.13.
barneygale 8959dfd
Merge branch 'main' into gh-73435-pathlib-match-recursive
barneygale fec7702
Merge branch 'main' into gh-73435-pathlib-match-recursive
barneygale 89bc380
Merge branch 'main' into gh-73435-pathlib-match-recursive
barneygale 9211297
Add more comments!
barneygale 73bb309
Update Lib/pathlib.py
barneygale File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -54,13 +54,30 @@ def _ignore_error(exception): | ||
| getattr(exception, 'winerror', None) in _IGNORED_WINERRORS) | ||
| @functools.cache | ||
| def _is_case_sensitive(flavour): | ||
| return flavour.normcase('Aa') == 'Aa' | ||
| # | ||
| # Globbing helpers | ||
| # | ||
| # fnmatch.translate() returns a regular expression that includes a prefix and | ||
| # a suffix, which enable matching newlines and ensure the end of the string is | ||
| # matched, respectively. These features are undesirable for our implementation | ||
| # of PurePatch.match(), which represents path separators as newlines and joins | ||
| # pattern segments together. As a workaround, we define a slice object that | ||
| # can remove the prefix and suffix from any translate() result. See the | ||
| # _compile_pattern_lines() function for more details. | ||
| _FNMATCH_PREFIX, _FNMATCH_SUFFIX = fnmatch.translate('_').split('_') | ||
| _FNMATCH_SLICE = slice(len(_FNMATCH_PREFIX), -len(_FNMATCH_SUFFIX)) | ||
| _SWAP_SEP_AND_NEWLINE = { | ||
| '/': str.maketrans({'/': '\n', '\n': '/'}), | ||
| '\\': str.maketrans({'\\': '\n', '\n': '\\'}), | ||
| } | ||
| @functools.lru_cache() | ||
| def _make_selector(pattern_parts, flavour, case_sensitive): | ||
| pat = pattern_parts[0] | ||
| @@ -92,6 +109,51 @@ def _compile_pattern(pat, case_sensitive): | ||
| return re.compile(fnmatch.translate(pat), flags).match | ||
| @functools.lru_cache() | ||
| def _compile_pattern_lines(pattern_lines, case_sensitive): | ||
| """Compile the given pattern lines to an `re.Pattern` object. | ||
| The *pattern_lines* argument is a glob-style pattern (e.g. '**/*.py') with | ||
| its path separators and newlines swapped (e.g. '**\n*.py`). By using | ||
| newlines to separate path components, and not setting `re.DOTALL`, we | ||
| ensure that the `*` wildcard cannot match path separators. | ||
| The returned `re.Pattern` object may have its `match()` method called to | ||
| match a complete pattern, or `search()` to match from the right. The | ||
| argument supplied to these methods must also have its path separators and | ||
| newlines swapped. | ||
| """ | ||
| # Match the start of the path, or just after a path separator | ||
| parts = ['^'] | ||
| for part in pattern_lines.splitlines(keepends=True): | ||
| if part == '**\n': | ||
| # '**/' component: we use '[\s\S]' rather than '.' so that path | ||
| # separators (i.e. newlines) are matched. The trailing '^' ensures | ||
| # we terminate after a path separator (i.e. on a new line). | ||
| part = r'[\s\S]*^' | ||
barneygale marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| elif part == '**': | ||
| # '**' component. | ||
| part = r'[\s\S]*' | ||
| elif '**' in part: | ||
| raise ValueError("Invalid pattern: '**' can only be an entire path component") | ||
| else: | ||
| # Any other component: pass to fnmatch.translate(). We slice off | ||
| # the common prefix and suffix added by translate() to ensure that | ||
| # re.DOTALL is not set, and the end of the string not matched, | ||
| # respectively. With DOTALL not set, '*' wildcards will not match | ||
| # path separators, because the '.' characters in the pattern will | ||
| # not match newlines. | ||
| part = fnmatch.translate(part)[_FNMATCH_SLICE] | ||
| parts.append(part) | ||
| # Match the end of the path, always. | ||
| parts.append(r'\Z') | ||
| flags = re.MULTILINE | ||
| if not case_sensitive: | ||
| flags |= re.IGNORECASE | ||
| return re.compile(''.join(parts), flags=flags) | ||
| class _Selector: | ||
| """A selector matches a specific glob pattern part against the children | ||
| of a given path.""" | ||
| @@ -276,6 +338,10 @@ class PurePath: | ||
| # to implement comparison methods like `__lt__()`. | ||
| '_parts_normcase_cached', | ||
| # The `_lines_cached` slot stores the string path with path separators | ||
| # and newlines swapped. This is used to implement `match()`. | ||
| '_lines_cached', | ||
| # The `_hash` slot stores the hash of the case-normalized string | ||
| # path. It's set when `__hash__()` is called for the first time. | ||
| '_hash', | ||
| @@ -441,6 +507,16 @@ def _parts_normcase(self): | ||
| self._parts_normcase_cached = self._str_normcase.split(self._flavour.sep) | ||
| return self._parts_normcase_cached | ||
| @property | ||
| def _lines(self): | ||
| # Path with separators and newlines swapped, for pattern matching. | ||
| try: | ||
| return self._lines_cached | ||
| except AttributeError: | ||
| trans = _SWAP_SEP_AND_NEWLINE[self._flavour.sep] | ||
| self._lines_cached = str(self).translate(trans) | ||
| return self._lines_cached | ||
| def __eq__(self, other): | ||
| if not isinstance(other, PurePath): | ||
| return NotImplemented | ||
| @@ -697,23 +773,18 @@ def match(self, path_pattern, *, case_sensitive=None): | ||
| """ | ||
| Return True if this path matches the given pattern. | ||
| """ | ||
| if not isinstance(path_pattern, PurePath): | ||
| path_pattern = self.with_segments(path_pattern) | ||
| if case_sensitive is None: | ||
| case_sensitive = _is_case_sensitive(self._flavour) | ||
| pat = self.with_segments(path_pattern) | ||
| if not pat.parts: | ||
| pattern = _compile_pattern_lines(path_pattern._lines, case_sensitive) | ||
| if path_pattern.drive or path_pattern.root: | ||
| return pattern.match(self._lines) is not None | ||
| elif path_pattern._tail: | ||
| return pattern.search(self._lines) is not None | ||
| else: | ||
| raise ValueError("empty pattern") | ||
| pat_parts = pat.parts | ||
| parts = self.parts | ||
| if pat.drive or pat.root: | ||
| if len(pat_parts) != len(parts): | ||
| return False | ||
| elif len(pat_parts) > len(parts): | ||
| return False | ||
| for part, pat in zip(reversed(parts), reversed(pat_parts)): | ||
| match = _compile_pattern(pat, case_sensitive) | ||
| if not match(part): | ||
| return False | ||
| return True | ||
| # Subclassing os.PathLike makes isinstance() checks slower, | ||
| # which in turn makes Path construction slower. Register instead! | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1 Misc/NEWS.d/next/Library/2023-02-17-18-56-46.gh-issue-73435.7sTJHk.rst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Add support for recursive wildcards in :meth:`pathlib.PurePath.match`. |
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.