Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pathspec/_backends/_utils.py: 52%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

85 statements  

1""" 

2This module provides private utility functions for backends. 

3 

4WARNING: The *pathspec._backends* package is not part of the public API. Its 

5contents and structure are likely to change. 

6""" 

7 

8import operator 

9import re 

10from collections.abc import ( 

11 Iterable) 

12from functools import ( 

13 reduce) 

14from typing import ( 

15 Union, # Replaced by `X | Y` in 3.10. 

16 TypeVar, 

17 overload) 

18 

19from pathspec.pattern import ( 

20 Pattern, 

21 RegexPattern) 

22 

23_END_ANCHOR_BYTES = re.compile(rb'\\.', re.DOTALL) 

24""" 

25Regular expression to match an escaped character. 

26""" 

27 

28_END_ANCHOR_STR = re.compile(r'\\.', re.DOTALL) 

29""" 

30Regular expression to match an escaped character. 

31""" 

32 

33_ASCII_FLAGS = int(re.ASCII) 

34""" 

35The compile flags that both engines satisfy by default because their character 

36classes and word boundaries are ASCII-only. 

37 

38`re.ASCII` cannot be relied upon this way when it is combined with 

39`re.IGNORECASE`: the engines fold the case of Unicode characters that Python 

40would leave alone. 

41""" 

42 

43_NOOP_FLAGS = int(re.UNICODE | re.DEBUG) 

44r""" 

45The compile flags that need no translation. 

46 

47- `re.UNICODE` is implied for str patterns, and both engines match `\w`, `\d`, 

48 `\s`, and `\b` as ASCII regardless. 

49 

50- `re.DEBUG` only affects how Python compiles the expression. 

51""" 

52 

53_TRANSLATABLE_FLAGS = [ 

54 (int(re.IGNORECASE), 'i'), 

55 (int(re.MULTILINE), 'm'), 

56 (int(re.DOTALL), 's'), 

57] 

58""" 

59The compile flags that both RE2 and Hyperscan understand as global inline flags 

60at the start of an expression (`(?ims)`). 

61 

62RE2 rejects `(?x)` (it has no verbose mode), and both engines reject `(?a)`, 

63`(?u)`, `(?U)`, and `(?L)`. Note that RE2 reads `(?U)` as *non-greedy*, which 

64is not what Python's `re.UNICODE` means. 

65""" 

66 

67_INCOMPATIBLE_FLAGS = ~( 

68 _ASCII_FLAGS 

69 | _NOOP_FLAGS 

70 | reduce(operator.or_, [flag for flag, _letter in _TRANSLATABLE_FLAGS]) 

71) 

72""" 

73The compile flags that the Hyperscan and RE2 backends do not support. 

74""" 

75 

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

77 

78 

79def enumerate_patterns( 

80 patterns: Iterable[TPattern], 

81 filter: bool, 

82 reverse: bool, 

83) -> list[tuple[int, TPattern]]: 

84 """ 

85 Enumerate the patterns. 

86 

87 *patterns* (:class:`Iterable` of :class:`.Pattern`) contains the patterns. 

88 

89 *filter* (:class:`bool`) is whether to remove no-op patterns (:data:`True`), 

90 or keep them (:data:`False`). 

91 

92 *reverse* (:class:`bool`) is whether to reverse the pattern order 

93 (:data:`True`), or keep the order (:data:`True`). 

94 

95 Returns the enumerated patterns (:class:`list` of :class:`tuple`). 

96 """ 

97 out_patterns = [ 

98 (__i, __pat) 

99 for __i, __pat in enumerate(patterns) 

100 if not filter or __pat.include is not None 

101 ] 

102 if reverse: 

103 out_patterns.reverse() 

104 

105 return out_patterns 

106 

107 

108def get_regex_flags(regex: Union[re.Pattern, str, bytes, None]) -> int: 

109 """ 

110 Get the compile flags of a pattern's regular expression. 

111 

112 *regex* (:class:`re.Pattern`, :class:`bytes`, :class:`str`, or 

113 :data:`None`) is the regular expression of a :class:`.RegexPattern`. 

114 

115 Returns the flags (:class:`int`) of a regular expression compiled by 

116 Python's *re* module, or zero (:class:`int`) for anything else (a native 

117 backend only ever sees the expression of a foreign pattern). 

118 """ 

119 if isinstance(regex, re.Pattern): 

120 return int(regex.flags) 

121 

122 return 0 

123 

124 

125def has_unsupported_flags(patterns: Iterable[Pattern]) -> bool: 

126 """ 

127 Check for active patterns with compile flags a native backend can't keep. 

128 

129 *patterns* (:class:`Iterable` of :class:`.Pattern`) contains the patterns. 

130 

131 Returns the answer (:class:`bool`). 

132 """ 

133 for pattern in patterns: 

134 if pattern.include is None or not isinstance(pattern, RegexPattern): 

135 continue 

136 

137 regex = pattern.regex 

138 if not isinstance(regex, re.Pattern): 

139 # The pattern was not compiled by Python's *re* module. Its flags are 

140 # unknown, and only the simple backend can use it faithfully. 

141 continue 

142 

143 if not supports_regex_flags(regex.flags): 

144 return True 

145 

146 return False 

147 

148 

149@overload 

150def translate_end_anchor(regex: str) -> str: 

151 ... 

152 

153 

154@overload 

155def translate_end_anchor(regex: bytes) -> bytes: 

156 ... 

157 

158 

159@overload 

160def translate_end_anchor(regex: Union[str, bytes]) -> Union[str, bytes]: 

161 ... 

162 

163 

164def translate_end_anchor(regex: Union[str, bytes]) -> Union[str, bytes]: 

165 """ 

166 Translate Python's strict end anchor to the RE2 and Hyperscan spelling. 

167 

168 *regex* (:class:`bytes` or :class:`str`) is the regular expression. 

169 

170 Returns the translated regular expression (:class:`bytes` or :class:`str`). 

171 """ 

172 # Escaped backslashes are consumed as pairs, keeping literal `\Z` names 

173 # intact. 

174 if isinstance(regex, bytes): 

175 return _END_ANCHOR_BYTES.sub(_sub_end_anchor_bytes, regex) 

176 else: 

177 return _END_ANCHOR_STR.sub(_sub_end_anchor_str, regex) 

178 

179 

180def _sub_end_anchor_bytes(match: re.Match) -> bytes: 

181 """ 

182 Replaces Python's strict end anchor to the RE2 and Hyperscan spelling. 

183 

184 *match* (:class:`re.Match`) is the match object. 

185 

186 Returns the string replacement (:class:`bytes`). 

187 """ 

188 val = match[0] 

189 if val == rb'\Z': 

190 return rb'\z' 

191 else: 

192 return val 

193 

194 

195def _sub_end_anchor_str(match: re.Match) -> str: 

196 """ 

197 Replaces Python's strict end anchor to the RE2 and Hyperscan spelling. 

198 

199 *match* (:class:`re.Match`) is the match object. 

200 

201 Returns the string replacement (:class:`str`). 

202 """ 

203 val = match[0] 

204 if val == r'\Z': 

205 return r'\z' 

206 else: 

207 return val 

208 

209 

210def supports_regex_flags(flags: int) -> bool: 

211 """ 

212 Check whether a native backend can preserve the compile *flags*. 

213 

214 *flags* (:class:`int`) is the flags of a compiled :class:`re.Pattern`. 

215 

216 Returns the answer (:class:`bool`). 

217 """ 

218 if flags & _INCOMPATIBLE_FLAGS: 

219 return False 

220 

221 if flags & re.ASCII and flags & re.IGNORECASE: 

222 return False 

223 

224 return True 

225 

226 

227@overload 

228def translate_regex_flags(regex: str, flags: int) -> str: 

229 ... 

230 

231 

232@overload 

233def translate_regex_flags(regex: bytes, flags: int) -> bytes: 

234 ... 

235 

236 

237@overload 

238def translate_regex_flags(regex: Union[str, bytes], flags: int) -> Union[str, bytes]: 

239 ... 

240 

241 

242def translate_regex_flags( 

243 regex: Union[str, bytes], 

244 flags: int, 

245) -> Union[str, bytes]: 

246 """ 

247 Prepend the global inline flags that a native backend understands. 

248 

249 *regex* (:class:`bytes` or :class:`str`) is the regular expression. 

250 

251 *flags* (:class:`int`) is the flags of the compiled :class:`re.Pattern` 

252 that *regex* came from. Only flags accepted by 

253 :func:`supports_regex_flags` may be translated. 

254 

255 Returns the regular expression (:class:`bytes` or :class:`str`). 

256 """ 

257 letters = ''.join( 

258 letter 

259 for flag, letter in _TRANSLATABLE_FLAGS 

260 if flags & flag 

261 ) 

262 if not letters: 

263 return regex 

264 

265 prefix = f'(?{letters})' 

266 if isinstance(regex, bytes): 

267 return prefix.encode('ascii') + regex 

268 

269 return prefix + regex