274 lines
8.1 KiB
Python
274 lines
8.1 KiB
Python
|
"""
|
||
|
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 <PathSpec.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 <PathSpec.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 <PathSpec.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
|