Skip to content

Commit a4459c3

Browse files
authored
GH-127381: pathlib ABCs: remove JoinablePath.match() (#129147)
Unlike `ReadablePath.[r]glob()` and `JoinablePath.full_match()`, the `JoinablePath.match()` method doesn't support the recursive wildcard `**`, and matches from the right when a fully relative pattern is given. These quirks means its probably unsuitable for inclusion in the pathlib ABCs, especially given `full_match()` handles the same use case.
1 parent d23f570 commit a4459c3

File tree

4 files changed

+104
-109
lines changed

4 files changed

+104
-109
lines changed

Lib/pathlib/_abc.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -358,33 +358,6 @@ def parents(self):
358358
parent = split(path)[0]
359359
return tuple(parents)
360360

361-
def match(self, path_pattern, *, case_sensitive=None):
362-
"""
363-
Return True if this path matches the given pattern. If the pattern is
364-
relative, matching is done from the right; otherwise, the entire path
365-
is matched. The recursive wildcard '**' is *not* supported by this
366-
method.
367-
"""
368-
if not isinstance(path_pattern, JoinablePath):
369-
path_pattern = self.with_segments(path_pattern)
370-
if case_sensitive is None:
371-
case_sensitive = _is_case_sensitive(self.parser)
372-
sep = path_pattern.parser.sep
373-
path_parts = self.parts[::-1]
374-
pattern_parts = path_pattern.parts[::-1]
375-
if not pattern_parts:
376-
raise ValueError("empty pattern")
377-
if len(path_parts) < len(pattern_parts):
378-
return False
379-
if len(path_parts) > len(pattern_parts) and path_pattern.anchor:
380-
return False
381-
globber = PathGlobber(sep, case_sensitive)
382-
for path_part, pattern_part in zip(path_parts, pattern_parts):
383-
match = globber.compile(pattern_part)
384-
if match(path_part) is None:
385-
return False
386-
return True
387-
388361
def full_match(self, pattern, *, case_sensitive=None):
389362
"""
390363
Return True if this path matches the given glob-style pattern. The

Lib/pathlib/_local.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,32 @@ def full_match(self, pattern, *, case_sensitive=None):
668668
globber = _StringGlobber(self.parser.sep, case_sensitive, recursive=True)
669669
return globber.compile(pattern)(path) is not None
670670

671+
def match(self, path_pattern, *, case_sensitive=None):
672+
"""
673+
Return True if this path matches the given pattern. If the pattern is
674+
relative, matching is done from the right; otherwise, the entire path
675+
is matched. The recursive wildcard '**' is *not* supported by this
676+
method.
677+
"""
678+
if not isinstance(path_pattern, PurePath):
679+
path_pattern = self.with_segments(path_pattern)
680+
if case_sensitive is None:
681+
case_sensitive = self.parser is posixpath
682+
path_parts = self.parts[::-1]
683+
pattern_parts = path_pattern.parts[::-1]
684+
if not pattern_parts:
685+
raise ValueError("empty pattern")
686+
if len(path_parts) < len(pattern_parts):
687+
return False
688+
if len(path_parts) > len(pattern_parts) and path_pattern.anchor:
689+
return False
690+
globber = _StringGlobber(self.parser.sep, case_sensitive)
691+
for path_part, pattern_part in zip(path_parts, pattern_parts):
692+
match = globber.compile(pattern_part)
693+
if match(path_part) is None:
694+
return False
695+
return True
696+
671697
# Subclassing os.PathLike makes isinstance() checks slower,
672698
# which in turn makes Path construction slower. Register instead!
673699
os.PathLike.register(PurePath)

Lib/test/test_pathlib/test_pathlib.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,84 @@ def test_match_empty(self):
438438
self.assertRaises(ValueError, P('a').match, '')
439439
self.assertRaises(ValueError, P('a').match, '.')
440440

441+
def test_match_common(self):
442+
P = self.cls
443+
# Simple relative pattern.
444+
self.assertTrue(P('b.py').match('b.py'))
445+
self.assertTrue(P('a/b.py').match('b.py'))
446+
self.assertTrue(P('/a/b.py').match('b.py'))
447+
self.assertFalse(P('a.py').match('b.py'))
448+
self.assertFalse(P('b/py').match('b.py'))
449+
self.assertFalse(P('/a.py').match('b.py'))
450+
self.assertFalse(P('b.py/c').match('b.py'))
451+
# Wildcard relative pattern.
452+
self.assertTrue(P('b.py').match('*.py'))
453+
self.assertTrue(P('a/b.py').match('*.py'))
454+
self.assertTrue(P('/a/b.py').match('*.py'))
455+
self.assertFalse(P('b.pyc').match('*.py'))
456+
self.assertFalse(P('b./py').match('*.py'))
457+
self.assertFalse(P('b.py/c').match('*.py'))
458+
# Multi-part relative pattern.
459+
self.assertTrue(P('ab/c.py').match('a*/*.py'))
460+
self.assertTrue(P('/d/ab/c.py').match('a*/*.py'))
461+
self.assertFalse(P('a.py').match('a*/*.py'))
462+
self.assertFalse(P('/dab/c.py').match('a*/*.py'))
463+
self.assertFalse(P('ab/c.py/d').match('a*/*.py'))
464+
# Absolute pattern.
465+
self.assertTrue(P('/b.py').match('/*.py'))
466+
self.assertFalse(P('b.py').match('/*.py'))
467+
self.assertFalse(P('a/b.py').match('/*.py'))
468+
self.assertFalse(P('/a/b.py').match('/*.py'))
469+
# Multi-part absolute pattern.
470+
self.assertTrue(P('/a/b.py').match('/a/*.py'))
471+
self.assertFalse(P('/ab.py').match('/a/*.py'))
472+
self.assertFalse(P('/a/b/c.py').match('/a/*.py'))
473+
# Multi-part glob-style pattern.
474+
self.assertFalse(P('/a/b/c.py').match('/**/*.py'))
475+
self.assertTrue(P('/a/b/c.py').match('/a/**/*.py'))
476+
# Case-sensitive flag
477+
self.assertFalse(P('A.py').match('a.PY', case_sensitive=True))
478+
self.assertTrue(P('A.py').match('a.PY', case_sensitive=False))
479+
self.assertFalse(P('c:/a/B.Py').match('C:/A/*.pY', case_sensitive=True))
480+
self.assertTrue(P('/a/b/c.py').match('/A/*/*.Py', case_sensitive=False))
481+
# Matching against empty path
482+
self.assertFalse(P('').match('*'))
483+
self.assertFalse(P('').match('**'))
484+
self.assertFalse(P('').match('**/*'))
485+
486+
@needs_posix
487+
def test_match_posix(self):
488+
P = self.cls
489+
self.assertFalse(P('A.py').match('a.PY'))
490+
491+
@needs_windows
492+
def test_match_windows(self):
493+
P = self.cls
494+
# Absolute patterns.
495+
self.assertTrue(P('c:/b.py').match('*:/*.py'))
496+
self.assertTrue(P('c:/b.py').match('c:/*.py'))
497+
self.assertFalse(P('d:/b.py').match('c:/*.py')) # wrong drive
498+
self.assertFalse(P('b.py').match('/*.py'))
499+
self.assertFalse(P('b.py').match('c:*.py'))
500+
self.assertFalse(P('b.py').match('c:/*.py'))
501+
self.assertFalse(P('c:b.py').match('/*.py'))
502+
self.assertFalse(P('c:b.py').match('c:/*.py'))
503+
self.assertFalse(P('/b.py').match('c:*.py'))
504+
self.assertFalse(P('/b.py').match('c:/*.py'))
505+
# UNC patterns.
506+
self.assertTrue(P('//some/share/a.py').match('//*/*/*.py'))
507+
self.assertTrue(P('//some/share/a.py').match('//some/share/*.py'))
508+
self.assertFalse(P('//other/share/a.py').match('//some/share/*.py'))
509+
self.assertFalse(P('//some/share/a/b.py').match('//some/share/*.py'))
510+
# Case-insensitivity.
511+
self.assertTrue(P('B.py').match('b.PY'))
512+
self.assertTrue(P('c:/a/B.Py').match('C:/A/*.pY'))
513+
self.assertTrue(P('//Some/Share/B.Py').match('//somE/sharE/*.pY'))
514+
# Path anchor doesn't match pattern anchor
515+
self.assertFalse(P('c:/b.py').match('/*.py')) # 'c:/' vs '/'
516+
self.assertFalse(P('c:/b.py').match('c:*.py')) # 'c:/' vs 'c:'
517+
self.assertFalse(P('//some/share/a.py').match('/*.py')) # '//some/share/' vs '/'
518+
441519
@needs_posix
442520
def test_parse_path_posix(self):
443521
check = self._check_parse_path

Lib/test/test_pathlib/test_pathlib_abc.py

Lines changed: 0 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -296,88 +296,6 @@ def test_str_windows(self):
296296
p = self.cls('//a/b/c/d')
297297
self.assertEqual(str(p), '\\\\a\\b\\c\\d')
298298

299-
def test_match_empty(self):
300-
P = self.cls
301-
self.assertRaises(ValueError, P('a').match, '')
302-
303-
def test_match_common(self):
304-
P = self.cls
305-
# Simple relative pattern.
306-
self.assertTrue(P('b.py').match('b.py'))
307-
self.assertTrue(P('a/b.py').match('b.py'))
308-
self.assertTrue(P('/a/b.py').match('b.py'))
309-
self.assertFalse(P('a.py').match('b.py'))
310-
self.assertFalse(P('b/py').match('b.py'))
311-
self.assertFalse(P('/a.py').match('b.py'))
312-
self.assertFalse(P('b.py/c').match('b.py'))
313-
# Wildcard relative pattern.
314-
self.assertTrue(P('b.py').match('*.py'))
315-
self.assertTrue(P('a/b.py').match('*.py'))
316-
self.assertTrue(P('/a/b.py').match('*.py'))
317-
self.assertFalse(P('b.pyc').match('*.py'))
318-
self.assertFalse(P('b./py').match('*.py'))
319-
self.assertFalse(P('b.py/c').match('*.py'))
320-
# Multi-part relative pattern.
321-
self.assertTrue(P('ab/c.py').match('a*/*.py'))
322-
self.assertTrue(P('/d/ab/c.py').match('a*/*.py'))
323-
self.assertFalse(P('a.py').match('a*/*.py'))
324-
self.assertFalse(P('/dab/c.py').match('a*/*.py'))
325-
self.assertFalse(P('ab/c.py/d').match('a*/*.py'))
326-
# Absolute pattern.
327-
self.assertTrue(P('/b.py').match('/*.py'))
328-
self.assertFalse(P('b.py').match('/*.py'))
329-
self.assertFalse(P('a/b.py').match('/*.py'))
330-
self.assertFalse(P('/a/b.py').match('/*.py'))
331-
# Multi-part absolute pattern.
332-
self.assertTrue(P('/a/b.py').match('/a/*.py'))
333-
self.assertFalse(P('/ab.py').match('/a/*.py'))
334-
self.assertFalse(P('/a/b/c.py').match('/a/*.py'))
335-
# Multi-part glob-style pattern.
336-
self.assertFalse(P('/a/b/c.py').match('/**/*.py'))
337-
self.assertTrue(P('/a/b/c.py').match('/a/**/*.py'))
338-
# Case-sensitive flag
339-
self.assertFalse(P('A.py').match('a.PY', case_sensitive=True))
340-
self.assertTrue(P('A.py').match('a.PY', case_sensitive=False))
341-
self.assertFalse(P('c:/a/B.Py').match('C:/A/*.pY', case_sensitive=True))
342-
self.assertTrue(P('/a/b/c.py').match('/A/*/*.Py', case_sensitive=False))
343-
# Matching against empty path
344-
self.assertFalse(P('').match('*'))
345-
self.assertFalse(P('').match('**'))
346-
self.assertFalse(P('').match('**/*'))
347-
348-
@needs_posix
349-
def test_match_posix(self):
350-
P = self.cls
351-
self.assertFalse(P('A.py').match('a.PY'))
352-
353-
@needs_windows
354-
def test_match_windows(self):
355-
P = self.cls
356-
# Absolute patterns.
357-
self.assertTrue(P('c:/b.py').match('*:/*.py'))
358-
self.assertTrue(P('c:/b.py').match('c:/*.py'))
359-
self.assertFalse(P('d:/b.py').match('c:/*.py')) # wrong drive
360-
self.assertFalse(P('b.py').match('/*.py'))
361-
self.assertFalse(P('b.py').match('c:*.py'))
362-
self.assertFalse(P('b.py').match('c:/*.py'))
363-
self.assertFalse(P('c:b.py').match('/*.py'))
364-
self.assertFalse(P('c:b.py').match('c:/*.py'))
365-
self.assertFalse(P('/b.py').match('c:*.py'))
366-
self.assertFalse(P('/b.py').match('c:/*.py'))
367-
# UNC patterns.
368-
self.assertTrue(P('//some/share/a.py').match('//*/*/*.py'))
369-
self.assertTrue(P('//some/share/a.py').match('//some/share/*.py'))
370-
self.assertFalse(P('//other/share/a.py').match('//some/share/*.py'))
371-
self.assertFalse(P('//some/share/a/b.py').match('//some/share/*.py'))
372-
# Case-insensitivity.
373-
self.assertTrue(P('B.py').match('b.PY'))
374-
self.assertTrue(P('c:/a/B.Py').match('C:/A/*.pY'))
375-
self.assertTrue(P('//Some/Share/B.Py').match('//somE/sharE/*.pY'))
376-
# Path anchor doesn't match pattern anchor
377-
self.assertFalse(P('c:/b.py').match('/*.py')) # 'c:/' vs '/'
378-
self.assertFalse(P('c:/b.py').match('c:*.py')) # 'c:/' vs 'c:'
379-
self.assertFalse(P('//some/share/a.py').match('/*.py')) # '//some/share/' vs '/'
380-
381299
def test_full_match_common(self):
382300
P = self.cls
383301
# Simple relative pattern.

0 commit comments

Comments
 (0)