]>
Commit | Line | Data |
---|---|---|
1 | """ | |
2 | This module provides utility methods for dealing with path-specs. | |
3 | """ | |
4 | ||
5 | import os | |
6 | import os.path | |
7 | import pathlib | |
8 | import posixpath | |
9 | import stat | |
10 | import sys | |
11 | import warnings | |
12 | from collections.abc import ( | |
13 | Collection as CollectionType, | |
14 | Iterable as IterableType) | |
15 | from os import ( | |
16 | PathLike) | |
17 | from typing import ( | |
18 | Any, | |
19 | AnyStr, | |
20 | Callable, | |
21 | Collection, | |
22 | Dict, | |
23 | Iterable, | |
24 | Iterator, | |
25 | List, | |
26 | Optional, | |
27 | Sequence, | |
28 | Set, | |
29 | Union) | |
30 | ||
31 | from .pattern import ( | |
32 | Pattern) | |
33 | ||
34 | if sys.version_info >= (3, 9): | |
35 | StrPath = Union[str, PathLike[str]] | |
36 | else: | |
37 | StrPath = Union[str, PathLike] | |
38 | ||
39 | NORMALIZE_PATH_SEPS = [ | |
40 | __sep | |
41 | for __sep in [os.sep, os.altsep] | |
42 | if __sep and __sep != posixpath.sep | |
43 | ] | |
44 | """ | |
45 | *NORMALIZE_PATH_SEPS* (:class:`list` of :class:`str`) contains the path | |
46 | separators that need to be normalized to the POSIX separator for the | |
47 | current operating system. The separators are determined by examining | |
48 | :data:`os.sep` and :data:`os.altsep`. | |
49 | """ | |
50 | ||
51 | _registered_patterns = {} | |
52 | """ | |
53 | *_registered_patterns* (:class:`dict`) maps a name (:class:`str`) to the | |
54 | registered pattern factory (:class:`~collections.abc.Callable`). | |
55 | """ | |
56 | ||
57 | ||
58 | def append_dir_sep(path: pathlib.Path) -> str: | |
59 | """ | |
60 | Appends the path separator to the path if the path is a directory. | |
61 | This can be used to aid in distinguishing between directories and | |
62 | files on the file-system by relying on the presence of a trailing path | |
63 | separator. | |
64 | ||
65 | *path* (:class:`pathlib.path`) is the path to use. | |
66 | ||
67 | Returns the path (:class:`str`). | |
68 | """ | |
69 | str_path = str(path) | |
70 | if path.is_dir(): | |
71 | str_path += os.sep | |
72 | ||
73 | return str_path | |
74 | ||
75 | ||
76 | def detailed_match_files( | |
77 | patterns: Iterable[Pattern], | |
78 | files: Iterable[str], | |
79 | all_matches: Optional[bool] = None, | |
80 | ) -> Dict[str, 'MatchDetail']: | |
81 | """ | |
82 | Matches the files to the patterns, and returns which patterns matched | |
83 | the files. | |
84 | ||
85 | *patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.Pattern`) | |
86 | contains the patterns to use. | |
87 | ||
88 | *files* (:class:`~collections.abc.Iterable` of :class:`str`) contains | |
89 | the normalized file paths to be matched against *patterns*. | |
90 | ||
91 | *all_matches* (:class:`boot` or :data:`None`) is whether to return all | |
92 | matches patterns (:data:`True`), or only the last matched pattern | |
93 | (:data:`False`). Default is :data:`None` for :data:`False`. | |
94 | ||
95 | Returns the matched files (:class:`dict`) which maps each matched file | |
96 | (:class:`str`) to the patterns that matched in order (:class:`.MatchDetail`). | |
97 | """ | |
98 | all_files = files if isinstance(files, CollectionType) else list(files) | |
99 | return_files = {} | |
100 | for pattern in patterns: | |
101 | if pattern.include is not None: | |
102 | result_files = pattern.match(all_files) # TODO: Replace with `.match_file()`. | |
103 | if pattern.include: | |
104 | # Add files and record pattern. | |
105 | for result_file in result_files: | |
106 | if result_file in return_files: | |
107 | if all_matches: | |
108 | return_files[result_file].patterns.append(pattern) | |
109 | else: | |
110 | return_files[result_file].patterns[0] = pattern | |
111 | else: | |
112 | return_files[result_file] = MatchDetail([pattern]) | |
113 | ||
114 | else: | |
115 | # Remove files. | |
116 | for file in result_files: | |
117 | del return_files[file] | |
118 | ||
119 | return return_files | |
120 | ||
121 | ||
122 | def _filter_patterns(patterns: Iterable[Pattern]) -> List[Pattern]: | |
123 | """ | |
124 | Filters out null-patterns. | |
125 | ||
126 | *patterns* (:class:`Iterable` of :class:`.Pattern`) contains the | |
127 | patterns. | |
128 | ||
129 | Returns the patterns (:class:`list` of :class:`.Pattern`). | |
130 | """ | |
131 | return [ | |
132 | __pat | |
133 | for __pat in patterns | |
134 | if __pat.include is not None | |
135 | ] | |
136 | ||
137 | ||
138 | def _is_iterable(value: Any) -> bool: | |
139 | """ | |
140 | Check whether the value is an iterable (excludes strings). | |
141 | ||
142 | *value* is the value to check, | |
143 | ||
144 | Returns whether *value* is a iterable (:class:`bool`). | |
145 | """ | |
146 | return isinstance(value, IterableType) and not isinstance(value, (str, bytes)) | |
147 | ||
148 | ||
149 | def iter_tree_entries( | |
150 | root: StrPath, | |
151 | on_error: Optional[Callable] = None, | |
152 | follow_links: Optional[bool] = None, | |
153 | ) -> Iterator['TreeEntry']: | |
154 | """ | |
155 | Walks the specified directory for all files and directories. | |
156 | ||
157 | *root* (:class:`str` or :class:`os.PathLike[str]`) is the root directory to | |
158 | search. | |
159 | ||
160 | *on_error* (:class:`~collections.abc.Callable` or :data:`None`) | |
161 | optionally is the error handler for file-system exceptions. It will be | |
162 | called with the exception (:exc:`OSError`). Reraise the exception to | |
163 | abort the walk. Default is :data:`None` to ignore file-system | |
164 | exceptions. | |
165 | ||
166 | *follow_links* (:class:`bool` or :data:`None`) optionally is whether | |
167 | to walk symbolic links that resolve to directories. Default is | |
168 | :data:`None` for :data:`True`. | |
169 | ||
170 | Raises :exc:`RecursionError` if recursion is detected. | |
171 | ||
172 | Returns an :class:`~collections.abc.Iterator` yielding each file or | |
173 | directory entry (:class:`.TreeEntry`) relative to *root*. | |
174 | """ | |
175 | if on_error is not None and not callable(on_error): | |
176 | raise TypeError(f"on_error:{on_error!r} is not callable.") | |
177 | ||
178 | if follow_links is None: | |
179 | follow_links = True | |
180 | ||
181 | yield from _iter_tree_entries_next(os.path.abspath(root), '', {}, on_error, follow_links) | |
182 | ||
183 | ||
184 | def _iter_tree_entries_next( | |
185 | root_full: str, | |
186 | dir_rel: str, | |
187 | memo: Dict[str, str], | |
188 | on_error: Callable, | |
189 | follow_links: bool, | |
190 | ) -> Iterator['TreeEntry']: | |
191 | """ | |
192 | Scan the directory for all descendant files. | |
193 | ||
194 | *root_full* (:class:`str`) the absolute path to the root directory. | |
195 | ||
196 | *dir_rel* (:class:`str`) the path to the directory to scan relative to | |
197 | *root_full*. | |
198 | ||
199 | *memo* (:class:`dict`) keeps track of ancestor directories | |
200 | encountered. Maps each ancestor real path (:class:`str`) to relative | |
201 | path (:class:`str`). | |
202 | ||
203 | *on_error* (:class:`~collections.abc.Callable` or :data:`None`) | |
204 | optionally is the error handler for file-system exceptions. | |
205 | ||
206 | *follow_links* (:class:`bool`) is whether to walk symbolic links that | |
207 | resolve to directories. | |
208 | ||
209 | Yields each entry (:class:`.TreeEntry`). | |
210 | """ | |
211 | dir_full = os.path.join(root_full, dir_rel) | |
212 | dir_real = os.path.realpath(dir_full) | |
213 | ||
214 | # Remember each encountered ancestor directory and its canonical | |
215 | # (real) path. If a canonical path is encountered more than once, | |
216 | # recursion has occurred. | |
217 | if dir_real not in memo: | |
218 | memo[dir_real] = dir_rel | |
219 | else: | |
220 | raise RecursionError(real_path=dir_real, first_path=memo[dir_real], second_path=dir_rel) | |
221 | ||
222 | with os.scandir(dir_full) as scan_iter: | |
223 | node_ent: os.DirEntry | |
224 | for node_ent in scan_iter: | |
225 | node_rel = os.path.join(dir_rel, node_ent.name) | |
226 | ||
227 | # Inspect child node. | |
228 | try: | |
229 | node_lstat = node_ent.stat(follow_symlinks=False) | |
230 | except OSError as e: | |
231 | if on_error is not None: | |
232 | on_error(e) | |
233 | continue | |
234 | ||
235 | if node_ent.is_symlink(): | |
236 | # Child node is a link, inspect the target node. | |
237 | try: | |
238 | node_stat = node_ent.stat() | |
239 | except OSError as e: | |
240 | if on_error is not None: | |
241 | on_error(e) | |
242 | continue | |
243 | else: | |
244 | node_stat = node_lstat | |
245 | ||
246 | if node_ent.is_dir(follow_symlinks=follow_links): | |
247 | # Child node is a directory, recurse into it and yield its | |
248 | # descendant files. | |
249 | yield TreeEntry(node_ent.name, node_rel, node_lstat, node_stat) | |
250 | ||
251 | yield from _iter_tree_entries_next(root_full, node_rel, memo, on_error, follow_links) | |
252 | ||
253 | elif node_ent.is_file() or node_ent.is_symlink(): | |
254 | # Child node is either a file or an unfollowed link, yield it. | |
255 | yield TreeEntry(node_ent.name, node_rel, node_lstat, node_stat) | |
256 | ||
257 | # NOTE: Make sure to remove the canonical (real) path of the directory | |
258 | # from the ancestors memo once we are done with it. This allows the | |
259 | # same directory to appear multiple times. If this is not done, the | |
260 | # second occurrence of the directory will be incorrectly interpreted | |
261 | # as a recursion. See <https://github.com/cpburnz/python-path-specification/pull/7>. | |
262 | del memo[dir_real] | |
263 | ||
264 | ||
265 | def iter_tree_files( | |
266 | root: StrPath, | |
267 | on_error: Optional[Callable] = None, | |
268 | follow_links: Optional[bool] = None, | |
269 | ) -> Iterator[str]: | |
270 | """ | |
271 | Walks the specified directory for all files. | |
272 | ||
273 | *root* (:class:`str` or :class:`os.PathLike[str]`) is the root directory to | |
274 | search for files. | |
275 | ||
276 | *on_error* (:class:`~collections.abc.Callable` or :data:`None`) | |
277 | optionally is the error handler for file-system exceptions. It will be | |
278 | called with the exception (:exc:`OSError`). Reraise the exception to | |
279 | abort the walk. Default is :data:`None` to ignore file-system | |
280 | exceptions. | |
281 | ||
282 | *follow_links* (:class:`bool` or :data:`None`) optionally is whether | |
283 | to walk symbolic links that resolve to directories. Default is | |
284 | :data:`None` for :data:`True`. | |
285 | ||
286 | Raises :exc:`RecursionError` if recursion is detected. | |
287 | ||
288 | Returns an :class:`~collections.abc.Iterator` yielding the path to | |
289 | each file (:class:`str`) relative to *root*. | |
290 | """ | |
291 | for entry in iter_tree_entries(root, on_error=on_error, follow_links=follow_links): | |
292 | if not entry.is_dir(follow_links): | |
293 | yield entry.path | |
294 | ||
295 | ||
296 | def iter_tree(root, on_error=None, follow_links=None): | |
297 | """ | |
298 | DEPRECATED: The :func:`.iter_tree` function is an alias for the | |
299 | :func:`.iter_tree_files` function. | |
300 | """ | |
301 | warnings.warn(( | |
302 | "util.iter_tree() is deprecated. Use util.iter_tree_files() instead." | |
303 | ), DeprecationWarning, stacklevel=2) | |
304 | return iter_tree_files(root, on_error=on_error, follow_links=follow_links) | |
305 | ||
306 | ||
307 | def lookup_pattern(name: str) -> Callable[[AnyStr], Pattern]: | |
308 | """ | |
309 | Lookups a registered pattern factory by name. | |
310 | ||
311 | *name* (:class:`str`) is the name of the pattern factory. | |
312 | ||
313 | Returns the registered pattern factory (:class:`~collections.abc.Callable`). | |
314 | If no pattern factory is registered, raises :exc:`KeyError`. | |
315 | """ | |
316 | return _registered_patterns[name] | |
317 | ||
318 | ||
319 | def match_file(patterns: Iterable[Pattern], file: str) -> bool: | |
320 | """ | |
321 | Matches the file to the patterns. | |
322 | ||
323 | *patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.Pattern`) | |
324 | contains the patterns to use. | |
325 | ||
326 | *file* (:class:`str`) is the normalized file path to be matched | |
327 | against *patterns*. | |
328 | ||
329 | Returns :data:`True` if *file* matched; otherwise, :data:`False`. | |
330 | """ | |
331 | matched = False | |
332 | for pattern in patterns: | |
333 | if pattern.include is not None: | |
334 | if pattern.match_file(file) is not None: | |
335 | matched = pattern.include | |
336 | ||
337 | return matched | |
338 | ||
339 | ||
340 | def match_files( | |
341 | patterns: Iterable[Pattern], | |
342 | files: Iterable[str], | |
343 | ) -> Set[str]: | |
344 | """ | |
345 | DEPRECATED: This is an old function no longer used. Use the :func:`.match_file` | |
346 | function with a loop for better results. | |
347 | ||
348 | Matches the files to the patterns. | |
349 | ||
350 | *patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.Pattern`) | |
351 | contains the patterns to use. | |
352 | ||
353 | *files* (:class:`~collections.abc.Iterable` of :class:`str`) contains | |
354 | the normalized file paths to be matched against *patterns*. | |
355 | ||
356 | Returns the matched files (:class:`set` of :class:`str`). | |
357 | """ | |
358 | warnings.warn(( | |
359 | "util.match_files() is deprecated. Use util.match_file() with a " | |
360 | "loop for better results." | |
361 | ), DeprecationWarning, stacklevel=2) | |
362 | ||
363 | use_patterns = _filter_patterns(patterns) | |
364 | ||
365 | return_files = set() | |
366 | for file in files: | |
367 | if match_file(use_patterns, file): | |
368 | return_files.add(file) | |
369 | ||
370 | return return_files | |
371 | ||
372 | ||
373 | def normalize_file( | |
374 | file: StrPath, | |
375 | separators: Optional[Collection[str]] = None, | |
376 | ) -> str: | |
377 | """ | |
378 | Normalizes the file path to use the POSIX path separator (i.e., | |
379 | :data:`'/'`), and make the paths relative (remove leading :data:`'/'`). | |
380 | ||
381 | *file* (:class:`str` or :class:`os.PathLike[str]`) is the file path. | |
382 | ||
383 | *separators* (:class:`~collections.abc.Collection` of :class:`str`; or | |
384 | :data:`None`) optionally contains the path separators to normalize. | |
385 | This does not need to include the POSIX path separator (:data:`'/'`), | |
386 | but including it will not affect the results. Default is :data:`None` | |
387 | for :data:`NORMALIZE_PATH_SEPS`. To prevent normalization, pass an | |
388 | empty container (e.g., an empty tuple :data:`()`). | |
389 | ||
390 | Returns the normalized file path (:class:`str`). | |
391 | """ | |
392 | # Normalize path separators. | |
393 | if separators is None: | |
394 | separators = NORMALIZE_PATH_SEPS | |
395 | ||
396 | # Convert path object to string. | |
397 | norm_file: str = os.fspath(file) | |
398 | ||
399 | for sep in separators: | |
400 | norm_file = norm_file.replace(sep, posixpath.sep) | |
401 | ||
402 | if norm_file.startswith('/'): | |
403 | # Make path relative. | |
404 | norm_file = norm_file[1:] | |
405 | ||
406 | elif norm_file.startswith('./'): | |
407 | # Remove current directory prefix. | |
408 | norm_file = norm_file[2:] | |
409 | ||
410 | return norm_file | |
411 | ||
412 | ||
413 | def normalize_files( | |
414 | files: Iterable[StrPath], | |
415 | separators: Optional[Collection[str]] = None, | |
416 | ) -> Dict[str, List[StrPath]]: | |
417 | """ | |
418 | DEPRECATED: This function is no longer used. Use the :func:`.normalize_file` | |
419 | function with a loop for better results. | |
420 | ||
421 | Normalizes the file paths to use the POSIX path separator. | |
422 | ||
423 | *files* (:class:`~collections.abc.Iterable` of :class:`str` or | |
424 | :class:`os.PathLike[str]`) contains the file paths to be normalized. | |
425 | ||
426 | *separators* (:class:`~collections.abc.Collection` of :class:`str`; or | |
427 | :data:`None`) optionally contains the path separators to normalize. | |
428 | See :func:`normalize_file` for more information. | |
429 | ||
430 | Returns a :class:`dict` mapping each normalized file path (:class:`str`) | |
431 | to the original file paths (:class:`list` of :class:`str` or | |
432 | :class:`os.PathLike[str]`). | |
433 | """ | |
434 | warnings.warn(( | |
435 | "util.normalize_files() is deprecated. Use util.normalize_file() " | |
436 | "with a loop for better results." | |
437 | ), DeprecationWarning, stacklevel=2) | |
438 | ||
439 | norm_files = {} | |
440 | for path in files: | |
441 | norm_file = normalize_file(path, separators=separators) | |
442 | if norm_file in norm_files: | |
443 | norm_files[norm_file].append(path) | |
444 | else: | |
445 | norm_files[norm_file] = [path] | |
446 | ||
447 | return norm_files | |
448 | ||
449 | ||
450 | def register_pattern( | |
451 | name: str, | |
452 | pattern_factory: Callable[[AnyStr], Pattern], | |
453 | override: Optional[bool] = None, | |
454 | ) -> None: | |
455 | """ | |
456 | Registers the specified pattern factory. | |
457 | ||
458 | *name* (:class:`str`) is the name to register the pattern factory | |
459 | under. | |
460 | ||
461 | *pattern_factory* (:class:`~collections.abc.Callable`) is used to | |
462 | compile patterns. It must accept an uncompiled pattern (:class:`str`) | |
463 | and return the compiled pattern (:class:`.Pattern`). | |
464 | ||
465 | *override* (:class:`bool` or :data:`None`) optionally is whether to | |
466 | allow overriding an already registered pattern under the same name | |
467 | (:data:`True`), instead of raising an :exc:`AlreadyRegisteredError` | |
468 | (:data:`False`). Default is :data:`None` for :data:`False`. | |
469 | """ | |
470 | if not isinstance(name, str): | |
471 | raise TypeError(f"name:{name!r} is not a string.") | |
472 | ||
473 | if not callable(pattern_factory): | |
474 | raise TypeError(f"pattern_factory:{pattern_factory!r} is not callable.") | |
475 | ||
476 | if name in _registered_patterns and not override: | |
477 | raise AlreadyRegisteredError(name, _registered_patterns[name]) | |
478 | ||
479 | _registered_patterns[name] = pattern_factory | |
480 | ||
481 | ||
482 | class AlreadyRegisteredError(Exception): | |
483 | """ | |
484 | The :exc:`AlreadyRegisteredError` exception is raised when a pattern | |
485 | factory is registered under a name already in use. | |
486 | """ | |
487 | ||
488 | def __init__( | |
489 | self, | |
490 | name: str, | |
491 | pattern_factory: Callable[[AnyStr], Pattern], | |
492 | ) -> None: | |
493 | """ | |
494 | Initializes the :exc:`AlreadyRegisteredError` instance. | |
495 | ||
496 | *name* (:class:`str`) is the name of the registered pattern. | |
497 | ||
498 | *pattern_factory* (:class:`~collections.abc.Callable`) is the | |
499 | registered pattern factory. | |
500 | """ | |
501 | super(AlreadyRegisteredError, self).__init__(name, pattern_factory) | |
502 | ||
503 | @property | |
504 | def message(self) -> str: | |
505 | """ | |
506 | *message* (:class:`str`) is the error message. | |
507 | """ | |
508 | return "{name!r} is already registered for pattern factory:{pattern_factory!r}.".format( | |
509 | name=self.name, | |
510 | pattern_factory=self.pattern_factory, | |
511 | ) | |
512 | ||
513 | @property | |
514 | def name(self) -> str: | |
515 | """ | |
516 | *name* (:class:`str`) is the name of the registered pattern. | |
517 | """ | |
518 | return self.args[0] | |
519 | ||
520 | @property | |
521 | def pattern_factory(self) -> Callable[[AnyStr], Pattern]: | |
522 | """ | |
523 | *pattern_factory* (:class:`~collections.abc.Callable`) is the | |
524 | registered pattern factory. | |
525 | """ | |
526 | return self.args[1] | |
527 | ||
528 | ||
529 | class RecursionError(Exception): | |
530 | """ | |
531 | The :exc:`RecursionError` exception is raised when recursion is | |
532 | detected. | |
533 | """ | |
534 | ||
535 | def __init__( | |
536 | self, | |
537 | real_path: str, | |
538 | first_path: str, | |
539 | second_path: str, | |
540 | ) -> None: | |
541 | """ | |
542 | Initializes the :exc:`RecursionError` instance. | |
543 | ||
544 | *real_path* (:class:`str`) is the real path that recursion was | |
545 | encountered on. | |
546 | ||
547 | *first_path* (:class:`str`) is the first path encountered for | |
548 | *real_path*. | |
549 | ||
550 | *second_path* (:class:`str`) is the second path encountered for | |
551 | *real_path*. | |
552 | """ | |
553 | super(RecursionError, self).__init__(real_path, first_path, second_path) | |
554 | ||
555 | @property | |
556 | def first_path(self) -> str: | |
557 | """ | |
558 | *first_path* (:class:`str`) is the first path encountered for | |
559 | :attr:`self.real_path <RecursionError.real_path>`. | |
560 | """ | |
561 | return self.args[1] | |
562 | ||
563 | @property | |
564 | def message(self) -> str: | |
565 | """ | |
566 | *message* (:class:`str`) is the error message. | |
567 | """ | |
568 | return "Real path {real!r} was encountered at {first!r} and then {second!r}.".format( | |
569 | real=self.real_path, | |
570 | first=self.first_path, | |
571 | second=self.second_path, | |
572 | ) | |
573 | ||
574 | @property | |
575 | def real_path(self) -> str: | |
576 | """ | |
577 | *real_path* (:class:`str`) is the real path that recursion was | |
578 | encountered on. | |
579 | """ | |
580 | return self.args[0] | |
581 | ||
582 | @property | |
583 | def second_path(self) -> str: | |
584 | """ | |
585 | *second_path* (:class:`str`) is the second path encountered for | |
586 | :attr:`self.real_path <RecursionError.real_path>`. | |
587 | """ | |
588 | return self.args[2] | |
589 | ||
590 | ||
591 | class MatchDetail(object): | |
592 | """ | |
593 | The :class:`.MatchDetail` class contains information about | |
594 | """ | |
595 | ||
596 | # Make the class dict-less. | |
597 | __slots__ = ('patterns',) | |
598 | ||
599 | def __init__(self, patterns: Sequence[Pattern]) -> None: | |
600 | """ | |
601 | Initialize the :class:`.MatchDetail` instance. | |
602 | ||
603 | *patterns* (:class:`~collections.abc.Sequence` of :class:`~pathspec.pattern.Pattern`) | |
604 | contains the patterns that matched the file in the order they were | |
605 | encountered. | |
606 | """ | |
607 | ||
608 | self.patterns = patterns | |
609 | """ | |
610 | *patterns* (:class:`~collections.abc.Sequence` of :class:`~pathspec.pattern.Pattern`) | |
611 | contains the patterns that matched the file in the order they were | |
612 | encountered. | |
613 | """ | |
614 | ||
615 | ||
616 | class TreeEntry(object): | |
617 | """ | |
618 | The :class:`.TreeEntry` class contains information about a file-system | |
619 | entry. | |
620 | """ | |
621 | ||
622 | # Make the class dict-less. | |
623 | __slots__ = ('_lstat', 'name', 'path', '_stat') | |
624 | ||
625 | def __init__( | |
626 | self, | |
627 | name: str, | |
628 | path: str, | |
629 | lstat: os.stat_result, | |
630 | stat: os.stat_result, | |
631 | ) -> None: | |
632 | """ | |
633 | Initialize the :class:`.TreeEntry` instance. | |
634 | ||
635 | *name* (:class:`str`) is the base name of the entry. | |
636 | ||
637 | *path* (:class:`str`) is the relative path of the entry. | |
638 | ||
639 | *lstat* (:class:`os.stat_result`) is the stat result of the direct | |
640 | entry. | |
641 | ||
642 | *stat* (:class:`os.stat_result`) is the stat result of the entry, | |
643 | potentially linked. | |
644 | """ | |
645 | ||
646 | self._lstat: os.stat_result = lstat | |
647 | """ | |
648 | *_lstat* (:class:`os.stat_result`) is the stat result of the direct | |
649 | entry. | |
650 | """ | |
651 | ||
652 | self.name: str = name | |
653 | """ | |
654 | *name* (:class:`str`) is the base name of the entry. | |
655 | """ | |
656 | ||
657 | self.path: str = path | |
658 | """ | |
659 | *path* (:class:`str`) is the path of the entry. | |
660 | """ | |
661 | ||
662 | self._stat: os.stat_result = stat | |
663 | """ | |
664 | *_stat* (:class:`os.stat_result`) is the stat result of the linked | |
665 | entry. | |
666 | """ | |
667 | ||
668 | def is_dir(self, follow_links: Optional[bool] = None) -> bool: | |
669 | """ | |
670 | Get whether the entry is a directory. | |
671 | ||
672 | *follow_links* (:class:`bool` or :data:`None`) is whether to follow | |
673 | symbolic links. If this is :data:`True`, a symlink to a directory | |
674 | will result in :data:`True`. Default is :data:`None` for :data:`True`. | |
675 | ||
676 | Returns whether the entry is a directory (:class:`bool`). | |
677 | """ | |
678 | if follow_links is None: | |
679 | follow_links = True | |
680 | ||
681 | node_stat = self._stat if follow_links else self._lstat | |
682 | return stat.S_ISDIR(node_stat.st_mode) | |
683 | ||
684 | def is_file(self, follow_links: Optional[bool] = None) -> bool: | |
685 | """ | |
686 | Get whether the entry is a regular file. | |
687 | ||
688 | *follow_links* (:class:`bool` or :data:`None`) is whether to follow | |
689 | symbolic links. If this is :data:`True`, a symlink to a regular file | |
690 | will result in :data:`True`. Default is :data:`None` for :data:`True`. | |
691 | ||
692 | Returns whether the entry is a regular file (:class:`bool`). | |
693 | """ | |
694 | if follow_links is None: | |
695 | follow_links = True | |
696 | ||
697 | node_stat = self._stat if follow_links else self._lstat | |
698 | return stat.S_ISREG(node_stat.st_mode) | |
699 | ||
700 | def is_symlink(self) -> bool: | |
701 | """ | |
702 | Returns whether the entry is a symbolic link (:class:`bool`). | |
703 | """ | |
704 | return stat.S_ISLNK(self._lstat.st_mode) | |
705 | ||
706 | def stat(self, follow_links: Optional[bool] = None) -> os.stat_result: | |
707 | """ | |
708 | Get the cached stat result for the entry. | |
709 | ||
710 | *follow_links* (:class:`bool` or :data:`None`) is whether to follow | |
711 | symbolic links. If this is :data:`True`, the stat result of the | |
712 | linked file will be returned. Default is :data:`None` for :data:`True`. | |
713 | ||
714 | Returns that stat result (:class:`os.stat_result`). | |
715 | """ | |
716 | if follow_links is None: | |
717 | follow_links = True | |
718 | ||
719 | return self._stat if follow_links else self._lstat |