init
This commit is contained in:
commit
38355d2442
9083 changed files with 1225834 additions and 0 deletions
43
.venv/lib/python3.8/site-packages/pathspec/__init__.py
Normal file
43
.venv/lib/python3.8/site-packages/pathspec/__init__.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# encoding: utf-8
|
||||
"""
|
||||
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.pathspec.PathSpec`
|
||||
|
||||
- :class:`pathspec.pattern.Pattern`
|
||||
|
||||
- :class:`pathspec.pattern.RegexPattern`
|
||||
|
||||
- :class:`pathspec.util.RecursionError`
|
||||
|
||||
The following functions are also imported:
|
||||
|
||||
- :func:`pathspec.util.iter_tree`
|
||||
- :func:`pathspec.util.lookup_pattern`
|
||||
- :func:`pathspec.util.match_files`
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .pathspec import PathSpec
|
||||
from .pattern import Pattern, RegexPattern
|
||||
from .util import iter_tree, lookup_pattern, match_files, RecursionError
|
||||
|
||||
from ._meta import (
|
||||
__author__,
|
||||
__copyright__,
|
||||
__credits__,
|
||||
__license__,
|
||||
__version__,
|
||||
)
|
||||
|
||||
# Load pattern implementations.
|
||||
from . import patterns
|
||||
|
||||
# Expose `GitIgnorePattern` class in the root module for backward
|
||||
# compatibility with v0.4.
|
||||
from .patterns.gitwildmatch import GitIgnorePattern
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
43
.venv/lib/python3.8/site-packages/pathspec/_meta.py
Normal file
43
.venv/lib/python3.8/site-packages/pathspec/_meta.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# encoding: utf-8
|
||||
"""
|
||||
This module contains the project meta-data.
|
||||
"""
|
||||
|
||||
__author__ = "Caleb P. Burns"
|
||||
__copyright__ = "Copyright © 2013-2021 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>",
|
||||
"highb <https://github.com/highb>",
|
||||
"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>",
|
||||
]
|
||||
__license__ = "MPL 2.0"
|
||||
__version__ = "0.9.0"
|
||||
41
.venv/lib/python3.8/site-packages/pathspec/compat.py
Normal file
41
.venv/lib/python3.8/site-packages/pathspec/compat.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# encoding: utf-8
|
||||
"""
|
||||
This module provides compatibility between Python 2 and 3. Hardly
|
||||
anything is used by this project to constitute including `six`_.
|
||||
|
||||
.. _`six`: http://pythonhosted.org/six
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
if sys.version_info[0] < 3:
|
||||
# Python 2.
|
||||
unicode = unicode
|
||||
string_types = (basestring,)
|
||||
|
||||
from collections import Iterable
|
||||
from itertools import izip_longest
|
||||
|
||||
def iterkeys(mapping):
|
||||
return mapping.iterkeys()
|
||||
|
||||
else:
|
||||
# Python 3.
|
||||
unicode = str
|
||||
string_types = (unicode,)
|
||||
|
||||
from collections.abc import Iterable
|
||||
from itertools import zip_longest as izip_longest
|
||||
|
||||
def iterkeys(mapping):
|
||||
return mapping.keys()
|
||||
|
||||
try:
|
||||
# Python 3.6+.
|
||||
from collections.abc import Collection
|
||||
except ImportError:
|
||||
# Python 2.7 - 3.5.
|
||||
from collections import Container as Collection
|
||||
|
||||
CollectionType = Collection
|
||||
IterableType = Iterable
|
||||
243
.venv/lib/python3.8/site-packages/pathspec/pathspec.py
Normal file
243
.venv/lib/python3.8/site-packages/pathspec/pathspec.py
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
# encoding: utf-8
|
||||
"""
|
||||
This module provides an object oriented interface for pattern matching
|
||||
of files.
|
||||
"""
|
||||
|
||||
try:
|
||||
from typing import (
|
||||
Any,
|
||||
AnyStr,
|
||||
Callable,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Optional,
|
||||
Text,
|
||||
Union)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Python 3.6+ type hints.
|
||||
from os import PathLike
|
||||
from typing import Collection
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from . import util
|
||||
from .compat import (
|
||||
CollectionType,
|
||||
iterkeys,
|
||||
izip_longest,
|
||||
string_types)
|
||||
from .pattern import Pattern
|
||||
from .util import TreeEntry
|
||||
|
||||
|
||||
class PathSpec(object):
|
||||
"""
|
||||
The :class:`PathSpec` class is a wrapper around a list of compiled
|
||||
:class:`.Pattern` instances.
|
||||
"""
|
||||
|
||||
def __init__(self, patterns):
|
||||
# type: (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):
|
||||
# type: (PathSpec) -> 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 = izip_longest(self.patterns, other.patterns)
|
||||
return all(a == b for a, b in paired_patterns)
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of compiled patterns this path-spec contains
|
||||
(:class:`int`).
|
||||
"""
|
||||
return len(self.patterns)
|
||||
|
||||
def __add__(self, other):
|
||||
# type: (PathSpec) -> PathSpec
|
||||
"""
|
||||
Combines the :attr:`Pathspec.patterns` patterns from two
|
||||
:class:`PathSpec` instances.
|
||||
"""
|
||||
if isinstance(other, PathSpec):
|
||||
return PathSpec(self.patterns + other.patterns)
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __iadd__(self, other):
|
||||
# type: (PathSpec) -> PathSpec
|
||||
"""
|
||||
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, lines):
|
||||
# type: (Union[Text, Callable[[AnyStr], Pattern]], Iterable[AnyStr]) -> PathSpec
|
||||
"""
|
||||
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:`file` (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, string_types):
|
||||
pattern_factory = util.lookup_pattern(pattern_factory)
|
||||
if not callable(pattern_factory):
|
||||
raise TypeError("pattern_factory:{!r} is not callable.".format(pattern_factory))
|
||||
|
||||
if not util._is_iterable(lines):
|
||||
raise TypeError("lines:{!r} is not an iterable.".format(lines))
|
||||
|
||||
patterns = [pattern_factory(line) for line in lines if line]
|
||||
return cls(patterns)
|
||||
|
||||
def match_file(self, file, separators=None):
|
||||
# type: (Union[Text, PathLike], Optional[Collection[Text]]) -> bool
|
||||
"""
|
||||
Matches the file to this path-spec.
|
||||
|
||||
*file* (:class:`str` or :class:`~pathlib.PurePath`) 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 util.match_file(self.patterns, norm_file)
|
||||
|
||||
def match_entries(self, entries, separators=None):
|
||||
# type: (Iterable[TreeEntry], Optional[Collection[Text]]) -> 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 util._is_iterable(entries):
|
||||
raise TypeError("entries:{!r} is not an iterable.".format(entries))
|
||||
|
||||
entry_map = util._normalize_entries(entries, separators=separators)
|
||||
match_paths = util.match_files(self.patterns, iterkeys(entry_map))
|
||||
for path in match_paths:
|
||||
yield entry_map[path]
|
||||
|
||||
def match_files(self, files, separators=None):
|
||||
# type: (Iterable[Union[Text, PathLike]], Optional[Collection[Text]]) -> Iterator[Union[Text, PathLike]]
|
||||
"""
|
||||
Matches the files to this path-spec.
|
||||
|
||||
*files* (:class:`~collections.abc.Iterable` of :class:`str; or
|
||||
:class:`pathlib.PurePath`) 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:`pathlib.PurePath`).
|
||||
"""
|
||||
if not util._is_iterable(files):
|
||||
raise TypeError("files:{!r} is not an iterable.".format(files))
|
||||
|
||||
file_map = util.normalize_files(files, separators=separators)
|
||||
matched_files = util.match_files(self.patterns, iterkeys(file_map))
|
||||
for norm_file in matched_files:
|
||||
for orig_file in file_map[norm_file]:
|
||||
yield orig_file
|
||||
|
||||
def match_tree_entries(self, root, on_error=None, follow_links=None):
|
||||
# type: (Text, Optional[Callable], Optional[bool]) -> Iterator[TreeEntry]
|
||||
"""
|
||||
Walks the specified root path for all files and matches them to this
|
||||
path-spec.
|
||||
|
||||
*root* (:class:`str`; or :class:`pathlib.PurePath`) 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)
|
||||
return self.match_entries(entries)
|
||||
|
||||
def match_tree_files(self, root, on_error=None, follow_links=None):
|
||||
# type: (Text, Optional[Callable], Optional[bool]) -> Iterator[Text]
|
||||
"""
|
||||
Walks the specified root path for all files and matches them to this
|
||||
path-spec.
|
||||
|
||||
*root* (:class:`str`; or :class:`pathlib.PurePath`) 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)
|
||||
return self.match_files(files)
|
||||
|
||||
# Alias `match_tree_files()` as `match_tree()`.
|
||||
match_tree = match_tree_files
|
||||
164
.venv/lib/python3.8/site-packages/pathspec/pattern.py
Normal file
164
.venv/lib/python3.8/site-packages/pathspec/pattern.py
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
# encoding: utf-8
|
||||
"""
|
||||
This module provides the base definition for patterns.
|
||||
"""
|
||||
|
||||
import re
|
||||
try:
|
||||
from typing import (
|
||||
AnyStr,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Optional,
|
||||
Pattern as RegexHint,
|
||||
Text,
|
||||
Tuple,
|
||||
Union)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from .compat import unicode
|
||||
|
||||
|
||||
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):
|
||||
# type: (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):
|
||||
# type: (Iterable[Text]) -> Iterator[Text]
|
||||
"""
|
||||
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`).
|
||||
"""
|
||||
raise NotImplementedError("{}.{} must override match().".format(self.__class__.__module__, self.__class__.__name__))
|
||||
|
||||
|
||||
class RegexPattern(Pattern):
|
||||
"""
|
||||
The :class:`RegexPattern` class is an implementation of a pattern
|
||||
using regular expressions.
|
||||
"""
|
||||
|
||||
# Make the class dict-less.
|
||||
__slots__ = ('regex',)
|
||||
|
||||
def __init__(self, pattern, include=None):
|
||||
# type: (Union[AnyStr, RegexHint], Optional[bool]) -> None
|
||||
"""
|
||||
Initializes the :class:`RegexPattern` instance.
|
||||
|
||||
*pattern* (:class:`unicode`, :class:`bytes`, :class:`re.RegexObject`,
|
||||
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.RegexObject`)
|
||||
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.
|
||||
"""
|
||||
|
||||
self.regex = None
|
||||
"""
|
||||
*regex* (:class:`re.RegexObject`) is the regular expression for the
|
||||
pattern.
|
||||
"""
|
||||
|
||||
if isinstance(pattern, (unicode, bytes)):
|
||||
assert include is None, "include:{!r} must be null when pattern:{!r} is a string.".format(include, pattern)
|
||||
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, "include:{!r} must be null when pattern:{!r} is null.".format(include, pattern)
|
||||
|
||||
else:
|
||||
raise TypeError("pattern:{!r} is not a string, RegexObject, or None.".format(pattern))
|
||||
|
||||
super(RegexPattern, self).__init__(include)
|
||||
self.regex = regex
|
||||
|
||||
def __eq__(self, other):
|
||||
# type: (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(self, files):
|
||||
# type: (Iterable[Text]) -> Iterable[Text]
|
||||
"""
|
||||
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`).
|
||||
"""
|
||||
if self.include is not None:
|
||||
for path in files:
|
||||
if self.regex.match(path) is not None:
|
||||
yield path
|
||||
|
||||
@classmethod
|
||||
def pattern_to_regex(cls, pattern):
|
||||
# type: (Text) -> Tuple[Text, 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
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# encoding: utf-8
|
||||
"""
|
||||
The *pathspec.patterns* package contains the pattern matching
|
||||
implementations.
|
||||
"""
|
||||
|
||||
# Load pattern implementations.
|
||||
from .gitwildmatch import GitWildMatchPattern
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,400 @@
|
|||
# encoding: utf-8
|
||||
"""
|
||||
This module implements Git's wildmatch pattern matching which itself is
|
||||
derived from Rsync's wildmatch. Git uses wildmatch for its ".gitignore"
|
||||
files.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import warnings
|
||||
try:
|
||||
from typing import (
|
||||
AnyStr,
|
||||
Optional,
|
||||
Text,
|
||||
Tuple)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from .. import util
|
||||
from ..compat import unicode
|
||||
from ..pattern import RegexPattern
|
||||
|
||||
#: The encoding to use when parsing a byte string pattern.
|
||||
_BYTES_ENCODING = 'latin1'
|
||||
|
||||
|
||||
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):
|
||||
# type: (AnyStr) -> Tuple[Optional[AnyStr], Optional[bool]]
|
||||
"""
|
||||
Convert the pattern into a regular expression.
|
||||
|
||||
*pattern* (:class:`unicode` or :class:`bytes`) is the pattern to
|
||||
convert into a regular expression.
|
||||
|
||||
Returns the uncompiled regular expression (:class:`unicode`, :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, unicode):
|
||||
return_type = unicode
|
||||
elif isinstance(pattern, bytes):
|
||||
return_type = bytes
|
||||
pattern = pattern.decode(_BYTES_ENCODING)
|
||||
else:
|
||||
raise TypeError("pattern:{!r} is not a unicode or byte string.".format(pattern))
|
||||
|
||||
original_pattern = pattern
|
||||
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
|
||||
|
||||
if pattern.startswith('\\'):
|
||||
# Remove leading back-slash escape for escaped hash ('#') or
|
||||
# exclamation mark ('!').
|
||||
pattern = pattern[1:]
|
||||
|
||||
# Allow a regex override for edge cases that cannot be handled
|
||||
# through normalization.
|
||||
override_regex = None
|
||||
|
||||
# Split pattern into segments.
|
||||
pattern_segs = pattern.split('/')
|
||||
|
||||
# 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 = '^.+/.*$'
|
||||
|
||||
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("Invalid git pattern: %r" % (original_pattern,))
|
||||
|
||||
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('.+')
|
||||
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.
|
||||
output.append('/.*')
|
||||
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('[^/]+')
|
||||
need_slash = True
|
||||
|
||||
else:
|
||||
# Match segment glob pattern.
|
||||
if need_slash:
|
||||
output.append('/')
|
||||
|
||||
output.append(cls._translate_segment_glob(seg))
|
||||
if i == end and include is True:
|
||||
# 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.
|
||||
# EDGE CASE: However, this does not hold for exclusion cases
|
||||
# according to `git check-ignore` (v2.4.1).
|
||||
output.append('(?:/.*)?')
|
||||
|
||||
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):
|
||||
# type: (Text) -> Text
|
||||
"""
|
||||
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 brack expression negation.
|
||||
if j < end and 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] == '!':
|
||||
# Braket 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. To maintain consistency with undefined behavior,
|
||||
# I am escaping the '^' as well.
|
||||
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)
|
||||
|
||||
return regex
|
||||
|
||||
@staticmethod
|
||||
def escape(s):
|
||||
# type: (AnyStr) -> AnyStr
|
||||
"""
|
||||
Escape special characters in the given string.
|
||||
|
||||
*s* (:class:`unicode` 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:`unicode` or :class:`bytes`)
|
||||
"""
|
||||
if isinstance(s, unicode):
|
||||
return_type = unicode
|
||||
string = s
|
||||
elif isinstance(s, bytes):
|
||||
return_type = bytes
|
||||
string = s.decode(_BYTES_ENCODING)
|
||||
else:
|
||||
raise TypeError("s:{!r} is not a unicode or byte string.".format(s))
|
||||
|
||||
# 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):
|
||||
"""
|
||||
Warn about deprecation.
|
||||
"""
|
||||
self._deprecated()
|
||||
super(GitIgnorePattern, self).__init__(*args, **kw)
|
||||
|
||||
@staticmethod
|
||||
def _deprecated():
|
||||
"""
|
||||
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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,548 @@
|
|||
# encoding: utf-8
|
||||
"""
|
||||
This script tests ``GitWildMatchPattern``.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import pathspec.patterns.gitwildmatch
|
||||
import pathspec.util
|
||||
from pathspec.patterns.gitwildmatch import GitWildMatchPattern, GitWildMatchPatternError
|
||||
|
||||
if sys.version_info[0] >= 3:
|
||||
unichr = chr
|
||||
|
||||
|
||||
class GitWildMatchTest(unittest.TestCase):
|
||||
"""
|
||||
The ``GitWildMatchTest`` class tests the ``GitWildMatchPattern``
|
||||
implementation.
|
||||
"""
|
||||
|
||||
def test_00_empty(self):
|
||||
"""
|
||||
Tests an empty pattern.
|
||||
"""
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('')
|
||||
self.assertIsNone(include)
|
||||
self.assertIsNone(regex)
|
||||
|
||||
def test_01_absolute(self):
|
||||
"""
|
||||
Tests an absolute path pattern.
|
||||
|
||||
This should match:
|
||||
|
||||
an/absolute/file/path
|
||||
an/absolute/file/path/foo
|
||||
|
||||
This should NOT match:
|
||||
|
||||
foo/an/absolute/file/path
|
||||
"""
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('/an/absolute/file/path')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(regex, '^an/absolute/file/path(?:/.*)?$')
|
||||
|
||||
pattern = GitWildMatchPattern(re.compile(regex), include)
|
||||
results = set(pattern.match([
|
||||
'an/absolute/file/path',
|
||||
'an/absolute/file/path/foo',
|
||||
'foo/an/absolute/file/path',
|
||||
]))
|
||||
self.assertEqual(results, {
|
||||
'an/absolute/file/path',
|
||||
'an/absolute/file/path/foo',
|
||||
})
|
||||
|
||||
def test_01_absolute_root(self):
|
||||
"""
|
||||
Tests a single root absolute path pattern.
|
||||
|
||||
This should NOT match any file (according to git check-ignore
|
||||
(v2.4.1)).
|
||||
"""
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('/')
|
||||
self.assertIsNone(include)
|
||||
self.assertIsNone(regex)
|
||||
|
||||
def test_01_relative(self):
|
||||
"""
|
||||
Tests a relative path pattern.
|
||||
|
||||
This should match:
|
||||
|
||||
spam
|
||||
spam/
|
||||
foo/spam
|
||||
spam/foo
|
||||
foo/spam/bar
|
||||
"""
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('spam')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(regex, '^(?:.+/)?spam(?:/.*)?$')
|
||||
|
||||
pattern = GitWildMatchPattern(re.compile(regex), include)
|
||||
results = set(pattern.match([
|
||||
'spam',
|
||||
'spam/',
|
||||
'foo/spam',
|
||||
'spam/foo',
|
||||
'foo/spam/bar',
|
||||
]))
|
||||
self.assertEqual(results, {
|
||||
'spam',
|
||||
'spam/',
|
||||
'foo/spam',
|
||||
'spam/foo',
|
||||
'foo/spam/bar',
|
||||
})
|
||||
|
||||
def test_01_relative_nested(self):
|
||||
"""
|
||||
Tests a relative nested path pattern.
|
||||
|
||||
This should match:
|
||||
|
||||
foo/spam
|
||||
foo/spam/bar
|
||||
|
||||
This should **not** match (according to git check-ignore (v2.4.1)):
|
||||
|
||||
bar/foo/spam
|
||||
"""
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('foo/spam')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(regex, '^foo/spam(?:/.*)?$')
|
||||
|
||||
pattern = GitWildMatchPattern(re.compile(regex), include)
|
||||
results = set(pattern.match([
|
||||
'foo/spam',
|
||||
'foo/spam/bar',
|
||||
'bar/foo/spam',
|
||||
]))
|
||||
self.assertEqual(results, {
|
||||
'foo/spam',
|
||||
'foo/spam/bar',
|
||||
})
|
||||
|
||||
def test_02_comment(self):
|
||||
"""
|
||||
Tests a comment pattern.
|
||||
"""
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('# Cork soakers.')
|
||||
self.assertIsNone(include)
|
||||
self.assertIsNone(regex)
|
||||
|
||||
def test_02_ignore(self):
|
||||
"""
|
||||
Tests an exclude pattern.
|
||||
|
||||
This should NOT match (according to git check-ignore (v2.4.1)):
|
||||
|
||||
temp/foo
|
||||
"""
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('!temp')
|
||||
self.assertIsNotNone(include)
|
||||
self.assertFalse(include)
|
||||
self.assertEqual(regex, '^(?:.+/)?temp$')
|
||||
|
||||
pattern = GitWildMatchPattern(re.compile(regex), include)
|
||||
results = set(pattern.match(['temp/foo']))
|
||||
self.assertEqual(results, set())
|
||||
|
||||
def test_03_child_double_asterisk(self):
|
||||
"""
|
||||
Tests a directory name with a double-asterisk child
|
||||
directory.
|
||||
|
||||
This should match:
|
||||
|
||||
spam/bar
|
||||
|
||||
This should **not** match (according to git check-ignore (v2.4.1)):
|
||||
|
||||
foo/spam/bar
|
||||
"""
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('spam/**')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(regex, '^spam/.*$')
|
||||
|
||||
pattern = GitWildMatchPattern(re.compile(regex), include)
|
||||
results = set(pattern.match([
|
||||
'spam/bar',
|
||||
'foo/spam/bar',
|
||||
]))
|
||||
self.assertEqual(results, {'spam/bar'})
|
||||
|
||||
def test_03_inner_double_asterisk(self):
|
||||
"""
|
||||
Tests a path with an inner double-asterisk directory.
|
||||
|
||||
This should match:
|
||||
|
||||
left/right
|
||||
left/bar/right
|
||||
left/foo/bar/right
|
||||
left/bar/right/foo
|
||||
|
||||
This should **not** match (according to git check-ignore (v2.4.1)):
|
||||
|
||||
foo/left/bar/right
|
||||
"""
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('left/**/right')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(regex, '^left(?:/.+)?/right(?:/.*)?$')
|
||||
|
||||
pattern = GitWildMatchPattern(re.compile(regex), include)
|
||||
results = set(pattern.match([
|
||||
'left/right',
|
||||
'left/bar/right',
|
||||
'left/foo/bar/right',
|
||||
'left/bar/right/foo',
|
||||
'foo/left/bar/right',
|
||||
]))
|
||||
self.assertEqual(results, {
|
||||
'left/right',
|
||||
'left/bar/right',
|
||||
'left/foo/bar/right',
|
||||
'left/bar/right/foo',
|
||||
})
|
||||
|
||||
def test_03_only_double_asterisk(self):
|
||||
"""
|
||||
Tests a double-asterisk pattern which matches everything.
|
||||
"""
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('**')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(regex, '^.+$')
|
||||
|
||||
def test_03_parent_double_asterisk(self):
|
||||
"""
|
||||
Tests a file name with a double-asterisk parent directory.
|
||||
|
||||
This should match:
|
||||
|
||||
spam
|
||||
foo/spam
|
||||
foo/spam/bar
|
||||
"""
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('**/spam')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(regex, '^(?:.+/)?spam(?:/.*)?$')
|
||||
|
||||
pattern = GitWildMatchPattern(re.compile(regex), include)
|
||||
results = set(pattern.match([
|
||||
'spam',
|
||||
'foo/spam',
|
||||
'foo/spam/bar',
|
||||
]))
|
||||
self.assertEqual(results, {
|
||||
'spam',
|
||||
'foo/spam',
|
||||
'foo/spam/bar',
|
||||
})
|
||||
|
||||
def test_03_duplicate_leading_double_asterisk_edge_case(self):
|
||||
"""
|
||||
Regression test for duplicate leading **/ bug.
|
||||
"""
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('**')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(regex, '^.+$')
|
||||
|
||||
equivalent_regex, include = GitWildMatchPattern.pattern_to_regex('**/**')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(equivalent_regex, regex)
|
||||
|
||||
equivalent_regex, include = GitWildMatchPattern.pattern_to_regex('**/**/**')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(equivalent_regex, regex)
|
||||
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('**/api')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(regex, '^(?:.+/)?api(?:/.*)?$')
|
||||
|
||||
equivalent_regex, include = GitWildMatchPattern.pattern_to_regex('**/**/api')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(equivalent_regex, regex)
|
||||
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('**/api/')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(regex, '^(?:.+/)?api/.*$')
|
||||
|
||||
equivalent_regex, include = GitWildMatchPattern.pattern_to_regex('**/api/**')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(equivalent_regex, regex)
|
||||
|
||||
equivalent_regex, include = GitWildMatchPattern.pattern_to_regex('**/**/api/**/**')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(equivalent_regex, regex)
|
||||
|
||||
def test_03_double_asterisk_trailing_slash_edge_case(self):
|
||||
"""
|
||||
Tests the edge-case **/ pattern.
|
||||
|
||||
This should match everything except individual files in the root directory.
|
||||
"""
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('**/')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(regex, '^.+/.*$')
|
||||
|
||||
equivalent_regex, include = GitWildMatchPattern.pattern_to_regex('**/**/')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(equivalent_regex, regex)
|
||||
|
||||
def test_04_infix_wildcard(self):
|
||||
"""
|
||||
Tests a pattern with an infix wildcard.
|
||||
|
||||
This should match:
|
||||
|
||||
foo--bar
|
||||
foo-hello-bar
|
||||
a/foo-hello-bar
|
||||
foo-hello-bar/b
|
||||
a/foo-hello-bar/b
|
||||
"""
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('foo-*-bar')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(regex, '^(?:.+/)?foo\\-[^/]*\\-bar(?:/.*)?$')
|
||||
|
||||
pattern = GitWildMatchPattern(re.compile(regex), include)
|
||||
results = set(pattern.match([
|
||||
'foo--bar',
|
||||
'foo-hello-bar',
|
||||
'a/foo-hello-bar',
|
||||
'foo-hello-bar/b',
|
||||
'a/foo-hello-bar/b',
|
||||
]))
|
||||
self.assertEqual(results, {
|
||||
'foo--bar',
|
||||
'foo-hello-bar',
|
||||
'a/foo-hello-bar',
|
||||
'foo-hello-bar/b',
|
||||
'a/foo-hello-bar/b',
|
||||
})
|
||||
|
||||
def test_04_postfix_wildcard(self):
|
||||
"""
|
||||
Tests a pattern with a postfix wildcard.
|
||||
|
||||
This should match:
|
||||
|
||||
~temp-
|
||||
~temp-foo
|
||||
~temp-foo/bar
|
||||
foo/~temp-bar
|
||||
foo/~temp-bar/baz
|
||||
"""
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('~temp-*')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(regex, '^(?:.+/)?\\~temp\\-[^/]*(?:/.*)?$')
|
||||
|
||||
pattern = GitWildMatchPattern(re.compile(regex), include)
|
||||
results = set(pattern.match([
|
||||
'~temp-',
|
||||
'~temp-foo',
|
||||
'~temp-foo/bar',
|
||||
'foo/~temp-bar',
|
||||
'foo/~temp-bar/baz',
|
||||
]))
|
||||
self.assertEqual(results, {
|
||||
'~temp-',
|
||||
'~temp-foo',
|
||||
'~temp-foo/bar',
|
||||
'foo/~temp-bar',
|
||||
'foo/~temp-bar/baz',
|
||||
})
|
||||
|
||||
def test_04_prefix_wildcard(self):
|
||||
"""
|
||||
Tests a pattern with a prefix wildcard.
|
||||
|
||||
This should match:
|
||||
|
||||
bar.py
|
||||
bar.py/
|
||||
foo/bar.py
|
||||
foo/bar.py/baz
|
||||
"""
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('*.py')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(regex, '^(?:.+/)?[^/]*\\.py(?:/.*)?$')
|
||||
|
||||
pattern = GitWildMatchPattern(re.compile(regex), include)
|
||||
results = set(pattern.match([
|
||||
'bar.py',
|
||||
'bar.py/',
|
||||
'foo/bar.py',
|
||||
'foo/bar.py/baz',
|
||||
]))
|
||||
self.assertEqual(results, {
|
||||
'bar.py',
|
||||
'bar.py/',
|
||||
'foo/bar.py',
|
||||
'foo/bar.py/baz',
|
||||
})
|
||||
|
||||
def test_05_directory(self):
|
||||
"""
|
||||
Tests a directory pattern.
|
||||
|
||||
This should match:
|
||||
|
||||
dir/
|
||||
foo/dir/
|
||||
foo/dir/bar
|
||||
|
||||
This should **not** match:
|
||||
|
||||
dir
|
||||
"""
|
||||
regex, include = GitWildMatchPattern.pattern_to_regex('dir/')
|
||||
self.assertTrue(include)
|
||||
self.assertEqual(regex, '^(?:.+/)?dir/.*$')
|
||||
|
||||
pattern = GitWildMatchPattern(re.compile(regex), include)
|
||||
results = set(pattern.match([
|
||||
'dir/',
|
||||
'foo/dir/',
|
||||
'foo/dir/bar',
|
||||
'dir',
|
||||
]))
|
||||
self.assertEqual(results, {
|
||||
'dir/',
|
||||
'foo/dir/',
|
||||
'foo/dir/bar',
|
||||
})
|
||||
|
||||
def test_06_registered(self):
|
||||
"""
|
||||
Tests that the pattern is registered.
|
||||
"""
|
||||
self.assertIs(pathspec.util.lookup_pattern('gitwildmatch'), GitWildMatchPattern)
|
||||
|
||||
def test_06_access_deprecated(self):
|
||||
"""
|
||||
Tests that the pattern is accessible from the root module using the
|
||||
deprecated alias.
|
||||
"""
|
||||
self.assertTrue(hasattr(pathspec, 'GitIgnorePattern'))
|
||||
self.assertTrue(issubclass(pathspec.GitIgnorePattern, GitWildMatchPattern))
|
||||
|
||||
def test_06_registered_deprecated(self):
|
||||
"""
|
||||
Tests that the pattern is registered under the deprecated alias.
|
||||
"""
|
||||
self.assertIs(pathspec.util.lookup_pattern('gitignore'), pathspec.GitIgnorePattern)
|
||||
|
||||
def test_07_encode_bytes(self):
|
||||
"""
|
||||
Test encoding bytes.
|
||||
"""
|
||||
encoded = "".join(map(unichr, range(0,256))).encode(pathspec.patterns.gitwildmatch._BYTES_ENCODING)
|
||||
expected = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
|
||||
self.assertEqual(encoded, expected)
|
||||
|
||||
def test_07_decode_bytes(self):
|
||||
"""
|
||||
Test decoding bytes.
|
||||
"""
|
||||
decoded = bytes(bytearray(range(0,256))).decode(pathspec.patterns.gitwildmatch._BYTES_ENCODING)
|
||||
expected = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
|
||||
self.assertEqual(decoded, expected)
|
||||
|
||||
def test_07_match_bytes_and_bytes(self):
|
||||
"""
|
||||
Test byte string patterns matching byte string paths.
|
||||
"""
|
||||
pattern = GitWildMatchPattern(b'*.py')
|
||||
results = set(pattern.match([b'a.py']))
|
||||
self.assertEqual(results, {b'a.py'})
|
||||
|
||||
def test_07_match_bytes_and_bytes_complete(self):
|
||||
"""
|
||||
Test byte string patterns matching byte string paths.
|
||||
"""
|
||||
encoded = bytes(bytearray(range(0,256)))
|
||||
escaped = b"".join(b"\\" + encoded[i:i+1] for i in range(len(encoded)))
|
||||
pattern = GitWildMatchPattern(escaped)
|
||||
results = set(pattern.match([encoded]))
|
||||
self.assertEqual(results, {encoded})
|
||||
|
||||
@unittest.skipIf(sys.version_info[0] >= 3, "Python 3 is strict")
|
||||
def test_07_match_bytes_and_unicode(self):
|
||||
"""
|
||||
Test byte string patterns matching byte string paths.
|
||||
"""
|
||||
pattern = GitWildMatchPattern(b'*.py')
|
||||
results = set(pattern.match(['a.py']))
|
||||
self.assertEqual(results, {'a.py'})
|
||||
|
||||
@unittest.skipIf(sys.version_info[0] == 2, "Python 2 is lenient")
|
||||
def test_07_match_bytes_and_unicode_fail(self):
|
||||
"""
|
||||
Test byte string patterns matching byte string paths.
|
||||
"""
|
||||
pattern = GitWildMatchPattern(b'*.py')
|
||||
with self.assertRaises(TypeError):
|
||||
for _ in pattern.match(['a.py']):
|
||||
pass
|
||||
|
||||
@unittest.skipIf(sys.version_info[0] >= 3, "Python 3 is strict")
|
||||
def test_07_match_unicode_and_bytes(self):
|
||||
"""
|
||||
Test unicode patterns with byte paths.
|
||||
"""
|
||||
pattern = GitWildMatchPattern('*.py')
|
||||
results = set(pattern.match([b'a.py']))
|
||||
self.assertEqual(results, {b'a.py'})
|
||||
|
||||
@unittest.skipIf(sys.version_info[0] == 2, "Python 2 is lenient")
|
||||
def test_07_match_unicode_and_bytes_fail(self):
|
||||
"""
|
||||
Test unicode patterns with byte paths.
|
||||
"""
|
||||
pattern = GitWildMatchPattern('*.py')
|
||||
with self.assertRaises(TypeError):
|
||||
for _ in pattern.match([b'a.py']):
|
||||
pass
|
||||
|
||||
def test_07_match_unicode_and_unicode(self):
|
||||
"""
|
||||
Test unicode patterns with unicode paths.
|
||||
"""
|
||||
pattern = GitWildMatchPattern('*.py')
|
||||
results = set(pattern.match(['a.py']))
|
||||
self.assertEqual(results, {'a.py'})
|
||||
|
||||
def test_08_escape(self):
|
||||
"""
|
||||
Test escaping a string with meta-characters
|
||||
"""
|
||||
fname = "file!with*weird#naming_[1].t?t"
|
||||
escaped = r"file\!with\*weird\#naming_\[1\].t\?t"
|
||||
result = GitWildMatchPattern.escape(fname)
|
||||
self.assertEqual(result, escaped)
|
||||
|
||||
def test_09_single_escape_fail(self):
|
||||
"""
|
||||
Test an escape on a line by itself.
|
||||
"""
|
||||
self._check_invalid_pattern("\\")
|
||||
|
||||
def test_09_single_exclamation_mark_fail(self):
|
||||
"""
|
||||
Test an escape on a line by itself.
|
||||
"""
|
||||
self._check_invalid_pattern("!")
|
||||
|
||||
def _check_invalid_pattern(self, git_ignore_pattern):
|
||||
expected_message_pattern = re.escape(repr(git_ignore_pattern))
|
||||
with self.assertRaisesRegexp(GitWildMatchPatternError, expected_message_pattern):
|
||||
GitWildMatchPattern(git_ignore_pattern)
|
||||
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
# encoding: utf-8
|
||||
"""
|
||||
This script tests ``PathSpec``.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
import pathspec
|
||||
|
||||
|
||||
class PathSpecTest(unittest.TestCase):
|
||||
"""
|
||||
The ``PathSpecTest`` class tests the ``PathSpec`` class.
|
||||
"""
|
||||
|
||||
def test_01_absolute_dir_paths_1(self):
|
||||
"""
|
||||
Tests that absolute paths will be properly normalized and matched.
|
||||
"""
|
||||
spec = pathspec.PathSpec.from_lines('gitwildmatch', [
|
||||
'foo',
|
||||
])
|
||||
results = set(spec.match_files([
|
||||
'/a.py',
|
||||
'/foo/a.py',
|
||||
'/x/a.py',
|
||||
'/x/foo/a.py',
|
||||
'a.py',
|
||||
'foo/a.py',
|
||||
'x/a.py',
|
||||
'x/foo/a.py',
|
||||
]))
|
||||
self.assertEqual(results, {
|
||||
'/foo/a.py',
|
||||
'/x/foo/a.py',
|
||||
'foo/a.py',
|
||||
'x/foo/a.py',
|
||||
})
|
||||
|
||||
def test_01_absolute_dir_paths_2(self):
|
||||
"""
|
||||
Tests that absolute paths will be properly normalized and matched.
|
||||
"""
|
||||
spec = pathspec.PathSpec.from_lines('gitwildmatch', [
|
||||
'/foo',
|
||||
])
|
||||
results = set(spec.match_files([
|
||||
'/a.py',
|
||||
'/foo/a.py',
|
||||
'/x/a.py',
|
||||
'/x/foo/a.py',
|
||||
'a.py',
|
||||
'foo/a.py',
|
||||
'x/a.py',
|
||||
'x/foo/a.py',
|
||||
]))
|
||||
self.assertEqual(results, {
|
||||
'/foo/a.py',
|
||||
'foo/a.py',
|
||||
})
|
||||
|
||||
def test_01_current_dir_paths(self):
|
||||
"""
|
||||
Tests that paths referencing the current directory will be properly
|
||||
normalized and matched.
|
||||
"""
|
||||
spec = pathspec.PathSpec.from_lines('gitwildmatch', [
|
||||
'*.txt',
|
||||
'!test1/',
|
||||
])
|
||||
results = set(spec.match_files([
|
||||
'./src/test1/a.txt',
|
||||
'./src/test1/b.txt',
|
||||
'./src/test1/c/c.txt',
|
||||
'./src/test2/a.txt',
|
||||
'./src/test2/b.txt',
|
||||
'./src/test2/c/c.txt',
|
||||
]))
|
||||
self.assertEqual(results, {
|
||||
'./src/test2/a.txt',
|
||||
'./src/test2/b.txt',
|
||||
'./src/test2/c/c.txt',
|
||||
})
|
||||
|
||||
def test_01_match_files(self):
|
||||
"""
|
||||
Tests that matching files one at a time yields the same results as
|
||||
matching multiples files at once.
|
||||
"""
|
||||
spec = pathspec.PathSpec.from_lines('gitwildmatch', [
|
||||
'*.txt',
|
||||
'!test1/',
|
||||
])
|
||||
test_files = [
|
||||
'src/test1/a.txt',
|
||||
'src/test1/b.txt',
|
||||
'src/test1/c/c.txt',
|
||||
'src/test2/a.txt',
|
||||
'src/test2/b.txt',
|
||||
'src/test2/c/c.txt',
|
||||
]
|
||||
single_results = set(filter(spec.match_file, test_files))
|
||||
multi_results = set(spec.match_files(test_files))
|
||||
self.assertEqual(single_results, multi_results)
|
||||
|
||||
def test_01_windows_current_dir_paths(self):
|
||||
"""
|
||||
Tests that paths referencing the current directory will be properly
|
||||
normalized and matched.
|
||||
"""
|
||||
spec = pathspec.PathSpec.from_lines('gitwildmatch', [
|
||||
'*.txt',
|
||||
'!test1/',
|
||||
])
|
||||
results = set(spec.match_files([
|
||||
'.\\src\\test1\\a.txt',
|
||||
'.\\src\\test1\\b.txt',
|
||||
'.\\src\\test1\\c\\c.txt',
|
||||
'.\\src\\test2\\a.txt',
|
||||
'.\\src\\test2\\b.txt',
|
||||
'.\\src\\test2\\c\\c.txt',
|
||||
], separators=('\\',)))
|
||||
self.assertEqual(results, {
|
||||
'.\\src\\test2\\a.txt',
|
||||
'.\\src\\test2\\b.txt',
|
||||
'.\\src\\test2\\c\\c.txt',
|
||||
})
|
||||
|
||||
def test_01_windows_paths(self):
|
||||
"""
|
||||
Tests that Windows paths will be properly normalized and matched.
|
||||
"""
|
||||
spec = pathspec.PathSpec.from_lines('gitwildmatch', [
|
||||
'*.txt',
|
||||
'!test1/',
|
||||
])
|
||||
results = set(spec.match_files([
|
||||
'src\\test1\\a.txt',
|
||||
'src\\test1\\b.txt',
|
||||
'src\\test1\\c\\c.txt',
|
||||
'src\\test2\\a.txt',
|
||||
'src\\test2\\b.txt',
|
||||
'src\\test2\\c\\c.txt',
|
||||
], separators=('\\',)))
|
||||
self.assertEqual(results, {
|
||||
'src\\test2\\a.txt',
|
||||
'src\\test2\\b.txt',
|
||||
'src\\test2\\c\\c.txt',
|
||||
})
|
||||
|
||||
def test_02_eq(self):
|
||||
"""
|
||||
Tests equality.
|
||||
"""
|
||||
first_spec = pathspec.PathSpec.from_lines('gitwildmatch', [
|
||||
'*.txt',
|
||||
'!test1/',
|
||||
])
|
||||
second_spec = pathspec.PathSpec.from_lines('gitwildmatch', [
|
||||
'*.txt',
|
||||
'!test1/',
|
||||
])
|
||||
self.assertEqual(first_spec, second_spec)
|
||||
|
||||
def test_02_ne(self):
|
||||
"""
|
||||
Tests equality.
|
||||
"""
|
||||
first_spec = pathspec.PathSpec.from_lines('gitwildmatch', [
|
||||
'*.txt',
|
||||
])
|
||||
second_spec = pathspec.PathSpec.from_lines('gitwildmatch', [
|
||||
'!*.txt',
|
||||
])
|
||||
self.assertNotEqual(first_spec, second_spec)
|
||||
|
||||
def test_01_addition(self):
|
||||
"""
|
||||
Test pattern addition using + operator
|
||||
"""
|
||||
first_spec = pathspec.PathSpec.from_lines('gitwildmatch', [
|
||||
'test.txt',
|
||||
'test.png'
|
||||
])
|
||||
second_spec = pathspec.PathSpec.from_lines('gitwildmatch', [
|
||||
'test.html',
|
||||
'test.jpg'
|
||||
])
|
||||
combined_spec = first_spec + second_spec
|
||||
results = set(combined_spec.match_files([
|
||||
'test.txt',
|
||||
'test.png',
|
||||
'test.html',
|
||||
'test.jpg'
|
||||
], separators=('\\',)))
|
||||
self.assertEqual(results, {
|
||||
'test.txt',
|
||||
'test.png',
|
||||
'test.html',
|
||||
'test.jpg'
|
||||
})
|
||||
|
||||
def test_02_addition(self):
|
||||
"""
|
||||
Test pattern addition using += operator
|
||||
"""
|
||||
spec = pathspec.PathSpec.from_lines('gitwildmatch', [
|
||||
'test.txt',
|
||||
'test.png'
|
||||
])
|
||||
spec += pathspec.PathSpec.from_lines('gitwildmatch', [
|
||||
'test.html',
|
||||
'test.jpg'
|
||||
])
|
||||
results = set(spec.match_files([
|
||||
'test.txt',
|
||||
'test.png',
|
||||
'test.html',
|
||||
'test.jpg'
|
||||
], separators=('\\',)))
|
||||
self.assertEqual(results, {
|
||||
'test.txt',
|
||||
'test.png',
|
||||
'test.html',
|
||||
'test.jpg'
|
||||
})
|
||||
380
.venv/lib/python3.8/site-packages/pathspec/tests/test_util.py
Normal file
380
.venv/lib/python3.8/site-packages/pathspec/tests/test_util.py
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
# encoding: utf-8
|
||||
"""
|
||||
This script tests utility functions.
|
||||
"""
|
||||
|
||||
import errno
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from pathspec.util import iter_tree_entries, iter_tree_files, RecursionError, normalize_file
|
||||
|
||||
|
||||
class IterTreeTest(unittest.TestCase):
|
||||
"""
|
||||
The ``IterTreeTest`` class tests `pathspec.util.iter_tree_files()`.
|
||||
"""
|
||||
|
||||
def make_dirs(self, dirs):
|
||||
"""
|
||||
Create the specified directories.
|
||||
"""
|
||||
for dir in dirs:
|
||||
os.mkdir(os.path.join(self.temp_dir, self.ospath(dir)))
|
||||
|
||||
def make_files(self, files):
|
||||
"""
|
||||
Create the specified files.
|
||||
"""
|
||||
for file in files:
|
||||
self.mkfile(os.path.join(self.temp_dir, self.ospath(file)))
|
||||
|
||||
def make_links(self, links):
|
||||
"""
|
||||
Create the specified links.
|
||||
"""
|
||||
for link, node in links:
|
||||
os.symlink(os.path.join(self.temp_dir, self.ospath(node)), os.path.join(self.temp_dir, self.ospath(link)))
|
||||
|
||||
@staticmethod
|
||||
def mkfile(file):
|
||||
"""
|
||||
Creates an empty file.
|
||||
"""
|
||||
with open(file, 'wb'):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def ospath(path):
|
||||
"""
|
||||
Convert the POSIX path to a native OS path.
|
||||
"""
|
||||
return os.path.join(*path.split('/'))
|
||||
|
||||
def require_realpath(self):
|
||||
"""
|
||||
Skips the test if `os.path.realpath` does not properly support
|
||||
symlinks.
|
||||
"""
|
||||
if self.broken_realpath:
|
||||
raise unittest.SkipTest("`os.path.realpath` is broken.")
|
||||
|
||||
def require_symlink(self):
|
||||
"""
|
||||
Skips the test if `os.symlink` is not supported.
|
||||
"""
|
||||
if self.no_symlink:
|
||||
raise unittest.SkipTest("`os.symlink` is not supported.")
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Called before each test.
|
||||
"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Called after each test.
|
||||
"""
|
||||
shutil.rmtree(self.temp_dir)
|
||||
|
||||
def test_1_files(self):
|
||||
"""
|
||||
Tests to make sure all files are found.
|
||||
"""
|
||||
self.make_dirs([
|
||||
'Empty',
|
||||
'Dir',
|
||||
'Dir/Inner',
|
||||
])
|
||||
self.make_files([
|
||||
'a',
|
||||
'b',
|
||||
'Dir/c',
|
||||
'Dir/d',
|
||||
'Dir/Inner/e',
|
||||
'Dir/Inner/f',
|
||||
])
|
||||
results = set(iter_tree_files(self.temp_dir))
|
||||
self.assertEqual(results, set(map(self.ospath, [
|
||||
'a',
|
||||
'b',
|
||||
'Dir/c',
|
||||
'Dir/d',
|
||||
'Dir/Inner/e',
|
||||
'Dir/Inner/f',
|
||||
])))
|
||||
|
||||
def test_2_0_check_symlink(self):
|
||||
"""
|
||||
Tests whether links can be created.
|
||||
"""
|
||||
# NOTE: Windows does not support `os.symlink` for Python 2. Windows
|
||||
# Vista and greater supports `os.symlink` for Python 3.2+.
|
||||
no_symlink = None
|
||||
try:
|
||||
file = os.path.join(self.temp_dir, 'file')
|
||||
link = os.path.join(self.temp_dir, 'link')
|
||||
self.mkfile(file)
|
||||
|
||||
try:
|
||||
os.symlink(file, link)
|
||||
except (AttributeError, NotImplementedError):
|
||||
no_symlink = True
|
||||
raise
|
||||
no_symlink = False
|
||||
|
||||
finally:
|
||||
self.__class__.no_symlink = no_symlink
|
||||
|
||||
def test_2_1_check_realpath(self):
|
||||
"""
|
||||
Tests whether `os.path.realpath` works properly with symlinks.
|
||||
"""
|
||||
# NOTE: Windows does not follow symlinks with `os.path.realpath`
|
||||
# which is what we use to detect recursion. See <https://bugs.python.org/issue9949>
|
||||
# for details.
|
||||
broken_realpath = None
|
||||
try:
|
||||
self.require_symlink()
|
||||
file = os.path.join(self.temp_dir, 'file')
|
||||
link = os.path.join(self.temp_dir, 'link')
|
||||
self.mkfile(file)
|
||||
os.symlink(file, link)
|
||||
|
||||
try:
|
||||
self.assertEqual(os.path.realpath(file), os.path.realpath(link))
|
||||
except AssertionError:
|
||||
broken_realpath = True
|
||||
raise
|
||||
broken_realpath = False
|
||||
|
||||
finally:
|
||||
self.__class__.broken_realpath = broken_realpath
|
||||
|
||||
def test_2_2_links(self):
|
||||
"""
|
||||
Tests to make sure links to directories and files work.
|
||||
"""
|
||||
self.require_symlink()
|
||||
self.make_dirs([
|
||||
'Dir',
|
||||
])
|
||||
self.make_files([
|
||||
'a',
|
||||
'b',
|
||||
'Dir/c',
|
||||
'Dir/d',
|
||||
])
|
||||
self.make_links([
|
||||
('ax', 'a'),
|
||||
('bx', 'b'),
|
||||
('Dir/cx', 'Dir/c'),
|
||||
('Dir/dx', 'Dir/d'),
|
||||
('DirX', 'Dir'),
|
||||
])
|
||||
results = set(iter_tree_files(self.temp_dir))
|
||||
self.assertEqual(results, set(map(self.ospath, [
|
||||
'a',
|
||||
'ax',
|
||||
'b',
|
||||
'bx',
|
||||
'Dir/c',
|
||||
'Dir/cx',
|
||||
'Dir/d',
|
||||
'Dir/dx',
|
||||
'DirX/c',
|
||||
'DirX/cx',
|
||||
'DirX/d',
|
||||
'DirX/dx',
|
||||
])))
|
||||
|
||||
def test_2_3_sideways_links(self):
|
||||
"""
|
||||
Tests to make sure the same directory can be encountered multiple
|
||||
times via links.
|
||||
"""
|
||||
self.require_symlink()
|
||||
self.make_dirs([
|
||||
'Dir',
|
||||
'Dir/Target',
|
||||
])
|
||||
self.make_files([
|
||||
'Dir/Target/file',
|
||||
])
|
||||
self.make_links([
|
||||
('Ax', 'Dir'),
|
||||
('Bx', 'Dir'),
|
||||
('Cx', 'Dir/Target'),
|
||||
('Dx', 'Dir/Target'),
|
||||
('Dir/Ex', 'Dir/Target'),
|
||||
('Dir/Fx', 'Dir/Target'),
|
||||
])
|
||||
results = set(iter_tree_files(self.temp_dir))
|
||||
self.assertEqual(results, set(map(self.ospath, [
|
||||
'Ax/Ex/file',
|
||||
'Ax/Fx/file',
|
||||
'Ax/Target/file',
|
||||
'Bx/Ex/file',
|
||||
'Bx/Fx/file',
|
||||
'Bx/Target/file',
|
||||
'Cx/file',
|
||||
'Dx/file',
|
||||
'Dir/Ex/file',
|
||||
'Dir/Fx/file',
|
||||
'Dir/Target/file',
|
||||
])))
|
||||
|
||||
def test_2_4_recursive_links(self):
|
||||
"""
|
||||
Tests detection of recursive links.
|
||||
"""
|
||||
self.require_symlink()
|
||||
self.require_realpath()
|
||||
self.make_dirs([
|
||||
'Dir',
|
||||
])
|
||||
self.make_files([
|
||||
'Dir/file',
|
||||
])
|
||||
self.make_links([
|
||||
('Dir/Self', 'Dir'),
|
||||
])
|
||||
with self.assertRaises(RecursionError) as context:
|
||||
set(iter_tree_files(self.temp_dir))
|
||||
self.assertEqual(context.exception.first_path, 'Dir')
|
||||
self.assertEqual(context.exception.second_path, self.ospath('Dir/Self'))
|
||||
|
||||
def test_2_5_recursive_circular_links(self):
|
||||
"""
|
||||
Tests detection of recursion through circular links.
|
||||
"""
|
||||
self.require_symlink()
|
||||
self.require_realpath()
|
||||
self.make_dirs([
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
])
|
||||
self.make_files([
|
||||
'A/d',
|
||||
'B/e',
|
||||
'C/f',
|
||||
])
|
||||
self.make_links([
|
||||
('A/Bx', 'B'),
|
||||
('B/Cx', 'C'),
|
||||
('C/Ax', 'A'),
|
||||
])
|
||||
with self.assertRaises(RecursionError) as context:
|
||||
set(iter_tree_files(self.temp_dir))
|
||||
self.assertIn(context.exception.first_path, ('A', 'B', 'C'))
|
||||
self.assertEqual(context.exception.second_path, {
|
||||
'A': self.ospath('A/Bx/Cx/Ax'),
|
||||
'B': self.ospath('B/Cx/Ax/Bx'),
|
||||
'C': self.ospath('C/Ax/Bx/Cx'),
|
||||
}[context.exception.first_path])
|
||||
|
||||
def test_2_6_detect_broken_links(self):
|
||||
"""
|
||||
Tests that broken links are detected.
|
||||
"""
|
||||
def reraise(e):
|
||||
raise e
|
||||
|
||||
self.require_symlink()
|
||||
self.make_links([
|
||||
('A', 'DOES_NOT_EXIST'),
|
||||
])
|
||||
with self.assertRaises(OSError) as context:
|
||||
set(iter_tree_files(self.temp_dir, on_error=reraise))
|
||||
self.assertEqual(context.exception.errno, errno.ENOENT)
|
||||
|
||||
def test_2_7_ignore_broken_links(self):
|
||||
"""
|
||||
Tests that broken links are ignored.
|
||||
"""
|
||||
self.require_symlink()
|
||||
self.make_links([
|
||||
('A', 'DOES_NOT_EXIST'),
|
||||
])
|
||||
results = set(iter_tree_files(self.temp_dir))
|
||||
self.assertEqual(results, set())
|
||||
|
||||
def test_2_8_no_follow_links(self):
|
||||
"""
|
||||
Tests to make sure directory links can be ignored.
|
||||
"""
|
||||
self.require_symlink()
|
||||
self.make_dirs([
|
||||
'Dir',
|
||||
])
|
||||
self.make_files([
|
||||
'A',
|
||||
'B',
|
||||
'Dir/C',
|
||||
'Dir/D',
|
||||
])
|
||||
self.make_links([
|
||||
('Ax', 'A'),
|
||||
('Bx', 'B'),
|
||||
('Dir/Cx', 'Dir/C'),
|
||||
('Dir/Dx', 'Dir/D'),
|
||||
('DirX', 'Dir'),
|
||||
])
|
||||
results = set(iter_tree_files(self.temp_dir, follow_links=False))
|
||||
self.assertEqual(results, set(map(self.ospath, [
|
||||
'A',
|
||||
'Ax',
|
||||
'B',
|
||||
'Bx',
|
||||
'Dir/C',
|
||||
'Dir/Cx',
|
||||
'Dir/D',
|
||||
'Dir/Dx',
|
||||
'DirX',
|
||||
])))
|
||||
|
||||
def test_3_entries(self):
|
||||
"""
|
||||
Tests to make sure all files are found.
|
||||
"""
|
||||
self.make_dirs([
|
||||
'Empty',
|
||||
'Dir',
|
||||
'Dir/Inner',
|
||||
])
|
||||
self.make_files([
|
||||
'a',
|
||||
'b',
|
||||
'Dir/c',
|
||||
'Dir/d',
|
||||
'Dir/Inner/e',
|
||||
'Dir/Inner/f',
|
||||
])
|
||||
results = {entry.path for entry in iter_tree_entries(self.temp_dir)}
|
||||
self.assertEqual(results, set(map(self.ospath, [
|
||||
'a',
|
||||
'b',
|
||||
'Dir',
|
||||
'Dir/c',
|
||||
'Dir/d',
|
||||
'Dir/Inner',
|
||||
'Dir/Inner/e',
|
||||
'Dir/Inner/f',
|
||||
'Empty',
|
||||
])))
|
||||
|
||||
@unittest.skipIf(sys.version_info < (3, 4), "pathlib entered stdlib in Python 3.4")
|
||||
def test_4_normalizing_pathlib_path(self):
|
||||
"""
|
||||
Tests passing pathlib.Path as argument.
|
||||
"""
|
||||
from pathlib import Path
|
||||
first_spec = normalize_file(Path('a.txt'))
|
||||
second_spec = normalize_file('a.txt')
|
||||
self.assertEqual(first_spec, second_spec)
|
||||
665
.venv/lib/python3.8/site-packages/pathspec/util.py
Normal file
665
.venv/lib/python3.8/site-packages/pathspec/util.py
Normal file
|
|
@ -0,0 +1,665 @@
|
|||
# encoding: utf-8
|
||||
"""
|
||||
This module provides utility methods for dealing with path-specs.
|
||||
"""
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import posixpath
|
||||
import stat
|
||||
try:
|
||||
from typing import (
|
||||
Any,
|
||||
AnyStr,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Text,
|
||||
Union)
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
# Python 3.6+ type hints.
|
||||
from os import PathLike
|
||||
from typing import Collection
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from .compat import (
|
||||
CollectionType,
|
||||
IterableType,
|
||||
string_types,
|
||||
unicode)
|
||||
from .pattern import Pattern
|
||||
|
||||
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):
|
||||
# type: (Iterable[Pattern], Iterable[Text], Optional[bool]) -> Dict[Text, '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:`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, CollectionType) 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):
|
||||
# type: (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, (unicode, bytes))
|
||||
|
||||
|
||||
def iter_tree_entries(root, on_error=None, follow_links=None):
|
||||
# type: (Text, Optional[Callable], Optional[bool]) -> Iterator['TreeEntry']
|
||||
"""
|
||||
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.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("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):
|
||||
# type: (Text, Optional[Callable], Optional[bool]) -> Iterator[Text]
|
||||
"""
|
||||
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.Iterator` 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):
|
||||
# type: (Text, Text, Dict[Text, Text], Callable, 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)
|
||||
|
||||
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):
|
||||
# type: (Text) -> 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, file):
|
||||
# type: (Iterable[Pattern], Text) -> 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:
|
||||
if file in pattern.match((file,)):
|
||||
matched = pattern.include
|
||||
return matched
|
||||
|
||||
|
||||
def match_files(patterns, files):
|
||||
# type: (Iterable[Pattern], Iterable[Text]) -> Set[Text]
|
||||
"""
|
||||
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, CollectionType) 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):
|
||||
# type: (Iterable['TreeEntry'], Optional[Collection[Text]]) -> Dict[Text, 'TreeEntry']
|
||||
"""
|
||||
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):
|
||||
# type: (Union[Text, PathLike], Optional[Collection[Text]]) -> Text
|
||||
"""
|
||||
Normalizes the file path to use the POSIX path separator (i.e.,
|
||||
``'/'``), and make the paths relative (remove leading ``'/'``).
|
||||
|
||||
*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)
|
||||
|
||||
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, separators=None):
|
||||
# type: (Iterable[Union[str, PathLike]], Optional[Collection[Text]]) -> Dict[Text, List[Union[str, PathLike]]]
|
||||
"""
|
||||
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 paths (:class:`list` of
|
||||
:class:`str` or :class:`pathlib.PurePath`).
|
||||
"""
|
||||
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, pattern_factory, override=None):
|
||||
# type: (Text, Callable[[AnyStr], Pattern], Optional[bool]) -> 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):
|
||||
# type: (Text, 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):
|
||||
# type: () -> Text
|
||||
"""
|
||||
*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):
|
||||
# type: () -> Text
|
||||
"""
|
||||
*name* (:class:`str`) is the name of the registered pattern.
|
||||
"""
|
||||
return self.args[0]
|
||||
|
||||
@property
|
||||
def pattern_factory(self):
|
||||
# type: () -> 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, first_path, second_path):
|
||||
# type: (Text, Text, Text) -> 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):
|
||||
# type: () -> Text
|
||||
"""
|
||||
*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):
|
||||
# type: () -> Text
|
||||
"""
|
||||
*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):
|
||||
# type: () -> Text
|
||||
"""
|
||||
*real_path* (:class:`str`) is the real path that recursion was
|
||||
encountered on.
|
||||
"""
|
||||
return self.args[0]
|
||||
|
||||
@property
|
||||
def second_path(self):
|
||||
# type: () -> Text
|
||||
"""
|
||||
*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):
|
||||
# type: (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, path, lstat, stat):
|
||||
# type: (Text, Text, os.stat_result, 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 = 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):
|
||||
# type: (Optional[bool]) -> 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=None):
|
||||
# type: (Optional[bool]) -> 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):
|
||||
# type: () -> bool
|
||||
"""
|
||||
Returns whether the entry is a symbolic link (:class:`bool`).
|
||||
"""
|
||||
return stat.S_ISLNK(self._lstat.st_mode)
|
||||
|
||||
def stat(self, follow_links=None):
|
||||
# type: (Optional[bool]) -> 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue