Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pathspec/util.py: 35%
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
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
1"""
2This module provides utility methods for dealing with path-specs.
3"""
4from __future__ import annotations
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)
27from .pattern import (
28 Pattern)
29from ._typing import (
30 AnyStr, # Removed in 3.18.
31 deprecated) # Added in 3.13.
33StrPath = Union[str, os.PathLike[str]]
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"""
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"""
47TStrPath = TypeVar('TStrPath', bound=StrPath)
48"""
49Type variable for :class:`str` or :class:`os.PathLike`.
50"""
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"""
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"""
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.
77 *path* (:class:`pathlib.Path`) is the path to use.
79 Returns the path (:class:`str`).
80 """
81 str_path = str(path)
82 if path.is_dir():
83 str_path += os.sep
85 return str_path
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.
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`).
100 *file* (:class:`str`) is the normalized file path to be matched against
101 *patterns*.
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.
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
118 return None, None
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
129 return out_include, out_index
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.
141 *patterns* (:class:`~collections.abc.Iterable` of :class:`.Pattern`) contains
142 the patterns to use.
144 *files* (:class:`~collections.abc.Iterable` of :class:`str`) contains the
145 normalized file paths to be matched against *patterns*.
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`.
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])
171 else:
172 # Remove files.
173 for file in result_files:
174 del return_files[file]
176 return return_files
179def _filter_check_patterns(
180 patterns: Iterable[Pattern],
181) -> list[tuple[int, Pattern]]:
182 """
183 Filters out null-patterns.
185 *patterns* (:class:`~collections.abc.Iterable` of :class:`.Pattern`) contains
186 the patterns.
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 ]
199def _get_sub_path_safe(root: str, sub_dir: StrPath) -> str:
200 """
201 Get the sub-directory path relative to the root directory. This ensures the
202 root path cannot be escaped.
204 *root* (:class:`str`) is the root directory.
206 *sub_dir* (:class:`str` or :class:`os.PathLike`) is the sub-directory path.
207 This path can be relative or absolute.
209 Returns the sub-directory path relative to the root directory (:class:`str`).
210 """
211 sub_abs = os.path.normpath(os.path.join(root, sub_dir))
212 if sub_abs == root:
213 return ''
214 elif sub_abs.startswith(root + os.sep):
215 return os.path.relpath(sub_abs, root)
216 else:
217 raise ValueError(f"{sub_dir=!r} must be relative to {root=!r}.")
220def _is_iterable(value: Any) -> bool:
221 """
222 Check whether the value is an iterable (excludes strings).
224 *value* is the value to check.
226 Returns whether *value* is an iterable (:class:`bool`).
227 """
228 return isinstance(value, Iterable) and not isinstance(value, (str, bytes))
231@deprecated((
232 "pathspec.util.iter_tree() is deprecated. Use iter_tree_files() instead."
233))
234def iter_tree(root, on_error=None, follow_links=None):
235 """
236 .. version-deprecated:: 0.10.0
237 This is an alias for the :func:`.iter_tree_files` function.
238 """
239 return iter_tree_files(root, on_error=on_error, follow_links=follow_links)
242def iter_tree_entries(
243 root: StrPath,
244 on_error: Optional[Callable[[OSError], None]] = None,
245 follow_links: Optional[bool] = None,
246 subdir: Optional[StrPath] = None,
247) -> Iterator['TreeEntry']:
248 """
249 Walks the specified directory for all files and directories.
251 *root* (:class:`str` or :class:`os.PathLike`) is the root directory to search.
253 *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally is
254 the error handler for file-system exceptions. It will be called with the
255 exception (:exc:`OSError`). Reraise the exception to abort the walk. Default
256 is :data:`None` to ignore file-system exceptions.
258 *follow_links* (:class:`bool` or :data:`None`) optionally is whether to walk
259 symbolic links that resolve to directories. Default is :data:`None` for
260 :data:`True`.
262 *subdir* (:class:`str`, :class:`os.PathLike`, or :data:`None`) is a sub
263 directory of *root* to constrain searching to. If a relative path, it is
264 treated as relative to *root*. If an absolute path, it must be a descendant of
265 *root*. Default is :data:`None` to search the entire *root* directory tree.
267 Raises :exc:`.RecursionError` if recursion is detected.
269 Returns an :class:`~collections.abc.Iterator` yielding each file or directory
270 entry (:class:`.TreeEntry`) relative to *root*.
271 """
272 if on_error is not None and not callable(on_error):
273 raise TypeError(f"on_error:{on_error!r} is not callable.")
275 if follow_links is None:
276 follow_links = True
278 root_abs = os.path.abspath(root)
280 # Ensure sub_dir does not escape root.
281 if subdir is not None:
282 dir_rel = _get_sub_path_safe(root_abs, subdir)
283 else:
284 dir_rel = ''
286 yield from _iter_tree_entries_next(root_abs, dir_rel, {}, on_error, follow_links)
289def _iter_tree_entries_next(
290 root_full: str,
291 dir_rel: str,
292 memo: dict[str, str],
293 on_error: Optional[Callable[[OSError], None]],
294 follow_links: bool,
295) -> Iterator['TreeEntry']:
296 """
297 Scan the directory for all descendant files.
299 *root_full* (:class:`str`) the absolute path to the root directory.
301 *dir_rel* (:class:`str`) the path to the directory to scan relative to
302 *root_full*.
304 *memo* (:class:`dict`) keeps track of ancestor directories encountered. Maps
305 each ancestor real path (:class:`str`) to relative path (:class:`str`).
307 *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally is
308 the error handler for file-system exceptions.
310 *follow_links* (:class:`bool`) is whether to walk symbolic links that resolve
311 to directories.
313 Yields each entry (:class:`.TreeEntry`).
314 """
315 dir_full = os.path.join(root_full, dir_rel)
316 dir_real = os.path.realpath(dir_full)
318 # Remember each encountered ancestor directory and its canonical (real) path.
319 # If a canonical path is encountered more than once, recursion has occurred.
320 if dir_real not in memo:
321 memo[dir_real] = dir_rel
322 else:
323 raise RecursionError(real_path=dir_real, first_path=memo[dir_real], second_path=dir_rel)
325 with os.scandir(dir_full) as scan_iter:
326 node_ent: os.DirEntry
327 for node_ent in scan_iter:
328 node_rel = os.path.join(dir_rel, node_ent.name)
330 # Inspect child node.
331 try:
332 node_lstat = node_ent.stat(follow_symlinks=False)
333 except OSError as e:
334 if on_error is not None:
335 on_error(e)
336 continue
338 if node_ent.is_symlink():
339 # Child node is a link, inspect the target node.
340 try:
341 node_stat = node_ent.stat()
342 except OSError as e:
343 if on_error is not None:
344 on_error(e)
345 continue
346 else:
347 node_stat = node_lstat
349 if node_ent.is_dir(follow_symlinks=follow_links):
350 # Child node is a directory, recurse into it and yield its descendant
351 # files.
352 yield TreeEntry(node_ent.name, node_rel, node_lstat, node_stat)
354 yield from _iter_tree_entries_next(root_full, node_rel, memo, on_error, follow_links)
356 elif node_ent.is_file() or node_ent.is_symlink():
357 # Child node is either a file or an unfollowed link, yield it.
358 yield TreeEntry(node_ent.name, node_rel, node_lstat, node_stat)
360 # NOTE: Make sure to remove the canonical (real) path of the directory from
361 # the ancestors memo once we are done with it. This allows the same directory
362 # to appear multiple times. If this is not done, the second occurrence of the
363 # directory will be incorrectly interpreted as a recursion. See
364 # <https://github.com/cpburnz/python-path-specification/pull/7>.
365 del memo[dir_real]
368# TODO: Add tests for subdir.
369def iter_tree_files(
370 root: StrPath,
371 on_error: Optional[Callable[[OSError], None]] = None,
372 follow_links: Optional[bool] = None,
373 subdir: Optional[StrPath] = None,
374) -> Iterator[str]:
375 """
376 Walks the specified directory for all files.
378 *root* (:class:`str` or :class:`os.PathLike`) is the root directory to search
379 for files.
381 *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally is
382 the error handler for file-system exceptions. It will be called with the
383 exception (:exc:`OSError`). Reraise the exception to abort the walk. Default
384 is :data:`None` to ignore file-system exceptions.
386 *follow_links* (:class:`bool` or :data:`None`) optionally is whether to walk
387 symbolic links that resolve to directories. Default is :data:`None` for
388 :data:`True`.
390 *subdir* (:class:`str`, :class:`os.PathLike`, or :data:`None`) is a sub
391 directory of *root* to constrain searching to. If a relative path, it is
392 treated as relative to *root*. If an absolute path, it must be a descendant of
393 *root*. Default is :data:`None` to search the entire *root* directory tree.
395 Raises :exc:`.RecursionError` if recursion is detected.
397 Returns an :class:`~collections.abc.Iterator` yielding the path to each file
398 (:class:`str`) relative to *root*.
399 """
400 if on_error is not None and not callable(on_error):
401 raise TypeError(f"on_error:{on_error!r} is not callable.")
403 if follow_links is None:
404 follow_links = True
406 root_abs = os.path.abspath(root)
408 # Ensure subdir does not escape root.
409 if subdir is not None:
410 dir_rel = _get_sub_path_safe(root_abs, subdir)
411 else:
412 dir_rel = ''
414 yield from _iter_tree_files_next(root_abs, dir_rel, {}, on_error, follow_links)
417def _iter_tree_files_next(
418 root_full: str,
419 dir_rel: str,
420 memo: dict[str, str],
421 on_error: Optional[Callable[[OSError], None]],
422 follow_links: bool,
423) -> Iterator[str]:
424 """
425 Scan the directory for all descendant files.
427 *root_full* (:class:`str`) the absolute path to the root directory.
429 *dir_rel* (:class:`str`) the path to the directory to scan relative to
430 *root_full*.
432 *memo* (:class:`dict`) keeps track of ancestor directories encountered. Maps
433 each ancestor real path (:class:`str`) to relative path (:class:`str`).
435 *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally is
436 the error handler for file-system exceptions.
438 *follow_links* (:class:`bool`) is whether to walk symbolic links that resolve
439 to directories.
441 Yields each file path (:class:`str`).
442 """
443 dir_full = os.path.join(root_full, dir_rel)
444 dir_real = os.path.realpath(dir_full)
446 # Remember each encountered ancestor directory and its canonical (real) path.
447 # If a canonical path is encountered more than once, recursion has occurred.
448 if dir_real not in memo:
449 memo[dir_real] = dir_rel
450 else:
451 raise RecursionError(real_path=dir_real, first_path=memo[dir_real], second_path=dir_rel)
453 with os.scandir(dir_full) as scan_iter:
454 node_ent: os.DirEntry
455 for node_ent in scan_iter:
456 node_rel = os.path.join(dir_rel, node_ent.name)
458 if node_ent.is_dir(follow_symlinks=follow_links):
459 # Child node is a directory, recurse into it and yield its descendant
460 # files.
461 yield from _iter_tree_files_next(root_full, node_rel, memo, on_error, follow_links)
463 elif node_ent.is_file():
464 # Child node is a file, yield it.
465 yield node_rel
467 elif not follow_links and node_ent.is_symlink():
468 # Child node is an unfollowed link, yield it.
469 yield node_rel
471 # NOTE: Make sure to remove the canonical (real) path of the directory from
472 # the ancestors memo once we are done with it. This allows the same directory
473 # to appear multiple times. If this is not done, the second occurrence of the
474 # directory will be incorrectly interpreted as a recursion. See
475 # <https://github.com/cpburnz/python-path-specification/pull/7>.
476 del memo[dir_real]
479def lookup_pattern(name: str) -> Callable[[AnyStr], Pattern]:
480 """
481 Looks up a registered pattern factory by name.
483 *name* (:class:`str`) is the name of the pattern factory.
485 Returns the registered pattern factory (:class:`~collections.abc.Callable`).
486 If no pattern factory is registered, raises :exc:`KeyError`.
487 """
488 return _registered_patterns[name] # type: ignore[return-value]
491def match_file(patterns: Iterable[Pattern], file: str) -> bool:
492 """
493 Matches the file to the patterns.
495 *patterns* (:class:`~collections.abc.Iterable` of :class:`.Pattern`) contains
496 the patterns to use.
498 *file* (:class:`str`) is the normalized file path to be matched against
499 *patterns*.
501 Returns :data:`True` if *file* matched; otherwise, :data:`False`.
502 """
503 matched = False
504 for pattern in patterns:
505 if pattern.include is not None and pattern.match_file(file) is not None:
506 matched = pattern.include
508 return matched
511@deprecated((
512 "pathspec.util.match_files() is deprecated. Use match_file() with a loop for "
513 "better results."
514))
515def match_files(
516 patterns: Iterable[Pattern],
517 files: Iterable[str],
518) -> set[str]:
519 """
520 .. version-deprecated:: 0.10.0
521 This function is no longer used. Use the :func:`.match_file` function with a
522 loop for better results.
524 Matches the files to the patterns.
526 *patterns* (:class:`~collections.abc.Iterable` of :class:`.Pattern`) contains
527 the patterns to use.
529 *files* (:class:`~collections.abc.Iterable` of :class:`str`) contains the
530 normalized file paths to be matched against *patterns*.
532 Returns the matched files (:class:`set` of :class:`str`).
533 """
534 use_patterns = [__pat for __pat in patterns if __pat.include is not None]
536 return_files = set()
537 for file in files:
538 if match_file(use_patterns, file):
539 return_files.add(file)
541 return return_files
544def normalize_file(
545 file: StrPath,
546 separators: Optional[Collection[str]] = None,
547) -> str:
548 """
549 Normalizes the file path to use the POSIX path separator (i.e., ``"/"``), and
550 make the paths relative (remove leading ``"/"``).
552 *file* (:class:`str` or :class:`os.PathLike`) is the file path.
554 *separators* (:class:`~collections.abc.Collection` of :class:`str`; or
555 :data:`None`) optionally contains the path separators to normalize. This does
556 not need to include the POSIX path separator (``"/"``), but including it will
557 not affect the results. Default is ``None`` for :data:`.NORMALIZE_PATH_SEPS`.
558 To prevent normalization, pass an empty container (e.g., an empty tuple
559 ``()``).
561 Returns the normalized file path (:class:`str`).
562 """
563 # Normalize path separators.
564 if separators is None:
565 separators = NORMALIZE_PATH_SEPS
567 assert separators is not None, separators
569 # Convert path object to string.
570 norm_file: str = os.fspath(file)
572 for sep in separators:
573 norm_file = norm_file.replace(sep, posixpath.sep)
575 if norm_file.startswith('/'):
576 # Make path relative.
577 norm_file = norm_file[1:]
579 elif norm_file.startswith('./'):
580 # Remove current directory prefix.
581 norm_file = norm_file[2:]
583 return norm_file
586@deprecated((
587 "pathspec.util.normalize_files() is deprecated. Use normalize_file() with a "
588 "loop for better results."
589))
590def normalize_files(
591 files: Iterable[StrPath],
592 separators: Optional[Collection[str]] = None,
593) -> dict[str, list[StrPath]]:
594 """
595 .. version-deprecated:: 0.10.0
596 This function is no longer used. Use the :func:`.normalize_file` function
597 with a loop for better results.
599 Normalizes the file paths to use the POSIX path separator.
601 *files* (:class:`~collections.abc.Iterable` of :class:`str` or
602 :class:`os.PathLike`) contains the file paths to be normalized.
604 *separators* (:class:`~collections.abc.Collection` of :class:`str`; or
605 :data:`None`) optionally contains the path separators to normalize. See
606 :func:`.normalize_file` for more information.
608 Returns a :class:`dict` mapping each normalized file path (:class:`str`) to
609 the original file paths (:class:`list` of :class:`str` or
610 :class:`os.PathLike`).
611 """
612 norm_files: dict[str, list[StrPath]] = {}
613 for path in files:
614 norm_file = normalize_file(path, separators=separators)
615 if norm_file in norm_files:
616 norm_files[norm_file].append(path)
617 else:
618 norm_files[norm_file] = [path]
620 return norm_files
623def register_pattern(
624 name: str,
625 pattern_factory: Union[Callable[[Union[str, bytes]], Pattern], type[Pattern]],
626 override: Optional[bool] = None,
627) -> None:
628 """
629 Registers the specified pattern factory.
631 *name* (:class:`str`) is the name to register the pattern factory under.
633 *pattern_factory* (:class:`~collections.abc.Callable`) is used to compile
634 patterns. It must accept an uncompiled pattern (:class:`str`) and return the
635 compiled pattern (:class:`.Pattern`).
637 *override* (:class:`bool` or :data:`None`) optionally is whether to allow
638 overriding an already registered pattern under the same name (:data:`True`),
639 instead of raising an :exc:`.AlreadyRegisteredError` (:data:`False`). Default
640 is :data:`None` for :data:`False`.
641 """
642 if not isinstance(name, str):
643 raise TypeError(f"{name=!r} is not a string.")
645 if not callable(pattern_factory):
646 raise TypeError(f"{pattern_factory=!r} is not callable.")
648 if name in _registered_patterns and not override:
649 raise AlreadyRegisteredError(name, _registered_patterns[name])
651 _registered_patterns[name] = pattern_factory # type: ignore
654class AlreadyRegisteredError(Exception):
655 """
656 The :exc:`AlreadyRegisteredError` exception is raised when a pattern factory
657 is registered under a name already in use.
658 """
660 def __init__(
661 self,
662 name: str,
663 pattern_factory: Callable[[Union[str, bytes]], Pattern],
664 ) -> None:
665 """
666 Initializes the :exc:`AlreadyRegisteredError` instance.
668 *name* (:class:`str`) is the name of the registered pattern.
670 *pattern_factory* (:class:`~collections.abc.Callable`) is the registered
671 pattern factory.
672 """
673 super().__init__(name, pattern_factory)
675 @property
676 def message(self) -> str:
677 """
678 *message* (:class:`str`) is the error message.
679 """
680 return (
681 f"{self.name!r} is already registered for pattern factory="
682 f"{self.pattern_factory!r}."
683 )
685 @property
686 def name(self) -> str:
687 """
688 *name* (:class:`str`) is the name of the registered pattern.
689 """
690 return self.args[0]
692 @property
693 def pattern_factory(self) -> Callable[[Union[str, bytes]], Pattern]:
694 """
695 *pattern_factory* (:class:`~collections.abc.Callable`) is the registered
696 pattern factory.
697 """
698 return self.args[1]
701# TODO: Rename to RecursivePathError because RecursionError is a built-in
702# exception as of Python 3.5. Keep RecursionError as a deprecated alias.
703class RecursionError(Exception):
704 """
705 The :exc:`RecursionError` exception is raised when recursion is detected.
706 """
708 def __init__(
709 self,
710 real_path: str,
711 first_path: str,
712 second_path: str,
713 ) -> None:
714 """
715 Initializes the :exc:`RecursionError` instance.
717 *real_path* (:class:`str`) is the real path that recursion was encountered
718 on.
720 *first_path* (:class:`str`) is the first path encountered for *real_path*.
722 *second_path* (:class:`str`) is the second path encountered for *real_path*.
723 """
724 super().__init__(real_path, first_path, second_path)
726 @property
727 def first_path(self) -> str:
728 """
729 *first_path* (:class:`str`) is the first path encountered for
730 :attr:`self.real_path <RecursionError.real_path>`.
731 """
732 return self.args[1]
734 @property
735 def message(self) -> str:
736 """
737 *message* (:class:`str`) is the error message.
738 """
739 return (
740 f"Real path {self.real_path!r} was encountered at {self.first_path!r} "
741 f"and then {self.second_path!r}."
742 )
744 @property
745 def real_path(self) -> str:
746 """
747 *real_path* (:class:`str`) is the real path that recursion was
748 encountered on.
749 """
750 return self.args[0]
752 @property
753 def second_path(self) -> str:
754 """
755 *second_path* (:class:`str`) is the second path encountered for
756 :attr:`self.real_path <RecursionError.real_path>`.
757 """
758 return self.args[2]
761@dataclass(frozen=True)
762class CheckResult(Generic[TStrPath]):
763 """
764 The :class:`CheckResult` class contains information about the file and which
765 pattern matched it.
766 """
768 # Make the class dict-less.
769 __slots__ = (
770 'file',
771 'include',
772 'index',
773 )
775 file: TStrPath
776 """
777 *file* (:class:`str` or :class:`os.PathLike`) is the file path.
778 """
780 include: Optional[bool]
781 """
782 *include* (:class:`bool` or :data:`None`) is whether to include or exclude the
783 file. If :data:`None`, no pattern matched.
784 """
786 index: Optional[int]
787 """
788 *index* (:class:`int` or :data:`None`) is the index of the last pattern that
789 matched. If :data:`None`, no pattern matched.
790 """
793class MatchDetail(object):
794 """
795 The :class:`.MatchDetail` class contains information about
796 """
798 # Make the class dict-less.
799 __slots__ = ('patterns',)
801 def __init__(self, patterns: Sequence[Pattern]) -> None:
802 """
803 Initialize the :class:`.MatchDetail` instance.
805 *patterns* (:class:`~collections.abc.Sequence` of :class:`.Pattern`)
806 contains the patterns that matched the file in the order they were encountered.
807 """
809 self.patterns = patterns
810 """
811 *patterns* (:class:`~collections.abc.Sequence` of :class:`.Pattern`)
812 contains the patterns that matched the file in the order they were
813 encountered.
814 """
817class TreeEntry(object):
818 """
819 The :class:`TreeEntry` class contains information about a file-system entry.
820 """
822 # Make the class dict-less.
823 __slots__ = ('_lstat', 'name', 'path', '_stat')
825 def __init__(
826 self,
827 name: str,
828 path: str,
829 lstat: os.stat_result,
830 stat: os.stat_result,
831 ) -> None:
832 """
833 Initialize the :class:`TreeEntry` instance.
835 *name* (:class:`str`) is the base name of the entry.
837 *path* (:class:`str`) is the relative path of the entry.
839 *lstat* (:class:`os.stat_result`) is the stat result of the direct entry.
841 *stat* (:class:`os.stat_result`) is the stat result of the entry,
842 potentially linked.
843 """
845 self._lstat: os.stat_result = lstat
846 """
847 *_lstat* (:class:`os.stat_result`) is the stat result of the direct entry.
848 """
850 self.name: str = name
851 """
852 *name* (:class:`str`) is the base name of the entry.
853 """
855 self.path: str = path
856 """
857 *path* (:class:`str`) is the path of the entry.
858 """
860 self._stat: os.stat_result = stat
861 """
862 *_stat* (:class:`os.stat_result`) is the stat result of the linked entry.
863 """
865 def is_dir(self, follow_links: Optional[bool] = None) -> bool:
866 """
867 Get whether the entry is a directory.
869 *follow_links* (:class:`bool` or :data:`None`) is whether to follow symbolic
870 links. If this is :data:`True`, a symlink to a directory will result in
871 :data:`True`. Default is :data:`None` for :data:`True`.
873 Returns whether the entry is a directory (:class:`bool`).
874 """
875 if follow_links is None:
876 follow_links = True
878 node_stat = self._stat if follow_links else self._lstat
879 return stat.S_ISDIR(node_stat.st_mode)
881 def is_file(self, follow_links: Optional[bool] = None) -> bool:
882 """
883 Get whether the entry is a regular file.
885 *follow_links* (:class:`bool` or :data:`None`) is whether to follow symbolic
886 links. If this is :data:`True`, a symlink to a regular file will result in
887 :data:`True`. Default is :data:`None` for :data:`True`.
889 Returns whether the entry is a regular file (:class:`bool`).
890 """
891 if follow_links is None:
892 follow_links = True
894 node_stat = self._stat if follow_links else self._lstat
895 return stat.S_ISREG(node_stat.st_mode)
897 def is_symlink(self) -> bool:
898 """
899 Returns whether the entry is a symbolic link (:class:`bool`).
900 """
901 return stat.S_ISLNK(self._lstat.st_mode)
903 def stat(self, follow_links: Optional[bool] = None) -> os.stat_result:
904 """
905 Get the cached stat result for the entry.
907 *follow_links* (:class:`bool` or :data:`None`) is whether to follow symbolic
908 links. If this is :data:`True`, the stat result of the linked file will be
909 returned. Default is :data:`None` for :data:`True`.
911 Returns that stat result (:class:`os.stat_result`).
912 """
913 if follow_links is None:
914 follow_links = True
916 return self._stat if follow_links else self._lstat