1"""
2This module provides the :module:`re2` backend for :class:`~pathspec.pathspec.PathSpec`.
3
4WARNING: The *pathspec._backends.re2* package is not part of the public API. Its
5contents and structure are likely to change.
6"""
7from __future__ import annotations
8
9from collections.abc import (
10 Sequence)
11from typing import (
12 Callable, # Replaced by `collections.abc.Callable` in 3.9.2.
13 Optional) # Replaced by `X | None` in 3.10.
14
15try:
16 import re2
17except ModuleNotFoundError:
18 re2 = None # type: ignore[assignment]
19
20from pathspec.backend import (
21 _Backend)
22from pathspec.pattern import (
23 RegexPattern)
24from pathspec._typing import (
25 override) # Added in 3.12.
26
27from .._utils import (
28 enumerate_patterns,
29 get_regex_flags,
30 has_unsupported_flags,
31 translate_end_anchor,
32 translate_regex_flags)
33
34from .base import (
35 re2_error)
36from ._base import (
37 RE2_OPTIONS,
38 Re2RegexDat,
39 Re2RegexDebug)
40
41
42class Re2PsBackend(_Backend):
43 """
44 The :class:`Re2PsBackend` class is the :module:`re2` implementation used by
45 :class:`~pathspec.pathspec.PathSpec` for matching files.
46 """
47
48 def __init__(
49 self,
50 patterns: Sequence[RegexPattern],
51 *,
52 _debug_regex: Optional[bool] = None,
53 _test_sort: Optional[Callable[[list], None]] = None,
54 ) -> None:
55 """
56 Initialize the :class:`Re2PsBackend` instance.
57
58 *patterns* (:class:`Sequence` of :class:`.RegexPattern`) contains the
59 compiled patterns.
60 """
61 if re2_error is not None:
62 raise re2_error
63
64 if patterns and not isinstance(patterns[0], RegexPattern):
65 raise TypeError(f"{patterns[0]=!r} must be a RegexPattern.")
66
67 if has_unsupported_flags(patterns):
68 raise ValueError((
69 "The re2 backend cannot preserve the regex compile flags. "
70 "Use backend='simple'."
71 ))
72
73 use_patterns = dict(enumerate_patterns(
74 patterns, filter=True, reverse=False,
75 ))
76 regex_set = self._make_set()
77
78 self._debug_regex = bool(_debug_regex)
79 """
80 *_debug_regex* (:class:`bool`) is whether to include additional debugging
81 information for the regular expressions.
82 """
83
84 self._patterns: dict[int, RegexPattern] = use_patterns
85 """
86 *_patterns* (:class:`dict`) maps pattern index (:class:`int`) to pattern
87 (:class:`RegexPattern`).
88 """
89
90 self._regex_data: list[Re2RegexDat] = self._init_set(
91 debug=self._debug_regex,
92 patterns=use_patterns,
93 regex_set=regex_set,
94 sort_indices=_test_sort,
95 )
96 """
97 *_regex_data* (:class:`list`) maps regex index (:class:`int`) to regex data
98 (:class:`Re2RegexDat`).
99 """
100
101 self._set: re2.Set = regex_set # type: ignore
102 """
103 *_set* (:class:`re2.Set`) is the re2 regex set.
104 """
105
106 @staticmethod
107 def _init_set(
108 debug: bool,
109 patterns: dict[int, RegexPattern],
110 regex_set: re2.Set, # type: ignore
111 sort_indices: Optional[Callable[[list[int]], None]],
112 ) -> list[Re2RegexDat]:
113 """
114 Create the re2 regex set.
115
116 *debug* (:class:`bool`) is whether to include additional debugging
117 information for the regular expressions.
118
119 *patterns* (:class:`dict`) maps pattern index (:class:`int`) to pattern
120 (:class:`.RegexPattern`).
121
122 *regex_set* (:class:`re2.Set`) is the regex set.
123
124 *sort_indices* (:class:`callable` or :data:`None`) is a function used to
125 sort the patterns by index. This is used during testing to ensure the order
126 of patterns is not accidentally relied on.
127
128 Returns a :class:`list` indexed by regex id (:class:`int`) to its data
129 (:class:`Re2RegexDat`).
130 """
131 # Sort patterns.
132 indices = list(patterns.keys())
133 if sort_indices is not None:
134 sort_indices(indices)
135
136 # Prepare patterns.
137 regex_data: list[Re2RegexDat] = []
138 for pattern_index in indices:
139 pattern = patterns[pattern_index]
140 pattern_include = pattern.include
141 if pattern_include is None:
142 continue
143
144 assert pattern.regex is not None, pattern
145 assert isinstance(pattern, RegexPattern), pattern
146 regex = translate_regex_flags(
147 translate_end_anchor(pattern.regex.pattern),
148 get_regex_flags(pattern.regex),
149 )
150
151 if debug:
152 regex_data.append(Re2RegexDebug(
153 include=pattern_include,
154 index=pattern_index,
155 is_dir_pattern=False,
156 regex=regex,
157 ))
158 else:
159 regex_data.append(Re2RegexDat(
160 include=pattern_include,
161 index=pattern_index,
162 is_dir_pattern=False,
163 ))
164
165 regex_set.Add(regex)
166
167 # Compile patterns.
168 regex_set.Compile()
169 return regex_data
170
171 @staticmethod
172 def _make_set() -> re2.Set: # type: ignore
173 """
174 Create the re2 regex set.
175
176 Returns the set (:class:`re2.Set`).
177 """
178 assert re2 is not None, (re2, re2_error)
179 return re2.Set.SearchSet(RE2_OPTIONS)
180
181 @override
182 def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]:
183 """
184 Check the file against the patterns.
185
186 *file* (:class:`str`) is the normalized file path to check.
187
188 Returns a :class:`tuple` containing whether to include *file* (:class:`bool`
189 or :data:`None`), and the index of the last matched pattern (:class:`int` or
190 :data:`None`).
191 """
192 # Find best match.
193 # - WARNING: According to the documentation on `RE2::Set::Match()`, there is
194 # no guarantee matches will be produced in order! Later expressions have
195 # higher priority.
196 match_ids: Optional[list[int]] = self._set.Match(file) # type: ignore[assignment]
197 if not match_ids:
198 return (None, None)
199
200 regex_data = self._regex_data
201 pattern_index = max(regex_data[__id].index for __id in match_ids)
202 pattern = self._patterns[pattern_index]
203 return (pattern.include, pattern_index)