""" This module provides an object oriented interface for pattern matching of files. """ from collections.abc import ( Collection as CollectionType) from itertools import ( zip_longest) from os import ( PathLike) from typing import ( AnyStr, Callable, Collection, Iterable, Iterator, Optional, TYPE_CHECKING, Union) from . import util from .pattern import ( Pattern) from .util import ( TreeEntry, _filter_patterns, _is_iterable, match_file, normalize_file) class PathSpec(object): """ The :class:`PathSpec` class is a wrapper around a list of compiled :class:`.Pattern` instances. """ def __init__(self, patterns: Iterable[Pattern]) -> None: """ Initializes the :class:`PathSpec` instance. *patterns* (:class:`~collections.abc.Collection` or :class:`~collections.abc.Iterable`) yields each compiled pattern (:class:`.Pattern`). """ self.patterns = patterns if isinstance(patterns, CollectionType) else list(patterns) """ *patterns* (:class:`~collections.abc.Collection` of :class:`.Pattern`) contains the compiled patterns. """ def __eq__(self, other: 'Self') -> bool: """ Tests the equality of this path-spec with *other* (:class:`PathSpec`) by comparing their :attr:`~PathSpec.patterns` attributes. """ if isinstance(other, PathSpec): paired_patterns = zip_longest(self.patterns, other.patterns) return all(a == b for a, b in paired_patterns) else: return NotImplemented def __len__(self) -> int: """ Returns the number of compiled patterns this path-spec contains (:class:`int`). """ return len(self.patterns) def __add__(self, other: 'Self') -> 'Self': """ Combines the :attr:`Pathspec.patterns` patterns from two :class:`PathSpec` instances. """ if isinstance(other, PathSpec): return self.__class__(self.patterns + other.patterns) else: return NotImplemented def __iadd__(self, other: 'Self') -> 'Self': """ Adds the :attr:`Pathspec.patterns` patterns from one :class:`PathSpec` instance to this instance. """ if isinstance(other, PathSpec): self.patterns += other.patterns return self else: return NotImplemented @classmethod def from_lines( cls, pattern_factory: Union[str, Callable[[AnyStr], Pattern]], lines: Iterable[AnyStr], ) -> 'Self': """ Compiles the pattern lines. *pattern_factory* can be either the name of a registered pattern factory (:class:`str`), or a :class:`~collections.abc.Callable` used to compile patterns. It must accept an uncompiled pattern (:class:`str`) and return the compiled pattern (:class:`.Pattern`). *lines* (:class:`~collections.abc.Iterable`) yields each uncompiled pattern (:class:`str`). This simply has to yield each line so it can be a :class:`io.TextIOBase` (e.g., from :func:`open` or :class:`io.StringIO`) or the result from :meth:`str.splitlines`. Returns the :class:`PathSpec` instance. """ if isinstance(pattern_factory, str): pattern_factory = util.lookup_pattern(pattern_factory) if not callable(pattern_factory): raise TypeError(f"pattern_factory:{pattern_factory!r} is not callable.") if not _is_iterable(lines): raise TypeError(f"lines:{lines!r} is not an iterable.") patterns = [pattern_factory(line) for line in lines if line] return cls(patterns) def match_entries( self, entries: Iterable[TreeEntry], separators: Optional[Collection[str]] = None, ) -> Iterator[TreeEntry]: """ Matches the entries to this path-spec. *entries* (:class:`~collections.abc.Iterable` of :class:`~util.TreeEntry`) contains the entries to be matched against :attr:`self.patterns `. *separators* (:class:`~collections.abc.Collection` of :class:`str`; or :data:`None`) optionally contains the path separators to normalize. See :func:`~pathspec.util.normalize_file` for more information. Returns the matched entries (:class:`~collections.abc.Iterator` of :class:`~util.TreeEntry`). """ if not _is_iterable(entries): raise TypeError(f"entries:{entries!r} is not an iterable.") use_patterns = _filter_patterns(self.patterns) for entry in entries: norm_file = normalize_file(entry.path, separators) if self._match_file(use_patterns, norm_file): yield entry # Match files using the `match_file()` utility function. Subclasses # may override this method as an instance method. It does not have to # be a static method. _match_file = staticmethod(match_file) def match_file( self, file: Union[str, PathLike], separators: Optional[Collection[str]] = None, ) -> bool: """ Matches the file to this path-spec. *file* (:class:`str` or :class:`os.PathLike`) is the file path to be matched against :attr:`self.patterns `. *separators* (:class:`~collections.abc.Collection` of :class:`str`) optionally contains the path separators to normalize. See :func:`~pathspec.util.normalize_file` for more information. Returns :data:`True` if *file* matched; otherwise, :data:`False`. """ norm_file = util.normalize_file(file, separators=separators) return self._match_file(self.patterns, norm_file) def match_files( self, files: Iterable[Union[str, PathLike]], separators: Optional[Collection[str]] = None, ) -> Iterator[Union[str, PathLike]]: """ Matches the files to this path-spec. *files* (:class:`~collections.abc.Iterable` of :class:`str` or :class:`os.PathLike`) contains the file paths to be matched against :attr:`self.patterns `. *separators* (:class:`~collections.abc.Collection` of :class:`str`; or :data:`None`) optionally contains the path separators to normalize. See :func:`~pathspec.util.normalize_file` for more information. Returns the matched files (:class:`~collections.abc.Iterator` of :class:`str` or :class:`os.PathLike`). """ if not _is_iterable(files): raise TypeError(f"files:{files!r} is not an iterable.") use_patterns = _filter_patterns(self.patterns) for orig_file in files: norm_file = normalize_file(orig_file, separators) if self._match_file(use_patterns, norm_file): yield orig_file def match_tree_entries( self, root: str, on_error: Optional[Callable] = None, follow_links: Optional[bool] = None, ) -> Iterator[TreeEntry]: """ Walks the specified root path for all files and matches them to this path-spec. *root* (:class:`str` or :class:`os.PathLike`) is the root directory to search. *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally is the error handler for file-system exceptions. See :func:`~pathspec.util.iter_tree_entries` for more information. *follow_links* (:class:`bool` or :data:`None`) optionally is whether to walk symbolic links that resolve to directories. See :func:`~pathspec.util.iter_tree_files` for more information. Returns the matched files (:class:`~collections.abc.Iterator` of :class:`.TreeEntry`). """ entries = util.iter_tree_entries(root, on_error=on_error, follow_links=follow_links) yield from self.match_entries(entries) def match_tree_files( self, root: str, on_error: Optional[Callable] = None, follow_links: Optional[bool] = None, ) -> Iterator[str]: """ Walks the specified root path for all files and matches them to this path-spec. *root* (:class:`str` or :class:`os.PathLike`) 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. See :func:`~pathspec.util.iter_tree_files` for more information. *follow_links* (:class:`bool` or :data:`None`) optionally is whether to walk symbolic links that resolve to directories. See :func:`~pathspec.util.iter_tree_files` for more information. Returns the matched files (:class:`~collections.abc.Iterable` of :class:`str`). """ files = util.iter_tree_files(root, on_error=on_error, follow_links=follow_links) yield from self.match_files(files) # Alias `match_tree_files()` as `match_tree()` for backward # compatibility before v0.3.2. match_tree = match_tree_files if TYPE_CHECKING: try: from typing import Self except ImportError: try: from typing_extensions import Self except ImportError: Self = PathSpec