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

191 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:35 +0000

1# encoding: utf-8 

2""" 

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

4""" 

5 

6import os 

7import os.path 

8import posixpath 

9import stat 

10try: 

11 from typing import ( 

12 Any, 

13 AnyStr, 

14 Callable, 

15 Dict, 

16 Iterable, 

17 Iterator, 

18 List, 

19 Optional, 

20 Sequence, 

21 Set, 

22 Text, 

23 Union) 

24except ImportError: 

25 pass 

26try: 

27 # Python 3.6+ type hints. 

28 from os import PathLike 

29 from typing import Collection 

30except ImportError: 

31 pass 

32 

33from .compat import ( 

34 CollectionType, 

35 IterableType, 

36 string_types, 

37 unicode) 

38from .pattern import Pattern 

39 

40NORMALIZE_PATH_SEPS = [sep for sep in [os.sep, os.altsep] if sep and sep != posixpath.sep] 

41""" 

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

43separators that need to be normalized to the POSIX separator for the 

44current operating system. The separators are determined by examining 

45:data:`os.sep` and :data:`os.altsep`. 

46""" 

47 

48_registered_patterns = {} 

49""" 

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

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

52""" 

53 

54 

55def detailed_match_files(patterns, files, all_matches=None): 

56 # type: (Iterable[Pattern], Iterable[Text], Optional[bool]) -> Dict[Text, 'MatchDetail'] 

57 """ 

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

59 the files. 

60 

61 *patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.Pattern`) 

62 contains the patterns to use. 

63 

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

65 the normalized file paths to be matched against *patterns*. 

66 

67 *all_matches* (:class:`boot` or :data:`None`) is whether to return all 

68 matches patterns (:data:`True`), or only the last matched pattern 

69 (:data:`False`). Default is :data:`None` for :data:`False`. 

70 

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

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

73 """ 

74 all_files = files if isinstance(files, CollectionType) else list(files) 

75 return_files = {} 

76 for pattern in patterns: 

77 if pattern.include is not None: 

78 result_files = pattern.match(all_files) 

79 if pattern.include: 

80 # Add files and record pattern. 

81 for result_file in result_files: 

82 if result_file in return_files: 

83 if all_matches: 

84 return_files[result_file].patterns.append(pattern) 

85 else: 

86 return_files[result_file].patterns[0] = pattern 

87 else: 

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

89 

90 else: 

91 # Remove files. 

92 for file in result_files: 

93 del return_files[file] 

94 

95 return return_files 

96 

97 

98def _is_iterable(value): 

99 # type: (Any) -> bool 

100 """ 

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

102 

103 *value* is the value to check, 

104 

105 Returns whether *value* is a iterable (:class:`bool`). 

106 """ 

107 return isinstance(value, IterableType) and not isinstance(value, (unicode, bytes)) 

108 

109 

110def iter_tree_entries(root, on_error=None, follow_links=None): 

111 # type: (Text, Optional[Callable], Optional[bool]) -> Iterator['TreeEntry'] 

112 """ 

113 Walks the specified directory for all files and directories. 

114 

115 *root* (:class:`str`) is the root directory to search. 

116 

117 *on_error* (:class:`~collections.abc.Callable` or :data:`None`) 

118 optionally is the error handler for file-system exceptions. It will be 

119 called with the exception (:exc:`OSError`). Reraise the exception to 

120 abort the walk. Default is :data:`None` to ignore file-system 

121 exceptions. 

122 

123 *follow_links* (:class:`bool` or :data:`None`) optionally is whether 

124 to walk symbolic links that resolve to directories. Default is 

125 :data:`None` for :data:`True`. 

126 

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

128 

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

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

131 """ 

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

133 raise TypeError("on_error:{!r} is not callable.".format(on_error)) 

134 

135 if follow_links is None: 

136 follow_links = True 

137 

138 for entry in _iter_tree_entries_next(os.path.abspath(root), '', {}, on_error, follow_links): 

139 yield entry 

140 

141 

142def iter_tree_files(root, on_error=None, follow_links=None): 

143 # type: (Text, Optional[Callable], Optional[bool]) -> Iterator[Text] 

144 """ 

145 Walks the specified directory for all files. 

146 

147 *root* (:class:`str`) is the root directory to search for files. 

148 

149 *on_error* (:class:`~collections.abc.Callable` or :data:`None`) 

150 optionally is the error handler for file-system exceptions. It will be 

151 called with the exception (:exc:`OSError`). Reraise the exception to 

152 abort the walk. Default is :data:`None` to ignore file-system 

153 exceptions. 

154 

155 *follow_links* (:class:`bool` or :data:`None`) optionally is whether 

156 to walk symbolic links that resolve to directories. Default is 

157 :data:`None` for :data:`True`. 

158 

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

160 

161 Returns an :class:`~collections.abc.Iterator` yielding the path to 

162 each file (:class:`str`) relative to *root*. 

163 """ 

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

165 raise TypeError("on_error:{!r} is not callable.".format(on_error)) 

166 

167 if follow_links is None: 

168 follow_links = True 

169 

170 for entry in _iter_tree_entries_next(os.path.abspath(root), '', {}, on_error, follow_links): 

171 if not entry.is_dir(follow_links): 

172 yield entry.path 

173 

174 

175# Alias `iter_tree_files()` as `iter_tree()`. 

176iter_tree = iter_tree_files 

177 

178 

179def _iter_tree_entries_next(root_full, dir_rel, memo, on_error, follow_links): 

180 # type: (Text, Text, Dict[Text, Text], Callable, bool) -> Iterator['TreeEntry'] 

181 """ 

182 Scan the directory for all descendant files. 

183 

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

185 

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

187 *root_full*. 

188 

189 *memo* (:class:`dict`) keeps track of ancestor directories 

190 encountered. Maps each ancestor real path (:class:`str`) to relative 

191 path (:class:`str`). 

192 

193 *on_error* (:class:`~collections.abc.Callable` or :data:`None`) 

194 optionally is the error handler for file-system exceptions. 

195 

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

197 resolve to directories. 

198 

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

200 """ 

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

202 dir_real = os.path.realpath(dir_full) 

203 

204 # Remember each encountered ancestor directory and its canonical 

205 # (real) path. If a canonical path is encountered more than once, 

206 # recursion has occurred. 

207 if dir_real not in memo: 

208 memo[dir_real] = dir_rel 

209 else: 

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

211 

212 for node_name in os.listdir(dir_full): 

213 node_rel = os.path.join(dir_rel, node_name) 

214 node_full = os.path.join(root_full, node_rel) 

215 

216 # Inspect child node. 

217 try: 

218 node_lstat = os.lstat(node_full) 

219 except OSError as e: 

220 if on_error is not None: 

221 on_error(e) 

222 continue 

223 

224 if stat.S_ISLNK(node_lstat.st_mode): 

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

226 is_link = True 

227 try: 

228 node_stat = os.stat(node_full) 

229 except OSError as e: 

230 if on_error is not None: 

231 on_error(e) 

232 continue 

233 else: 

234 is_link = False 

235 node_stat = node_lstat 

236 

237 if stat.S_ISDIR(node_stat.st_mode) and (follow_links or not is_link): 

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

239 # descendant files. 

240 yield TreeEntry(node_name, node_rel, node_lstat, node_stat) 

241 

242 for entry in _iter_tree_entries_next(root_full, node_rel, memo, on_error, follow_links): 

243 yield entry 

244 

245 elif stat.S_ISREG(node_stat.st_mode) or is_link: 

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

247 yield TreeEntry(node_name, node_rel, node_lstat, node_stat) 

248 

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

250 # from the ancestors memo once we are done with it. This allows the 

251 # same directory to appear multiple times. If this is not done, the 

252 # second occurrence of the directory will be incorrectly interpreted 

253 # as a recursion. See <https://github.com/cpburnz/python-path-specification/pull/7>. 

254 del memo[dir_real] 

255 

256 

257def lookup_pattern(name): 

258 # type: (Text) -> Callable[[AnyStr], Pattern] 

259 """ 

260 Lookups a registered pattern factory by name. 

261 

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

263 

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

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

266 """ 

267 return _registered_patterns[name] 

268 

269 

270def match_file(patterns, file): 

271 # type: (Iterable[Pattern], Text) -> bool 

272 """ 

273 Matches the file to the patterns. 

274 

275 *patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.Pattern`) 

276 contains the patterns to use. 

277 

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

279 against *patterns*. 

280 

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

282 """ 

283 matched = False 

284 for pattern in patterns: 

285 if pattern.include is not None: 

286 if file in pattern.match((file,)): 

287 matched = pattern.include 

288 return matched 

289 

290 

291def match_files(patterns, files): 

292 # type: (Iterable[Pattern], Iterable[Text]) -> Set[Text] 

293 """ 

294 Matches the files to the patterns. 

295 

296 *patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.Pattern`) 

297 contains the patterns to use. 

298 

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

300 the normalized file paths to be matched against *patterns*. 

301 

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

303 """ 

304 all_files = files if isinstance(files, CollectionType) else list(files) 

305 return_files = set() 

306 for pattern in patterns: 

307 if pattern.include is not None: 

308 result_files = pattern.match(all_files) 

309 if pattern.include: 

310 return_files.update(result_files) 

311 else: 

312 return_files.difference_update(result_files) 

313 return return_files 

314 

315 

316def _normalize_entries(entries, separators=None): 

317 # type: (Iterable['TreeEntry'], Optional[Collection[Text]]) -> Dict[Text, 'TreeEntry'] 

318 """ 

319 Normalizes the entry paths to use the POSIX path separator. 

320 

321 *entries* (:class:`~collections.abc.Iterable` of :class:`.TreeEntry`) 

322 contains the entries to be normalized. 

323 

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

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

326 See :func:`normalize_file` for more information. 

327 

328 Returns a :class:`dict` mapping the each normalized file path (:class:`str`) 

329 to the entry (:class:`.TreeEntry`) 

330 """ 

331 norm_files = {} 

332 for entry in entries: 

333 norm_files[normalize_file(entry.path, separators=separators)] = entry 

334 return norm_files 

335 

336 

337def normalize_file(file, separators=None): 

338 # type: (Union[Text, PathLike], Optional[Collection[Text]]) -> Text 

339 """ 

340 Normalizes the file path to use the POSIX path separator (i.e., 

341 ``'/'``), and make the paths relative (remove leading ``'/'``). 

342 

343 *file* (:class:`str` or :class:`pathlib.PurePath`) is the file path. 

344 

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

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

347 This does not need to include the POSIX path separator (``'/'``), but 

348 including it will not affect the results. Default is :data:`None` for 

349 :data:`NORMALIZE_PATH_SEPS`. To prevent normalization, pass an empty 

350 container (e.g., an empty tuple ``()``). 

351 

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

353 """ 

354 # Normalize path separators. 

355 if separators is None: 

356 separators = NORMALIZE_PATH_SEPS 

357 

358 # Convert path object to string. 

359 norm_file = str(file) 

360 

361 for sep in separators: 

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

363 

364 if norm_file.startswith('/'): 

365 # Make path relative. 

366 norm_file = norm_file[1:] 

367 

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

369 # Remove current directory prefix. 

370 norm_file = norm_file[2:] 

371 

372 return norm_file 

373 

374 

375def normalize_files(files, separators=None): 

376 # type: (Iterable[Union[str, PathLike]], Optional[Collection[Text]]) -> Dict[Text, List[Union[str, PathLike]]] 

377 """ 

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

379 

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

381 :class:`pathlib.PurePath`) contains the file paths to be normalized. 

382 

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

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

385 See :func:`normalize_file` for more information. 

386 

387 Returns a :class:`dict` mapping the each normalized file path 

388 (:class:`str`) to the original file paths (:class:`list` of 

389 :class:`str` or :class:`pathlib.PurePath`). 

390 """ 

391 norm_files = {} 

392 for path in files: 

393 norm_file = normalize_file(path, separators=separators) 

394 if norm_file in norm_files: 

395 norm_files[norm_file].append(path) 

396 else: 

397 norm_files[norm_file] = [path] 

398 

399 return norm_files 

400 

401 

402def register_pattern(name, pattern_factory, override=None): 

403 # type: (Text, Callable[[AnyStr], Pattern], Optional[bool]) -> None 

404 """ 

405 Registers the specified pattern factory. 

406 

407 *name* (:class:`str`) is the name to register the pattern factory 

408 under. 

409 

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

411 compile patterns. It must accept an uncompiled pattern (:class:`str`) 

412 and return the compiled pattern (:class:`.Pattern`). 

413 

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

415 allow overriding an already registered pattern under the same name 

416 (:data:`True`), instead of raising an :exc:`AlreadyRegisteredError` 

417 (:data:`False`). Default is :data:`None` for :data:`False`. 

418 """ 

419 if not isinstance(name, string_types): 

420 raise TypeError("name:{!r} is not a string.".format(name)) 

421 if not callable(pattern_factory): 

422 raise TypeError("pattern_factory:{!r} is not callable.".format(pattern_factory)) 

423 if name in _registered_patterns and not override: 

424 raise AlreadyRegisteredError(name, _registered_patterns[name]) 

425 _registered_patterns[name] = pattern_factory 

426 

427 

428class AlreadyRegisteredError(Exception): 

429 """ 

430 The :exc:`AlreadyRegisteredError` exception is raised when a pattern 

431 factory is registered under a name already in use. 

432 """ 

433 

434 def __init__(self, name, pattern_factory): 

435 # type: (Text, Callable[[AnyStr], Pattern]) -> None 

436 """ 

437 Initializes the :exc:`AlreadyRegisteredError` instance. 

438 

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

440 

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

442 registered pattern factory. 

443 """ 

444 super(AlreadyRegisteredError, self).__init__(name, pattern_factory) 

445 

446 @property 

447 def message(self): 

448 # type: () -> Text 

449 """ 

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

451 """ 

452 return "{name!r} is already registered for pattern factory:{pattern_factory!r}.".format( 

453 name=self.name, 

454 pattern_factory=self.pattern_factory, 

455 ) 

456 

457 @property 

458 def name(self): 

459 # type: () -> Text 

460 """ 

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

462 """ 

463 return self.args[0] 

464 

465 @property 

466 def pattern_factory(self): 

467 # type: () -> Callable[[AnyStr], Pattern] 

468 """ 

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

470 registered pattern factory. 

471 """ 

472 return self.args[1] 

473 

474 

475class RecursionError(Exception): 

476 """ 

477 The :exc:`RecursionError` exception is raised when recursion is 

478 detected. 

479 """ 

480 

481 def __init__(self, real_path, first_path, second_path): 

482 # type: (Text, Text, Text) -> None 

483 """ 

484 Initializes the :exc:`RecursionError` instance. 

485 

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

487 encountered on. 

488 

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

490 *real_path*. 

491 

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

493 *real_path*. 

494 """ 

495 super(RecursionError, self).__init__(real_path, first_path, second_path) 

496 

497 @property 

498 def first_path(self): 

499 # type: () -> Text 

500 """ 

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

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

503 """ 

504 return self.args[1] 

505 

506 @property 

507 def message(self): 

508 # type: () -> Text 

509 """ 

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

511 """ 

512 return "Real path {real!r} was encountered at {first!r} and then {second!r}.".format( 

513 real=self.real_path, 

514 first=self.first_path, 

515 second=self.second_path, 

516 ) 

517 

518 @property 

519 def real_path(self): 

520 # type: () -> Text 

521 """ 

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

523 encountered on. 

524 """ 

525 return self.args[0] 

526 

527 @property 

528 def second_path(self): 

529 # type: () -> Text 

530 """ 

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

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

533 """ 

534 return self.args[2] 

535 

536 

537class MatchDetail(object): 

538 """ 

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

540 """ 

541 

542 #: Make the class dict-less. 

543 __slots__ = ('patterns',) 

544 

545 def __init__(self, patterns): 

546 # type: (Sequence[Pattern]) -> None 

547 """ 

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

549 

550 *patterns* (:class:`~collections.abc.Sequence` of :class:`~pathspec.pattern.Pattern`) 

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

552 encountered. 

553 """ 

554 

555 self.patterns = patterns 

556 """ 

557 *patterns* (:class:`~collections.abc.Sequence` of :class:`~pathspec.pattern.Pattern`) 

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

559 encountered. 

560 """ 

561 

562 

563class TreeEntry(object): 

564 """ 

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

566 entry. 

567 """ 

568 

569 #: Make the class dict-less. 

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

571 

572 def __init__(self, name, path, lstat, stat): 

573 # type: (Text, Text, os.stat_result, os.stat_result) -> None 

574 """ 

575 Initialize the :class:`.TreeEntry` instance. 

576 

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

578 

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

580 

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

582 entry. 

583 

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

585 potentially linked. 

586 """ 

587 

588 self._lstat = lstat 

589 """ 

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

591 entry. 

592 """ 

593 

594 self.name = name 

595 """ 

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

597 """ 

598 

599 self.path = path 

600 """ 

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

602 """ 

603 

604 self._stat = stat 

605 """ 

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

607 entry. 

608 """ 

609 

610 def is_dir(self, follow_links=None): 

611 # type: (Optional[bool]) -> bool 

612 """ 

613 Get whether the entry is a directory. 

614 

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

616 symbolic links. If this is :data:`True`, a symlink to a directory 

617 will result in :data:`True`. Default is :data:`None` for :data:`True`. 

618 

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

620 """ 

621 if follow_links is None: 

622 follow_links = True 

623 

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

625 return stat.S_ISDIR(node_stat.st_mode) 

626 

627 def is_file(self, follow_links=None): 

628 # type: (Optional[bool]) -> bool 

629 """ 

630 Get whether the entry is a regular file. 

631 

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

633 symbolic links. If this is :data:`True`, a symlink to a regular file 

634 will result in :data:`True`. Default is :data:`None` for :data:`True`. 

635 

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

637 """ 

638 if follow_links is None: 

639 follow_links = True 

640 

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

642 return stat.S_ISREG(node_stat.st_mode) 

643 

644 def is_symlink(self): 

645 # type: () -> bool 

646 """ 

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

648 """ 

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

650 

651 def stat(self, follow_links=None): 

652 # type: (Optional[bool]) -> os.stat_result 

653 """ 

654 Get the cached stat result for the entry. 

655 

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

657 symbolic links. If this is :data:`True`, the stat result of the 

658 linked file will be returned. Default is :data:`None` for :data:`True`. 

659 

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

661 """ 

662 if follow_links is None: 

663 follow_links = True 

664 

665 return self._stat if follow_links else self._lstat