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

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

234 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 

11from collections.abc import ( 

12 Collection, 

13 Iterable, 

14 Iterator, 

15 Sequence) 

16from dataclasses import ( 

17 dataclass) 

18from typing import ( 

19 Any, 

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

21 Generic, 

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

23 TypeVar, 

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

25 cast) 

26 

27from .pattern import ( 

28 Pattern) 

29from ._typing import ( 

30 AnyStr, # Removed in 3.18. 

31 deprecated) # Added in 3.13. 

32 

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

34 

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

36""" 

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

38to specialize the type of patterns. 

39""" 

40 

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

42""" 

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

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

45""" 

46 

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

48""" 

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

50""" 

51 

52NORMALIZE_PATH_SEPS = [ 

53 cast(str, __sep) 

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

55 if __sep and __sep != posixpath.sep 

56] 

57""" 

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

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

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

61:data:`os.altsep`. 

62""" 

63 

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

65""" 

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

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

68""" 

69 

70 

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

72 """ 

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

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

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

76 

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

78 

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

80 """ 

81 str_path = str(path) 

82 if path.is_dir(): 

83 str_path += os.sep 

84 

85 return str_path 

86 

87 

88def check_match_file( 

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

90 file: str, 

91 is_reversed: Optional[bool] = None, 

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

93 """ 

94 Check the file against the patterns. 

95 

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

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

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

99 

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

101 *patterns*. 

102 

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

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

105 Reversing the order of the patterns is an optimization. 

106 

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

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

109 :data:`None`). 

110 """ 

111 if is_reversed: 

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

113 # precedence. 

114 for index, pattern in patterns: 

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

116 return pattern.include, index 

117 

118 return None, None 

119 

120 else: 

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

122 out_include: Optional[bool] = None 

123 out_index: Optional[int] = None 

124 for index, pattern in patterns: 

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

126 out_include = pattern.include 

127 out_index = index 

128 

129 return out_include, out_index 

130 

131 

132def detailed_match_files( 

133 patterns: Iterable[Pattern], 

134 files: Iterable[str], 

135 all_matches: Optional[bool] = None, 

136) -> dict[str, MatchDetail]: 

137 """ 

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

139 files. 

140 

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

142 the patterns to use. 

143 

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

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

146 

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

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

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

150 

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

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

153 """ 

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

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

156 for pattern in patterns: 

157 if pattern.include is not None: 

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

159 if pattern.include: 

160 # Add files and record pattern. 

161 for result_file in result_files: 

162 if result_file in return_files: 

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

164 if all_matches: 

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

166 else: 

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

168 else: 

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

170 

171 else: 

172 # Remove files. 

173 for file in result_files: 

174 del return_files[file] 

175 

176 return return_files 

177 

178 

179def _filter_check_patterns( 

180 patterns: Iterable[Pattern], 

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

182 """ 

183 Filters out null-patterns. 

184 

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

186 the patterns. 

187 

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

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

190 (:class:`.Pattern`). 

191 """ 

192 return [ 

193 (__index, __pat) 

194 for __index, __pat in enumerate(patterns) 

195 if __pat.include is not None 

196 ] 

197 

198 

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

200 """ 

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

202 

203 *value* is the value to check, 

204 

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

206 """ 

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

208 

209 

210@deprecated(( 

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

212)) 

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

214 """ 

215 .. version-deprecated:: 0.10.0 

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

217 """ 

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

219 

220 

221def iter_tree_entries( 

222 root: StrPath, 

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

224 follow_links: Optional[bool] = None, 

225) -> Iterator['TreeEntry']: 

226 """ 

227 Walks the specified directory for all files and directories. 

228 

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

230 

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

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

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

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

235 

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

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

238 :data:`True`. 

239 

240 Raises :exc:`.RecursionError` if recursion is detected. 

241 

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

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

244 """ 

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

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

247 

248 if follow_links is None: 

249 follow_links = True 

250 

251 yield from _iter_tree_entries_next(os.path.abspath(root), '', {}, on_error, follow_links) 

252 

253 

254def _iter_tree_entries_next( 

255 root_full: str, 

256 dir_rel: str, 

257 memo: dict[str, str], 

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

259 follow_links: bool, 

260) -> Iterator['TreeEntry']: 

261 """ 

262 Scan the directory for all descendant files. 

263 

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

265 

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

267 *root_full*. 

268 

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

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

271 

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

273 the error handler for file-system exceptions. 

274 

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

276 to directories. 

277 

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

279 """ 

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

281 dir_real = os.path.realpath(dir_full) 

282 

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

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

285 if dir_real not in memo: 

286 memo[dir_real] = dir_rel 

287 else: 

288 raise RecursionError(real_path=dir_real, first_path=memo[dir_real], second_path=dir_rel) 

289 

290 with os.scandir(dir_full) as scan_iter: 

291 node_ent: os.DirEntry 

292 for node_ent in scan_iter: 

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

294 

295 # Inspect child node. 

296 try: 

297 node_lstat = node_ent.stat(follow_symlinks=False) 

298 except OSError as e: 

299 if on_error is not None: 

300 on_error(e) 

301 continue 

302 

303 if node_ent.is_symlink(): 

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

305 try: 

306 node_stat = node_ent.stat() 

307 except OSError as e: 

308 if on_error is not None: 

309 on_error(e) 

310 continue 

311 else: 

312 node_stat = node_lstat 

313 

314 if node_ent.is_dir(follow_symlinks=follow_links): 

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

316 # files. 

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

318 

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

320 

321 elif node_ent.is_file() or node_ent.is_symlink(): 

322 # Child node is either a file or an unfollowed link, yield it. 

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

324 

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

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

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

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

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

330 del memo[dir_real] 

331 

332 

333def iter_tree_files( 

334 root: StrPath, 

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

336 follow_links: Optional[bool] = None, 

337) -> Iterator[str]: 

338 """ 

339 Walks the specified directory for all files. 

340 

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

342 for files. 

343 

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

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

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

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

348 

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

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

351 :data:`True`. 

352 

353 Raises :exc:`.RecursionError` if recursion is detected. 

354 

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

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

357 """ 

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

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

360 

361 if follow_links is None: 

362 follow_links = True 

363 

364 yield from _iter_tree_files_next(os.path.abspath(root), '', {}, on_error, follow_links) 

365 

366 

367def _iter_tree_files_next( 

368 root_full: str, 

369 dir_rel: str, 

370 memo: dict[str, str], 

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

372 follow_links: bool, 

373) -> Iterator[str]: 

374 """ 

375 Scan the directory for all descendant files. 

376 

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

378 

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

380 *root_full*. 

381 

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

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

384 

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

386 the error handler for file-system exceptions. 

387 

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

389 to directories. 

390 

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

392 """ 

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

394 dir_real = os.path.realpath(dir_full) 

395 

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

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

398 if dir_real not in memo: 

399 memo[dir_real] = dir_rel 

400 else: 

401 raise RecursionError(real_path=dir_real, first_path=memo[dir_real], second_path=dir_rel) 

402 

403 with os.scandir(dir_full) as scan_iter: 

404 node_ent: os.DirEntry 

405 for node_ent in scan_iter: 

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

407 

408 if node_ent.is_dir(follow_symlinks=follow_links): 

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

410 # files. 

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

412 

413 elif node_ent.is_file(): 

414 # Child node is a file, yield it. 

415 yield node_rel 

416 

417 elif not follow_links and node_ent.is_symlink(): 

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

419 yield node_rel 

420 

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

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

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

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

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

426 del memo[dir_real] 

427 

428 

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

430 """ 

431 Looks up a registered pattern factory by name. 

432 

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

434 

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

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

437 """ 

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

439 

440 

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

442 """ 

443 Matches the file to the patterns. 

444 

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

446 the patterns to use. 

447 

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

449 *patterns*. 

450 

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

452 """ 

453 matched = False 

454 for pattern in patterns: 

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

456 matched = pattern.include 

457 

458 return matched 

459 

460 

461@deprecated(( 

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

463 "better results." 

464)) 

465def match_files( 

466 patterns: Iterable[Pattern], 

467 files: Iterable[str], 

468) -> set[str]: 

469 """ 

470 .. version-deprecated:: 0.10.0 

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

472 loop for better results. 

473 

474 Matches the files to the patterns. 

475 

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

477 the patterns to use. 

478 

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

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

481 

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

483 """ 

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

485 

486 return_files = set() 

487 for file in files: 

488 if match_file(use_patterns, file): 

489 return_files.add(file) 

490 

491 return return_files 

492 

493 

494def normalize_file( 

495 file: StrPath, 

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

497) -> str: 

498 """ 

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

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

501 

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

503 

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

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

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

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

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

509 ``()``). 

510 

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

512 """ 

513 # Normalize path separators. 

514 if separators is None: 

515 separators = NORMALIZE_PATH_SEPS 

516 

517 assert separators is not None, separators 

518 

519 # Convert path object to string. 

520 norm_file: str = os.fspath(file) 

521 

522 for sep in separators: 

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

524 

525 if norm_file.startswith('/'): 

526 # Make path relative. 

527 norm_file = norm_file[1:] 

528 

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

530 # Remove current directory prefix. 

531 norm_file = norm_file[2:] 

532 

533 return norm_file 

534 

535 

536@deprecated(( 

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

538 "loop for better results." 

539)) 

540def normalize_files( 

541 files: Iterable[StrPath], 

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

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

544 """ 

545 .. version-deprecated:: 0.10.0 

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

547 with a loop for better results. 

548 

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

550 

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

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

553 

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

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

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

557 

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

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

560 :class:`os.PathLike`). 

561 """ 

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

563 for path in files: 

564 norm_file = normalize_file(path, separators=separators) 

565 if norm_file in norm_files: 

566 norm_files[norm_file].append(path) 

567 else: 

568 norm_files[norm_file] = [path] 

569 

570 return norm_files 

571 

572 

573def register_pattern( 

574 name: str, 

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

576 override: Optional[bool] = None, 

577) -> None: 

578 """ 

579 Registers the specified pattern factory. 

580 

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

582 

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

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

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

586 

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

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

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

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

591 """ 

592 if not isinstance(name, str): 

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

594 

595 if not callable(pattern_factory): 

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

597 

598 if name in _registered_patterns and not override: 

599 raise AlreadyRegisteredError(name, _registered_patterns[name]) 

600 

601 _registered_patterns[name] = pattern_factory # type: ignore 

602 

603 

604class AlreadyRegisteredError(Exception): 

605 """ 

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

607 is registered under a name already in use. 

608 """ 

609 

610 def __init__( 

611 self, 

612 name: str, 

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

614 ) -> None: 

615 """ 

616 Initializes the :exc:`AlreadyRegisteredError` instance. 

617 

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

619 

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

621 pattern factory. 

622 """ 

623 super().__init__(name, pattern_factory) 

624 

625 @property 

626 def message(self) -> str: 

627 """ 

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

629 """ 

630 return ( 

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

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

633 ) 

634 

635 @property 

636 def name(self) -> str: 

637 """ 

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

639 """ 

640 return self.args[0] 

641 

642 @property 

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

644 """ 

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

646 pattern factory. 

647 """ 

648 return self.args[1] 

649 

650 

651class RecursionError(Exception): 

652 """ 

653 The :exc:`RecursionError` exception is raised when recursion is detected. 

654 """ 

655 

656 def __init__( 

657 self, 

658 real_path: str, 

659 first_path: str, 

660 second_path: str, 

661 ) -> None: 

662 """ 

663 Initializes the :exc:`RecursionError` instance. 

664 

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

666 on. 

667 

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

669 

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

671 """ 

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

673 

674 @property 

675 def first_path(self) -> str: 

676 """ 

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

678 :attr:`self.real_path <RecursionError.real_path>`. 

679 """ 

680 return self.args[1] 

681 

682 @property 

683 def message(self) -> str: 

684 """ 

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

686 """ 

687 return ( 

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

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

690 ) 

691 

692 @property 

693 def real_path(self) -> str: 

694 """ 

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

696 encountered on. 

697 """ 

698 return self.args[0] 

699 

700 @property 

701 def second_path(self) -> str: 

702 """ 

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

704 :attr:`self.real_path <RecursionError.real_path>`. 

705 """ 

706 return self.args[2] 

707 

708 

709@dataclass(frozen=True) 

710class CheckResult(Generic[TStrPath]): 

711 """ 

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

713 pattern matched it. 

714 """ 

715 

716 # Make the class dict-less. 

717 __slots__ = ( 

718 'file', 

719 'include', 

720 'index', 

721 ) 

722 

723 file: TStrPath 

724 """ 

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

726 """ 

727 

728 include: Optional[bool] 

729 """ 

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

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

732 """ 

733 

734 index: Optional[int] 

735 """ 

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

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

738 """ 

739 

740 

741class MatchDetail(object): 

742 """ 

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

744 """ 

745 

746 # Make the class dict-less. 

747 __slots__ = ('patterns',) 

748 

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

750 """ 

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

752 

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

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

755 """ 

756 

757 self.patterns = patterns 

758 """ 

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

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

761 encountered. 

762 """ 

763 

764 

765class TreeEntry(object): 

766 """ 

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

768 """ 

769 

770 # Make the class dict-less. 

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

772 

773 def __init__( 

774 self, 

775 name: str, 

776 path: str, 

777 lstat: os.stat_result, 

778 stat: os.stat_result, 

779 ) -> None: 

780 """ 

781 Initialize the :class:`TreeEntry` instance. 

782 

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

784 

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

786 

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

788 

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

790 potentially linked. 

791 """ 

792 

793 self._lstat: os.stat_result = lstat 

794 """ 

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

796 """ 

797 

798 self.name: str = name 

799 """ 

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

801 """ 

802 

803 self.path: str = path 

804 """ 

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

806 """ 

807 

808 self._stat: os.stat_result = stat 

809 """ 

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

811 """ 

812 

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

814 """ 

815 Get whether the entry is a directory. 

816 

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

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

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

820 

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

822 """ 

823 if follow_links is None: 

824 follow_links = True 

825 

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

827 return stat.S_ISDIR(node_stat.st_mode) 

828 

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

830 """ 

831 Get whether the entry is a regular file. 

832 

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

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

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

836 

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

838 """ 

839 if follow_links is None: 

840 follow_links = True 

841 

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

843 return stat.S_ISREG(node_stat.st_mode) 

844 

845 def is_symlink(self) -> bool: 

846 """ 

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

848 """ 

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

850 

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

852 """ 

853 Get the cached stat result for the entry. 

854 

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

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

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

858 

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

860 """ 

861 if follow_links is None: 

862 follow_links = True 

863 

864 return self._stat if follow_links else self._lstat