Skip to content

Commit ac8f149

Browse files
committed
GH-139282: Support recursive wildcard in pathlib.PurePath.match()
Add support for the recursive wildcard `**` in `pathlib.PurePath.match()`. This is a behaviour change for anyone currently calling `match()` with `**` in their pattern, but I reckon the vast majority of those users are expecting recursive matching and haven't realised that it's not working (: This brings `match()` behaviour closer to `full_match()`.
1 parent 9e64938 commit ac8f149

File tree

4 files changed

+25
-20
lines changed

4 files changed

+25
-20
lines changed

Doc/library/pathlib.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -592,9 +592,8 @@ Pure paths provide the following methods and properties:
592592
Return ``True`` if matching is successful, ``False`` otherwise.
593593

594594
This method is similar to :meth:`~PurePath.full_match`, but empty patterns
595-
aren't allowed (:exc:`ValueError` is raised), the recursive wildcard
596-
"``**``" isn't supported (it acts like non-recursive "``*``"), and if a
597-
relative pattern is provided, then matching is done from the right::
595+
aren't allowed (:exc:`ValueError` is raised), and if a relative pattern is
596+
provided, then matching is done from the right::
598597

599598
>>> PurePath('a/b.py').match('*.py')
600599
True
@@ -609,6 +608,10 @@ Pure paths provide the following methods and properties:
609608
.. versionchanged:: 3.12
610609
The *case_sensitive* parameter was added.
611610

611+
.. versionchanged:: next
612+
Added support for the recursive wildcard "``**``". In previous versions,
613+
it acted like non-recursive "``*``".
614+
612615

613616
.. method:: PurePath.relative_to(other, walk_up=False)
614617

Lib/pathlib/__init__.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -566,27 +566,20 @@ def match(self, path_pattern, *, case_sensitive=None):
566566
"""
567567
Return True if this path matches the given pattern. If the pattern is
568568
relative, matching is done from the right; otherwise, the entire path
569-
is matched. The recursive wildcard '**' is *not* supported by this
570-
method.
569+
is matched.
571570
"""
572571
if not hasattr(path_pattern, 'with_segments'):
573572
path_pattern = self.with_segments(path_pattern)
574573
if case_sensitive is None:
575574
case_sensitive = self.parser is posixpath
576-
path_parts = self.parts[::-1]
577-
pattern_parts = path_pattern.parts[::-1]
578-
if not pattern_parts:
575+
if not path_pattern.parts:
579576
raise ValueError("empty pattern")
580-
if len(path_parts) < len(pattern_parts):
581-
return False
582-
if len(path_parts) > len(pattern_parts) and path_pattern.anchor:
583-
return False
584-
globber = _StringGlobber(self.parser.sep, case_sensitive)
585-
for path_part, pattern_part in zip(path_parts, pattern_parts):
586-
match = globber.compile(pattern_part)
587-
if match(path_part) is None:
588-
return False
589-
return True
577+
if not path_pattern.anchor:
578+
path_pattern = '**' / path_pattern
579+
path = str(self) if self.parts else ''
580+
pattern = str(path_pattern)
581+
globber = _StringGlobber(self.parser.sep, case_sensitive, recursive=True)
582+
return globber.compile(pattern)(path) is not None
590583

591584
# Subclassing os.PathLike makes isinstance() checks slower,
592585
# which in turn makes Path construction slower. Register instead!

Lib/test/test_pathlib/test_pathlib.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -584,16 +584,23 @@ def test_match_common(self):
584584
self.assertFalse(P('/ab.py').match('/a/*.py'))
585585
self.assertFalse(P('/a/b/c.py').match('/a/*.py'))
586586
# Multi-part glob-style pattern.
587-
self.assertFalse(P('/a/b/c.py').match('/**/*.py'))
587+
self.assertTrue(P('/a/b/c.py').match('/**/*.py'))
588588
self.assertTrue(P('/a/b/c.py').match('/a/**/*.py'))
589+
self.assertTrue(P('/a/b/c.py').match('/a/b/**/*.py'))
590+
self.assertTrue(P('/a/b/c.py').match('/**/**/**/**/*.py'))
591+
self.assertTrue(P('/a/b/c.py').match('/**'))
592+
self.assertTrue(P('/a/b/c.py').match('/a/**'))
593+
self.assertFalse(P('a/b/c.py').match('/a/b/c.py/**'))
594+
self.assertFalse(P('a/b/c.py').match('/**/a/b/c.py'))
595+
self.assertFalse(P('c.py').match('c/**'))
589596
# Case-sensitive flag
590597
self.assertFalse(P('A.py').match('a.PY', case_sensitive=True))
591598
self.assertTrue(P('A.py').match('a.PY', case_sensitive=False))
592599
self.assertFalse(P('c:/a/B.Py').match('C:/A/*.pY', case_sensitive=True))
593600
self.assertTrue(P('/a/b/c.py').match('/A/*/*.Py', case_sensitive=False))
594601
# Matching against empty path
595602
self.assertFalse(P('').match('*'))
596-
self.assertFalse(P('').match('**'))
603+
self.assertTrue(P('').match('**'))
597604
self.assertFalse(P('').match('**/*'))
598605

599606
@needs_posix
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add support for the recursive wildcard "``**``" in
2+
:meth:`pathlib.PurePath.match`.

0 commit comments

Comments
 (0)