Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions pathspec/_backends/agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,12 @@ def make_pathspec_backend(
"""
if name == 'best':
name = _BEST_BACKEND
if name != 'simple' and has_unsupported_flags(patterns):
# Only the simple backend can keep the compile flags of the patterns it
# was given.
if name != 'simple' and (
any(not isinstance(pattern, RegexPattern) for pattern in patterns)
or has_unsupported_flags(patterns)
):
# Only the simple backend supports custom patterns and compile flags that
# cannot be translated.
name = 'simple'

if name == 'hyperscan':
Expand Down
2 changes: 2 additions & 0 deletions pathspec/pathspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ def __init__(
matching backend to use. Default is :data:`None` for "best" to use the best
available backend. Priority of backends is: "re2", "hyperscan", "simple".
The "simple" backend is always available.
Automatic selection uses "simple" when any pattern is not a
:class:`.RegexPattern`. Native backends only support regular expressions.
"""
if isinstance(patterns, Sequence):
use_patterns = patterns
Expand Down
56 changes: 56 additions & 0 deletions tests/test_05_pathspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -1410,3 +1410,59 @@ def test_3_character_after_slash(self):
self.assertTrue(spec.match_file(name))
self.assertFalse(spec.match_file('foo/'))
self.assertFalse(spec.match_file('foo/other'))


class SuffixPattern(Pattern):
def match_file(self, file: str) -> Optional[str]:
return file if file.endswith('.secret') else None


class CustomPatternBackendTest(TestCase):
"""
Automatic backend selection must preserve custom matching implementations.
"""

def test_custom_pattern(self):
for preferred in ('simple', 're2', 'hyperscan'):
for backend in (None, 'best'):
with self.subTest(preferred=preferred, backend=backend):
with patch('pathspec._backends.agg._BEST_BACKEND', preferred):
spec = PathSpec([SuffixPattern(True)], backend=backend)
self.assertEqual(list(spec.match_files(['public.txt', 'key.secret'])), ['key.secret'])
self.assertEqual(list(spec.match_files(['public.txt', 'key.secret'], negate=True)), ['public.txt'])
self.assertEqual(spec.check_file('key.secret').index, 0)

def test_custom_pattern_factory(self):
for preferred in ('simple', 're2', 'hyperscan'):
with self.subTest(preferred=preferred):
with patch('pathspec._backends.agg._BEST_BACKEND', preferred):
spec = PathSpec.from_lines(lambda line: SuffixPattern(True), ['secrets'])
self.assertTrue(spec.match_file('key.secret'))
self.assertFalse(spec.match_file('public.txt'))

def test_mixed_patterns(self):
for preferred in ('simple', 're2', 'hyperscan'):
for include in (False, True):
patterns = [
RegexPattern(re.compile('.*'), include=not include),
Pattern(None),
SuffixPattern(include),
]
with self.subTest(preferred=preferred, include=include):
with patch('pathspec._backends.agg._BEST_BACKEND', preferred):
spec = PathSpec(patterns)
result = spec.check_file('key.secret')
self.assertEqual((result.include, result.index), (include, 2))
self.assertEqual(spec.match_file('public.txt'), not include)

def test_add_custom_pattern(self):
spec = PathSpec.from_lines('gitignore', ['*'])
other = PathSpec([SuffixPattern(False)], backend='simple')
combined = spec + other
self.assertTrue(spec.match_file('key.secret'))
self.assertFalse(combined.match_file('key.secret'))
original = spec
spec += other
self.assertIs(spec, original)
self.assertFalse(spec.match_file('key.secret'))
self.assertTrue(spec.match_file('public.txt'))
Loading