Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pathspec/util.py: 32%

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

282 statements  

1""" 

2This module provides utility methods for dealing with path-specs. 

3""" 

4from __future__ import annotations 

5 

6import os 

7import os.path 

8import pathlib 

9import posixpath 

10import stat 

11import warnings 

12from collections.abc import ( 

13 Collection, 

14 Iterable, 

15 Iterator, 

16 Sequence) 

17from dataclasses import ( 

18 dataclass) 

19from typing import ( 

20 Any, 

21 Callable, # Replaced by `collections.abc.Callable` in 3.9.2. 

22 Generic, 

23 Optional, # Replaced by `X | None` in 3.10. 

24 TypeVar, 

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

26 cast) 

27 

28from .pattern import ( 

29 Pattern) 

30from ._typing import ( 

31 AnyStr, # Removed in 3.18. 

32 deprecated) # Added in 3.13. 

33 

34StrPath = Union[str, os.PathLike[str]] 

35 

36TPattern = TypeVar('TPattern', bound=Pattern) 

37""" 

38Type variable for :class:`.Pattern`. This is used by :class:`pathspec.pathspec.PathSpec` 

39to specialize the type of patterns. 

40""" 

41 

42TPattern_co = TypeVar('TPattern_co', bound=Pattern, covariant=True) 

43""" 

44Type variable for :class:`.Pattern` that is covariant. This is used by 

45:class:`pathspec.pathspec.PathSpec` to specialize the type of patterns. 

46""" 

47 

48TStrPath = TypeVar('TStrPath', bound=StrPath) 

49""" 

50Type variable for :class:`str` or :class:`os.PathLike`. 

51""" 

52 

53NORMALIZE_PATH_SEPS = [ 

54 cast(str, __sep) 

55 for __sep in [os.sep, os.altsep] 

56 if __sep and __sep != posixpath.sep 

57] 

58""" 

59*NORMALIZE_PATH_SEPS* (:class:`list` of :class:`str`) contains the path 

60separators that need to be normalized to the POSIX separator for the current 

61operating system. The separators are determined by examining :data:`os.sep` and 

62:data:`os.altsep`. 

63""" 

64 

65_registered_patterns: dict[str, Callable[[Union[str, bytes]], Pattern]] = {} 

66""" 

67*_registered_patterns* (:class:`dict`) maps a name (:class:`str`) to the 

68registered pattern factory (:class:`~collections.abc.Callable`). 

69""" 

70 

71 

72def append_dir_sep(path: pathlib.Path) -> str: 

73 """ 

74 Appends the path separator to the path if the path is a directory. This can be 

75 used to aid in distinguishing between directories and files on the file-system 

76 by relying on the presence of a trailing path separator. 

77 

78 *path* (:class:`pathlib.Path`) is the path to use. 

79 

80 Returns the path (:class:`str`). 

81 """ 

82 str_path = str(path) 

83 if path.is_dir(): 

84 str_path += os.sep 

85 

86 return str_path 

87 

88 

89def check_match_file( 

90 patterns: Iterable[tuple[int, Pattern]], 

91 file: str, 

92 is_reversed: Optional[bool] = None, 

93) -> tuple[Optional[bool], Optional[int]]: 

94 """ 

95 Check the file against the patterns. 

96 

97 *patterns* (:class:`~collections.abc.Iterable`) yields each indexed pattern 

98 (:class:`tuple`) which contains the pattern index (:class:`int`) and actua 

99 pattern (:class:`.Pattern`). 

100 

101 *file* (:class:`str`) is the normalized file path to be matched against 

102 *patterns*. 

103 

104 *is_reversed* (:class:`bool` or :data:`None`) is whether the order of the 

105 patterns has been reversed. Default is :data:`None` for :data:`False`. 

106 Reversing the order of the patterns is an optimization. 

107 

108 Returns a :class:`tuple` containing whether to include *file* (:class:`bool` 

109 or :data:`None`), and the index of the last matched pattern (:class:`int` or 

110 :data:`None`). 

111 """ 

112 if is_reversed: 

113 # Check patterns in reverse order. The first pattern that matches takes 

114 # precedence. 

115 for index, pattern in patterns: 

116 if pattern.include is not None and pattern.match_file(file) is not None: 

117 return pattern.include, index 

118 

119 return None, None 

120 

121 else: 

122 # Check all patterns. The last pattern that matches takes precedence. 

123 out_include: Optional[bool] = None 

124 out_index: Optional[int] = None 

125 for index, pattern in patterns: 

126 if pattern.include is not None and pattern.match_file(file) is not None: 

127 out_include = pattern.include 

128 out_index = index 

129 

130 return out_include, out_index 

131 

132 

133def detailed_match_files( 

134 patterns: Iterable[Pattern], 

135 files: Iterable[str], 

136 all_matches: Optional[bool] = None, 

137) -> dict[str, MatchDetail]: 

138 """ 

139 Matches the files to the patterns, and returns which patterns matched the 

140 files. 

141 

142 *patterns* (:class:`~collections.abc.Iterable` of :class:`.Pattern`) contains 

143 the patterns to use. 

144 

145 *files* (:class:`~collections.abc.Iterable` of :class:`str`) contains the 

146 normalized file paths to be matched against *patterns*. 

147 

148 *all_matches* (:class:`bool` or :data:`None`) is whether to return all matches 

149 patterns (:data:`True`), or only the last matched pattern (:data:`False`). 

150 Default is :data:`None` for :data:`False`. 

151 

152 Returns the matched files (:class:`dict`) which maps each matched file 

153 (:class:`str`) to the patterns that matched in order (:class:`.MatchDetail`). 

154 """ 

155 all_files = files if isinstance(files, Collection) else list(files) 

156 return_files: dict[str, MatchDetail] = {} 

157 for pattern in patterns: 

158 if pattern.include is not None: 

159 result_files = pattern.match(all_files) # TODO: Replace with `.match_file()`. 

160 if pattern.include: 

161 # Add files and record pattern. 

162 for result_file in result_files: 

163 if result_file in return_files: 

164 # We know here that .patterns is a list, because we made it here 

165 if all_matches: 

166 return_files[result_file].patterns.append(pattern) # type: ignore[attr-defined] 

167 else: 

168 return_files[result_file].patterns[0] = pattern # type: ignore[index] 

169 else: 

170 return_files[result_file] = MatchDetail([pattern]) 

171 

172 else: 

173 # Remove files. 

174 for file in result_files: 

175 return_files.pop(file, None) 

176 

177 return return_files 

178 

179 

180def _filter_check_patterns( 

181 patterns: Iterable[Pattern], 

182) -> list[tuple[int, Pattern]]: 

183 """ 

184 Filters out null-patterns. 

185 

186 *patterns* (:class:`~collections.abc.Iterable` of :class:`.Pattern`) contains 

187 the patterns. 

188 

189 Returns a :class:`list` containing each indexed pattern (:class:`tuple`) which 

190 contains the pattern index (:class:`int`) and the actual pattern 

191 (:class:`.Pattern`). 

192 """ 

193 return [ 

194 (__index, __pat) 

195 for __index, __pat in enumerate(patterns) 

196 if __pat.include is not None 

197 ] 

198 

199 

200def _get_sub_path_safe(root: str, sub_dir: StrPath) -> str: 

201 """ 

202 Get the sub-directory path relative to the root directory. This ensures the 

203 root path cannot be escaped. 

204 

205 *root* (:class:`str`) is the root directory. 

206 

207 *sub_dir* (:class:`str` or :class:`os.PathLike`) is the sub-directory path. 

208 This path can be relative or absolute. 

209 

210 Returns the sub-directory path relative to the root directory (:class:`str`). 

211 """ 

212 sub_abs = os.path.normpath(os.path.join(root, sub_dir)) 

213 if os.path.normcase(sub_abs) == os.path.normcase(root): 

214 return '' 

215 elif os.path.commonpath((root, sub_abs)) == root: 

216 return os.path.relpath(sub_abs, root) 

217 else: 

218 raise ValueError(f"{sub_dir=!r} must be relative to {root=!r}.") 

219 

220 

221def _is_iterable(value: Any) -> bool: 

222 """ 

223 Check whether the value is an iterable (excludes strings). 

224 

225 *value* is the value to check. 

226 

227 Returns whether *value* is an iterable (:class:`bool`). 

228 """ 

229 return isinstance(value, Iterable) and not isinstance(value, (str, bytes)) 

230 

231 

232@deprecated(( 

233 "pathspec.util.iter_tree() is deprecated. Use iter_tree_files() instead." 

234)) 

235def iter_tree(root, on_error=None, follow_links=None): 

236 """ 

237 .. version-deprecated:: 0.10.0 

238 This is an alias for the :func:`.iter_tree_files` function. 

239 """ 

240 return iter_tree_files(root, on_error=on_error, follow_links=follow_links) 

241 

242 

243def iter_tree_entries( 

244 root: StrPath, 

245 on_error: Optional[Callable[[OSError], None]] = None, 

246 follow_links: Optional[bool] = None, 

247 subdir: Optional[StrPath] = None, 

248) -> Iterator[TreeEntry]: 

249 """ 

250 Walks the specified directory for all files and directories. 

251 

252 *root* (:class:`str` or :class:`os.PathLike`) is the root directory to search. 

253 

254 *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally is 

255 the error handler for file-system exceptions. It will be called with the 

256 exception (:exc:`OSError`). Reraise the exception to abort the walk. Default 

257 is :data:`None` to ignore file-system exceptions. 

258 

259 *follow_links* (:class:`bool` or :data:`None`) optionally is whether to walk 

260 symbolic links that resolve to directories. Default is :data:`None` for 

261 :data:`True`. 

262 

263 *subdir* (:class:`str`, :class:`os.PathLike`, or :data:`None`) is a sub 

264 directory of *root* to constrain searching to. If a relative path, it is 

265 treated as relative to *root*. If an absolute path, it must be a descendant of 

266 *root*. Default is :data:`None` to search the entire *root* directory tree. 

267 

268 Raises :exc:`.RecursivePathError` if recursion is detected. 

269 

270 Returns an :class:`~collections.abc.Iterator` yielding each file or directory 

271 entry (:class:`.TreeEntry`) relative to *root*. 

272 """ 

273 if on_error is not None and not callable(on_error): 

274 raise TypeError(f"on_error:{on_error!r} is not callable.") 

275 

276 if follow_links is None: 

277 follow_links = True 

278 

279 root_abs = os.path.abspath(root) 

280 

281 # Ensure subdir does not escape root. 

282 if subdir is not None: 

283 dir_rel = _get_sub_path_safe(root_abs, subdir) 

284 else: 

285 dir_rel = '' 

286 

287 yield from _iter_tree_entries_next(root_abs, dir_rel, {}, on_error, follow_links) 

288 

289 

290def _iter_tree_entries_next( 

291 root_full: str, 

292 dir_rel: str, 

293 memo: dict[str, str], 

294 on_error: Optional[Callable[[OSError], None]], 

295 follow_links: bool, 

296) -> Iterator[TreeEntry]: 

297 """ 

298 Scan the directory for all descendant files. 

299 

300 *root_full* (:class:`str`) the absolute path to the root directory. 

301 

302 *dir_rel* (:class:`str`) the path to the directory to scan relative to 

303 *root_full*. 

304 

305 *memo* (:class:`dict`) keeps track of ancestor directories encountered. Maps 

306 each ancestor real path (:class:`str`) to relative path (:class:`str`). 

307 

308 *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally is 

309 the error handler for file-system exceptions. 

310 

311 *follow_links* (:class:`bool`) is whether to walk symbolic links that resolve 

312 to directories. 

313 

314 Yields each entry (:class:`.TreeEntry`). 

315 """ 

316 dir_full = os.path.join(root_full, dir_rel) 

317 dir_real = os.path.realpath(dir_full) 

318 

319 # Remember each encountered ancestor directory and its canonical (real) path. 

320 # If a canonical path is encountered more than once, recursion has occurred. 

321 if dir_real not in memo: 

322 memo[dir_real] = dir_rel 

323 else: 

324 raise RecursivePathError( 

325 real_path=dir_real, first_path=memo[dir_real], second_path=dir_rel, 

326 ) 

327 

328 try: 

329 scan_iter = os.scandir(dir_full) 

330 except OSError as e: 

331 del memo[dir_real] 

332 if on_error is not None: 

333 on_error(e) 

334 return 

335 

336 with scan_iter: 

337 node_ent: os.DirEntry 

338 for node_ent in scan_iter: 

339 node_rel = os.path.join(dir_rel, node_ent.name) 

340 

341 # Inspect child node. 

342 try: 

343 node_lstat = node_ent.stat(follow_symlinks=False) 

344 is_dir = node_ent.is_dir(follow_symlinks=follow_links) 

345 is_file = node_ent.is_file(follow_symlinks=follow_links) 

346 is_symlink = node_ent.is_symlink() 

347 except OSError as e: 

348 if on_error is not None: 

349 on_error(e) 

350 continue 

351 

352 if follow_links and is_symlink: 

353 # Child node is a link, inspect the target node. 

354 try: 

355 node_stat = node_ent.stat() 

356 except OSError as e: 

357 if on_error is not None: 

358 on_error(e) 

359 continue 

360 else: 

361 node_stat = node_lstat 

362 

363 if is_dir: 

364 # Child node is a directory, recurse into it and yield its descendant 

365 # files. 

366 yield TreeEntry(node_ent.name, node_rel, node_lstat, node_stat) 

367 

368 yield from _iter_tree_entries_next(root_full, node_rel, memo, on_error, follow_links) 

369 

370 elif is_file: 

371 # Child node is a file, yield it. 

372 yield TreeEntry(node_ent.name, node_rel, node_lstat, node_stat) 

373 

374 elif not follow_links and is_symlink: 

375 # Child node is an unfollowed link, yield it. 

376 yield TreeEntry(node_ent.name, node_rel, node_lstat, node_stat) 

377 

378 # NOTE: Make sure to remove the canonical (real) path of the directory from 

379 # the ancestors memo once we are done with it. This allows the same directory 

380 # to appear multiple times. If this is not done, the second occurrence of the 

381 # directory will be incorrectly interpreted as a recursion. See 

382 # <https://github.com/cpburnz/python-path-specification/pull/7>. 

383 del memo[dir_real] 

384 

385 

386# TODO: Add tests for subdir. 

387def iter_tree_files( 

388 root: StrPath, 

389 on_error: Optional[Callable[[OSError], None]] = None, 

390 follow_links: Optional[bool] = None, 

391 subdir: Optional[StrPath] = None, 

392) -> Iterator[str]: 

393 """ 

394 Walks the specified directory for all files. 

395 

396 *root* (:class:`str` or :class:`os.PathLike`) is the root directory to search 

397 for files. 

398 

399 *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally is 

400 the error handler for file-system exceptions. It will be called with the 

401 exception (:exc:`OSError`). Reraise the exception to abort the walk. Default 

402 is :data:`None` to ignore file-system exceptions. 

403 

404 *follow_links* (:class:`bool` or :data:`None`) optionally is whether to walk 

405 symbolic links that resolve to directories. Default is :data:`None` for 

406 :data:`True`. 

407 

408 *subdir* (:class:`str`, :class:`os.PathLike`, or :data:`None`) is a sub 

409 directory of *root* to constrain searching to. If a relative path, it is 

410 treated as relative to *root*. If an absolute path, it must be a descendant of 

411 *root*. Default is :data:`None` to search the entire *root* directory tree. 

412 

413 Raises :exc:`.RecursivePathError` if recursion is detected. 

414 

415 Returns an :class:`~collections.abc.Iterator` yielding the path to each file 

416 (:class:`str`) relative to *root*. 

417 """ 

418 if on_error is not None and not callable(on_error): 

419 raise TypeError(f"on_error:{on_error!r} is not callable.") 

420 

421 if follow_links is None: 

422 follow_links = True 

423 

424 root_abs = os.path.abspath(root) 

425 

426 # Ensure subdir does not escape root. 

427 if subdir is not None: 

428 dir_rel = _get_sub_path_safe(root_abs, subdir) 

429 else: 

430 dir_rel = '' 

431 

432 yield from _iter_tree_files_next(root_abs, dir_rel, {}, on_error, follow_links) 

433 

434 

435def _iter_tree_files_next( 

436 root_full: str, 

437 dir_rel: str, 

438 memo: dict[str, str], 

439 on_error: Optional[Callable[[OSError], None]], 

440 follow_links: bool, 

441) -> Iterator[str]: 

442 """ 

443 Scan the directory for all descendant files. 

444 

445 *root_full* (:class:`str`) the absolute path to the root directory. 

446 

447 *dir_rel* (:class:`str`) the path to the directory to scan relative to 

448 *root_full*. 

449 

450 *memo* (:class:`dict`) keeps track of ancestor directories encountered. Maps 

451 each ancestor real path (:class:`str`) to relative path (:class:`str`). 

452 

453 *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally is 

454 the error handler for file-system exceptions. 

455 

456 *follow_links* (:class:`bool`) is whether to walk symbolic links that resolve 

457 to directories. 

458 

459 Yields each file path (:class:`str`). 

460 """ 

461 dir_full = os.path.join(root_full, dir_rel) 

462 dir_real = os.path.realpath(dir_full) 

463 

464 # Remember each encountered ancestor directory and its canonical (real) path. 

465 # If a canonical path is encountered more than once, recursion has occurred. 

466 if dir_real not in memo: 

467 memo[dir_real] = dir_rel 

468 else: 

469 raise RecursivePathError( 

470 real_path=dir_real, first_path=memo[dir_real], second_path=dir_rel, 

471 ) 

472 

473 try: 

474 scan_iter = os.scandir(dir_full) 

475 except OSError as e: 

476 del memo[dir_real] 

477 if on_error is not None: 

478 on_error(e) 

479 return 

480 

481 with scan_iter: 

482 node_ent: os.DirEntry 

483 for node_ent in scan_iter: 

484 node_rel = os.path.join(dir_rel, node_ent.name) 

485 

486 try: 

487 is_dir = node_ent.is_dir(follow_symlinks=follow_links) 

488 is_file = node_ent.is_file(follow_symlinks=follow_links) 

489 is_symlink = node_ent.is_symlink() 

490 except OSError as e: 

491 if on_error is not None: 

492 on_error(e) 

493 continue 

494 

495 if is_dir: 

496 # Child node is a directory, recurse into it and yield its descendant 

497 # files. 

498 yield from _iter_tree_files_next(root_full, node_rel, memo, on_error, follow_links) 

499 

500 elif is_file: 

501 # Child node is a file, yield it. 

502 yield node_rel 

503 

504 elif not follow_links and is_symlink: 

505 # Child node is an unfollowed link, yield it. 

506 yield node_rel 

507 

508 # NOTE: Make sure to remove the canonical (real) path of the directory from 

509 # the ancestors memo once we are done with it. This allows the same directory 

510 # to appear multiple times. If this is not done, the second occurrence of the 

511 # directory will be incorrectly interpreted as a recursion. See 

512 # <https://github.com/cpburnz/python-path-specification/pull/7>. 

513 del memo[dir_real] 

514 

515 

516def lookup_pattern(name: str) -> Callable[[AnyStr], Pattern]: 

517 """ 

518 Looks up a registered pattern factory by name. 

519 

520 *name* (:class:`str`) is the name of the pattern factory. 

521 

522 Returns the registered pattern factory (:class:`~collections.abc.Callable`). 

523 If no pattern factory is registered, raises :exc:`KeyError`. 

524 """ 

525 return _registered_patterns[name] # type: ignore[return-value] 

526 

527 

528def match_file(patterns: Iterable[Pattern], file: str) -> bool: 

529 """ 

530 Matches the file to the patterns. 

531 

532 *patterns* (:class:`~collections.abc.Iterable` of :class:`.Pattern`) contains 

533 the patterns to use. 

534 

535 *file* (:class:`str`) is the normalized file path to be matched against 

536 *patterns*. 

537 

538 Returns :data:`True` if *file* matched; otherwise, :data:`False`. 

539 """ 

540 matched = False 

541 for pattern in patterns: 

542 if pattern.include is not None and pattern.match_file(file) is not None: 

543 matched = pattern.include 

544 

545 return matched 

546 

547 

548@deprecated(( 

549 "pathspec.util.match_files() is deprecated. Use match_file() with a loop for " 

550 "better results." 

551)) 

552def match_files( 

553 patterns: Iterable[Pattern], 

554 files: Iterable[str], 

555) -> set[str]: 

556 """ 

557 .. version-deprecated:: 0.10.0 

558 This function is no longer used. Use the :func:`.match_file` function with a 

559 loop for better results. 

560 

561 Matches the files to the patterns. 

562 

563 *patterns* (:class:`~collections.abc.Iterable` of :class:`.Pattern`) contains 

564 the patterns to use. 

565 

566 *files* (:class:`~collections.abc.Iterable` of :class:`str`) contains the 

567 normalized file paths to be matched against *patterns*. 

568 

569 Returns the matched files (:class:`set` of :class:`str`). 

570 """ 

571 use_patterns = [__pat for __pat in patterns if __pat.include is not None] 

572 

573 return_files = set() 

574 for file in files: 

575 if match_file(use_patterns, file): 

576 return_files.add(file) 

577 

578 return return_files 

579 

580 

581def normalize_file( 

582 file: StrPath, 

583 separators: Optional[Collection[str]] = None, 

584) -> str: 

585 """ 

586 Normalizes the file path to use the POSIX path separator (i.e., ``"/"``), and 

587 make the paths relative (remove leading ``"/"``). 

588 

589 *file* (:class:`str` or :class:`os.PathLike`) is the file path. 

590 

591 *separators* (:class:`~collections.abc.Collection` of :class:`str`; or 

592 :data:`None`) optionally contains the path separators to normalize. This does 

593 not need to include the POSIX path separator (``"/"``), but including it will 

594 not affect the results. Default is ``None`` for :data:`.NORMALIZE_PATH_SEPS`. 

595 To prevent normalization, pass an empty container (e.g., an empty tuple 

596 ``()``). 

597 

598 Returns the normalized file path (:class:`str`). 

599 """ 

600 # Normalize path separators. 

601 if separators is None: 

602 separators = NORMALIZE_PATH_SEPS 

603 

604 assert separators is not None, separators 

605 

606 # Convert path object to string. 

607 norm_file: str = os.fspath(file) 

608 

609 for sep in separators: 

610 norm_file = norm_file.replace(sep, posixpath.sep) 

611 

612 if norm_file.startswith('/'): 

613 # Make path relative. 

614 norm_file = norm_file[1:] 

615 

616 elif norm_file.startswith('./'): 

617 # Remove current directory prefix. 

618 norm_file = norm_file[2:] 

619 

620 return norm_file 

621 

622 

623@deprecated(( 

624 "pathspec.util.normalize_files() is deprecated. Use normalize_file() with a " 

625 "loop for better results." 

626)) 

627def normalize_files( 

628 files: Iterable[StrPath], 

629 separators: Optional[Collection[str]] = None, 

630) -> dict[str, list[StrPath]]: 

631 """ 

632 .. version-deprecated:: 0.10.0 

633 This function is no longer used. Use the :func:`.normalize_file` function 

634 with a loop for better results. 

635 

636 Normalizes the file paths to use the POSIX path separator. 

637 

638 *files* (:class:`~collections.abc.Iterable` of :class:`str` or 

639 :class:`os.PathLike`) contains the file paths to be normalized. 

640 

641 *separators* (:class:`~collections.abc.Collection` of :class:`str`; or 

642 :data:`None`) optionally contains the path separators to normalize. See 

643 :func:`.normalize_file` for more information. 

644 

645 Returns a :class:`dict` mapping each normalized file path (:class:`str`) to 

646 the original file paths (:class:`list` of :class:`str` or 

647 :class:`os.PathLike`). 

648 """ 

649 norm_files: dict[str, list[StrPath]] = {} 

650 for path in files: 

651 norm_file = normalize_file(path, separators=separators) 

652 if norm_file in norm_files: 

653 norm_files[norm_file].append(path) 

654 else: 

655 norm_files[norm_file] = [path] 

656 

657 return norm_files 

658 

659 

660def register_pattern( 

661 name: str, 

662 pattern_factory: Union[Callable[[Union[str, bytes]], Pattern], type[Pattern]], 

663 override: Optional[bool] = None, 

664) -> None: 

665 """ 

666 Registers the specified pattern factory. 

667 

668 *name* (:class:`str`) is the name to register the pattern factory under. 

669 

670 *pattern_factory* (:class:`~collections.abc.Callable`) is used to compile 

671 patterns. It must accept an uncompiled pattern (:class:`str`) and return the 

672 compiled pattern (:class:`.Pattern`). 

673 

674 *override* (:class:`bool` or :data:`None`) optionally is whether to allow 

675 overriding an already registered pattern under the same name (:data:`True`), 

676 instead of raising an :exc:`.AlreadyRegisteredError` (:data:`False`). Default 

677 is :data:`None` for :data:`False`. 

678 """ 

679 if not isinstance(name, str): 

680 raise TypeError(f"{name=!r} is not a string.") 

681 

682 if not callable(pattern_factory): 

683 raise TypeError(f"{pattern_factory=!r} is not callable.") 

684 

685 if name in _registered_patterns and not override: 

686 raise AlreadyRegisteredError(name, _registered_patterns[name]) 

687 

688 _registered_patterns[name] = pattern_factory # type: ignore 

689 

690 

691class AlreadyRegisteredError(Exception): 

692 """ 

693 The :exc:`AlreadyRegisteredError` exception is raised when a pattern factory 

694 is registered under a name already in use. 

695 """ 

696 

697 def __init__( 

698 self, 

699 name: str, 

700 pattern_factory: Callable[[Union[str, bytes]], Pattern], 

701 ) -> None: 

702 """ 

703 Initializes the :exc:`AlreadyRegisteredError` instance. 

704 

705 *name* (:class:`str`) is the name of the registered pattern. 

706 

707 *pattern_factory* (:class:`~collections.abc.Callable`) is the registered 

708 pattern factory. 

709 """ 

710 super().__init__(name, pattern_factory) 

711 

712 @property 

713 def message(self) -> str: 

714 """ 

715 *message* (:class:`str`) is the error message. 

716 """ 

717 return ( 

718 f"{self.name!r} is already registered for pattern factory=" 

719 f"{self.pattern_factory!r}." 

720 ) 

721 

722 @property 

723 def name(self) -> str: 

724 """ 

725 *name* (:class:`str`) is the name of the registered pattern. 

726 """ 

727 return self.args[0] 

728 

729 @property 

730 def pattern_factory(self) -> Callable[[Union[str, bytes]], Pattern]: 

731 """ 

732 *pattern_factory* (:class:`~collections.abc.Callable`) is the registered 

733 pattern factory. 

734 """ 

735 return self.args[1] 

736 

737 

738class RecursivePathError(Exception): 

739 """ 

740 The :exc:`RecursivePathError` exception is raised when recursion is detected. 

741 """ 

742 

743 def __init__( 

744 self, 

745 real_path: str, 

746 first_path: str, 

747 second_path: str, 

748 ) -> None: 

749 """ 

750 Initializes the :exc:`RecursivePathError` instance. 

751 

752 *real_path* (:class:`str`) is the real path that recursion was encountered 

753 on. 

754 

755 *first_path* (:class:`str`) is the first path encountered for *real_path*. 

756 

757 *second_path* (:class:`str`) is the second path encountered for *real_path*. 

758 """ 

759 super().__init__(real_path, first_path, second_path) 

760 

761 @property 

762 def first_path(self) -> str: 

763 """ 

764 *first_path* (:class:`str`) is the first path encountered for 

765 :attr:`self.real_path <RecursivePathError.real_path>`. 

766 """ 

767 return self.args[1] 

768 

769 @property 

770 def message(self) -> str: 

771 """ 

772 *message* (:class:`str`) is the error message. 

773 """ 

774 return ( 

775 f"Real path {self.real_path!r} was encountered at {self.first_path!r} " 

776 f"and then {self.second_path!r}." 

777 ) 

778 

779 @property 

780 def real_path(self) -> str: 

781 """ 

782 *real_path* (:class:`str`) is the real path that recursion was 

783 encountered on. 

784 """ 

785 return self.args[0] 

786 

787 @property 

788 def second_path(self) -> str: 

789 """ 

790 *second_path* (:class:`str`) is the second path encountered for 

791 :attr:`self.real_path <RecursivePathError.real_path>`. 

792 """ 

793 return self.args[2] 

794 

795 

796@dataclass(frozen=True) 

797class CheckResult(Generic[TStrPath]): 

798 """ 

799 The :class:`CheckResult` class contains information about the file and which 

800 pattern matched it. 

801 """ 

802 

803 # Make the class dict-less. 

804 __slots__ = ( 

805 'file', 

806 'include', 

807 'index', 

808 ) 

809 

810 file: TStrPath 

811 """ 

812 *file* (:class:`str` or :class:`os.PathLike`) is the file path. 

813 """ 

814 

815 include: Optional[bool] 

816 """ 

817 *include* (:class:`bool` or :data:`None`) is whether to include or exclude the 

818 file. If :data:`None`, no pattern matched. 

819 """ 

820 

821 index: Optional[int] 

822 """ 

823 *index* (:class:`int` or :data:`None`) is the index of the last pattern that 

824 matched. If :data:`None`, no pattern matched. 

825 """ 

826 

827 

828class MatchDetail(object): 

829 """ 

830 The :class:`.MatchDetail` class contains information about 

831 """ 

832 

833 # Make the class dict-less. 

834 __slots__ = ('patterns',) 

835 

836 def __init__(self, patterns: Sequence[Pattern]) -> None: 

837 """ 

838 Initialize the :class:`.MatchDetail` instance. 

839 

840 *patterns* (:class:`~collections.abc.Sequence` of :class:`.Pattern`) 

841 contains the patterns that matched the file in the order they were encountered. 

842 """ 

843 

844 self.patterns = patterns 

845 """ 

846 *patterns* (:class:`~collections.abc.Sequence` of :class:`.Pattern`) 

847 contains the patterns that matched the file in the order they were 

848 encountered. 

849 """ 

850 

851 

852class TreeEntry(object): 

853 """ 

854 The :class:`TreeEntry` class contains information about a file-system entry. 

855 """ 

856 

857 # Make the class dict-less. 

858 __slots__ = ('_lstat', 'name', 'path', '_stat') 

859 

860 def __init__( 

861 self, 

862 name: str, 

863 path: str, 

864 lstat: os.stat_result, 

865 stat: os.stat_result, 

866 ) -> None: 

867 """ 

868 Initialize the :class:`TreeEntry` instance. 

869 

870 *name* (:class:`str`) is the base name of the entry. 

871 

872 *path* (:class:`str`) is the relative path of the entry. 

873 

874 *lstat* (:class:`os.stat_result`) is the stat result of the direct entry. 

875 

876 *stat* (:class:`os.stat_result`) is the stat result of the entry, 

877 potentially linked. 

878 """ 

879 

880 self._lstat: os.stat_result = lstat 

881 """ 

882 *_lstat* (:class:`os.stat_result`) is the stat result of the direct entry. 

883 """ 

884 

885 self.name: str = name 

886 """ 

887 *name* (:class:`str`) is the base name of the entry. 

888 """ 

889 

890 self.path: str = path 

891 """ 

892 *path* (:class:`str`) is the path of the entry. 

893 """ 

894 

895 self._stat: os.stat_result = stat 

896 """ 

897 *_stat* (:class:`os.stat_result`) is the stat result of the linked entry. 

898 """ 

899 

900 def is_dir(self, follow_links: Optional[bool] = None) -> bool: 

901 """ 

902 Get whether the entry is a directory. 

903 

904 *follow_links* (:class:`bool` or :data:`None`) is whether to follow symbolic 

905 links. If this is :data:`True`, a symlink to a directory will result in 

906 :data:`True`. Default is :data:`None` for :data:`True`. 

907 

908 Returns whether the entry is a directory (:class:`bool`). 

909 """ 

910 if follow_links is None: 

911 follow_links = True 

912 

913 node_stat = self._stat if follow_links else self._lstat 

914 return stat.S_ISDIR(node_stat.st_mode) 

915 

916 def is_file(self, follow_links: Optional[bool] = None) -> bool: 

917 """ 

918 Get whether the entry is a regular file. 

919 

920 *follow_links* (:class:`bool` or :data:`None`) is whether to follow symbolic 

921 links. If this is :data:`True`, a symlink to a regular file will result in 

922 :data:`True`. Default is :data:`None` for :data:`True`. 

923 

924 Returns whether the entry is a regular file (:class:`bool`). 

925 """ 

926 if follow_links is None: 

927 follow_links = True 

928 

929 node_stat = self._stat if follow_links else self._lstat 

930 return stat.S_ISREG(node_stat.st_mode) 

931 

932 def is_symlink(self) -> bool: 

933 """ 

934 Returns whether the entry is a symbolic link (:class:`bool`). 

935 """ 

936 return stat.S_ISLNK(self._lstat.st_mode) 

937 

938 def stat(self, follow_links: Optional[bool] = None) -> os.stat_result: 

939 """ 

940 Get the cached stat result for the entry. 

941 

942 *follow_links* (:class:`bool` or :data:`None`) is whether to follow symbolic 

943 links. If this is :data:`True`, the stat result of the linked file will be 

944 returned. Default is :data:`None` for :data:`True`. 

945 

946 Returns that stat result (:class:`os.stat_result`). 

947 """ 

948 if follow_links is None: 

949 follow_links = True 

950 

951 return self._stat if follow_links else self._lstat 

952 

953 

954def __getattr__(name: str): 

955 """ 

956 Look up a deprecated module-level attribute. 

957 """ 

958 if name == 'RecursionError': 

959 # Deprecated since 1.2.0. It conflicts with Python 3.5's built-in 

960 # RecursionError. 

961 warnings.warn(( 

962 "pathspec.util.RecursionError is deprecated, use RecursivePathError " 

963 "instead." 

964 ), DeprecationWarning, stacklevel=2) 

965 return RecursivePathError 

966 

967 raise AttributeError(f"module {__name__!r} has no attribute {name!r}")