Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pathspec/patterns/gitignore/basic.py: 12%

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

120 statements  

1""" 

2This module provides :class:`GitIgnoreBasicPattern` which implements Git's 

3`gitignore`_ patterns as documented. This differs from how Git actually behaves 

4when including files in excluded directories. 

5 

6.. _`gitignore`: https://git-scm.com/docs/gitignore 

7""" 

8 

9from typing import ( 

10 Literal, 

11 Optional) # Replaced by `X | None` in 3.10. 

12 

13from pathspec import util 

14from pathspec._typing import ( 

15 AnyStr, # Removed in 3.18. 

16 assert_unreachable, 

17 override) # Added in 3.12. 

18 

19from .base import ( 

20 GitIgnorePatternError, 

21 _BYTES_ENCODING, 

22 _GitIgnoreBasePattern, 

23 _PosixClassError, 

24 _RangeNotationError, 

25 _TrailingBackslashError, 

26 _collapse_star_runs, 

27 _strip_trailing_ws) 

28 

29 

30class GitIgnoreBasicPattern(_GitIgnoreBasePattern): 

31 """ 

32 The :class:`GitIgnoreBasicPattern` class represents a compiled gitignore 

33 pattern as documented. This is registered as "gitignore". 

34 """ 

35 

36 # Keep the dict-less class hierarchy. 

37 __slots__ = () 

38 

39 @staticmethod 

40 def __normalize_segments( 

41 is_dir_pattern: bool, 

42 pattern_segs: list[str], 

43 ) -> tuple[Optional[list[str]], Optional[str]]: 

44 """ 

45 Normalize the pattern segments to make processing easier. 

46 

47 *is_dir_pattern* (:class:`bool`) is whether the pattern is a directory 

48 pattern (i.e., ends with a slash '/'). 

49 

50 *pattern_segs* (:class:`list` of :class:`str`) contains the pattern 

51 segments. This may be modified in place. 

52 

53 Raises :exc:`ValueError` if the pattern normalizes to nothing. 

54 

55 Returns a :class:`tuple` containing either: 

56 

57 - The normalized segments (:class:`list` of :class:`str`; or :data:`None`). 

58 

59 - The regular expression override (:class:`str` or :data:`None`). 

60 """ 

61 if not pattern_segs[0]: 

62 # A pattern beginning with a slash ('/') should match relative to the root 

63 # directory. Remove the empty first segment to make the pattern relative 

64 # to root. 

65 del pattern_segs[0] 

66 

67 elif len(pattern_segs) == 1 or (len(pattern_segs) == 2 and not pattern_segs[1]): 

68 # A single segment pattern with or without a trailing slash ('/') will 

69 # match any descendant path. This is equivalent to "**/{pattern}". Prepend 

70 # double-asterisk segment to make pattern relative to root. 

71 if pattern_segs[0] != '**': 

72 pattern_segs.insert(0, '**') 

73 

74 else: 

75 # A pattern without a beginning slash ('/') but contains at least one 

76 # prepended directory (e.g., "dir/{pattern}") should match relative to the 

77 # root directory. No segment modification is needed. 

78 pass 

79 

80 if not pattern_segs: 

81 # After normalization, we end up with no pattern at all. This must be 

82 # because the pattern is invalid. 

83 raise ValueError("Pattern normalized to nothing.") 

84 

85 if not pattern_segs[-1]: 

86 # A pattern ending with a slash ('/') will match all descendant paths if 

87 # it is a directory but not if it is a regular file. This is equivalent to 

88 # "{pattern}/**". Set empty last segment to a double-asterisk to include 

89 # all descendants. 

90 pattern_segs[-1] = '**' 

91 

92 # EDGE CASE: Collapse duplicate double-asterisk sequences (i.e., '**/**'). 

93 # Iterate over the segments in reverse order and remove the duplicate double 

94 # asterisks as we go. 

95 for i in range(len(pattern_segs) - 1, 0, -1): 

96 prev = pattern_segs[i-1] 

97 seg = pattern_segs[i] 

98 if prev == '**' and seg == '**': 

99 del pattern_segs[i] 

100 

101 seg_count = len(pattern_segs) 

102 if seg_count == 1 and pattern_segs[0] == '**': 

103 if is_dir_pattern: 

104 # The pattern "**/" will be normalized to "**", but it should match 

105 # everything except for files in the root. Special case this pattern. 

106 return (None, '/') 

107 else: 

108 # The pattern "**" will match every path. Special case this pattern. 

109 return (None, '(?s:.)') 

110 

111 elif ( 

112 seg_count == 2 

113 and pattern_segs[0] == '**' 

114 and pattern_segs[1] == '*' 

115 ): 

116 # The pattern "*" will be normalized to "**/*" and will match every 

117 # path. Special case this pattern for efficiency. 

118 return (None, '(?s:.)') 

119 

120 elif ( 

121 seg_count == 3 

122 and pattern_segs[0] == '**' 

123 and pattern_segs[1] == '*' 

124 and pattern_segs[2] == '**' 

125 ): 

126 # The pattern "*/" will be normalized to "**/*/**" which will match every 

127 # file not in the root directory. Special case this pattern for 

128 # efficiency. 

129 return (None, '/') 

130 

131 # No regular expression override, return modified pattern segments. 

132 return (pattern_segs, None) 

133 

134 @override 

135 @classmethod 

136 def pattern_to_regex( 

137 cls, 

138 pattern: AnyStr, 

139 *, 

140 errors: Optional[Literal['literal', 'null', 'raise']] = None, 

141 ) -> tuple[Optional[AnyStr], Optional[bool]]: 

142 """ 

143 Convert the pattern into a regular expression. 

144 

145 *pattern* (:class:`str` or :class:`bytes`) is the pattern to convert into a 

146 regular expression. 

147 

148 *errors* (:class:`str` :data:`None`) is how to handle invalid notation in 

149 the pattern. Default is :data:`None` for ``'literal'`` because that is 

150 historic behavior of this library. 

151 

152 - ``'literal'``: Most invalid notation will be treated as a literal 

153 string. In cases where the gitignore documentation clearly states the 

154 pattern should never match, a null-operation will be returned. 

155 

156 - ``'null'``: Invalid notation will result in a null-operation. 

157 

158 - ``'raise'``: Invalid notation will raise a :exc:`.GitIgnorePatternError`. 

159 

160 Raises :exc:`.GitIgnorePatternError` if the pattern fails to process, 

161 regardless of the value of *errors*. 

162 

163 Returns a :class:`tuple` containing: 

164 

165 - *pattern* (:class:`str`, :class:`bytes` or :data:`None`) is the uncompiled 

166 regular expression. 

167 

168 - *include* (:class:`bool` or :data:`None`) is whether matched files should 

169 be included (:data:`True`), excluded (:data:`False`), or is a 

170 null-operation (:data:`None`). 

171 """ 

172 if isinstance(pattern, str): 

173 pattern_str = pattern 

174 return_type = str 

175 elif isinstance(pattern, bytes): 

176 pattern_str = pattern.decode(_BYTES_ENCODING) 

177 return_type = bytes 

178 else: 

179 raise TypeError(f"{pattern=!r} is not a unicode or byte string.") 

180 

181 original_pattern = pattern_str 

182 del pattern 

183 

184 if errors is None: 

185 errors = 'literal' 

186 elif errors not in ('literal', 'null', 'raise'): 

187 raise ValueError(f"{errors=!r} is not a valid value.") 

188 

189 seg_errors: Literal['literal', 'raise'] 

190 if errors == 'null': 

191 seg_errors = 'raise' 

192 elif errors in ('literal', 'raise'): 

193 seg_errors = errors 

194 else: 

195 assert_unreachable(f"Failed to map {errors=!r} to seg_errors.") 

196 

197 # Strip trailing whitespace. 

198 pattern_str = _strip_trailing_ws(pattern_str) 

199 

200 regex: Optional[str] 

201 include: Optional[bool] 

202 

203 if pattern_str.startswith('#'): 

204 # A pattern starting with a hash ('#') serves as a comment (neither 

205 # includes nor excludes files). Escape the hash with a backslash to match 

206 # a literal hash (i.e., '\#'). 

207 return (None, None) 

208 

209 if pattern_str.startswith('!'): 

210 # A pattern starting with an exclamation mark ('!') negates the pattern 

211 # (exclude instead of include). Escape the exclamation mark with a back 

212 # slash to match a literal exclamation mark (i.e., '\!'). 

213 include = False 

214 # Remove leading exclamation mark. 

215 pattern_str = pattern_str[1:] 

216 else: 

217 include = True 

218 

219 if not pattern_str: 

220 # A blank pattern is a null-operation (neither includes nor excludes 

221 # files). 

222 return (None, None) 

223 

224 # Split pattern into segments. 

225 orig_segs = _collapse_star_runs(pattern_str.split('/')) 

226 

227 # Check whether the pattern is specifically a directory pattern before 

228 # normalization. 

229 is_dir_pattern = not orig_segs[-1] 

230 

231 if pattern_str == '/': 

232 # EDGE CASE: A single slash ('/') is not addressed by the gitignore 

233 # documentation. Git treats it as a no-op (does not match any files). The 

234 # straight forward interpretation is to treat it as a directory and match 

235 # every descendant path (equivalent to '**'). Remove the directory pattern 

236 # flag so that it is treated as '**' instead of '**/'. 

237 is_dir_pattern = False 

238 

239 # Normalize pattern to make processing easier. 

240 try: 

241 pattern_segs, override_regex = cls.__normalize_segments( 

242 is_dir_pattern, orig_segs, 

243 ) 

244 except ValueError as e: 

245 raise GitIgnorePatternError(( 

246 f"Invalid git pattern: {original_pattern!r}" 

247 )) from e # GitIgnorePatternError 

248 

249 if override_regex is not None: 

250 # Use regex override. 

251 regex = override_regex 

252 

253 elif pattern_segs is not None: 

254 # Build regular expression from pattern. 

255 try: 

256 regex_parts = cls.__translate_segments( 

257 seg_errors, is_dir_pattern, pattern_segs, 

258 ) 

259 except (_PosixClassError, _RangeNotationError) as e: 

260 # EDGE CASE: Git discards patterns with an invalid range notation or an 

261 # invalid POSIX class. 

262 if errors == 'raise': 

263 raise GitIgnorePatternError(( 

264 f"Invalid git pattern: {original_pattern!r}" 

265 )) from e # GitIgnorePatternError 

266 else: 

267 return (None, None) 

268 except _TrailingBackslashError as e: 

269 # EDGE CASE: The gitignore docs say a trailing backlash is invalid and 

270 # never matches. 

271 if errors == 'raise': 

272 raise GitIgnorePatternError(( 

273 f"Invalid git pattern: {original_pattern!r}" 

274 )) from e # GitIgnorePatternError 

275 else: 

276 return (None, None) 

277 

278 regex = ''.join(regex_parts) 

279 

280 else: 

281 assert_unreachable(( 

282 f"{override_regex=} and {pattern_segs=} cannot both be null." 

283 )) # assert_unreachable 

284 

285 # Encode regex if needed. 

286 out_regex: AnyStr 

287 if regex is not None and return_type is bytes: 

288 regex_bytes = regex.encode(_BYTES_ENCODING) 

289 out_regex = regex_bytes # type: ignore[assignment] 

290 else: 

291 out_regex = regex # type: ignore[assignment] 

292 

293 return (out_regex, include) 

294 

295 @classmethod 

296 def __translate_segments( 

297 cls, 

298 errors: Literal['literal', 'raise'], 

299 is_dir_pattern: bool, 

300 pattern_segs: list[str], 

301 ) -> list[str]: 

302 """ 

303 Translate the pattern segments to regular expressions. 

304 

305 *errors* (:class:`str`) is how to handle invalid pattern notation in the 

306 pattern: 

307 

308 - ``'literal'``: Invalid notation will be treated as a literal string. 

309 

310 - ``'raise'``: Invalid notation will raise an exception. 

311 

312 *is_dir_pattern* (:class:`bool`) is whether the original pattern ends 

313 with a slash. 

314 

315 *pattern_segs* (:class:`list` of :class:`str`) contains the pattern 

316 segments. 

317 

318 Raises :exc:`_PosixClassError` when an invalid POXIS class is found and 

319 *errors* is ``'literal'``. 

320 

321 Raises :exc:`_RangeNotationError` when an invalid range notation is found 

322 and *errors* is ``'literal'``. 

323 

324 Raises :exc:`_TrailingBackslashError` when a trailing backslash is found at 

325 the end of the pattern, regardless of the value of *errors*. 

326 

327 Returns the regular expression parts (:class:`list` of :class:`str`). 

328 """ 

329 # Build regular expression from pattern. 

330 out_parts = [] 

331 need_slash = False 

332 end = len(pattern_segs) - 1 

333 for i, seg in enumerate(pattern_segs): 

334 if seg == '**': 

335 if i == 0: 

336 # A normalized pattern beginning with double-asterisks ('**') will 

337 # match any leading path segments. 

338 # - NOTICE: '(?:^|/)' benchmarks slower using p15 (sm=0.9382, 

339 # hs=0.9966, re2=0.9337). 

340 out_parts.append('^(?s:.+/)?') 

341 

342 elif i < end: 

343 # A pattern with inner double-asterisks ('**') will match multiple (or 

344 # zero) inner path segments. 

345 out_parts.append('(?s:/.+)?') 

346 need_slash = True 

347 

348 else: 

349 assert i == end, (i, end) 

350 # A normalized pattern ending with double-asterisks ('**') will match 

351 # nonempty trailing path segments, not the parent directory itself. 

352 out_parts.append('/' if is_dir_pattern else '/[^/]') 

353 

354 else: 

355 # Match path segment. 

356 if i == 0: 

357 # Anchor to root directory. 

358 out_parts.append('^') 

359 

360 if need_slash: 

361 out_parts.append('/') 

362 

363 if seg == '*': 

364 # Match whole path segment. 

365 out_parts.append('[^/]+') 

366 

367 else: 

368 # Match segment glob pattern. 

369 # - EDGE CASE: The gitignore docs defer to *fnmatch(3)* which treats 

370 # invalid range notation as a literal. 

371 out_parts.append(cls._translate_segment_glob(seg, errors)) 

372 

373 if i == end: 

374 if seg == '*': 

375 # A pattern ending with an asterisk ('*') will match a file or 

376 # directory (without matching descendant paths). E.g., "foo/*" 

377 # matches "foo/test.json", "foo/bar/", but not "foo/bar/hello.c". 

378 out_parts.append(r'/?\Z') 

379 

380 else: 

381 # A pattern ending without a slash ('/') will match a file or a 

382 # directory (with paths underneath it). E.g., "foo" matches "foo", 

383 # "foo/bar", "foo/bar/baz", etc. 

384 out_parts.append(r'(?:/|\Z)') 

385 

386 need_slash = True 

387 

388 return out_parts 

389 

390 

391# Register GitIgnoreBasicPattern as "gitignore". 

392util.register_pattern('gitignore', GitIgnoreBasicPattern)