Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pathspec/util.py: 31%
191 statements
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-25 06:11 +0000
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-25 06:11 +0000
1# encoding: utf-8
2"""
3This module provides utility methods for dealing with path-specs.
4"""
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
33from .compat import (
34 CollectionType,
35 IterableType,
36 string_types,
37 unicode)
38from .pattern import Pattern
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"""
48_registered_patterns = {}
49"""
50*_registered_patterns* (:class:`dict`) maps a name (:class:`str`) to the
51registered pattern factory (:class:`~collections.abc.Callable`).
52"""
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.
61 *patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.Pattern`)
62 contains the patterns to use.
64 *files* (:class:`~collections.abc.Iterable` of :class:`str`) contains
65 the normalized file paths to be matched against *patterns*.
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`.
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])
90 else:
91 # Remove files.
92 for file in result_files:
93 del return_files[file]
95 return return_files
98def _is_iterable(value):
99 # type: (Any) -> bool
100 """
101 Check whether the value is an iterable (excludes strings).
103 *value* is the value to check,
105 Returns whether *value* is a iterable (:class:`bool`).
106 """
107 return isinstance(value, IterableType) and not isinstance(value, (unicode, bytes))
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.
115 *root* (:class:`str`) is the root directory to search.
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.
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`.
127 Raises :exc:`RecursionError` if recursion is detected.
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))
135 if follow_links is None:
136 follow_links = True
138 for entry in _iter_tree_entries_next(os.path.abspath(root), '', {}, on_error, follow_links):
139 yield entry
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.
147 *root* (:class:`str`) is the root directory to search for files.
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.
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`.
159 Raises :exc:`RecursionError` if recursion is detected.
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))
167 if follow_links is None:
168 follow_links = True
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
175# Alias `iter_tree_files()` as `iter_tree()`.
176iter_tree = iter_tree_files
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.
184 *root_full* (:class:`str`) the absolute path to the root directory.
186 *dir_rel* (:class:`str`) the path to the directory to scan relative to
187 *root_full*.
189 *memo* (:class:`dict`) keeps track of ancestor directories
190 encountered. Maps each ancestor real path (:class:`str`) to relative
191 path (:class:`str`).
193 *on_error* (:class:`~collections.abc.Callable` or :data:`None`)
194 optionally is the error handler for file-system exceptions.
196 *follow_links* (:class:`bool`) is whether to walk symbolic links that
197 resolve to directories.
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)
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)
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)
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
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
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)
242 for entry in _iter_tree_entries_next(root_full, node_rel, memo, on_error, follow_links):
243 yield entry
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)
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]
257def lookup_pattern(name):
258 # type: (Text) -> Callable[[AnyStr], Pattern]
259 """
260 Lookups a registered pattern factory by name.
262 *name* (:class:`str`) is the name of the pattern factory.
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]
270def match_file(patterns, file):
271 # type: (Iterable[Pattern], Text) -> bool
272 """
273 Matches the file to the patterns.
275 *patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.Pattern`)
276 contains the patterns to use.
278 *file* (:class:`str`) is the normalized file path to be matched
279 against *patterns*.
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
291def match_files(patterns, files):
292 # type: (Iterable[Pattern], Iterable[Text]) -> Set[Text]
293 """
294 Matches the files to the patterns.
296 *patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.Pattern`)
297 contains the patterns to use.
299 *files* (:class:`~collections.abc.Iterable` of :class:`str`) contains
300 the normalized file paths to be matched against *patterns*.
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
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.
321 *entries* (:class:`~collections.abc.Iterable` of :class:`.TreeEntry`)
322 contains the entries to be normalized.
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.
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
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 ``'/'``).
343 *file* (:class:`str` or :class:`pathlib.PurePath`) is the file path.
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 ``()``).
352 Returns the normalized file path (:class:`str`).
353 """
354 # Normalize path separators.
355 if separators is None:
356 separators = NORMALIZE_PATH_SEPS
358 # Convert path object to string.
359 norm_file = str(file)
361 for sep in separators:
362 norm_file = norm_file.replace(sep, posixpath.sep)
364 if norm_file.startswith('/'):
365 # Make path relative.
366 norm_file = norm_file[1:]
368 elif norm_file.startswith('./'):
369 # Remove current directory prefix.
370 norm_file = norm_file[2:]
372 return norm_file
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.
380 *files* (:class:`~collections.abc.Iterable` of :class:`str` or
381 :class:`pathlib.PurePath`) contains the file paths to be normalized.
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.
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]
399 return norm_files
402def register_pattern(name, pattern_factory, override=None):
403 # type: (Text, Callable[[AnyStr], Pattern], Optional[bool]) -> None
404 """
405 Registers the specified pattern factory.
407 *name* (:class:`str`) is the name to register the pattern factory
408 under.
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`).
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
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 """
434 def __init__(self, name, pattern_factory):
435 # type: (Text, Callable[[AnyStr], Pattern]) -> None
436 """
437 Initializes the :exc:`AlreadyRegisteredError` instance.
439 *name* (:class:`str`) is the name of the registered pattern.
441 *pattern_factory* (:class:`~collections.abc.Callable`) is the
442 registered pattern factory.
443 """
444 super(AlreadyRegisteredError, self).__init__(name, pattern_factory)
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 )
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]
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]
475class RecursionError(Exception):
476 """
477 The :exc:`RecursionError` exception is raised when recursion is
478 detected.
479 """
481 def __init__(self, real_path, first_path, second_path):
482 # type: (Text, Text, Text) -> None
483 """
484 Initializes the :exc:`RecursionError` instance.
486 *real_path* (:class:`str`) is the real path that recursion was
487 encountered on.
489 *first_path* (:class:`str`) is the first path encountered for
490 *real_path*.
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)
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]
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 )
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]
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]
537class MatchDetail(object):
538 """
539 The :class:`.MatchDetail` class contains information about
540 """
542 #: Make the class dict-less.
543 __slots__ = ('patterns',)
545 def __init__(self, patterns):
546 # type: (Sequence[Pattern]) -> None
547 """
548 Initialize the :class:`.MatchDetail` instance.
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 """
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 """
563class TreeEntry(object):
564 """
565 The :class:`.TreeEntry` class contains information about a file-system
566 entry.
567 """
569 #: Make the class dict-less.
570 __slots__ = ('_lstat', 'name', 'path', '_stat')
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.
577 *name* (:class:`str`) is the base name of the entry.
579 *path* (:class:`str`) is the relative path of the entry.
581 *lstat* (:class:`~os.stat_result`) is the stat result of the direct
582 entry.
584 *stat* (:class:`~os.stat_result`) is the stat result of the entry,
585 potentially linked.
586 """
588 self._lstat = lstat
589 """
590 *_lstat* (:class:`~os.stat_result`) is the stat result of the direct
591 entry.
592 """
594 self.name = name
595 """
596 *name* (:class:`str`) is the base name of the entry.
597 """
599 self.path = path
600 """
601 *path* (:class:`str`) is the path of the entry.
602 """
604 self._stat = stat
605 """
606 *_stat* (:class:`~os.stat_result`) is the stat result of the linked
607 entry.
608 """
610 def is_dir(self, follow_links=None):
611 # type: (Optional[bool]) -> bool
612 """
613 Get whether the entry is a directory.
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`.
619 Returns whether the entry is a directory (:class:`bool`).
620 """
621 if follow_links is None:
622 follow_links = True
624 node_stat = self._stat if follow_links else self._lstat
625 return stat.S_ISDIR(node_stat.st_mode)
627 def is_file(self, follow_links=None):
628 # type: (Optional[bool]) -> bool
629 """
630 Get whether the entry is a regular file.
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`.
636 Returns whether the entry is a regular file (:class:`bool`).
637 """
638 if follow_links is None:
639 follow_links = True
641 node_stat = self._stat if follow_links else self._lstat
642 return stat.S_ISREG(node_stat.st_mode)
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)
651 def stat(self, follow_links=None):
652 # type: (Optional[bool]) -> os.stat_result
653 """
654 Get the cached stat result for the entry.
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`.
660 Returns that stat result (:class:`~os.stat_result`).
661 """
662 if follow_links is None:
663 follow_links = True
665 return self._stat if follow_links else self._lstat