Skip to content
Draft
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
4 changes: 4 additions & 0 deletions README-dist.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ accepts ``(?u)``: both match ``\w``, ``\d``, ``\s``, and ``\b`` as ASCII
regardless, which is the same difference the native backends already have for
the patterns they generate themselves.

Precompiled expressions from other regex libraries keep their original engine
when using "best" or "simple". Explicit native backends reject these expressions
because their compile options cannot be preserved when rebuilding the pattern.

See `benchmarks_backends.md`_ for comparisons between native Python regular
expressions and the optional backends.

Expand Down
4 changes: 4 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ backends translate its compile flags to the global inline flags ``(?ims)``. The
flags are only supported by the "simple" backend. The ``re.UNICODE`` flag is
ignored.

Precompiled expressions from other regex libraries keep their original engine
when using "best" or "simple". Explicit native backends reject these expressions
because their compile options cannot be preserved when rebuilding the pattern.

See `benchmarks_backends.md`_ for comparisons between native Python regular
expressions and the optional backends.

Expand Down
2 changes: 1 addition & 1 deletion pathspec/_backends/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def has_unsupported_flags(patterns: Iterable[Pattern]) -> bool:
if not isinstance(regex, re.Pattern):
# The pattern was not compiled by Python's *re* module. Its flags are
# unknown, and only the simple backend can use it faithfully.
continue
return True

if not supports_regex_flags(regex.flags):
return True
Expand Down
68 changes: 67 additions & 1 deletion tests/test_08_regex_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,72 @@ def test_gitignore_directory_precedence(self):
plain_result = PathSpec(patterns, backend=backend).check_file('LOGS/KEEP')
self.assertEqual((plain_result.include, plain_result.index), (False, 1))

def test_foreign_regex_falls_back(self):
"""
Foreign compiled expressions keep the behavior of their original engine.
"""
class ForeignRegex:
pattern = 'secret'

def match(self, file):
return re.match(self.pattern, file, re.IGNORECASE)

def search(self, file):
return re.search(self.pattern, file, re.IGNORECASE)

for spec_class in (PathSpec, GitIgnoreSpec):
for backend in (None, 'best', 'simple'):
with self.subTest(spec_class=spec_class, backend=backend):
patterns = [RegexPattern(ForeignRegex(), include=True)]
spec = spec_class(patterns, backend=backend)
self.assertTrue(spec.match_file('SECRET'))
self.assertFalse(spec.match_file('public'))
self.assertTrue(type(spec._backend).__name__.startswith('Simple'))

for backend in ('re2', 'hyperscan'):
with self.subTest(backend=backend):
require_backend(backend)
for spec_class in (PathSpec, GitIgnoreSpec):
with self.subTest(spec_class=spec_class):
with self.assertRaisesRegex(ValueError, "backend='simple'"):
spec_class([RegexPattern(ForeignRegex(), include=True)], backend=backend)

def test_foreign_re2_options(self):
"""
Even an RE2 expression cannot be rebuilt without preserving its options.
"""
require_backend('re2')
import re2

options = re2.Options()
options.case_sensitive = False
pattern = RegexPattern(re2.compile('secret', options=options), include=True)
for spec_class in (PathSpec, GitIgnoreSpec):
with self.subTest(spec_class=spec_class):
self.assertTrue(spec_class([pattern]).match_file('SECRET'))

def test_inactive_foreign_regex_stays_native(self):
"""
A no-op foreign expression must not demote other active patterns.
"""
class ForeignRegex:
def match(self, file):
raise AssertionError('inactive expression must not be used')

patterns = [
RegexPattern(ForeignRegex(), include=None),
RegexPattern(re.compile('secret'), include=True),
]
self.assertFalse(has_unsupported_flags(patterns))
for backend in ('re2', 'hyperscan'):
with self.subTest(backend=backend):
require_backend(backend)
for spec_class in (PathSpec, GitIgnoreSpec):
with self.subTest(spec_class=spec_class):
spec = spec_class(patterns, backend=backend)
result = spec.check_file('secret')
self.assertEqual((result.include, result.index), (True, 1))

def test_translate_regex_flags(self):
"""
Only the flags a native engine understands are prepended.
Expand Down Expand Up @@ -306,7 +372,7 @@ class ForeignPattern(object):

def test_has_unsupported_flags(self):
"""
Null patterns and foreign patterns are not reported as unsupported.
Null patterns do not require preserving compile flags.
"""
cases = [
([], False),
Expand Down