]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
1 | """ |
2 | This module provides the base definition for patterns. | |
3 | """ | |
4 | ||
5 | import dataclasses | |
6 | import re | |
7 | import warnings | |
8 | from typing import ( | |
9 | Any, | |
10 | AnyStr, | |
11 | Iterable, | |
12 | Iterator, | |
13 | Match as MatchHint, | |
14 | Optional, | |
15 | Pattern as PatternHint, | |
16 | Tuple, | |
17 | Union) | |
18 | ||
19 | ||
20 | class Pattern(object): | |
21 | """ | |
22 | The :class:`Pattern` class is the abstract definition of a pattern. | |
23 | """ | |
24 | ||
25 | # Make the class dict-less. | |
26 | __slots__ = ('include',) | |
27 | ||
28 | def __init__(self, include: Optional[bool]) -> None: | |
29 | """ | |
30 | Initializes the :class:`Pattern` instance. | |
31 | ||
32 | *include* (:class:`bool` or :data:`None`) is whether the matched | |
33 | files should be included (:data:`True`), excluded (:data:`False`), | |
34 | or is a null-operation (:data:`None`). | |
35 | """ | |
36 | ||
37 | self.include = include | |
38 | """ | |
39 | *include* (:class:`bool` or :data:`None`) is whether the matched | |
40 | files should be included (:data:`True`), excluded (:data:`False`), | |
41 | or is a null-operation (:data:`None`). | |
42 | """ | |
43 | ||
44 | def match(self, files: Iterable[str]) -> Iterator[str]: | |
45 | """ | |
46 | DEPRECATED: This method is no longer used and has been replaced by | |
47 | :meth:`.match_file`. Use the :meth:`.match_file` method with a loop | |
48 | for similar results. | |
49 | ||
50 | Matches this pattern against the specified files. | |
51 | ||
52 | *files* (:class:`~collections.abc.Iterable` of :class:`str`) | |
53 | contains each file relative to the root directory (e.g., | |
54 | :data:`"relative/path/to/file"`). | |
55 | ||
56 | Returns an :class:`~collections.abc.Iterable` yielding each matched | |
57 | file path (:class:`str`). | |
58 | """ | |
59 | warnings.warn(( | |
60 | "{0.__module__}.{0.__qualname__}.match() is deprecated. Use " | |
61 | "{0.__module__}.{0.__qualname__}.match_file() with a loop for " | |
62 | "similar results." | |
63 | ).format(self.__class__), DeprecationWarning, stacklevel=2) | |
64 | ||
65 | for file in files: | |
66 | if self.match_file(file) is not None: | |
67 | yield file | |
68 | ||
69 | def match_file(self, file: str) -> Optional[Any]: | |
70 | """ | |
71 | Matches this pattern against the specified file. | |
72 | ||
73 | *file* (:class:`str`) is the normalized file path to match against. | |
74 | ||
75 | Returns the match result if *file* matched; otherwise, :data:`None`. | |
76 | """ | |
77 | raise NotImplementedError(( | |
78 | "{0.__module__}.{0.__qualname__} must override match_file()." | |
79 | ).format(self.__class__)) | |
80 | ||
81 | ||
82 | class RegexPattern(Pattern): | |
83 | """ | |
84 | The :class:`RegexPattern` class is an implementation of a pattern | |
85 | using regular expressions. | |
86 | """ | |
87 | ||
88 | # Keep the class dict-less. | |
89 | __slots__ = ('regex',) | |
90 | ||
91 | def __init__( | |
92 | self, | |
93 | pattern: Union[AnyStr, PatternHint], | |
94 | include: Optional[bool] = None, | |
95 | ) -> None: | |
96 | """ | |
97 | Initializes the :class:`RegexPattern` instance. | |
98 | ||
99 | *pattern* (:class:`str`, :class:`bytes`, :class:`re.Pattern`, or | |
100 | :data:`None`) is the pattern to compile into a regular expression. | |
101 | ||
102 | *include* (:class:`bool` or :data:`None`) must be :data:`None` | |
103 | unless *pattern* is a precompiled regular expression (:class:`re.Pattern`) | |
104 | in which case it is whether matched files should be included | |
105 | (:data:`True`), excluded (:data:`False`), or is a null operation | |
106 | (:data:`None`). | |
107 | ||
108 | .. NOTE:: Subclasses do not need to support the *include* | |
109 | parameter. | |
110 | """ | |
111 | ||
112 | if isinstance(pattern, (str, bytes)): | |
113 | assert include is None, ( | |
114 | "include:{!r} must be null when pattern:{!r} is a string." | |
115 | ).format(include, pattern) | |
116 | regex, include = self.pattern_to_regex(pattern) | |
117 | # NOTE: Make sure to allow a null regular expression to be | |
118 | # returned for a null-operation. | |
119 | if include is not None: | |
120 | regex = re.compile(regex) | |
121 | ||
122 | elif pattern is not None and hasattr(pattern, 'match'): | |
123 | # Assume pattern is a precompiled regular expression. | |
124 | # - NOTE: Used specified *include*. | |
125 | regex = pattern | |
126 | ||
127 | elif pattern is None: | |
128 | # NOTE: Make sure to allow a null pattern to be passed for a | |
129 | # null-operation. | |
130 | assert include is None, ( | |
131 | "include:{!r} must be null when pattern:{!r} is null." | |
132 | ).format(include, pattern) | |
133 | ||
134 | else: | |
135 | raise TypeError("pattern:{!r} is not a string, re.Pattern, or None.".format(pattern)) | |
136 | ||
137 | super(RegexPattern, self).__init__(include) | |
138 | ||
139 | self.regex: PatternHint = regex | |
140 | """ | |
141 | *regex* (:class:`re.Pattern`) is the regular expression for the | |
142 | pattern. | |
143 | """ | |
144 | ||
145 | def __eq__(self, other: 'RegexPattern') -> bool: | |
146 | """ | |
147 | Tests the equality of this regex pattern with *other* (:class:`RegexPattern`) | |
148 | by comparing their :attr:`~Pattern.include` and :attr:`~RegexPattern.regex` | |
149 | attributes. | |
150 | """ | |
151 | if isinstance(other, RegexPattern): | |
152 | return self.include == other.include and self.regex == other.regex | |
153 | else: | |
154 | return NotImplemented | |
155 | ||
156 | def match_file(self, file: str) -> Optional['RegexMatchResult']: | |
157 | """ | |
158 | Matches this pattern against the specified file. | |
159 | ||
160 | *file* (:class:`str`) | |
161 | contains each file relative to the root directory (e.g., "relative/path/to/file"). | |
162 | ||
163 | Returns the match result (:class:`RegexMatchResult`) if *file* | |
164 | matched; otherwise, :data:`None`. | |
165 | """ | |
166 | if self.include is not None: | |
167 | match = self.regex.match(file) | |
168 | if match is not None: | |
169 | return RegexMatchResult(match) | |
170 | ||
171 | return None | |
172 | ||
173 | @classmethod | |
174 | def pattern_to_regex(cls, pattern: str) -> Tuple[str, bool]: | |
175 | """ | |
176 | Convert the pattern into an uncompiled regular expression. | |
177 | ||
178 | *pattern* (:class:`str`) is the pattern to convert into a regular | |
179 | expression. | |
180 | ||
181 | Returns the uncompiled regular expression (:class:`str` or :data:`None`), | |
182 | and whether matched files should be included (:data:`True`), | |
183 | excluded (:data:`False`), or is a null-operation (:data:`None`). | |
184 | ||
185 | .. NOTE:: The default implementation simply returns *pattern* and | |
186 | :data:`True`. | |
187 | """ | |
188 | return pattern, True | |
189 | ||
190 | ||
191 | @dataclasses.dataclass() | |
192 | class RegexMatchResult(object): | |
193 | """ | |
194 | The :class:`RegexMatchResult` data class is used to return information | |
195 | about the matched regular expression. | |
196 | """ | |
197 | ||
198 | # Keep the class dict-less. | |
199 | __slots__ = ( | |
200 | 'match', | |
201 | ) | |
202 | ||
203 | match: MatchHint | |
204 | """ | |
205 | *match* (:class:`re.Match`) is the regex match result. | |
206 | """ |