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
7 changes: 7 additions & 0 deletions README-dist.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ The "re2" backend uses the `google-re2`_ library (not to be confused with the
be significantly faster than "simple", and 3 times faster than "hyperscan" at
high pattern counts.

When an active ``RegexPattern`` wraps a standard-library ``re.Pattern`` with
compile flags that are not encoded in its expression (for example, ``re.compile('secret',
re.IGNORECASE)``), "best" uses "simple" to preserve those flags. Explicitly
selecting "re2" or "hyperscan" raises ``ValueError`` for these patterns; use
``backend='simple'`` instead. Inline flags remain subject to the selected
backend's regular expression syntax and semantics.

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

Expand Down
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ The "re2" backend uses the `google-re2`_ library (not to be confused with the
be significantly faster than "simple", and 3 times faster than "hyperscan" at
high pattern counts.

When an active ``RegexPattern`` wraps a standard-library ``re.Pattern`` with
compile flags that are not encoded in its expression (for example, ``re.compile('secret',
re.IGNORECASE)``), "best" uses "simple" to preserve those flags. Explicitly
selecting "re2" or "hyperscan" raises ``ValueError`` for these patterns; use
``backend='simple'`` instead. Inline flags remain subject to the selected
backend's regular expression syntax and semantics.

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

Expand Down
42 changes: 41 additions & 1 deletion pathspec/_backends/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,57 @@
contents and structure are likely to change.
"""

import re
import warnings
from collections.abc import (
Iterable)
from typing import (
TypeVar)

from pathspec.pattern import (
Pattern)
Pattern,
RegexPattern)

TPattern = TypeVar("TPattern", bound=Pattern)


def has_regex_flags(patterns: Iterable[Pattern]) -> bool:
"""
Check for active Python regexes with flags not encoded in the expression.

Native backends recompile the expression, losing external compile flags.
Inline flags are already part of the expression and need no translation.
"""
for pattern in patterns:
if pattern.include is None or not isinstance(pattern, RegexPattern):
continue

regex = pattern.regex
if not isinstance(regex, re.Pattern):
continue

# UNICODE is the default for str regexes. Avoid recompiling ordinary
# patterns (including the built-in gitignore patterns).
if not regex.flags & ~int(re.UNICODE):
continue

try:
# This expression is only a probe, not the regex used for matching.
# Do not expose warnings from, e.g., VERBOSE comment contents.
with warnings.catch_warnings():
warnings.simplefilter('ignore', FutureWarning)
inline_flags = re.compile(regex.pattern).flags
except re.error:
# The expression may only be valid with external flags, e.g., a
# VERBOSE comment containing an unmatched bracket.
return True

if regex.flags != inline_flags:
return True

return False


def enumerate_patterns(
patterns: Iterable[TPattern],
filter: bool,
Expand Down
6 changes: 6 additions & 0 deletions pathspec/_backends/agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
Pattern,
RegexPattern)

from ._utils import (
has_regex_flags)
from .hyperscan.base import (
hyperscan_error)
from .hyperscan.gitignore import (
Expand Down Expand Up @@ -65,6 +67,8 @@ def make_gitignore_backend(
"""
if name == 'best':
name = _BEST_BACKEND
if name != 'simple' and has_regex_flags(patterns):
name = 'simple'

if name == 'hyperscan':
return HyperscanGiBackend(cast(Sequence[RegexPattern], patterns))
Expand Down Expand Up @@ -93,6 +97,8 @@ def make_pathspec_backend(
"""
if name == 'best':
name = _BEST_BACKEND
if name != 'simple' and has_regex_flags(patterns):
name = 'simple'

if name == 'hyperscan':
return HyperscanPsBackend(cast(Sequence[RegexPattern], patterns))
Expand Down
6 changes: 5 additions & 1 deletion pathspec/_backends/hyperscan/pathspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
override) # Added in 3.12.

from .._utils import (
enumerate_patterns)
enumerate_patterns,
has_regex_flags)

from .base import (
hyperscan_error)
Expand Down Expand Up @@ -63,6 +64,9 @@ def __init__(
if patterns and not isinstance(patterns[0], RegexPattern):
raise TypeError(f"{patterns[0]=!r} must be a RegexPattern.")

if has_regex_flags(patterns):
raise ValueError("The hyperscan backend cannot preserve regex compile flags. Use backend='simple'.")

use_patterns = enumerate_patterns(
patterns, filter=True, reverse=False,
)
Expand Down
6 changes: 5 additions & 1 deletion pathspec/_backends/re2/pathspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
override) # Added in 3.12.

from .._utils import (
enumerate_patterns)
enumerate_patterns,
has_regex_flags)

from .base import (
re2_error)
Expand Down Expand Up @@ -60,6 +61,9 @@ def __init__(
if patterns and not isinstance(patterns[0], RegexPattern):
raise TypeError(f"{patterns[0]=!r} must be a RegexPattern.")

if has_regex_flags(patterns):
raise ValueError("The re2 backend cannot preserve regex compile flags. Use backend='simple'.")

use_patterns = dict(enumerate_patterns(
patterns, filter=True, reverse=False,
))
Expand Down
12 changes: 9 additions & 3 deletions pathspec/pathspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ def __init__(
*backend* (:class:`str` or :data:`None`) is the pattern (regular expression)
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.
The "simple" backend is always available. "best" falls back to "simple"
for active :class:`re.Pattern` objects with compile flags not encoded in
the expression.
Explicitly selecting a native backend raises :exc:`ValueError` for those
patterns. Use ``backend='simple'`` to preserve their compile flags.
"""
if isinstance(patterns, Sequence):
use_patterns = patterns
Expand Down Expand Up @@ -141,8 +145,10 @@ def __iadd__(self: Self, other: PathSpec) -> Self: # type: ignore[misc]
(:class:`PathSpec`) to this instance.
"""
if isinstance(other, PathSpec):
self.patterns = [*self.patterns, *other.patterns]
self._backend = self._make_backend(self._backend_name, self.patterns)
use_patterns = [*self.patterns, *other.patterns]
use_backend = self._make_backend(self._backend_name, use_patterns)
self.patterns = use_patterns
self._backend = use_backend
return self
else:
return NotImplemented
Expand Down
Loading