]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
1 | """ |
2 | This module provides an object oriented interface for pattern matching of files. | |
3 | """ | |
4 | ||
5 | from collections.abc import ( | |
6 | Collection as CollectionType) | |
7 | from itertools import ( | |
8 | zip_longest) | |
9 | from typing import ( | |
10 | AnyStr, | |
11 | Callable, | |
12 | Collection, | |
13 | Iterable, | |
14 | Iterator, | |
15 | Optional, | |
16 | Type, | |
17 | TypeVar, | |
18 | Union) | |
19 | ||
20 | from . import util | |
21 | from .pattern import ( | |
22 | Pattern) | |
23 | from .util import ( | |
24 | StrPath, | |
25 | TreeEntry, | |
26 | _filter_patterns, | |
27 | _is_iterable, | |
28 | match_file, | |
29 | normalize_file) | |
30 | ||
31 | Self = TypeVar("Self", bound="PathSpec") | |
32 | """ | |
33 | :class:`PathSpec` self type hint to support Python v<3.11 using PEP 673 | |
34 | recommendation. | |
35 | """ | |
36 | ||
37 | ||
38 | class PathSpec(object): | |
39 | """ | |
40 | The :class:`PathSpec` class is a wrapper around a list of compiled | |
41 | :class:`.Pattern` instances. | |
42 | """ | |
43 | ||
44 | def __init__(self, patterns: Iterable[Pattern]) -> None: | |
45 | """ | |
46 | Initializes the :class:`PathSpec` instance. | |
47 | ||
48 | *patterns* (:class:`~collections.abc.Collection` or :class:`~collections.abc.Iterable`) | |
49 | yields each compiled pattern (:class:`.Pattern`). | |
50 | """ | |
51 | ||
52 | self.patterns = patterns if isinstance(patterns, CollectionType) else list(patterns) | |
53 | """ | |
54 | *patterns* (:class:`~collections.abc.Collection` of :class:`.Pattern`) | |
55 | contains the compiled patterns. | |
56 | """ | |
57 | ||
58 | def __eq__(self, other: object) -> bool: | |
59 | """ | |
60 | Tests the equality of this path-spec with *other* (:class:`PathSpec`) | |
61 | by comparing their :attr:`~PathSpec.patterns` attributes. | |
62 | """ | |
63 | if isinstance(other, PathSpec): | |
64 | paired_patterns = zip_longest(self.patterns, other.patterns) | |
65 | return all(a == b for a, b in paired_patterns) | |
66 | else: | |
67 | return NotImplemented | |
68 | ||
69 | def __len__(self) -> int: | |
70 | """ | |
71 | Returns the number of compiled patterns this path-spec contains | |
72 | (:class:`int`). | |
73 | """ | |
74 | return len(self.patterns) | |
75 | ||
76 | def __add__(self: Self, other: "PathSpec") -> Self: | |
77 | """ | |
78 | Combines the :attr:`Pathspec.patterns` patterns from two | |
79 | :class:`PathSpec` instances. | |
80 | """ | |
81 | if isinstance(other, PathSpec): | |
82 | return self.__class__(self.patterns + other.patterns) | |
83 | else: | |
84 | return NotImplemented | |
85 | ||
86 | def __iadd__(self: Self, other: "PathSpec") -> Self: | |
87 | """ | |
88 | Adds the :attr:`Pathspec.patterns` patterns from one :class:`PathSpec` | |
89 | instance to this instance. | |
90 | """ | |
91 | if isinstance(other, PathSpec): | |
92 | self.patterns += other.patterns | |
93 | return self | |
94 | else: | |
95 | return NotImplemented | |
96 | ||
97 | @classmethod | |
98 | def from_lines( | |
99 | cls: Type[Self], | |
100 | pattern_factory: Union[str, Callable[[AnyStr], Pattern]], | |
101 | lines: Iterable[AnyStr], | |
102 | ) -> Self: | |
103 | """ | |
104 | Compiles the pattern lines. | |
105 | ||
106 | *pattern_factory* can be either the name of a registered pattern factory | |
107 | (:class:`str`), or a :class:`~collections.abc.Callable` used to compile | |
108 | patterns. It must accept an uncompiled pattern (:class:`str`) and return the | |
109 | compiled pattern (:class:`.Pattern`). | |
110 | ||
111 | *lines* (:class:`~collections.abc.Iterable`) yields each uncompiled pattern | |
112 | (:class:`str`). This simply has to yield each line so that it can be a | |
113 | :class:`io.TextIOBase` (e.g., from :func:`open` or :class:`io.StringIO`) or | |
114 | the result from :meth:`str.splitlines`. | |
115 | ||
116 | Returns the :class:`PathSpec` instance. | |
117 | """ | |
118 | if isinstance(pattern_factory, str): | |
119 | pattern_factory = util.lookup_pattern(pattern_factory) | |
120 | ||
121 | if not callable(pattern_factory): | |
122 | raise TypeError(f"pattern_factory:{pattern_factory!r} is not callable.") | |
123 | ||
124 | if not _is_iterable(lines): | |
125 | raise TypeError(f"lines:{lines!r} is not an iterable.") | |
126 | ||
127 | patterns = [pattern_factory(line) for line in lines if line] | |
128 | return cls(patterns) | |
129 | ||
130 | def match_entries( | |
131 | self, | |
132 | entries: Iterable[TreeEntry], | |
133 | separators: Optional[Collection[str]] = None, | |
134 | *, | |
135 | negate: Optional[bool] = None, | |
136 | ) -> Iterator[TreeEntry]: | |
137 | """ | |
138 | Matches the entries to this path-spec. | |
139 | ||
140 | *entries* (:class:`~collections.abc.Iterable` of :class:`~util.TreeEntry`) | |
141 | contains the entries to be matched against :attr:`self.patterns <PathSpec.patterns>`. | |
142 | ||
143 | *separators* (:class:`~collections.abc.Collection` of :class:`str`; or | |
144 | :data:`None`) optionally contains the path separators to normalize. See | |
145 | :func:`~pathspec.util.normalize_file` for more information. | |
146 | ||
147 | *negate* (:class:`bool` or :data:`None`) is whether to negate the match | |
148 | results of the patterns. If :data:`True`, a pattern matching a file will | |
149 | exclude the file rather than include it. Default is :data:`None` for | |
150 | :data:`False`. | |
151 | ||
152 | Returns the matched entries (:class:`~collections.abc.Iterator` of | |
153 | :class:`~util.TreeEntry`). | |
154 | """ | |
155 | if not _is_iterable(entries): | |
156 | raise TypeError(f"entries:{entries!r} is not an iterable.") | |
157 | ||
158 | use_patterns = _filter_patterns(self.patterns) | |
159 | for entry in entries: | |
160 | norm_file = normalize_file(entry.path, separators) | |
161 | is_match = self._match_file(use_patterns, norm_file) | |
162 | ||
163 | if negate: | |
164 | is_match = not is_match | |
165 | ||
166 | if is_match: | |
167 | yield entry | |
168 | ||
169 | # Match files using the `match_file()` utility function. Subclasses may | |
170 | # override this method as an instance method. It does not have to be a static | |
171 | # method. | |
172 | _match_file = staticmethod(match_file) | |
173 | ||
174 | def match_file( | |
175 | self, | |
176 | file: StrPath, | |
177 | separators: Optional[Collection[str]] = None, | |
178 | ) -> bool: | |
179 | """ | |
180 | Matches the file to this path-spec. | |
181 | ||
182 | *file* (:class:`str` or :class:`os.PathLike[str]`) is the file path to be | |
183 | matched against :attr:`self.patterns <PathSpec.patterns>`. | |
184 | ||
185 | *separators* (:class:`~collections.abc.Collection` of :class:`str`) | |
186 | optionally contains the path separators to normalize. See | |
187 | :func:`~pathspec.util.normalize_file` for more information. | |
188 | ||
189 | Returns :data:`True` if *file* matched; otherwise, :data:`False`. | |
190 | """ | |
191 | norm_file = util.normalize_file(file, separators=separators) | |
192 | return self._match_file(self.patterns, norm_file) | |
193 | ||
194 | def match_files( | |
195 | self, | |
196 | files: Iterable[StrPath], | |
197 | separators: Optional[Collection[str]] = None, | |
198 | *, | |
199 | negate: Optional[bool] = None, | |
200 | ) -> Iterator[StrPath]: | |
201 | """ | |
202 | Matches the files to this path-spec. | |
203 | ||
204 | *files* (:class:`~collections.abc.Iterable` of :class:`str` or | |
205 | :class:`os.PathLike[str]`) contains the file paths to be matched against | |
206 | :attr:`self.patterns <PathSpec.patterns>`. | |
207 | ||
208 | *separators* (:class:`~collections.abc.Collection` of :class:`str`; or | |
209 | :data:`None`) optionally contains the path separators to normalize. See | |
210 | :func:`~pathspec.util.normalize_file` for more information. | |
211 | ||
212 | *negate* (:class:`bool` or :data:`None`) is whether to negate the match | |
213 | results of the patterns. If :data:`True`, a pattern matching a file will | |
214 | exclude the file rather than include it. Default is :data:`None` for | |
215 | :data:`False`. | |
216 | ||
217 | Returns the matched files (:class:`~collections.abc.Iterator` of | |
218 | :class:`str` or :class:`os.PathLike[str]`). | |
219 | """ | |
220 | if not _is_iterable(files): | |
221 | raise TypeError(f"files:{files!r} is not an iterable.") | |
222 | ||
223 | use_patterns = _filter_patterns(self.patterns) | |
224 | for orig_file in files: | |
225 | norm_file = normalize_file(orig_file, separators) | |
226 | is_match = self._match_file(use_patterns, norm_file) | |
227 | ||
228 | if negate: | |
229 | is_match = not is_match | |
230 | ||
231 | if is_match: | |
232 | yield orig_file | |
233 | ||
234 | def match_tree_entries( | |
235 | self, | |
236 | root: StrPath, | |
237 | on_error: Optional[Callable] = None, | |
238 | follow_links: Optional[bool] = None, | |
239 | *, | |
240 | negate: Optional[bool] = None, | |
241 | ) -> Iterator[TreeEntry]: | |
242 | """ | |
243 | Walks the specified root path for all files and matches them to this | |
244 | path-spec. | |
245 | ||
246 | *root* (:class:`str` or :class:`os.PathLike[str]`) is the root directory to | |
247 | search. | |
248 | ||
249 | *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally | |
250 | is the error handler for file-system exceptions. See | |
251 | :func:`~pathspec.util.iter_tree_entries` for more information. | |
252 | ||
253 | *follow_links* (:class:`bool` or :data:`None`) optionally is whether to walk | |
254 | symbolic links that resolve to directories. See | |
255 | :func:`~pathspec.util.iter_tree_files` for more information. | |
256 | ||
257 | *negate* (:class:`bool` or :data:`None`) is whether to negate the match | |
258 | results of the patterns. If :data:`True`, a pattern matching a file will | |
259 | exclude the file rather than include it. Default is :data:`None` for | |
260 | :data:`False`. | |
261 | ||
262 | Returns the matched files (:class:`~collections.abc.Iterator` of | |
263 | :class:`.TreeEntry`). | |
264 | """ | |
265 | entries = util.iter_tree_entries(root, on_error=on_error, follow_links=follow_links) | |
266 | yield from self.match_entries(entries, negate=negate) | |
267 | ||
268 | def match_tree_files( | |
269 | self, | |
270 | root: StrPath, | |
271 | on_error: Optional[Callable] = None, | |
272 | follow_links: Optional[bool] = None, | |
273 | *, | |
274 | negate: Optional[bool] = None, | |
275 | ) -> Iterator[str]: | |
276 | """ | |
277 | Walks the specified root path for all files and matches them to this | |
278 | path-spec. | |
279 | ||
280 | *root* (:class:`str` or :class:`os.PathLike[str]`) is the root directory to | |
281 | search for files. | |
282 | ||
283 | *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally | |
284 | is the error handler for file-system exceptions. See | |
285 | :func:`~pathspec.util.iter_tree_files` for more information. | |
286 | ||
287 | *follow_links* (:class:`bool` or :data:`None`) optionally is whether to walk | |
288 | symbolic links that resolve to directories. See | |
289 | :func:`~pathspec.util.iter_tree_files` for more information. | |
290 | ||
291 | *negate* (:class:`bool` or :data:`None`) is whether to negate the match | |
292 | results of the patterns. If :data:`True`, a pattern matching a file will | |
293 | exclude the file rather than include it. Default is :data:`None` for | |
294 | :data:`False`. | |
295 | ||
296 | Returns the matched files (:class:`~collections.abc.Iterable` of | |
297 | :class:`str`). | |
298 | """ | |
299 | files = util.iter_tree_files(root, on_error=on_error, follow_links=follow_links) | |
300 | yield from self.match_files(files, negate=negate) | |
301 | ||
302 | # Alias `match_tree_files()` as `match_tree()` for backward compatibility | |
303 | # before v0.3.2. | |
304 | match_tree = match_tree_files |