GH-73435: Add pathlib.PurePath.full_match() (#114350)

In 49f90ba we added support for the recursive wildcard `**` in
`pathlib.PurePath.match()`. This should allow arbitrary prefix and suffix
matching, like `p.match('foo/**')` or `p.match('**/foo')`, but there's a
problem: for relative patterns only, `match()` implicitly inserts a `**`
token on the left hand side, causing all patterns to match from the right.
As a result, it's impossible to match relative patterns from the left:
`PurePath('foo/bar').match('bar/**')` is true!

This commit reverts the changes to `match()`, and instead adds a new
`full_match()` method that:

- Allows empty patterns
- Supports the recursive wildcard `**`
- Matches the *entire* path when given a relative pattern
This commit is contained in:
Barney Gale 2024-01-26 01:12:46 +00:00 committed by GitHub
parent 841eacd076
commit b69548a0f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 158 additions and 75 deletions

View file

@ -249,29 +249,8 @@ class DummyPurePathTest(unittest.TestCase):
self.assertFalse(P('/ab.py').match('/a/*.py'))
self.assertFalse(P('/a/b/c.py').match('/a/*.py'))
# Multi-part glob-style pattern.
self.assertTrue(P('a').match('**'))
self.assertTrue(P('c.py').match('**'))
self.assertTrue(P('a/b/c.py').match('**'))
self.assertTrue(P('/a/b/c.py').match('**'))
self.assertTrue(P('/a/b/c.py').match('/**'))
self.assertTrue(P('/a/b/c.py').match('/a/**'))
self.assertTrue(P('/a/b/c.py').match('**/*.py'))
self.assertTrue(P('/a/b/c.py').match('/**/*.py'))
self.assertFalse(P('/a/b/c.py').match('/**/*.py'))
self.assertTrue(P('/a/b/c.py').match('/a/**/*.py'))
self.assertTrue(P('/a/b/c.py').match('/a/b/**/*.py'))
self.assertTrue(P('/a/b/c.py').match('/**/**/**/**/*.py'))
self.assertFalse(P('c.py').match('**/a.py'))
self.assertFalse(P('c.py').match('c/**'))
self.assertFalse(P('a/b/c.py').match('**/a'))
self.assertFalse(P('a/b/c.py').match('**/a/b'))
self.assertFalse(P('a/b/c.py').match('**/a/b/c'))
self.assertFalse(P('a/b/c.py').match('**/a/b/c.'))
self.assertFalse(P('a/b/c.py').match('**/a/b/c./**'))
self.assertFalse(P('a/b/c.py').match('**/a/b/c./**'))
self.assertFalse(P('a/b/c.py').match('/a/b/c.py/**'))
self.assertFalse(P('a/b/c.py').match('/**/a/b/c.py'))
self.assertRaises(ValueError, P('a').match, '**a/b/c')
self.assertRaises(ValueError, P('a').match, 'a/b/c**')
# Case-sensitive flag
self.assertFalse(P('A.py').match('a.PY', case_sensitive=True))
self.assertTrue(P('A.py').match('a.PY', case_sensitive=False))
@ -279,9 +258,82 @@ class DummyPurePathTest(unittest.TestCase):
self.assertTrue(P('/a/b/c.py').match('/A/*/*.Py', case_sensitive=False))
# Matching against empty path
self.assertFalse(P('').match('*'))
self.assertTrue(P('').match('**'))
self.assertFalse(P('').match('**'))
self.assertFalse(P('').match('**/*'))
def test_full_match_common(self):
P = self.cls
# Simple relative pattern.
self.assertTrue(P('b.py').full_match('b.py'))
self.assertFalse(P('a/b.py').full_match('b.py'))
self.assertFalse(P('/a/b.py').full_match('b.py'))
self.assertFalse(P('a.py').full_match('b.py'))
self.assertFalse(P('b/py').full_match('b.py'))
self.assertFalse(P('/a.py').full_match('b.py'))
self.assertFalse(P('b.py/c').full_match('b.py'))
# Wildcard relative pattern.
self.assertTrue(P('b.py').full_match('*.py'))
self.assertFalse(P('a/b.py').full_match('*.py'))
self.assertFalse(P('/a/b.py').full_match('*.py'))
self.assertFalse(P('b.pyc').full_match('*.py'))
self.assertFalse(P('b./py').full_match('*.py'))
self.assertFalse(P('b.py/c').full_match('*.py'))
# Multi-part relative pattern.
self.assertTrue(P('ab/c.py').full_match('a*/*.py'))
self.assertFalse(P('/d/ab/c.py').full_match('a*/*.py'))
self.assertFalse(P('a.py').full_match('a*/*.py'))
self.assertFalse(P('/dab/c.py').full_match('a*/*.py'))
self.assertFalse(P('ab/c.py/d').full_match('a*/*.py'))
# Absolute pattern.
self.assertTrue(P('/b.py').full_match('/*.py'))
self.assertFalse(P('b.py').full_match('/*.py'))
self.assertFalse(P('a/b.py').full_match('/*.py'))
self.assertFalse(P('/a/b.py').full_match('/*.py'))
# Multi-part absolute pattern.
self.assertTrue(P('/a/b.py').full_match('/a/*.py'))
self.assertFalse(P('/ab.py').full_match('/a/*.py'))
self.assertFalse(P('/a/b/c.py').full_match('/a/*.py'))
# Multi-part glob-style pattern.
self.assertTrue(P('a').full_match('**'))
self.assertTrue(P('c.py').full_match('**'))
self.assertTrue(P('a/b/c.py').full_match('**'))
self.assertTrue(P('/a/b/c.py').full_match('**'))
self.assertTrue(P('/a/b/c.py').full_match('/**'))
self.assertTrue(P('/a/b/c.py').full_match('/a/**'))
self.assertTrue(P('/a/b/c.py').full_match('**/*.py'))
self.assertTrue(P('/a/b/c.py').full_match('/**/*.py'))
self.assertTrue(P('/a/b/c.py').full_match('/a/**/*.py'))
self.assertTrue(P('/a/b/c.py').full_match('/a/b/**/*.py'))
self.assertTrue(P('/a/b/c.py').full_match('/**/**/**/**/*.py'))
self.assertFalse(P('c.py').full_match('**/a.py'))
self.assertFalse(P('c.py').full_match('c/**'))
self.assertFalse(P('a/b/c.py').full_match('**/a'))
self.assertFalse(P('a/b/c.py').full_match('**/a/b'))
self.assertFalse(P('a/b/c.py').full_match('**/a/b/c'))
self.assertFalse(P('a/b/c.py').full_match('**/a/b/c.'))
self.assertFalse(P('a/b/c.py').full_match('**/a/b/c./**'))
self.assertFalse(P('a/b/c.py').full_match('**/a/b/c./**'))
self.assertFalse(P('a/b/c.py').full_match('/a/b/c.py/**'))
self.assertFalse(P('a/b/c.py').full_match('/**/a/b/c.py'))
self.assertRaises(ValueError, P('a').full_match, '**a/b/c')
self.assertRaises(ValueError, P('a').full_match, 'a/b/c**')
# Case-sensitive flag
self.assertFalse(P('A.py').full_match('a.PY', case_sensitive=True))
self.assertTrue(P('A.py').full_match('a.PY', case_sensitive=False))
self.assertFalse(P('c:/a/B.Py').full_match('C:/A/*.pY', case_sensitive=True))
self.assertTrue(P('/a/b/c.py').full_match('/A/*/*.Py', case_sensitive=False))
# Matching against empty path
self.assertFalse(P('').full_match('*'))
self.assertTrue(P('').full_match('**'))
self.assertFalse(P('').full_match('**/*'))
# Matching with empty pattern
self.assertTrue(P('').full_match(''))
self.assertTrue(P('.').full_match('.'))
self.assertFalse(P('/').full_match(''))
self.assertFalse(P('/').full_match('.'))
self.assertFalse(P('foo').full_match(''))
self.assertFalse(P('foo').full_match('.'))
def test_parts_common(self):
# `parts` returns a tuple.
sep = self.sep