| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600 |
- # encoding: utf-8
- """
- This module provides utility methods for dealing with path-specs.
- """
- import os
- import os.path
- import posixpath
- import stat
- from .compat import Collection, Iterable, string_types, unicode
- NORMALIZE_PATH_SEPS = [sep for sep in [os.sep, os.altsep] if sep and sep != posixpath.sep]
- """
- *NORMALIZE_PATH_SEPS* (:class:`list` of :class:`str`) contains the path
- separators that need to be normalized to the POSIX separator for the
- current operating system. The separators are determined by examining
- :data:`os.sep` and :data:`os.altsep`.
- """
- _registered_patterns = {}
- """
- *_registered_patterns* (:class:`dict`) maps a name (:class:`str`) to the
- registered pattern factory (:class:`~collections.abc.Callable`).
- """
- def detailed_match_files(patterns, files, all_matches=None):
- """
- Matches the files to the patterns, and returns which patterns matched
- the files.
- *patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.Pattern`)
- contains the patterns to use.
- *files* (:class:`~collections.abc.Iterable` of :class:`str`) contains
- the normalized file paths to be matched against *patterns*.
- *all_matches* (:class:`boot` or :data:`None`) is whether to return all
- matches patterns (:data:`True`), or only the last matched pattern
- (:data:`False`). Default is :data:`None` for :data:`False`.
- Returns the matched files (:class:`dict`) which maps each matched file
- (:class:`str`) to the patterns that matched in order (:class:`.MatchDetail`).
- """
- all_files = files if isinstance(files, Collection) else list(files)
- return_files = {}
- for pattern in patterns:
- if pattern.include is not None:
- result_files = pattern.match(all_files)
- if pattern.include:
- # Add files and record pattern.
- for result_file in result_files:
- if result_file in return_files:
- if all_matches:
- return_files[result_file].patterns.append(pattern)
- else:
- return_files[result_file].patterns[0] = pattern
- else:
- return_files[result_file] = MatchDetail([pattern])
- else:
- # Remove files.
- for file in result_files:
- del return_files[file]
- return return_files
- def _is_iterable(value):
- """
- Check whether the value is an iterable (excludes strings).
- *value* is the value to check,
- Returns whether *value* is a iterable (:class:`bool`).
- """
- return isinstance(value, Iterable) and not isinstance(value, (unicode, bytes))
- def iter_tree_entries(root, on_error=None, follow_links=None):
- """
- Walks the specified directory for all files and directories.
- *root* (:class:`str`) is the root directory to search.
- *on_error* (:class:`~collections.abc.Callable` or :data:`None`)
- optionally is the error handler for file-system exceptions. It will be
- called with the exception (:exc:`OSError`). Reraise the exception to
- abort the walk. Default is :data:`None` to ignore file-system
- exceptions.
- *follow_links* (:class:`bool` or :data:`None`) optionally is whether
- to walk symbolic links that resolve to directories. Default is
- :data:`None` for :data:`True`.
- Raises :exc:`RecursionError` if recursion is detected.
- Returns an :class:`~collections.abc.Iterable` yielding each file or
- directory entry (:class:`.TreeEntry`) relative to *root*.
- """
- if on_error is not None and not callable(on_error):
- raise TypeError("on_error:{!r} is not callable.".format(on_error))
- if follow_links is None:
- follow_links = True
- for entry in _iter_tree_entries_next(os.path.abspath(root), '', {}, on_error, follow_links):
- yield entry
- def iter_tree_files(root, on_error=None, follow_links=None):
- """
- Walks the specified directory for all files.
- *root* (:class:`str`) is the root directory to search for files.
- *on_error* (:class:`~collections.abc.Callable` or :data:`None`)
- optionally is the error handler for file-system exceptions. It will be
- called with the exception (:exc:`OSError`). Reraise the exception to
- abort the walk. Default is :data:`None` to ignore file-system
- exceptions.
- *follow_links* (:class:`bool` or :data:`None`) optionally is whether
- to walk symbolic links that resolve to directories. Default is
- :data:`None` for :data:`True`.
- Raises :exc:`RecursionError` if recursion is detected.
- Returns an :class:`~collections.abc.Iterable` yielding the path to
- each file (:class:`str`) relative to *root*.
- """
- if on_error is not None and not callable(on_error):
- raise TypeError("on_error:{!r} is not callable.".format(on_error))
- if follow_links is None:
- follow_links = True
- for entry in _iter_tree_entries_next(os.path.abspath(root), '', {}, on_error, follow_links):
- if not entry.is_dir(follow_links):
- yield entry.path
- # Alias `iter_tree_files()` as `iter_tree()`.
- iter_tree = iter_tree_files
- def _iter_tree_entries_next(root_full, dir_rel, memo, on_error, follow_links):
- """
- Scan the directory for all descendant files.
- *root_full* (:class:`str`) the absolute path to the root directory.
- *dir_rel* (:class:`str`) the path to the directory to scan relative to
- *root_full*.
- *memo* (:class:`dict`) keeps track of ancestor directories
- encountered. Maps each ancestor real path (:class:`str`) to relative
- path (:class:`str`).
- *on_error* (:class:`~collections.abc.Callable` or :data:`None`)
- optionally is the error handler for file-system exceptions.
- *follow_links* (:class:`bool`) is whether to walk symbolic links that
- resolve to directories.
- Yields each entry (:class:`.TreeEntry`).
- """
- dir_full = os.path.join(root_full, dir_rel)
- dir_real = os.path.realpath(dir_full)
- # Remember each encountered ancestor directory and its canonical
- # (real) path. If a canonical path is encountered more than once,
- # recursion has occurred.
- if dir_real not in memo:
- memo[dir_real] = dir_rel
- else:
- raise RecursionError(real_path=dir_real, first_path=memo[dir_real], second_path=dir_rel)
- for node_name in os.listdir(dir_full):
- node_rel = os.path.join(dir_rel, node_name)
- node_full = os.path.join(root_full, node_rel)
- # Inspect child node.
- try:
- node_lstat = os.lstat(node_full)
- except OSError as e:
- if on_error is not None:
- on_error(e)
- continue
- if stat.S_ISLNK(node_lstat.st_mode):
- # Child node is a link, inspect the target node.
- is_link = True
- try:
- node_stat = os.stat(node_full)
- except OSError as e:
- if on_error is not None:
- on_error(e)
- continue
- else:
- is_link = False
- node_stat = node_lstat
- if stat.S_ISDIR(node_stat.st_mode) and (follow_links or not is_link):
- # Child node is a directory, recurse into it and yield its
- # descendant files.
- yield TreeEntry(node_name, node_rel, node_lstat, node_stat)
- for entry in _iter_tree_entries_next(root_full, node_rel, memo, on_error, follow_links):
- yield entry
- elif stat.S_ISREG(node_stat.st_mode) or is_link:
- # Child node is either a file or an unfollowed link, yield it.
- yield TreeEntry(node_name, node_rel, node_lstat, node_stat)
- # NOTE: Make sure to remove the canonical (real) path of the directory
- # from the ancestors memo once we are done with it. This allows the
- # same directory to appear multiple times. If this is not done, the
- # second occurrence of the directory will be incorrectly interpreted
- # as a recursion. See <https://github.com/cpburnz/python-path-specification/pull/7>.
- del memo[dir_real]
- def lookup_pattern(name):
- """
- Lookups a registered pattern factory by name.
- *name* (:class:`str`) is the name of the pattern factory.
- Returns the registered pattern factory (:class:`~collections.abc.Callable`).
- If no pattern factory is registered, raises :exc:`KeyError`.
- """
- return _registered_patterns[name]
- def match_file(patterns, file):
- """
- Matches the file to the patterns.
- *patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.Pattern`)
- contains the patterns to use.
- *file* (:class:`str`) is the normalized file path to be matched
- against *patterns*.
- Returns :data:`True` if *file* matched; otherwise, :data:`False`.
- """
- matched = False
- for pattern in patterns:
- if pattern.include is not None:
- if file in pattern.match((file,)):
- matched = pattern.include
- return matched
- def match_files(patterns, files):
- """
- Matches the files to the patterns.
- *patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.Pattern`)
- contains the patterns to use.
- *files* (:class:`~collections.abc.Iterable` of :class:`str`) contains
- the normalized file paths to be matched against *patterns*.
- Returns the matched files (:class:`set` of :class:`str`).
- """
- all_files = files if isinstance(files, Collection) else list(files)
- return_files = set()
- for pattern in patterns:
- if pattern.include is not None:
- result_files = pattern.match(all_files)
- if pattern.include:
- return_files.update(result_files)
- else:
- return_files.difference_update(result_files)
- return return_files
- def _normalize_entries(entries, separators=None):
- """
- Normalizes the entry paths to use the POSIX path separator.
- *entries* (:class:`~collections.abc.Iterable` of :class:`.TreeEntry`)
- contains the entries to be normalized.
- *separators* (:class:`~collections.abc.Collection` of :class:`str`; or
- :data:`None`) optionally contains the path separators to normalize.
- See :func:`normalize_file` for more information.
- Returns a :class:`dict` mapping the each normalized file path (:class:`str`)
- to the entry (:class:`.TreeEntry`)
- """
- norm_files = {}
- for entry in entries:
- norm_files[normalize_file(entry.path, separators=separators)] = entry
- return norm_files
- def normalize_file(file, separators=None):
- """
- Normalizes the file path to use the POSIX path separator (i.e., ``'/'``).
- *file* (:class:`str` or :class:`pathlib.PurePath`) is the file path.
- *separators* (:class:`~collections.abc.Collection` of :class:`str`; or
- :data:`None`) optionally contains the path separators to normalize.
- This does not need to include the POSIX path separator (``'/'``), but
- including it will not affect the results. Default is :data:`None` for
- :data:`NORMALIZE_PATH_SEPS`. To prevent normalization, pass an empty
- container (e.g., an empty tuple ``()``).
- Returns the normalized file path (:class:`str`).
- """
- # Normalize path separators.
- if separators is None:
- separators = NORMALIZE_PATH_SEPS
- # Convert path object to string.
- norm_file = str(file)
- for sep in separators:
- norm_file = norm_file.replace(sep, posixpath.sep)
- # Remove current directory prefix.
- if norm_file.startswith('./'):
- norm_file = norm_file[2:]
- return norm_file
- def normalize_files(files, separators=None):
- """
- Normalizes the file paths to use the POSIX path separator.
- *files* (:class:`~collections.abc.Iterable` of :class:`str` or
- :class:`pathlib.PurePath`) contains the file paths to be normalized.
- *separators* (:class:`~collections.abc.Collection` of :class:`str`; or
- :data:`None`) optionally contains the path separators to normalize.
- See :func:`normalize_file` for more information.
- Returns a :class:`dict` mapping the each normalized file path (:class:`str`)
- to the original file path (:class:`str`)
- """
- norm_files = {}
- for path in files:
- norm_files[normalize_file(path, separators=separators)] = path
- return norm_files
- def register_pattern(name, pattern_factory, override=None):
- """
- Registers the specified pattern factory.
- *name* (:class:`str`) is the name to register the pattern factory
- under.
- *pattern_factory* (:class:`~collections.abc.Callable`) is used to
- compile patterns. It must accept an uncompiled pattern (:class:`str`)
- and return the compiled pattern (:class:`.Pattern`).
- *override* (:class:`bool` or :data:`None`) optionally is whether to
- allow overriding an already registered pattern under the same name
- (:data:`True`), instead of raising an :exc:`AlreadyRegisteredError`
- (:data:`False`). Default is :data:`None` for :data:`False`.
- """
- if not isinstance(name, string_types):
- raise TypeError("name:{!r} is not a string.".format(name))
- if not callable(pattern_factory):
- raise TypeError("pattern_factory:{!r} is not callable.".format(pattern_factory))
- if name in _registered_patterns and not override:
- raise AlreadyRegisteredError(name, _registered_patterns[name])
- _registered_patterns[name] = pattern_factory
- class AlreadyRegisteredError(Exception):
- """
- The :exc:`AlreadyRegisteredError` exception is raised when a pattern
- factory is registered under a name already in use.
- """
- def __init__(self, name, pattern_factory):
- """
- Initializes the :exc:`AlreadyRegisteredError` instance.
- *name* (:class:`str`) is the name of the registered pattern.
- *pattern_factory* (:class:`~collections.abc.Callable`) is the
- registered pattern factory.
- """
- super(AlreadyRegisteredError, self).__init__(name, pattern_factory)
- @property
- def message(self):
- """
- *message* (:class:`str`) is the error message.
- """
- return "{name!r} is already registered for pattern factory:{pattern_factory!r}.".format(
- name=self.name,
- pattern_factory=self.pattern_factory,
- )
- @property
- def name(self):
- """
- *name* (:class:`str`) is the name of the registered pattern.
- """
- return self.args[0]
- @property
- def pattern_factory(self):
- """
- *pattern_factory* (:class:`~collections.abc.Callable`) is the
- registered pattern factory.
- """
- return self.args[1]
- class RecursionError(Exception):
- """
- The :exc:`RecursionError` exception is raised when recursion is
- detected.
- """
- def __init__(self, real_path, first_path, second_path):
- """
- Initializes the :exc:`RecursionError` instance.
- *real_path* (:class:`str`) is the real path that recursion was
- encountered on.
- *first_path* (:class:`str`) is the first path encountered for
- *real_path*.
- *second_path* (:class:`str`) is the second path encountered for
- *real_path*.
- """
- super(RecursionError, self).__init__(real_path, first_path, second_path)
- @property
- def first_path(self):
- """
- *first_path* (:class:`str`) is the first path encountered for
- :attr:`self.real_path <RecursionError.real_path>`.
- """
- return self.args[1]
- @property
- def message(self):
- """
- *message* (:class:`str`) is the error message.
- """
- return "Real path {real!r} was encountered at {first!r} and then {second!r}.".format(
- real=self.real_path,
- first=self.first_path,
- second=self.second_path,
- )
- @property
- def real_path(self):
- """
- *real_path* (:class:`str`) is the real path that recursion was
- encountered on.
- """
- return self.args[0]
- @property
- def second_path(self):
- """
- *second_path* (:class:`str`) is the second path encountered for
- :attr:`self.real_path <RecursionError.real_path>`.
- """
- return self.args[2]
- class MatchDetail(object):
- """
- The :class:`.MatchDetail` class contains information about
- """
- #: Make the class dict-less.
- __slots__ = ('patterns',)
- def __init__(self, patterns):
- """
- Initialize the :class:`.MatchDetail` instance.
- *patterns* (:class:`~collections.abc.Sequence` of :class:`~pathspec.pattern.Pattern`)
- contains the patterns that matched the file in the order they were
- encountered.
- """
- self.patterns = patterns
- """
- *patterns* (:class:`~collections.abc.Sequence` of :class:`~pathspec.pattern.Pattern`)
- contains the patterns that matched the file in the order they were
- encountered.
- """
- class TreeEntry(object):
- """
- The :class:`.TreeEntry` class contains information about a file-system
- entry.
- """
- #: Make the class dict-less.
- __slots__ = ('_lstat', 'name', 'path', '_stat')
- def __init__(self, name, path, lstat, stat):
- """
- Initialize the :class:`.TreeEntry` instance.
- *name* (:class:`str`) is the base name of the entry.
- *path* (:class:`str`) is the relative path of the entry.
- *lstat* (:class:`~os.stat_result`) is the stat result of the direct
- entry.
- *stat* (:class:`~os.stat_result`) is the stat result of the entry,
- potentially linked.
- """
- self._lstat = lstat
- """
- *_lstat* (:class:`~os.stat_result`) is the stat result of the direct
- entry.
- """
- self.name = name
- """
- *name* (:class:`str`) is the base name of the entry.
- """
- self.path = path
- """
- *path* (:class:`str`) is the path of the entry.
- """
- self._stat = stat
- """
- *_stat* (:class:`~os.stat_result`) is the stat result of the linked
- entry.
- """
- def is_dir(self, follow_links=None):
- """
- Get whether the entry is a directory.
- *follow_links* (:class:`bool` or :data:`None`) is whether to follow
- symbolic links. If this is :data:`True`, a symlink to a directory
- will result in :data:`True`. Default is :data:`None` for :data:`True`.
- Returns whether the entry is a directory (:class:`bool`).
- """
- if follow_links is None:
- follow_links = True
- node_stat = self._stat if follow_links else self._lstat
- return stat.S_ISDIR(node_stat.st_mode)
- def is_file(self, follow_links=None):
- """
- Get whether the entry is a regular file.
- *follow_links* (:class:`bool` or :data:`None`) is whether to follow
- symbolic links. If this is :data:`True`, a symlink to a regular file
- will result in :data:`True`. Default is :data:`None` for :data:`True`.
- Returns whether the entry is a regular file (:class:`bool`).
- """
- if follow_links is None:
- follow_links = True
- node_stat = self._stat if follow_links else self._lstat
- return stat.S_ISREG(node_stat.st_mode)
- def is_symlink(self):
- """
- Returns whether the entry is a symbolic link (:class:`bool`).
- """
- return stat.S_ISLNK(self._lstat.st_mode)
- def stat(self, follow_links=None):
- """
- Get the cached stat result for the entry.
- *follow_links* (:class:`bool` or :data:`None`) is whether to follow
- symbolic links. If this is :data:`True`, the stat result of the
- linked file will be returned. Default is :data:`None` for :data:`True`.
- Returns that stat result (:class:`~os.stat_result`).
- """
- if follow_links is None:
- follow_links = True
- return self._stat if follow_links else self._lstat
|