1
0
mirror of https://gitlab.com/MoonTestUse1/AdministrationItDepartmens.git synced 2025-08-14 00:25:46 +02:00

Initial commit

This commit is contained in:
MoonTestUse1
2024-12-23 19:27:44 +06:00
commit e81df4c87e
4952 changed files with 1705479 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
"""
The *pathspec* package provides pattern matching for file paths. So far
this only includes Git's wildmatch pattern matching (the style used for
".gitignore" files).
The following classes are imported and made available from the root of
the `pathspec` package:
- :class:`pathspec.gitignore.GitIgnoreSpec`
- :class:`pathspec.pathspec.PathSpec`
- :class:`pathspec.pattern.Pattern`
- :class:`pathspec.pattern.RegexPattern`
- :class:`pathspec.util.RecursionError`
The following functions are also imported:
- :func:`pathspec.util.lookup_pattern`
The following deprecated functions are also imported to maintain
backward compatibility:
- :func:`pathspec.util.iter_tree` which is an alias for
:func:`pathspec.util.iter_tree_files`.
- :func:`pathspec.util.match_files`
"""
from .gitignore import (
GitIgnoreSpec)
from .pathspec import (
PathSpec)
from .pattern import (
Pattern,
RegexPattern)
from .util import (
RecursionError,
iter_tree,
lookup_pattern,
match_files)
from ._meta import (
__author__,
__copyright__,
__credits__,
__license__,
__version__,
)
# Load pattern implementations.
from . import patterns
# DEPRECATED: Expose the `GitIgnorePattern` class in the root module for
# backward compatibility with v0.4.
from .patterns.gitwildmatch import GitIgnorePattern
# Declare private imports as part of the public interface. Deprecated
# imports are deliberately excluded.
__all__ = [
'GitIgnoreSpec',
'PathSpec',
'Pattern',
'RecursionError',
'RegexPattern',
'__author__',
'__copyright__',
'__credits__',
'__license__',
'__version__',
'iter_tree',
'lookup_pattern',
'match_files',
]

View File

@@ -0,0 +1,58 @@
"""
This module contains the project meta-data.
"""
__author__ = "Caleb P. Burns"
__copyright__ = "Copyright © 2013-2023 Caleb P. Burns"
__credits__ = [
"dahlia <https://github.com/dahlia>",
"highb <https://github.com/highb>",
"029xue <https://github.com/029xue>",
"mikexstudios <https://github.com/mikexstudios>",
"nhumrich <https://github.com/nhumrich>",
"davidfraser <https://github.com/davidfraser>",
"demurgos <https://github.com/demurgos>",
"ghickman <https://github.com/ghickman>",
"nvie <https://github.com/nvie>",
"adrienverge <https://github.com/adrienverge>",
"AndersBlomdell <https://github.com/AndersBlomdell>",
"thmxv <https://github.com/thmxv>",
"wimglenn <https://github.com/wimglenn>",
"hugovk <https://github.com/hugovk>",
"dcecile <https://github.com/dcecile>",
"mroutis <https://github.com/mroutis>",
"jdufresne <https://github.com/jdufresne>",
"groodt <https://github.com/groodt>",
"ftrofin <https://github.com/ftrofin>",
"pykong <https://github.com/pykong>",
"nhhollander <https://github.com/nhhollander>",
"KOLANICH <https://github.com/KOLANICH>",
"JonjonHays <https://github.com/JonjonHays>",
"Isaac0616 <https://github.com/Isaac0616>",
"SebastiaanZ <https://github.com/SebastiaanZ>",
"RoelAdriaans <https://github.com/RoelAdriaans>",
"raviselker <https://github.com/raviselker>",
"johanvergeer <https://github.com/johanvergeer>",
"danjer <https://github.com/danjer>",
"jhbuhrman <https://github.com/jhbuhrman>",
"WPDOrdina <https://github.com/WPDOrdina>",
"tirkarthi <https://github.com/tirkarthi>",
"jayvdb <https://github.com/jayvdb>",
"jwodder <https://github.com/jwodder>",
"kloczek <https://github.com/kloczek>",
"orens <https://github.com/orens>",
"spMohanty <https://github.com/spMohanty>",
"ichard26 <https://github.com/ichard26>",
"jack1142 <https://github.com/jack1142>",
"mgorny <https://github.com/mgorny>",
"bzakdd <https://github.com/bzakdd>",
"haimat <https://github.com/haimat>",
"Avasam <https://github.com/Avasam>",
"yschroeder <https://github.com/yschroeder>",
"axesider <https://github.com/axesider>",
"tomruk <https://github.com/tomruk>",
"oprypin <https://github.com/oprypin>",
"kurtmckee <https://github.com/kurtmckee>",
]
__license__ = "MPL 2.0"
__version__ = "0.12.1"

View File

@@ -0,0 +1,157 @@
"""
This module provides :class:`.GitIgnoreSpec` which replicates
*.gitignore* behavior.
"""
from typing import (
AnyStr,
Callable, # Replaced by `collections.abc.Callable` in 3.9.
Iterable, # Replaced by `collections.abc.Iterable` in 3.9.
Optional, # Replaced by `X | None` in 3.10.
Tuple, # Replaced by `tuple` in 3.9.
Type, # Replaced by `type` in 3.9.
TypeVar,
Union, # Replaced by `X | Y` in 3.10.
cast,
overload)
from .pathspec import (
PathSpec)
from .pattern import (
Pattern)
from .patterns.gitwildmatch import (
GitWildMatchPattern,
_DIR_MARK)
from .util import (
_is_iterable)
Self = TypeVar("Self", bound="GitIgnoreSpec")
"""
:class:`GitIgnoreSpec` self type hint to support Python v<3.11 using PEP
673 recommendation.
"""
class GitIgnoreSpec(PathSpec):
"""
The :class:`GitIgnoreSpec` class extends :class:`pathspec.pathspec.PathSpec` to
replicate *.gitignore* behavior.
"""
def __eq__(self, other: object) -> bool:
"""
Tests the equality of this gitignore-spec with *other* (:class:`GitIgnoreSpec`)
by comparing their :attr:`~pathspec.pattern.Pattern`
attributes. A non-:class:`GitIgnoreSpec` will not compare equal.
"""
if isinstance(other, GitIgnoreSpec):
return super().__eq__(other)
elif isinstance(other, PathSpec):
return False
else:
return NotImplemented
# Support reversed order of arguments from PathSpec.
@overload
@classmethod
def from_lines(
cls: Type[Self],
pattern_factory: Union[str, Callable[[AnyStr], Pattern]],
lines: Iterable[AnyStr],
) -> Self:
...
@overload
@classmethod
def from_lines(
cls: Type[Self],
lines: Iterable[AnyStr],
pattern_factory: Union[str, Callable[[AnyStr], Pattern], None] = None,
) -> Self:
...
@classmethod
def from_lines(
cls: Type[Self],
lines: Iterable[AnyStr],
pattern_factory: Union[str, Callable[[AnyStr], Pattern], None] = None,
) -> Self:
"""
Compiles the pattern lines.
*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`.
*pattern_factory* can be :data:`None`, the name of a registered
pattern factory (:class:`str`), or a :class:`~collections.abc.Callable`
used to compile patterns. The callable must accept an uncompiled
pattern (:class:`str`) and return the compiled pattern
(:class:`pathspec.pattern.Pattern`).
Default is :data:`None` for :class:`.GitWildMatchPattern`).
Returns the :class:`GitIgnoreSpec` instance.
"""
if pattern_factory is None:
pattern_factory = GitWildMatchPattern
elif (isinstance(lines, (str, bytes)) or callable(lines)) and _is_iterable(pattern_factory):
# Support reversed order of arguments from PathSpec.
pattern_factory, lines = lines, pattern_factory
self = super().from_lines(pattern_factory, lines)
return cast(Self, self)
@staticmethod
def _match_file(
patterns: Iterable[Tuple[int, GitWildMatchPattern]],
file: str,
) -> Tuple[Optional[bool], Optional[int]]:
"""
Check the file against the patterns.
.. NOTE:: Subclasses of :class:`~pathspec.pathspec.PathSpec` may override
this method as an instance method. It does not have to be a static
method. The signature for this method is subject to change.
*patterns* (:class:`~collections.abc.Iterable`) yields each indexed pattern
(:class:`tuple`) which contains the pattern index (:class:`int`) and actual
pattern (:class:`~pathspec.pattern.Pattern`).
*file* (:class:`str`) is the normalized file path to be matched against
*patterns*.
Returns a :class:`tuple` containing whether to include *file* (:class:`bool`
or :data:`None`), and the index of the last matched pattern (:class:`int` or
:data:`None`).
"""
out_include: Optional[bool] = None
out_index: Optional[int] = None
out_priority = 0
for index, pattern in patterns:
if pattern.include is not None:
match = pattern.match_file(file)
if match is not None:
# Pattern matched.
# Check for directory marker.
dir_mark = match.match.groupdict().get(_DIR_MARK)
if dir_mark:
# Pattern matched by a directory pattern.
priority = 1
else:
# Pattern matched by a file pattern.
priority = 2
if pattern.include and dir_mark:
out_include = pattern.include
out_index = index
out_priority = priority
elif priority >= out_priority:
out_include = pattern.include
out_index = index
out_priority = priority
return out_include, out_index

View File

@@ -0,0 +1,394 @@
"""
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 typing import (
AnyStr,
Callable, # Replaced by `collections.abc.Callable` in 3.9.
Collection, # Replaced by `collections.abc.Collection` in 3.9.
Iterable, # Replaced by `collections.abc.Iterable` in 3.9.
Iterator, # Replaced by `collections.abc.Iterator` in 3.9.
Optional, # Replaced by `X | None` in 3.10.
Type, # Replaced by `type` in 3.9.
TypeVar,
Union) # Replaced by `X | Y` in 3.10.
from . import util
from .pattern import (
Pattern)
from .util import (
CheckResult,
StrPath,
TStrPath,
TreeEntry,
_filter_check_patterns,
_is_iterable,
normalize_file)
Self = TypeVar("Self", bound="PathSpec")
"""
:class:`PathSpec` self type hint to support Python v<3.11 using PEP 673
recommendation.
"""
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`).
"""
if not isinstance(patterns, CollectionType):
patterns = list(patterns)
self.patterns: Collection[Pattern] = patterns
"""
*patterns* (:class:`~collections.abc.Collection` of :class:`.Pattern`)
contains the compiled patterns.
"""
def __eq__(self, other: object) -> 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: Self, other: "PathSpec") -> 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: Self, other: "PathSpec") -> 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
def check_file(
self,
file: TStrPath,
separators: Optional[Collection[str]] = None,
) -> CheckResult[TStrPath]:
"""
Check the files against 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`; or
:data:`None`) optionally contains the path separators to normalize. See
:func:`~pathspec.util.normalize_file` for more information.
Returns the file check result (:class:`~pathspec.util.CheckResult`).
"""
norm_file = normalize_file(file, separators)
include, index = self._match_file(enumerate(self.patterns), norm_file)
return CheckResult(file, include, index)
def check_files(
self,
files: Iterable[TStrPath],
separators: Optional[Collection[str]] = None,
) -> Iterator[CheckResult[TStrPath]]:
"""
Check the files against this path-spec.
*files* (:class:`~collections.abc.Iterable` of :class:`str` or
:class:`os.PathLike`) contains the file paths to be checked 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 an :class:`~collections.abc.Iterator` yielding each file check
result (:class:`~pathspec.util.CheckResult`).
"""
if not _is_iterable(files):
raise TypeError(f"files:{files!r} is not an iterable.")
use_patterns = _filter_check_patterns(self.patterns)
for orig_file in files:
norm_file = normalize_file(orig_file, separators)
include, index = self._match_file(use_patterns, norm_file)
yield CheckResult(orig_file, include, index)
def check_tree_files(
self,
root: StrPath,
on_error: Optional[Callable[[OSError], None]] = None,
follow_links: Optional[bool] = None,
) -> Iterator[CheckResult[str]]:
"""
Walks the specified root path for all files and checks them against 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. 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`.
*negate* (:class:`bool` or :data:`None`) is whether to negate the match
results of the patterns. If :data:`True`, a pattern matching a file will
exclude the file rather than include it. Default is :data:`None` for
:data:`False`.
Returns an :class:`~collections.abc.Iterator` yielding each file check
result (:class:`~pathspec.util.CheckResult`).
"""
files = util.iter_tree_files(root, on_error=on_error, follow_links=follow_links)
yield from self.check_files(files)
@classmethod
def from_lines(
cls: Type[Self],
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 that 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,
*,
negate: Optional[bool] = None,
) -> Iterator[TreeEntry]:
"""
Matches the entries to this path-spec.
*entries* (:class:`~collections.abc.Iterable` of :class:`~pathspec.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.
*negate* (:class:`bool` or :data:`None`) is whether to negate the match
results of the patterns. If :data:`True`, a pattern matching a file will
exclude the file rather than include it. Default is :data:`None` for
:data:`False`.
Returns the matched entries (:class:`~collections.abc.Iterator` of
:class:`~pathspec.util.TreeEntry`).
"""
if not _is_iterable(entries):
raise TypeError(f"entries:{entries!r} is not an iterable.")
use_patterns = _filter_check_patterns(self.patterns)
for entry in entries:
norm_file = normalize_file(entry.path, separators)
include, _index = self._match_file(use_patterns, norm_file)
if negate:
include = not include
if include:
yield entry
_match_file = staticmethod(util.check_match_file)
"""
Match files using the `check_match_file()` utility function. Subclasses may
override this method as an instance method. It does not have to be a static
method. The signature for this method is subject to change.
"""
def match_file(
self,
file: StrPath,
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 = normalize_file(file, separators)
include, _index = self._match_file(enumerate(self.patterns), norm_file)
return bool(include)
def match_files(
self,
files: Iterable[StrPath],
separators: Optional[Collection[str]] = None,
*,
negate: Optional[bool] = None,
) -> Iterator[StrPath]:
"""
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.
*negate* (:class:`bool` or :data:`None`) is whether to negate the match
results of the patterns. If :data:`True`, a pattern matching a file will
exclude the file rather than include it. Default is :data:`None` for
:data:`False`.
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_check_patterns(self.patterns)
for orig_file in files:
norm_file = normalize_file(orig_file, separators)
include, _index = self._match_file(use_patterns, norm_file)
if negate:
include = not include
if include:
yield orig_file
def match_tree_entries(
self,
root: StrPath,
on_error: Optional[Callable[[OSError], None]] = None,
follow_links: Optional[bool] = None,
*,
negate: 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. 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`.
*negate* (:class:`bool` or :data:`None`) is whether to negate the match
results of the patterns. If :data:`True`, a pattern matching a file will
exclude the file rather than include it. Default is :data:`None` for
:data:`False`.
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, negate=negate)
def match_tree_files(
self,
root: StrPath,
on_error: Optional[Callable[[OSError], None]] = None,
follow_links: Optional[bool] = None,
*,
negate: 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. 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`.
*negate* (:class:`bool` or :data:`None`) is whether to negate the match
results of the patterns. If :data:`True`, a pattern matching a file will
exclude the file rather than include it. Default is :data:`None` for
:data:`False`.
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, negate=negate)
# Alias `match_tree_files()` as `match_tree()` for backward compatibility
# before v0.3.2.
match_tree = match_tree_files

View File

@@ -0,0 +1,213 @@
"""
This module provides the base definition for patterns.
"""
import dataclasses
import re
import warnings
from typing import (
Any,
AnyStr,
Iterable, # Replaced by `collections.abc.Iterable` in 3.9.
Iterator, # Replaced by `collections.abc.Iterator` in 3.9.
Match as MatchHint, # Replaced by `re.Match` in 3.9.
Optional, # Replaced by `X | None` in 3.10.
Pattern as PatternHint, # Replaced by `re.Pattern` in 3.9.
Tuple, # Replaced by `tuple` in 3.9.
Union) # Replaced by `X | Y` in 3.10.
class Pattern(object):
"""
The :class:`Pattern` class is the abstract definition of a pattern.
"""
# Make the class dict-less.
__slots__ = (
'include',
)
def __init__(self, include: Optional[bool]) -> None:
"""
Initializes the :class:`Pattern` instance.
*include* (:class:`bool` or :data:`None`) is whether the matched files
should be included (:data:`True`), excluded (:data:`False`), or is a
null-operation (:data:`None`).
"""
self.include = include
"""
*include* (:class:`bool` or :data:`None`) is whether the matched files
should be included (:data:`True`), excluded (:data:`False`), or is a
null-operation (:data:`None`).
"""
def match(self, files: Iterable[str]) -> Iterator[str]:
"""
DEPRECATED: This method is no longer used and has been replaced by
:meth:`.match_file`. Use the :meth:`.match_file` method with a loop for
similar results.
Matches this pattern against the specified files.
*files* (:class:`~collections.abc.Iterable` of :class:`str`) contains each
file relative to the root directory (e.g., ``"relative/path/to/file"``).
Returns an :class:`~collections.abc.Iterable` yielding each matched file
path (:class:`str`).
"""
warnings.warn((
"{cls.__module__}.{cls.__qualname__}.match() is deprecated. Use "
"{cls.__module__}.{cls.__qualname__}.match_file() with a loop for "
"similar results."
).format(cls=self.__class__), DeprecationWarning, stacklevel=2)
for file in files:
if self.match_file(file) is not None:
yield file
def match_file(self, file: str) -> Optional[Any]:
"""
Matches this pattern against the specified file.
*file* (:class:`str`) is the normalized file path to match against.
Returns the match result if *file* matched; otherwise, :data:`None`.
"""
raise NotImplementedError((
"{cls.__module__}.{cls.__qualname__} must override match_file()."
).format(cls=self.__class__))
class RegexPattern(Pattern):
"""
The :class:`RegexPattern` class is an implementation of a pattern using
regular expressions.
"""
# Keep the class dict-less.
__slots__ = (
'pattern',
'regex',
)
def __init__(
self,
pattern: Union[AnyStr, PatternHint, None],
include: Optional[bool] = None,
) -> None:
"""
Initializes the :class:`RegexPattern` instance.
*pattern* (:class:`str`, :class:`bytes`, :class:`re.Pattern`, or
:data:`None`) is the pattern to compile into a regular expression.
*include* (:class:`bool` or :data:`None`) must be :data:`None` unless
*pattern* is a precompiled regular expression (:class:`re.Pattern`) in which
case it is whether matched files should be included (:data:`True`), excluded
(:data:`False`), or is a null operation (:data:`None`).
.. NOTE:: Subclasses do not need to support the *include* parameter.
"""
if isinstance(pattern, (str, bytes)):
assert include is None, (
f"include:{include!r} must be null when pattern:{pattern!r} is a string."
)
regex, include = self.pattern_to_regex(pattern)
# NOTE: Make sure to allow a null regular expression to be
# returned for a null-operation.
if include is not None:
regex = re.compile(regex)
elif pattern is not None and hasattr(pattern, 'match'):
# Assume pattern is a precompiled regular expression.
# - NOTE: Used specified *include*.
regex = pattern
elif pattern is None:
# NOTE: Make sure to allow a null pattern to be passed for a
# null-operation.
assert include is None, (
f"include:{include!r} must be null when pattern:{pattern!r} is null."
)
else:
raise TypeError(f"pattern:{pattern!r} is not a string, re.Pattern, or None.")
super(RegexPattern, self).__init__(include)
self.pattern: Union[AnyStr, PatternHint, None] = pattern
"""
*pattern* (:class:`str`, :class:`bytes`, :class:`re.Pattern`, or
:data:`None`) is the uncompiled, input pattern. This is for reference.
"""
self.regex: PatternHint = regex
"""
*regex* (:class:`re.Pattern`) is the regular expression for the pattern.
"""
def __eq__(self, other: 'RegexPattern') -> bool:
"""
Tests the equality of this regex pattern with *other* (:class:`RegexPattern`)
by comparing their :attr:`~Pattern.include` and :attr:`~RegexPattern.regex`
attributes.
"""
if isinstance(other, RegexPattern):
return self.include == other.include and self.regex == other.regex
else:
return NotImplemented
def match_file(self, file: str) -> Optional['RegexMatchResult']:
"""
Matches this pattern against the specified file.
*file* (:class:`str`) contains each file relative to the root directory
(e.g., "relative/path/to/file").
Returns the match result (:class:`.RegexMatchResult`) if *file* matched;
otherwise, :data:`None`.
"""
if self.include is not None:
match = self.regex.match(file)
if match is not None:
return RegexMatchResult(match)
return None
@classmethod
def pattern_to_regex(cls, pattern: str) -> Tuple[str, bool]:
"""
Convert the pattern into an uncompiled regular expression.
*pattern* (:class:`str`) is the pattern to convert into a regular
expression.
Returns the uncompiled regular expression (:class:`str` or :data:`None`),
and whether matched files should be included (:data:`True`), excluded
(:data:`False`), or is a null-operation (:data:`None`).
.. NOTE:: The default implementation simply returns *pattern* and
:data:`True`.
"""
return pattern, True
@dataclasses.dataclass()
class RegexMatchResult(object):
"""
The :class:`RegexMatchResult` data class is used to return information about
the matched regular expression.
"""
# Keep the class dict-less.
__slots__ = (
'match',
)
match: MatchHint
"""
*match* (:class:`re.Match`) is the regex match result.
"""

View File

@@ -0,0 +1,11 @@
"""
The *pathspec.patterns* package contains the pattern matching
implementations.
"""
# Load pattern implementations.
from . import gitwildmatch
# DEPRECATED: Expose the `GitWildMatchPattern` class in this module for
# backward compatibility with v0.5.
from .gitwildmatch import GitWildMatchPattern

View File

@@ -0,0 +1,421 @@
"""
This module implements Git's wildmatch pattern matching which itself is derived
from Rsync's wildmatch. Git uses wildmatch for its ".gitignore" files.
"""
import re
import warnings
from typing import (
AnyStr,
Optional, # Replaced by `X | None` in 3.10.
Tuple) # Replaced by `tuple` in 3.9.
from .. import util
from ..pattern import RegexPattern
_BYTES_ENCODING = 'latin1'
"""
The encoding to use when parsing a byte string pattern.
"""
_DIR_MARK = 'ps_d'
"""
The regex group name for the directory marker. This is only used by
:class:`GitIgnoreSpec`.
"""
class GitWildMatchPatternError(ValueError):
"""
The :class:`GitWildMatchPatternError` indicates an invalid git wild match
pattern.
"""
pass
class GitWildMatchPattern(RegexPattern):
"""
The :class:`GitWildMatchPattern` class represents a compiled Git wildmatch
pattern.
"""
# Keep the dict-less class hierarchy.
__slots__ = ()
@classmethod
def pattern_to_regex(
cls,
pattern: AnyStr,
) -> Tuple[Optional[AnyStr], Optional[bool]]:
"""
Convert the pattern into a regular expression.
*pattern* (:class:`str` or :class:`bytes`) is the pattern to convert into a
regular expression.
Returns the uncompiled regular expression (:class:`str`, :class:`bytes`, or
:data:`None`); and whether matched files should be included (:data:`True`),
excluded (:data:`False`), or if it is a null-operation (:data:`None`).
"""
if isinstance(pattern, str):
return_type = str
elif isinstance(pattern, bytes):
return_type = bytes
pattern = pattern.decode(_BYTES_ENCODING)
else:
raise TypeError(f"pattern:{pattern!r} is not a unicode or byte string.")
original_pattern = pattern
if pattern.endswith('\\ '):
# EDGE CASE: Spaces can be escaped with backslash. If a pattern that ends
# with backslash followed by a space, only strip from left.
pattern = pattern.lstrip()
else:
pattern = pattern.strip()
if pattern.startswith('#'):
# A pattern starting with a hash ('#') serves as a comment (neither
# includes nor excludes files). Escape the hash with a back-slash to match
# a literal hash (i.e., '\#').
regex = None
include = None
elif pattern == '/':
# EDGE CASE: According to `git check-ignore` (v2.4.1), a single '/' does
# not match any file.
regex = None
include = None
elif pattern:
if pattern.startswith('!'):
# A pattern starting with an exclamation mark ('!') negates the pattern
# (exclude instead of include). Escape the exclamation mark with a
# back-slash to match a literal exclamation mark (i.e., '\!').
include = False
# Remove leading exclamation mark.
pattern = pattern[1:]
else:
include = True
# Allow a regex override for edge cases that cannot be handled through
# normalization.
override_regex = None
# Split pattern into segments.
pattern_segs = pattern.split('/')
# Check whether the pattern is specifically a directory pattern before
# normalization.
is_dir_pattern = not pattern_segs[-1]
# Normalize pattern to make processing easier.
# EDGE CASE: Deal with duplicate double-asterisk sequences. Collapse each
# sequence down to one double-asterisk. Iterate over the segments in
# reverse and remove the duplicate double asterisks as we go.
for i in range(len(pattern_segs) - 1, 0, -1):
prev = pattern_segs[i-1]
seg = pattern_segs[i]
if prev == '**' and seg == '**':
del pattern_segs[i]
if len(pattern_segs) == 2 and pattern_segs[0] == '**' and not pattern_segs[1]:
# EDGE CASE: The '**/' pattern should match everything except individual
# files in the root directory. This case cannot be adequately handled
# through normalization. Use the override.
override_regex = f'^.+(?P<{_DIR_MARK}>/).*$'
if not pattern_segs[0]:
# A pattern beginning with a slash ('/') will only match paths directly
# on the root directory instead of any descendant paths. So, remove
# empty first segment to make pattern relative to root.
del pattern_segs[0]
elif len(pattern_segs) == 1 or (len(pattern_segs) == 2 and not pattern_segs[1]):
# A single pattern without a beginning slash ('/') will match any
# descendant path. This is equivalent to "**/{pattern}". So, prepend
# with double-asterisks to make pattern relative to root.
# - EDGE CASE: This also holds for a single pattern with a trailing
# slash (e.g. dir/).
if pattern_segs[0] != '**':
pattern_segs.insert(0, '**')
else:
# EDGE CASE: A pattern without a beginning slash ('/') but contains at
# least one prepended directory (e.g. "dir/{pattern}") should not match
# "**/dir/{pattern}", according to `git check-ignore` (v2.4.1).
pass
if not pattern_segs:
# After resolving the edge cases, we end up with no pattern at all. This
# must be because the pattern is invalid.
raise GitWildMatchPatternError(f"Invalid git pattern: {original_pattern!r}")
if not pattern_segs[-1] and len(pattern_segs) > 1:
# A pattern ending with a slash ('/') will match all descendant paths if
# it is a directory but not if it is a regular file. This is equivalent
# to "{pattern}/**". So, set last segment to a double-asterisk to
# include all descendants.
pattern_segs[-1] = '**'
if override_regex is None:
# Build regular expression from pattern.
output = ['^']
need_slash = False
end = len(pattern_segs) - 1
for i, seg in enumerate(pattern_segs):
if seg == '**':
if i == 0 and i == end:
# A pattern consisting solely of double-asterisks ('**') will
# match every path.
output.append(f'[^/]+(?:/.*)?')
elif i == 0:
# A normalized pattern beginning with double-asterisks
# ('**') will match any leading path segments.
output.append('(?:.+/)?')
need_slash = False
elif i == end:
# A normalized pattern ending with double-asterisks ('**') will
# match any trailing path segments.
if is_dir_pattern:
output.append(f'(?P<{_DIR_MARK}>/).*')
else:
output.append(f'/.*')
else:
# A pattern with inner double-asterisks ('**') will match multiple
# (or zero) inner path segments.
output.append('(?:/.+)?')
need_slash = True
elif seg == '*':
# Match single path segment.
if need_slash:
output.append('/')
output.append('[^/]+')
if i == end:
# A pattern ending without a slash ('/') will match a file or a
# directory (with paths underneath it). E.g., "foo" matches "foo",
# "foo/bar", "foo/bar/baz", etc.
output.append(f'(?:(?P<{_DIR_MARK}>/).*)?')
need_slash = True
else:
# Match segment glob pattern.
if need_slash:
output.append('/')
try:
output.append(cls._translate_segment_glob(seg))
except ValueError as e:
raise GitWildMatchPatternError(f"Invalid git pattern: {original_pattern!r}") from e
if i == end:
# A pattern ending without a slash ('/') will match a file or a
# directory (with paths underneath it). E.g., "foo" matches "foo",
# "foo/bar", "foo/bar/baz", etc.
output.append(f'(?:(?P<{_DIR_MARK}>/).*)?')
need_slash = True
output.append('$')
regex = ''.join(output)
else:
# Use regex override.
regex = override_regex
else:
# A blank pattern is a null-operation (neither includes nor excludes
# files).
regex = None
include = None
if regex is not None and return_type is bytes:
regex = regex.encode(_BYTES_ENCODING)
return regex, include
@staticmethod
def _translate_segment_glob(pattern: str) -> str:
"""
Translates the glob pattern to a regular expression. This is used in the
constructor to translate a path segment glob pattern to its corresponding
regular expression.
*pattern* (:class:`str`) is the glob pattern.
Returns the regular expression (:class:`str`).
"""
# NOTE: This is derived from `fnmatch.translate()` and is similar to the
# POSIX function `fnmatch()` with the `FNM_PATHNAME` flag set.
escape = False
regex = ''
i, end = 0, len(pattern)
while i < end:
# Get next character.
char = pattern[i]
i += 1
if escape:
# Escape the character.
escape = False
regex += re.escape(char)
elif char == '\\':
# Escape character, escape next character.
escape = True
elif char == '*':
# Multi-character wildcard. Match any string (except slashes), including
# an empty string.
regex += '[^/]*'
elif char == '?':
# Single-character wildcard. Match any single character (except a
# slash).
regex += '[^/]'
elif char == '[':
# Bracket expression wildcard. Except for the beginning exclamation
# mark, the whole bracket expression can be used directly as regex, but
# we have to find where the expression ends.
# - "[][!]" matches ']', '[' and '!'.
# - "[]-]" matches ']' and '-'.
# - "[!]a-]" matches any character except ']', 'a' and '-'.
j = i
# Pass bracket expression negation.
if j < end and (pattern[j] == '!' or pattern[j] == '^'):
j += 1
# Pass first closing bracket if it is at the beginning of the
# expression.
if j < end and pattern[j] == ']':
j += 1
# Find closing bracket. Stop once we reach the end or find it.
while j < end and pattern[j] != ']':
j += 1
if j < end:
# Found end of bracket expression. Increment j to be one past the
# closing bracket:
#
# [...]
# ^ ^
# i j
#
j += 1
expr = '['
if pattern[i] == '!':
# Bracket expression needs to be negated.
expr += '^'
i += 1
elif pattern[i] == '^':
# POSIX declares that the regex bracket expression negation "[^...]"
# is undefined in a glob pattern. Python's `fnmatch.translate()`
# escapes the caret ('^') as a literal. Git supports the using a
# caret for negation. Maintain consistency with Git because that is
# the expected behavior.
expr += '^'
i += 1
# Build regex bracket expression. Escape slashes so they are treated
# as literal slashes by regex as defined by POSIX.
expr += pattern[i:j].replace('\\', '\\\\')
# Add regex bracket expression to regex result.
regex += expr
# Set i to one past the closing bracket.
i = j
else:
# Failed to find closing bracket, treat opening bracket as a bracket
# literal instead of as an expression.
regex += '\\['
else:
# Regular character, escape it for regex.
regex += re.escape(char)
if escape:
raise ValueError(f"Escape character found with no next character to escape: {pattern!r}")
return regex
@staticmethod
def escape(s: AnyStr) -> AnyStr:
"""
Escape special characters in the given string.
*s* (:class:`str` or :class:`bytes`) a filename or a string that you want to
escape, usually before adding it to a ".gitignore".
Returns the escaped string (:class:`str` or :class:`bytes`).
"""
if isinstance(s, str):
return_type = str
string = s
elif isinstance(s, bytes):
return_type = bytes
string = s.decode(_BYTES_ENCODING)
else:
raise TypeError(f"s:{s!r} is not a unicode or byte string.")
# Reference: https://git-scm.com/docs/gitignore#_pattern_format
meta_characters = r"[]!*#?"
out_string = "".join("\\" + x if x in meta_characters else x for x in string)
if return_type is bytes:
return out_string.encode(_BYTES_ENCODING)
else:
return out_string
util.register_pattern('gitwildmatch', GitWildMatchPattern)
class GitIgnorePattern(GitWildMatchPattern):
"""
The :class:`GitIgnorePattern` class is deprecated by :class:`GitWildMatchPattern`.
This class only exists to maintain compatibility with v0.4.
"""
def __init__(self, *args, **kw) -> None:
"""
Warn about deprecation.
"""
self._deprecated()
super(GitIgnorePattern, self).__init__(*args, **kw)
@staticmethod
def _deprecated() -> None:
"""
Warn about deprecation.
"""
warnings.warn((
"GitIgnorePattern ('gitignore') is deprecated. Use GitWildMatchPattern "
"('gitwildmatch') instead."
), DeprecationWarning, stacklevel=3)
@classmethod
def pattern_to_regex(cls, *args, **kw):
"""
Warn about deprecation.
"""
cls._deprecated()
return super(GitIgnorePattern, cls).pattern_to_regex(*args, **kw)
# Register `GitIgnorePattern` as "gitignore" for backward compatibility with
# v0.4.
util.register_pattern('gitignore', GitIgnorePattern)

View File

@@ -0,0 +1 @@
# Marker file for PEP 561. The pathspec package uses inline types.

View File

@@ -0,0 +1,792 @@
"""
This module provides utility methods for dealing with path-specs.
"""
import os
import os.path
import pathlib
import posixpath
import stat
import sys
import warnings
from collections.abc import (
Collection as CollectionType,
Iterable as IterableType)
from dataclasses import (
dataclass)
from os import (
PathLike)
from typing import (
Any,
AnyStr,
Callable, # Replaced by `collections.abc.Callable` in 3.9.
Collection, # Replaced by `collections.abc.Collection` in 3.9.
Dict, # Replaced by `dict` in 3.9.
Generic,
Iterable, # Replaced by `collections.abc.Iterable` in 3.9.
Iterator, # Replaced by `collections.abc.Iterator` in 3.9.
List, # Replaced by `list` in 3.9.
Optional, # Replaced by `X | None` in 3.10.
Sequence, # Replaced by `collections.abc.Sequence` in 3.9.
Set, # Replaced by `set` in 3.9.
Tuple, # Replaced by `tuple` in 3.9.
TypeVar,
Union) # Replaced by `X | Y` in 3.10.
from .pattern import (
Pattern)
if sys.version_info >= (3, 9):
StrPath = Union[str, PathLike[str]]
else:
StrPath = Union[str, PathLike]
TStrPath = TypeVar("TStrPath", bound=StrPath)
"""
Type variable for :class:`str` or :class:`os.PathLike`.
"""
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 append_dir_sep(path: pathlib.Path) -> str:
"""
Appends the path separator to the path if the path is a directory.
This can be used to aid in distinguishing between directories and
files on the file-system by relying on the presence of a trailing path
separator.
*path* (:class:`pathlib.Path`) is the path to use.
Returns the path (:class:`str`).
"""
str_path = str(path)
if path.is_dir():
str_path += os.sep
return str_path
def check_match_file(
patterns: Iterable[Tuple[int, Pattern]],
file: str,
) -> Tuple[Optional[bool], Optional[int]]:
"""
Check the file against the patterns.
*patterns* (:class:`~collections.abc.Iterable`) yields each indexed pattern
(:class:`tuple`) which contains the pattern index (:class:`int`) and actual
pattern (:class:`~pathspec.pattern.Pattern`).
*file* (:class:`str`) is the normalized file path to be matched
against *patterns*.
Returns a :class:`tuple` containing whether to include *file* (:class:`bool`
or :data:`None`), and the index of the last matched pattern (:class:`int` or
:data:`None`).
"""
out_include: Optional[bool] = None
out_index: Optional[int] = None
for index, pattern in patterns:
if pattern.include is not None and pattern.match_file(file) is not None:
out_include = pattern.include
out_index = index
return out_include, out_index
def detailed_match_files(
patterns: Iterable[Pattern],
files: Iterable[str],
all_matches: Optional[bool] = None,
) -> Dict[str, 'MatchDetail']:
"""
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:`bool` 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, CollectionType) else list(files)
return_files = {}
for pattern in patterns:
if pattern.include is not None:
result_files = pattern.match(all_files) # TODO: Replace with `.match_file()`.
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 _filter_check_patterns(
patterns: Iterable[Pattern],
) -> List[Tuple[int, Pattern]]:
"""
Filters out null-patterns.
*patterns* (:class:`Iterable` of :class:`.Pattern`) contains the
patterns.
Returns a :class:`list` containing each indexed pattern (:class:`tuple`) which
contains the pattern index (:class:`int`) and the actual pattern
(:class:`~pathspec.pattern.Pattern`).
"""
return [
(__index, __pat)
for __index, __pat in enumerate(patterns)
if __pat.include is not None
]
def _is_iterable(value: Any) -> bool:
"""
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, IterableType) and not isinstance(value, (str, bytes))
def iter_tree_entries(
root: StrPath,
on_error: Optional[Callable[[OSError], None]] = None,
follow_links: Optional[bool] = None,
) -> Iterator['TreeEntry']:
"""
Walks the specified directory for all files and directories.
*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. 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.Iterator` yielding each file or
directory entry (:class:`.TreeEntry`) relative to *root*.
"""
if on_error is not None and not callable(on_error):
raise TypeError(f"on_error:{on_error!r} is not callable.")
if follow_links is None:
follow_links = True
yield from _iter_tree_entries_next(os.path.abspath(root), '', {}, on_error, follow_links)
def _iter_tree_entries_next(
root_full: str,
dir_rel: str,
memo: Dict[str, str],
on_error: Callable[[OSError], None],
follow_links: bool,
) -> Iterator['TreeEntry']:
"""
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)
with os.scandir(dir_full) as scan_iter:
node_ent: os.DirEntry
for node_ent in scan_iter:
node_rel = os.path.join(dir_rel, node_ent.name)
# Inspect child node.
try:
node_lstat = node_ent.stat(follow_symlinks=False)
except OSError as e:
if on_error is not None:
on_error(e)
continue
if node_ent.is_symlink():
# Child node is a link, inspect the target node.
try:
node_stat = node_ent.stat()
except OSError as e:
if on_error is not None:
on_error(e)
continue
else:
node_stat = node_lstat
if node_ent.is_dir(follow_symlinks=follow_links):
# Child node is a directory, recurse into it and yield its
# descendant files.
yield TreeEntry(node_ent.name, node_rel, node_lstat, node_stat)
yield from _iter_tree_entries_next(root_full, node_rel, memo, on_error, follow_links)
elif node_ent.is_file() or node_ent.is_symlink():
# Child node is either a file or an unfollowed link, yield it.
yield TreeEntry(node_ent.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 iter_tree_files(
root: StrPath,
on_error: Optional[Callable[[OSError], None]] = None,
follow_links: Optional[bool] = None,
) -> Iterator[str]:
"""
Walks the specified directory for all files.
*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. 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.Iterator` yielding the path to
each file (:class:`str`) relative to *root*.
"""
for entry in iter_tree_entries(root, on_error=on_error, follow_links=follow_links):
if not entry.is_dir(follow_links):
yield entry.path
def iter_tree(root, on_error=None, follow_links=None):
"""
DEPRECATED: The :func:`.iter_tree` function is an alias for the
:func:`.iter_tree_files` function.
"""
warnings.warn((
"util.iter_tree() is deprecated. Use util.iter_tree_files() instead."
), DeprecationWarning, stacklevel=2)
return iter_tree_files(root, on_error=on_error, follow_links=follow_links)
def lookup_pattern(name: str) -> Callable[[AnyStr], Pattern]:
"""
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: Iterable[Pattern], file: str) -> bool:
"""
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 and pattern.match_file(file) is not None:
matched = pattern.include
return matched
def match_files(
patterns: Iterable[Pattern],
files: Iterable[str],
) -> Set[str]:
"""
DEPRECATED: This is an old function no longer used. Use the
:func:`~pathspec.util.match_file` function with a loop for better results.
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`).
"""
warnings.warn((
f"{__name__}.match_files() is deprecated. Use {__name__}.match_file() with "
f"a loop for better results."
), DeprecationWarning, stacklevel=2)
use_patterns = [__pat for __pat in patterns if __pat.include is not None]
return_files = set()
for file in files:
if match_file(use_patterns, file):
return_files.add(file)
return return_files
def normalize_file(
file: StrPath,
separators: Optional[Collection[str]] = None,
) -> str:
"""
Normalizes the file path to use the POSIX path separator (i.e.,
``"/"``), and make the paths relative (remove leading ``"/"``).
*file* (:class:`str` or :class:`os.PathLike`) is the file path.
*separators* (:class:`~collections.abc.Collection` of :class:`str`; or
``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 ``None``
for ``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 = os.fspath(file)
for sep in separators:
norm_file = norm_file.replace(sep, posixpath.sep)
if norm_file.startswith('/'):
# Make path relative.
norm_file = norm_file[1:]
elif norm_file.startswith('./'):
# Remove current directory prefix.
norm_file = norm_file[2:]
return norm_file
def normalize_files(
files: Iterable[StrPath],
separators: Optional[Collection[str]] = None,
) -> Dict[str, List[StrPath]]:
"""
DEPRECATED: This function is no longer used. Use the :func:`.normalize_file`
function with a loop for better results.
Normalizes the file paths to use the POSIX path separator.
*files* (:class:`~collections.abc.Iterable` of :class:`str` or
:class:`os.PathLike`) 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 each normalized file path (:class:`str`)
to the original file paths (:class:`list` of :class:`str` or
:class:`os.PathLike`).
"""
warnings.warn((
"util.normalize_files() is deprecated. Use util.normalize_file() "
"with a loop for better results."
), DeprecationWarning, stacklevel=2)
norm_files = {}
for path in files:
norm_file = normalize_file(path, separators=separators)
if norm_file in norm_files:
norm_files[norm_file].append(path)
else:
norm_files[norm_file] = [path]
return norm_files
def register_pattern(
name: str,
pattern_factory: Callable[[AnyStr], Pattern],
override: Optional[bool] = None,
) -> 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, str):
raise TypeError(f"name:{name!r} is not a string.")
if not callable(pattern_factory):
raise TypeError(f"pattern_factory:{pattern_factory!r} is not callable.")
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: str,
pattern_factory: Callable[[AnyStr], Pattern],
) -> None:
"""
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) -> str:
"""
*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) -> str:
"""
*name* (:class:`str`) is the name of the registered pattern.
"""
return self.args[0]
@property
def pattern_factory(self) -> Callable[[AnyStr], Pattern]:
"""
*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: str,
first_path: str,
second_path: str,
) -> None:
"""
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) -> str:
"""
*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) -> str:
"""
*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) -> str:
"""
*real_path* (:class:`str`) is the real path that recursion was
encountered on.
"""
return self.args[0]
@property
def second_path(self) -> str:
"""
*second_path* (:class:`str`) is the second path encountered for
:attr:`self.real_path <RecursionError.real_path>`.
"""
return self.args[2]
@dataclass(frozen=True)
class CheckResult(Generic[TStrPath]):
"""
The :class:`CheckResult` class contains information about the file and which
pattern matched it.
"""
# Make the class dict-less.
__slots__ = (
'file',
'include',
'index',
)
file: TStrPath
"""
*file* (:class:`str` or :class:`os.PathLike`) is the file path.
"""
include: Optional[bool]
"""
*include* (:class:`bool` or :data:`None`) is whether to include or exclude the
file. If :data:`None`, no pattern matched.
"""
index: Optional[int]
"""
*index* (:class:`int` or :data:`None`) is the index of the last pattern that
matched. If :data:`None`, no pattern matched.
"""
class MatchDetail(object):
"""
The :class:`.MatchDetail` class contains information about
"""
# Make the class dict-less.
__slots__ = ('patterns',)
def __init__(self, patterns: Sequence[Pattern]) -> None:
"""
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: str,
path: str,
lstat: os.stat_result,
stat: os.stat_result,
) -> None:
"""
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: os.stat_result = lstat
"""
*_lstat* (:class:`os.stat_result`) is the stat result of the direct
entry.
"""
self.name: str = name
"""
*name* (:class:`str`) is the base name of the entry.
"""
self.path: str = path
"""
*path* (:class:`str`) is the path of the entry.
"""
self._stat: os.stat_result = stat
"""
*_stat* (:class:`os.stat_result`) is the stat result of the linked
entry.
"""
def is_dir(self, follow_links: Optional[bool] = None) -> bool:
"""
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: Optional[bool] = None) -> bool:
"""
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) -> bool:
"""
Returns whether the entry is a symbolic link (:class:`bool`).
"""
return stat.S_ISLNK(self._lstat.st_mode)
def stat(self, follow_links: Optional[bool] = None) -> os.stat_result:
"""
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