Refactor code structure for improved readability and maintainability
This commit is contained in:
parent
389d72a136
commit
aa4c067ea8
1685 changed files with 393439 additions and 71932 deletions
|
|
@ -0,0 +1,4 @@
|
|||
"""
|
||||
WARNING: The *pathspec._backends* package is not part of the public API. Its
|
||||
contents and structure are likely to change.
|
||||
"""
|
||||
45
.venv_codegen/Lib/site-packages/pathspec/_backends/_utils.py
Normal file
45
.venv_codegen/Lib/site-packages/pathspec/_backends/_utils.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
"""
|
||||
This module provides private utility functions for backends.
|
||||
|
||||
WARNING: The *pathspec._backends* package is not part of the public API. Its
|
||||
contents and structure are likely to change.
|
||||
"""
|
||||
|
||||
from collections.abc import (
|
||||
Iterable)
|
||||
from typing import (
|
||||
TypeVar)
|
||||
|
||||
from pathspec.pattern import (
|
||||
Pattern)
|
||||
|
||||
TPattern = TypeVar("TPattern", bound=Pattern)
|
||||
|
||||
|
||||
def enumerate_patterns(
|
||||
patterns: Iterable[TPattern],
|
||||
filter: bool,
|
||||
reverse: bool,
|
||||
) -> list[tuple[int, TPattern]]:
|
||||
"""
|
||||
Enumerate the patterns.
|
||||
|
||||
*patterns* (:class:`Iterable` of :class:`.Pattern`) contains the patterns.
|
||||
|
||||
*filter* (:class:`bool`) is whether to remove no-op patterns (:data:`True`),
|
||||
or keep them (:data:`False`).
|
||||
|
||||
*reverse* (:class:`bool`) is whether to reverse the pattern order
|
||||
(:data:`True`), or keep the order (:data:`True`).
|
||||
|
||||
Returns the enumerated patterns (:class:`list` of :class:`tuple`).
|
||||
"""
|
||||
out_patterns = [
|
||||
(__i, __pat)
|
||||
for __i, __pat in enumerate(patterns)
|
||||
if not filter or __pat.include is not None
|
||||
]
|
||||
if reverse:
|
||||
out_patterns.reverse()
|
||||
|
||||
return out_patterns
|
||||
104
.venv_codegen/Lib/site-packages/pathspec/_backends/agg.py
Normal file
104
.venv_codegen/Lib/site-packages/pathspec/_backends/agg.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"""
|
||||
This module provides aggregated private data and utilities functions about the
|
||||
available backends.
|
||||
|
||||
WARNING: The *pathspec._backends* package is not part of the public API. Its
|
||||
contents and structure are likely to change.
|
||||
"""
|
||||
|
||||
from collections.abc import (
|
||||
Sequence)
|
||||
from typing import (
|
||||
cast)
|
||||
|
||||
from pathspec.backend import (
|
||||
BackendNamesHint,
|
||||
_Backend)
|
||||
from pathspec.pattern import (
|
||||
Pattern,
|
||||
RegexPattern)
|
||||
|
||||
from .hyperscan.base import (
|
||||
hyperscan_error)
|
||||
from .hyperscan.gitignore import (
|
||||
HyperscanGiBackend)
|
||||
from .hyperscan.pathspec import (
|
||||
HyperscanPsBackend)
|
||||
from .re2.base import (
|
||||
re2_error)
|
||||
from .re2.gitignore import (
|
||||
Re2GiBackend)
|
||||
from .re2.pathspec import (
|
||||
Re2PsBackend)
|
||||
from .simple.gitignore import (
|
||||
SimpleGiBackend)
|
||||
from .simple.pathspec import (
|
||||
SimplePsBackend)
|
||||
|
||||
_BEST_BACKEND: BackendNamesHint
|
||||
"""
|
||||
The best available backend.
|
||||
"""
|
||||
|
||||
if re2_error is None:
|
||||
_BEST_BACKEND = 're2'
|
||||
elif hyperscan_error is None:
|
||||
_BEST_BACKEND = 'hyperscan'
|
||||
else:
|
||||
_BEST_BACKEND = 'simple'
|
||||
|
||||
|
||||
def make_gitignore_backend(
|
||||
name: BackendNamesHint,
|
||||
patterns: Sequence[Pattern],
|
||||
) -> _Backend:
|
||||
"""
|
||||
Create the specified backend with the supplied patterns for
|
||||
:class:`~pathspec.gitignore.GitIgnoreSpec`.
|
||||
|
||||
*name* (:class:`str`) is the name of the backend.
|
||||
|
||||
*patterns* (:class:`.Iterable` of :class:`.Pattern`) contains the compiled
|
||||
patterns.
|
||||
|
||||
Returns the backend (:class:`._Backend`).
|
||||
"""
|
||||
if name == 'best':
|
||||
name = _BEST_BACKEND
|
||||
|
||||
if name == 'hyperscan':
|
||||
return HyperscanGiBackend(cast(Sequence[RegexPattern], patterns))
|
||||
elif name == 're2':
|
||||
return Re2GiBackend(cast(Sequence[RegexPattern], patterns))
|
||||
elif name == 'simple':
|
||||
return SimpleGiBackend(cast(Sequence[RegexPattern], patterns))
|
||||
else:
|
||||
raise ValueError(f"Backend {name=!r} is invalid.")
|
||||
|
||||
|
||||
def make_pathspec_backend(
|
||||
name: BackendNamesHint,
|
||||
patterns: Sequence[Pattern],
|
||||
) -> _Backend:
|
||||
"""
|
||||
Create the specified backend with the supplied patterns for
|
||||
:class:`~pathspec.pathspec.PathSpec`.
|
||||
|
||||
*name* (:class:`str`) is the name of the backend.
|
||||
|
||||
*patterns* (:class:`Iterable` of :class:`Pattern`) contains the compiled
|
||||
patterns.
|
||||
|
||||
Returns the backend (:class:`._Backend`).
|
||||
"""
|
||||
if name == 'best':
|
||||
name = _BEST_BACKEND
|
||||
|
||||
if name == 'hyperscan':
|
||||
return HyperscanPsBackend(cast(Sequence[RegexPattern], patterns))
|
||||
elif name == 're2':
|
||||
return Re2PsBackend(cast(Sequence[RegexPattern], patterns))
|
||||
elif name == 'simple':
|
||||
return SimplePsBackend(patterns)
|
||||
else:
|
||||
raise ValueError(f"Backend {name=!r} is invalid.")
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
"""
|
||||
This module provides private data for the base implementation for the
|
||||
:module:`hyperscan` library.
|
||||
|
||||
WARNING: The *pathspec._backends.hyperscan* package is not part of the public
|
||||
API. Its contents and structure are likely to change.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import (
|
||||
dataclass)
|
||||
from typing import (
|
||||
Union) # Replaced by `X | Y` in 3.10.
|
||||
|
||||
try:
|
||||
import hyperscan
|
||||
except ModuleNotFoundError:
|
||||
hyperscan = None
|
||||
HS_FLAGS = 0
|
||||
else:
|
||||
HS_FLAGS = hyperscan.HS_FLAG_SINGLEMATCH | hyperscan.HS_FLAG_UTF8
|
||||
|
||||
HS_FLAGS: int
|
||||
"""
|
||||
The hyperscan flags to use:
|
||||
|
||||
- HS_FLAG_SINGLEMATCH is needed to ensure the partial patterns only match once.
|
||||
|
||||
- HS_FLAG_UTF8 is required to support unicode paths.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HyperscanExprDat(object):
|
||||
"""
|
||||
The :class:`HyperscanExprDat` class is used to store data related to an
|
||||
expression.
|
||||
"""
|
||||
|
||||
# The slots argument is not supported until Python 3.10.
|
||||
__slots__ = [
|
||||
'include',
|
||||
'index',
|
||||
'is_dir_pattern',
|
||||
]
|
||||
|
||||
include: bool
|
||||
"""
|
||||
*include* (:class:`bool`) is whether is whether the matched files should be
|
||||
included (:data:`True`), or excluded (:data:`False`).
|
||||
"""
|
||||
|
||||
index: int
|
||||
"""
|
||||
*index* (:class:`int`) is the pattern index.
|
||||
"""
|
||||
|
||||
is_dir_pattern: bool
|
||||
"""
|
||||
*is_dir_pattern* (:class:`bool`) is whether the pattern is a directory
|
||||
pattern for gitignore.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HyperscanExprDebug(HyperscanExprDat):
|
||||
"""
|
||||
The :class:`HyperscanExprDebug` class stores additional debug information
|
||||
related to an expression.
|
||||
"""
|
||||
|
||||
# The slots argument is not supported until Python 3.10.
|
||||
__slots__ = ['regex']
|
||||
|
||||
regex: Union[str, bytes]
|
||||
"""
|
||||
*regex* (:class:`str` or :class:`bytes`) is the regular expression.
|
||||
"""
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
"""
|
||||
This module provides the base implementation for the :module:`hyperscan`
|
||||
backend.
|
||||
|
||||
WARNING: The *pathspec._backends.hyperscan* package is not part of the public
|
||||
API. Its contents and structure are likely to change.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import (
|
||||
Optional)
|
||||
|
||||
try:
|
||||
import hyperscan
|
||||
hyperscan_error = None
|
||||
except ModuleNotFoundError as e:
|
||||
hyperscan = None
|
||||
hyperscan_error = e
|
||||
|
||||
hyperscan_error: Optional[ModuleNotFoundError]
|
||||
"""
|
||||
*hyperscan_error* (:class:`ModuleNotFoundError` or :data:`None`) is the
|
||||
hyperscan import error.
|
||||
"""
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
"""
|
||||
This module provides the :module:`hyperscan` backend for :class:`~pathspec.gitignore.GitIgnoreSpec`.
|
||||
|
||||
WARNING: The *pathspec._backends.hyperscan* package is not part of the public
|
||||
API. Its contents and structure are likely to change.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import (
|
||||
Sequence)
|
||||
from typing import (
|
||||
Any,
|
||||
Callable, # Replaced by `collections.abc.Callable` in 3.9.2.
|
||||
Optional, # Replaced by `X | None` in 3.10.
|
||||
Union) # Replaced by `X | Y` in 3.10.
|
||||
|
||||
try:
|
||||
import hyperscan
|
||||
except ModuleNotFoundError:
|
||||
hyperscan = None
|
||||
|
||||
from pathspec.pattern import (
|
||||
RegexPattern)
|
||||
from pathspec.patterns.gitignore.spec import (
|
||||
GitIgnoreSpecPattern,
|
||||
_BYTES_ENCODING,
|
||||
_DIR_MARK_CG,
|
||||
_DIR_MARK_OPT)
|
||||
from pathspec._typing import (
|
||||
override) # Added in 3.12.
|
||||
|
||||
from ._base import (
|
||||
HS_FLAGS,
|
||||
HyperscanExprDat,
|
||||
HyperscanExprDebug)
|
||||
from .pathspec import (
|
||||
HyperscanPsBackend)
|
||||
|
||||
|
||||
class HyperscanGiBackend(HyperscanPsBackend):
|
||||
"""
|
||||
The :class:`HyperscanGiBackend` class is the :module:`hyperscan`
|
||||
implementation used by :class:`~pathspec.gitignore.GitIgnoreSpec`. The
|
||||
Hyperscan database uses block mode for matching files.
|
||||
"""
|
||||
|
||||
# Change type hint.
|
||||
_out: tuple[Optional[bool], int, int]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
patterns: Sequence[RegexPattern],
|
||||
*,
|
||||
_debug_exprs: Optional[bool] = None,
|
||||
_test_sort: Optional[Callable[[list], None]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the :class:`HyperscanMatcher` instance.
|
||||
|
||||
*patterns* (:class:`Sequence` of :class:`.RegexPattern`) contains the
|
||||
compiled patterns.
|
||||
"""
|
||||
super().__init__(patterns, _debug_exprs=_debug_exprs, _test_sort=_test_sort)
|
||||
|
||||
self._out = (None, -1, 0)
|
||||
"""
|
||||
*_out* (:class:`tuple`) stores the current match:
|
||||
|
||||
- *0* (:class:`bool` or :data:`None`) is the match include.
|
||||
|
||||
- *1* (:class:`int`) is the match index.
|
||||
|
||||
- *2* (:class:`int`) is the match priority.
|
||||
"""
|
||||
|
||||
@override
|
||||
@staticmethod
|
||||
def _init_db(
|
||||
db: hyperscan.Database,
|
||||
debug: bool,
|
||||
patterns: list[tuple[int, RegexPattern]],
|
||||
sort_ids: Optional[Callable[[list[int]], None]],
|
||||
) -> list[HyperscanExprDat]:
|
||||
"""
|
||||
Create the Hyperscan database from the given patterns.
|
||||
|
||||
*db* (:class:`hyperscan.Hyperscan`) is the Hyperscan database.
|
||||
|
||||
*debug* (:class:`bool`) is whether to include additional debugging
|
||||
information for the expressions.
|
||||
|
||||
*patterns* (:class:`~collections.abc.Sequence` of :class:`.RegexPattern`)
|
||||
contains the patterns.
|
||||
|
||||
*sort_ids* (:class:`callable` or :data:`None`) is a function used to sort
|
||||
the compiled expression ids. This is used during testing to ensure the order
|
||||
of expressions is not accidentally relied on.
|
||||
|
||||
Returns a :class:`list` indexed by expression id (:class:`int`) to its data
|
||||
(:class:`HyperscanExprDat`).
|
||||
"""
|
||||
# WARNING: Hyperscan raises a `hyperscan.error` exception when compiled with
|
||||
# zero elements.
|
||||
assert patterns, patterns
|
||||
|
||||
# Prepare patterns.
|
||||
expr_data: list[HyperscanExprDat] = []
|
||||
exprs: list[bytes] = []
|
||||
for pattern_index, pattern in patterns:
|
||||
assert pattern.include is not None, (pattern_index, pattern)
|
||||
|
||||
# Encode regex.
|
||||
assert isinstance(pattern, RegexPattern), pattern
|
||||
regex = pattern.regex.pattern
|
||||
|
||||
use_regexes: list[tuple[Union[str, bytes], bool]] = []
|
||||
if isinstance(pattern, GitIgnoreSpecPattern):
|
||||
# GitIgnoreSpecPattern uses capture groups for its directory marker but
|
||||
# Hyperscan does not support capture groups. Handle this scenario.
|
||||
regex_str: str
|
||||
if isinstance(regex, str):
|
||||
regex_str: str = regex
|
||||
else:
|
||||
assert isinstance(regex, bytes), regex
|
||||
regex_str = regex.decode(_BYTES_ENCODING)
|
||||
|
||||
if _DIR_MARK_CG in regex_str:
|
||||
# Found directory marker.
|
||||
if regex_str.endswith(_DIR_MARK_OPT):
|
||||
# Regex has optional directory marker. Split regex into directory
|
||||
# and file variants.
|
||||
base_regex = regex_str[:-len(_DIR_MARK_OPT)]
|
||||
use_regexes.append((f'{base_regex}/', True))
|
||||
use_regexes.append((f'{base_regex}$', False))
|
||||
else:
|
||||
# Remove capture group.
|
||||
base_regex = regex_str.replace(_DIR_MARK_CG, '/')
|
||||
use_regexes.append((base_regex, True))
|
||||
|
||||
if not use_regexes:
|
||||
# No special case for regex.
|
||||
use_regexes.append((regex, False))
|
||||
|
||||
for regex, is_dir_pattern in use_regexes:
|
||||
if isinstance(regex, bytes):
|
||||
regex_bytes = regex
|
||||
else:
|
||||
assert isinstance(regex, str), regex
|
||||
regex_bytes = regex.encode('utf8')
|
||||
|
||||
if debug:
|
||||
expr_data.append(HyperscanExprDebug(
|
||||
include=pattern.include,
|
||||
index=pattern_index,
|
||||
is_dir_pattern=is_dir_pattern,
|
||||
regex=regex,
|
||||
))
|
||||
else:
|
||||
expr_data.append(HyperscanExprDat(
|
||||
include=pattern.include,
|
||||
index=pattern_index,
|
||||
is_dir_pattern=is_dir_pattern,
|
||||
))
|
||||
|
||||
exprs.append(regex_bytes)
|
||||
|
||||
# Sort expressions.
|
||||
ids = list(range(len(exprs)))
|
||||
if sort_ids is not None:
|
||||
sort_ids(ids)
|
||||
exprs = [exprs[__id] for __id in ids]
|
||||
|
||||
# Compile patterns.
|
||||
db.compile(
|
||||
expressions=exprs,
|
||||
ids=ids,
|
||||
elements=len(exprs),
|
||||
flags=HS_FLAGS,
|
||||
)
|
||||
return expr_data
|
||||
|
||||
@override
|
||||
def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]:
|
||||
"""
|
||||
Check the file against the patterns.
|
||||
|
||||
*file* (:class:`str`) is the normalized file path to check.
|
||||
|
||||
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`).
|
||||
"""
|
||||
# NOTICE: According to benchmarking, a method callback is 13% faster than
|
||||
# using a closure here.
|
||||
db = self._db
|
||||
if self._db is None:
|
||||
# Database was not initialized because there were no patterns. Return no
|
||||
# match.
|
||||
return (None, None)
|
||||
|
||||
self._out = (None, -1, 0)
|
||||
db.scan(file.encode('utf8'), match_event_handler=self.__on_match)
|
||||
|
||||
out_include, out_index = self._out[:2]
|
||||
if out_index == -1:
|
||||
out_index = None
|
||||
|
||||
return (out_include, out_index)
|
||||
|
||||
@override
|
||||
def __on_match(
|
||||
self,
|
||||
expr_id: int,
|
||||
_from: int,
|
||||
_to: int,
|
||||
_flags: int,
|
||||
_context: Any,
|
||||
) -> Optional[bool]:
|
||||
"""
|
||||
Called on each match.
|
||||
|
||||
*expr_id* (:class:`int`) is the expression id (index) of the matched
|
||||
pattern.
|
||||
"""
|
||||
expr_dat = self._expr_data[expr_id]
|
||||
|
||||
is_dir_pattern = expr_dat.is_dir_pattern
|
||||
if is_dir_pattern:
|
||||
# Pattern matched by a directory pattern.
|
||||
priority = 1
|
||||
else:
|
||||
# Pattern matched by a file pattern.
|
||||
priority = 2
|
||||
|
||||
# WARNING: Hyperscan does not guarantee matches will be produced in order!
|
||||
include = expr_dat.include
|
||||
index = expr_dat.index
|
||||
prev_index = self._out[1]
|
||||
prev_priority = self._out[2]
|
||||
if (
|
||||
(include and is_dir_pattern and index > prev_index)
|
||||
or (priority == prev_priority and index > prev_index)
|
||||
or priority > prev_priority
|
||||
):
|
||||
self._out = (include, expr_dat.index, priority)
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
"""
|
||||
This module provides the :module:`hyperscan` backend for :class:`~pathspec.pathspec.PathSpec`.
|
||||
|
||||
WARNING: The *pathspec._backends.hyperscan* package is not part of the public
|
||||
API. Its contents and structure are likely to change.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import (
|
||||
Sequence)
|
||||
from typing import (
|
||||
Any,
|
||||
Callable, # Replaced by `collections.abc.Callable` in 3.9.2.
|
||||
Optional) # Replaced by `X | None` in 3.10.
|
||||
|
||||
try:
|
||||
import hyperscan
|
||||
except ModuleNotFoundError:
|
||||
hyperscan = None
|
||||
|
||||
from pathspec.backend import (
|
||||
_Backend)
|
||||
from pathspec.pattern import (
|
||||
RegexPattern)
|
||||
from pathspec._typing import (
|
||||
override) # Added in 3.12.
|
||||
|
||||
from .._utils import (
|
||||
enumerate_patterns)
|
||||
|
||||
from .base import (
|
||||
hyperscan_error)
|
||||
from ._base import (
|
||||
HS_FLAGS,
|
||||
HyperscanExprDat,
|
||||
HyperscanExprDebug)
|
||||
|
||||
|
||||
class HyperscanPsBackend(_Backend):
|
||||
"""
|
||||
The :class:`HyperscanPsBackend` class is the :module:`hyperscan`
|
||||
implementation used by :class:`~pathspec.pathspec.PathSpec` for matching
|
||||
files. The Hyperscan database uses block mode for matching files.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
patterns: Sequence[RegexPattern],
|
||||
*,
|
||||
_debug_exprs: Optional[bool] = None,
|
||||
_test_sort: Optional[Callable[[list], None]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the :class:`HyperscanPsBackend` instance.
|
||||
|
||||
*patterns* (:class:`Sequence` of :class:`.RegexPattern`) contains the
|
||||
compiled patterns.
|
||||
"""
|
||||
if hyperscan is None:
|
||||
raise hyperscan_error
|
||||
|
||||
if patterns and not isinstance(patterns[0], RegexPattern):
|
||||
raise TypeError(f"{patterns[0]=!r} must be a RegexPattern.")
|
||||
|
||||
use_patterns = enumerate_patterns(
|
||||
patterns, filter=True, reverse=False,
|
||||
)
|
||||
|
||||
debug_exprs = bool(_debug_exprs)
|
||||
if use_patterns:
|
||||
db = self._make_db()
|
||||
expr_data = self._init_db(
|
||||
db=db,
|
||||
debug=debug_exprs,
|
||||
patterns=use_patterns,
|
||||
sort_ids=_test_sort,
|
||||
)
|
||||
else:
|
||||
# WARNING: The hyperscan database cannot be initialized with zero
|
||||
# patterns.
|
||||
db = None
|
||||
expr_data = []
|
||||
|
||||
self._db: Optional[hyperscan.Database] = db
|
||||
"""
|
||||
*_db* (:class:`hyperscan.Database`) is the Hyperscan database.
|
||||
"""
|
||||
|
||||
self._debug_exprs = debug_exprs
|
||||
"""
|
||||
*_debug_exprs* (:class:`bool`) is whether to include additional debugging
|
||||
information for the expressions.
|
||||
"""
|
||||
|
||||
self._expr_data: list[HyperscanExprDat] = expr_data
|
||||
"""
|
||||
*_expr_data* (:class:`list`) maps expression index (:class:`int`) to
|
||||
expression data (:class:`:class:`HyperscanExprDat`).
|
||||
"""
|
||||
|
||||
self._out: tuple[Optional[bool], int] = (None, -1)
|
||||
"""
|
||||
*_out* (:class:`tuple`) stores the current match:
|
||||
|
||||
- *0* (:class:`bool` or :data:`None`) is the match include.
|
||||
|
||||
- *1* (:class:`int`) is the match index.
|
||||
"""
|
||||
|
||||
self._patterns: dict[int, RegexPattern] = dict(use_patterns)
|
||||
"""
|
||||
*_patterns* (:class:`dict`) maps pattern index (:class:`int`) to pattern
|
||||
(:class:`RegexPattern`).
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _init_db(
|
||||
db: hyperscan.Database,
|
||||
debug: bool,
|
||||
patterns: list[tuple[int, RegexPattern]],
|
||||
sort_ids: Optional[Callable[[list[int]], None]],
|
||||
) -> list[HyperscanExprDat]:
|
||||
"""
|
||||
Initialize the Hyperscan database from the given patterns.
|
||||
|
||||
*db* (:class:`hyperscan.Hyperscan`) is the Hyperscan database.
|
||||
|
||||
*debug* (:class:`bool`) is whether to include additional debugging
|
||||
information for the expressions.
|
||||
|
||||
*patterns* (:class:`~collections.abc.Sequence` of :class:`.RegexPattern`)
|
||||
contains the patterns.
|
||||
|
||||
*sort_ids* (:class:`callable` or :data:`None`) is a function used to sort
|
||||
the compiled expression ids. This is used during testing to ensure the order
|
||||
of expressions is not accidentally relied on.
|
||||
|
||||
Returns a :class:`list` indexed by expression id (:class:`int`) to its data
|
||||
(:class:`HyperscanExprDat`).
|
||||
"""
|
||||
# WARNING: Hyperscan raises a `hyperscan.error` exception when compiled with
|
||||
# zero elements.
|
||||
assert patterns, patterns
|
||||
|
||||
# Prepare patterns.
|
||||
expr_data: list[HyperscanExprDat] = []
|
||||
exprs: list[bytes] = []
|
||||
for pattern_index, pattern in patterns:
|
||||
assert pattern.include is not None, (pattern_index, pattern)
|
||||
|
||||
# Encode regex.
|
||||
assert isinstance(pattern, RegexPattern), pattern
|
||||
regex = pattern.regex.pattern
|
||||
|
||||
if isinstance(regex, bytes):
|
||||
regex_bytes = regex
|
||||
else:
|
||||
assert isinstance(regex, str), regex
|
||||
regex_bytes = regex.encode('utf8')
|
||||
|
||||
if debug:
|
||||
expr_data.append(HyperscanExprDebug(
|
||||
include=pattern.include,
|
||||
index=pattern_index,
|
||||
is_dir_pattern=False,
|
||||
regex=regex,
|
||||
))
|
||||
else:
|
||||
expr_data.append(HyperscanExprDat(
|
||||
include=pattern.include,
|
||||
index=pattern_index,
|
||||
is_dir_pattern=False,
|
||||
))
|
||||
|
||||
exprs.append(regex_bytes)
|
||||
|
||||
# Sort expressions.
|
||||
ids = list(range(len(exprs)))
|
||||
if sort_ids is not None:
|
||||
sort_ids(ids)
|
||||
exprs = [exprs[__id] for __id in ids]
|
||||
|
||||
# Compile patterns.
|
||||
db.compile(
|
||||
expressions=exprs,
|
||||
ids=ids,
|
||||
elements=len(exprs),
|
||||
flags=HS_FLAGS,
|
||||
)
|
||||
|
||||
return expr_data
|
||||
|
||||
@override
|
||||
def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]:
|
||||
"""
|
||||
Check the file against the patterns.
|
||||
|
||||
*file* (:class:`str`) is the normalized file path to check.
|
||||
|
||||
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`).
|
||||
"""
|
||||
# NOTICE: According to benchmarking, a method callback is 20% faster than
|
||||
# using a closure here.
|
||||
db = self._db
|
||||
if self._db is None:
|
||||
# Database was not initialized because there were no patterns. Return no
|
||||
# match.
|
||||
return (None, None)
|
||||
|
||||
self._out = (None, -1)
|
||||
db.scan(file.encode('utf8'), match_event_handler=self.__on_match)
|
||||
|
||||
out_include, out_index = self._out
|
||||
if out_index == -1:
|
||||
out_index = None
|
||||
|
||||
return (out_include, out_index)
|
||||
|
||||
@staticmethod
|
||||
def _make_db() -> hyperscan.Database:
|
||||
"""
|
||||
Create the Hyperscan database.
|
||||
|
||||
Returns the database (:class:`hyperscan.Database`).
|
||||
"""
|
||||
return hyperscan.Database(mode=hyperscan.HS_MODE_BLOCK)
|
||||
|
||||
def __on_match(
|
||||
self,
|
||||
expr_id: int,
|
||||
_from: int,
|
||||
_to: int,
|
||||
_flags: int,
|
||||
_context: Any,
|
||||
) -> Optional[bool]:
|
||||
"""
|
||||
Called on each match.
|
||||
|
||||
*expr_id* (:class:`int`) is the expression id (index) of the matched
|
||||
pattern.
|
||||
"""
|
||||
# Store match.
|
||||
# - WARNING: Hyperscan does not guarantee matches will be produced in order!
|
||||
# Later expressions have higher priority.
|
||||
expr_dat = self._expr_data[expr_id]
|
||||
index = expr_dat.index
|
||||
prev_index = self._out[1]
|
||||
if index > prev_index:
|
||||
self._out = (expr_dat.include, index)
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
"""
|
||||
This module provides private data for the base implementation for the
|
||||
:module:`re2` library.
|
||||
|
||||
WARNING: The *pathspec._backends.re2* package is not part of the public API. Its
|
||||
contents and structure are likely to change.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import (
|
||||
dataclass)
|
||||
from typing import (
|
||||
Optional, # Replaced by `X | None` in 3.10.
|
||||
Union) # Replaced by `X | Y` in 3.10.
|
||||
|
||||
try:
|
||||
import re2
|
||||
re2_error = None
|
||||
except ModuleNotFoundError as e:
|
||||
re2 = None
|
||||
re2_error = e
|
||||
RE2_OPTIONS = None
|
||||
else:
|
||||
# Both the `google-re2` and `pyre2` libraries use the `re2` namespace.
|
||||
# `google-re2` is the only one currently supported.
|
||||
try:
|
||||
RE2_OPTIONS = re2.Options()
|
||||
RE2_OPTIONS.log_errors = False
|
||||
RE2_OPTIONS.never_capture = True
|
||||
except Exception as e:
|
||||
re2_error = e
|
||||
RE2_OPTIONS = None
|
||||
|
||||
RE2_OPTIONS: re2.Options
|
||||
"""
|
||||
The re2 options to use:
|
||||
|
||||
- `log_errors=False` disables logging to stderr.
|
||||
|
||||
- `never_capture=True` disables capture groups because they effectively cannot
|
||||
be utilized with :class:`re2.Set`.
|
||||
"""
|
||||
|
||||
re2_error: Optional[Exception]
|
||||
"""
|
||||
*re2_error* (:class:`Exception` or :data:`None`) is the re2 import error.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Re2RegexDat(object):
|
||||
"""
|
||||
The :class:`Re2RegexDat` class is used to store data related to a regular
|
||||
expression.
|
||||
"""
|
||||
|
||||
# The slots argument is not supported until Python 3.10.
|
||||
__slots__ = [
|
||||
'include',
|
||||
'index',
|
||||
'is_dir_pattern',
|
||||
]
|
||||
|
||||
include: bool
|
||||
"""
|
||||
*include* (:class:`bool`) is whether is whether the matched files should be
|
||||
included (:data:`True`), or excluded (:data:`False`).
|
||||
"""
|
||||
|
||||
index: int
|
||||
"""
|
||||
*index* (:class:`int`) is the pattern index.
|
||||
"""
|
||||
|
||||
is_dir_pattern: bool
|
||||
"""
|
||||
*is_dir_pattern* (:class:`bool`) is whether the pattern is a directory
|
||||
pattern for gitignore.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Re2RegexDebug(Re2RegexDat):
|
||||
"""
|
||||
The :class:`Re2RegexDebug` class stores additional debug information related
|
||||
to a regular expression.
|
||||
"""
|
||||
|
||||
# The slots argument is not supported until Python 3.10.
|
||||
__slots__ = ['regex']
|
||||
|
||||
regex: Union[str, bytes]
|
||||
"""
|
||||
*regex* (:class:`str` or :class:`bytes`) is the regular expression.
|
||||
"""
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
"""
|
||||
This module provides the base implementation for the :module:`re2` backend.
|
||||
|
||||
WARNING: The *pathspec._backends.re2* package is not part of the public API. Its
|
||||
contents and structure are likely to change.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import (
|
||||
Optional) # Replaced by `X | None` in 3.10.
|
||||
|
||||
from ._base import (
|
||||
re2_error)
|
||||
|
||||
re2_error: Optional[Exception]
|
||||
"""
|
||||
*re2_error* (:class:`Exception` or :data:`None`) is the re2 import error.
|
||||
"""
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
"""
|
||||
This module provides the :module:`re2` backend for :class:`~pathspec.gitignore.GitIgnoreSpec`.
|
||||
|
||||
WARNING: The *pathspec._backends.re2* package is not part of the public API. Its
|
||||
contents and structure are likely to change.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import (
|
||||
Callable, # Replaced by `collections.abc.Callable` in 3.9.2.
|
||||
Optional, # Replaced by `X | None` in 3.10.
|
||||
Union) # Replaced by `X | Y` in 3.10.
|
||||
|
||||
try:
|
||||
import re2
|
||||
except ModuleNotFoundError:
|
||||
re2 = None
|
||||
|
||||
from pathspec.pattern import (
|
||||
RegexPattern)
|
||||
from pathspec.patterns.gitignore.spec import (
|
||||
GitIgnoreSpecPattern,
|
||||
_BYTES_ENCODING,
|
||||
_DIR_MARK_CG,
|
||||
_DIR_MARK_OPT)
|
||||
from pathspec._typing import (
|
||||
override) # Added in 3.12.
|
||||
|
||||
from ._base import (
|
||||
Re2RegexDat,
|
||||
Re2RegexDebug)
|
||||
from .pathspec import (
|
||||
Re2PsBackend)
|
||||
|
||||
|
||||
class Re2GiBackend(Re2PsBackend):
|
||||
"""
|
||||
The :class:`Re2GiBackend` class is the :module:`re2` implementation used by
|
||||
:class:`~pathspec.gitignore.GitIgnoreSpec` for matching files.
|
||||
"""
|
||||
|
||||
@override
|
||||
@staticmethod
|
||||
def _init_set(
|
||||
debug: bool,
|
||||
patterns: dict[int, RegexPattern],
|
||||
regex_set: re2.Set,
|
||||
sort_indices: Optional[Callable[[list[int]], None]],
|
||||
) -> list[Re2RegexDat]:
|
||||
"""
|
||||
Create the re2 regex set.
|
||||
|
||||
*debug* (:class:`bool`) is whether to include additional debugging
|
||||
information for the regular expressions.
|
||||
|
||||
*patterns* (:class:`dict`) maps pattern index (:class:`int`) to pattern
|
||||
(:class:`.RegexPattern`).
|
||||
|
||||
*regex_set* (:class:`re2.Set`) is the regex set.
|
||||
|
||||
*sort_indices* (:class:`callable` or :data:`None`) is a function used to
|
||||
sort the patterns by index. This is used during testing to ensure the order
|
||||
of patterns is not accidentally relied on.
|
||||
|
||||
Returns a :class:`list` indexed by regex id (:class:`int`) to its data
|
||||
(:class:`Re2RegexDat`).
|
||||
"""
|
||||
# Sort patterns.
|
||||
indices = list(patterns.keys())
|
||||
if sort_indices is not None:
|
||||
sort_indices(indices)
|
||||
|
||||
# Prepare patterns.
|
||||
regex_data: list[Re2RegexDat] = []
|
||||
for pattern_index in indices:
|
||||
pattern = patterns[pattern_index]
|
||||
if pattern.include is None:
|
||||
continue
|
||||
|
||||
assert isinstance(pattern, RegexPattern), pattern
|
||||
regex = pattern.regex.pattern
|
||||
|
||||
use_regexes: list[tuple[Union[str, bytes], bool]] = []
|
||||
if isinstance(pattern, GitIgnoreSpecPattern):
|
||||
# GitIgnoreSpecPattern uses capture groups for its directory marker. Re2
|
||||
# supports capture groups, but they cannot be utilized when using
|
||||
# `re2.Set`. Handle this scenario.
|
||||
regex_str: str
|
||||
if isinstance(regex, str):
|
||||
regex_str = regex
|
||||
else:
|
||||
assert isinstance(regex, bytes), regex
|
||||
regex_str = regex.decode(_BYTES_ENCODING)
|
||||
|
||||
if _DIR_MARK_CG in regex_str:
|
||||
# Found directory marker.
|
||||
if regex_str.endswith(_DIR_MARK_OPT):
|
||||
# Regex has optional directory marker. Split regex into directory
|
||||
# and file variants.
|
||||
base_regex = regex_str[:-len(_DIR_MARK_OPT)]
|
||||
use_regexes.append((f'{base_regex}/', True))
|
||||
use_regexes.append((f'{base_regex}$', False))
|
||||
else:
|
||||
# Remove capture group.
|
||||
base_regex = regex_str.replace(_DIR_MARK_CG, '/')
|
||||
use_regexes.append((base_regex, True))
|
||||
|
||||
if not use_regexes:
|
||||
# No special case for regex.
|
||||
use_regexes.append((regex, False))
|
||||
|
||||
for regex, is_dir_pattern in use_regexes:
|
||||
if debug:
|
||||
regex_data.append(Re2RegexDebug(
|
||||
include=pattern.include,
|
||||
index=pattern_index,
|
||||
is_dir_pattern=is_dir_pattern,
|
||||
regex=regex,
|
||||
))
|
||||
else:
|
||||
regex_data.append(Re2RegexDat(
|
||||
include=pattern.include,
|
||||
index=pattern_index,
|
||||
is_dir_pattern=is_dir_pattern,
|
||||
))
|
||||
|
||||
regex_set.Add(regex)
|
||||
|
||||
# Compile patterns.
|
||||
regex_set.Compile()
|
||||
return regex_data
|
||||
|
||||
@override
|
||||
def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]:
|
||||
"""
|
||||
Check the file against the patterns.
|
||||
|
||||
*file* (:class:`str`) is the normalized file path to check.
|
||||
|
||||
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`).
|
||||
"""
|
||||
# Find best match.
|
||||
match_ids: Optional[list[int]] = self._set.Match(file)
|
||||
if not match_ids:
|
||||
return (None, None)
|
||||
|
||||
out_include: Optional[bool] = None
|
||||
out_index: int = -1
|
||||
out_priority = -1
|
||||
|
||||
regex_data = self._regex_data
|
||||
for regex_id in match_ids:
|
||||
regex_dat = regex_data[regex_id]
|
||||
|
||||
is_dir_pattern = regex_dat.is_dir_pattern
|
||||
if is_dir_pattern:
|
||||
# Pattern matched by a directory pattern.
|
||||
priority = 1
|
||||
else:
|
||||
# Pattern matched by a file pattern.
|
||||
priority = 2
|
||||
|
||||
# WARNING: According to the documentation on `RE2::Set::Match()`, there is
|
||||
# no guarantee matches will be produced in order!
|
||||
include = regex_dat.include
|
||||
index = regex_dat.index
|
||||
if (
|
||||
(include and is_dir_pattern and index > out_index)
|
||||
or (priority == out_priority and index > out_index)
|
||||
or priority > out_priority
|
||||
):
|
||||
out_include = include
|
||||
out_index = index
|
||||
out_priority = priority
|
||||
|
||||
assert out_index != -1, (out_index, out_include, out_priority)
|
||||
return (out_include, out_index)
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
"""
|
||||
This module provides the :module:`re2` backend for :class:`~pathspec.pathspec.PathSpec`.
|
||||
|
||||
WARNING: The *pathspec._backends.re2* package is not part of the public API. Its
|
||||
contents and structure are likely to change.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import (
|
||||
Sequence)
|
||||
from typing import (
|
||||
Callable, # Replaced by `collections.abc.Callable` in 3.9.2.
|
||||
Optional) # Replaced by `X | None` in 3.10.
|
||||
|
||||
try:
|
||||
import re2
|
||||
except ModuleNotFoundError:
|
||||
re2 = None
|
||||
|
||||
from pathspec.backend import (
|
||||
_Backend)
|
||||
from pathspec.pattern import (
|
||||
RegexPattern)
|
||||
from pathspec._typing import (
|
||||
override) # Added in 3.12.
|
||||
|
||||
from .._utils import (
|
||||
enumerate_patterns)
|
||||
|
||||
from .base import (
|
||||
re2_error)
|
||||
from ._base import (
|
||||
RE2_OPTIONS,
|
||||
Re2RegexDat,
|
||||
Re2RegexDebug)
|
||||
|
||||
|
||||
class Re2PsBackend(_Backend):
|
||||
"""
|
||||
The :class:`Re2PsBackend` class is the :module:`re2` implementation used by
|
||||
:class:`~pathspec.pathspec.PathSpec` for matching files.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
patterns: Sequence[RegexPattern],
|
||||
*,
|
||||
_debug_regex: Optional[bool] = None,
|
||||
_test_sort: Optional[Callable[[list], None]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the :class:`Re2PsBackend` instance.
|
||||
|
||||
*patterns* (:class:`Sequence` of :class:`.RegexPattern`) contains the
|
||||
compiled patterns.
|
||||
"""
|
||||
if re2_error is not None:
|
||||
raise re2_error
|
||||
|
||||
if patterns and not isinstance(patterns[0], RegexPattern):
|
||||
raise TypeError(f"{patterns[0]=!r} must be a RegexPattern.")
|
||||
|
||||
use_patterns = dict(enumerate_patterns(
|
||||
patterns, filter=True, reverse=False,
|
||||
))
|
||||
regex_set = self._make_set()
|
||||
|
||||
self._debug_regex = bool(_debug_regex)
|
||||
"""
|
||||
*_debug_regex* (:class:`bool`) is whether to include additional debugging
|
||||
information for the regular expressions.
|
||||
"""
|
||||
|
||||
self._patterns: dict[int, RegexPattern] = use_patterns
|
||||
"""
|
||||
*_patterns* (:class:`dict`) maps pattern index (:class:`int`) to pattern
|
||||
(:class:`RegexPattern`).
|
||||
"""
|
||||
|
||||
self._regex_data: list[Re2RegexDat] = self._init_set(
|
||||
debug=self._debug_regex,
|
||||
patterns=use_patterns,
|
||||
regex_set=regex_set,
|
||||
sort_indices=_test_sort,
|
||||
)
|
||||
"""
|
||||
*_regex_data* (:class:`list`) maps regex index (:class:`int`) to regex data
|
||||
(:class:`Re2RegexDat`).
|
||||
"""
|
||||
|
||||
self._set: re2.Set = regex_set
|
||||
"""
|
||||
*_set* (:class:`re2.Set`) is the re2 regex set.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _init_set(
|
||||
debug: bool,
|
||||
patterns: dict[int, RegexPattern],
|
||||
regex_set: re2.Set,
|
||||
sort_indices: Optional[Callable[[list[int]], None]],
|
||||
) -> list[Re2RegexDat]:
|
||||
"""
|
||||
Create the re2 regex set.
|
||||
|
||||
*debug* (:class:`bool`) is whether to include additional debugging
|
||||
information for the regular expressions.
|
||||
|
||||
*patterns* (:class:`dict`) maps pattern index (:class:`int`) to pattern
|
||||
(:class:`.RegexPattern`).
|
||||
|
||||
*regex_set* (:class:`re2.Set`) is the regex set.
|
||||
|
||||
*sort_indices* (:class:`callable` or :data:`None`) is a function used to
|
||||
sort the patterns by index. This is used during testing to ensure the order
|
||||
of patterns is not accidentally relied on.
|
||||
|
||||
Returns a :class:`list` indexed by regex id (:class:`int`) to its data
|
||||
(:class:`Re2RegexDat`).
|
||||
"""
|
||||
# Sort patterns.
|
||||
indices = list(patterns.keys())
|
||||
if sort_indices is not None:
|
||||
sort_indices(indices)
|
||||
|
||||
# Prepare patterns.
|
||||
regex_data: list[Re2RegexDat] = []
|
||||
for pattern_index in indices:
|
||||
pattern = patterns[pattern_index]
|
||||
if pattern.include is None:
|
||||
continue
|
||||
|
||||
assert isinstance(pattern, RegexPattern), pattern
|
||||
regex = pattern.regex.pattern
|
||||
|
||||
if debug:
|
||||
regex_data.append(Re2RegexDebug(
|
||||
include=pattern.include,
|
||||
index=pattern_index,
|
||||
is_dir_pattern=False,
|
||||
regex=regex,
|
||||
))
|
||||
else:
|
||||
regex_data.append(Re2RegexDat(
|
||||
include=pattern.include,
|
||||
index=pattern_index,
|
||||
is_dir_pattern=False,
|
||||
))
|
||||
|
||||
regex_set.Add(regex)
|
||||
|
||||
# Compile patterns.
|
||||
regex_set.Compile()
|
||||
return regex_data
|
||||
|
||||
@staticmethod
|
||||
def _make_set() -> re2.Set:
|
||||
"""
|
||||
Create the re2 regex set.
|
||||
|
||||
Returns the set (:class:`re2.Set`).
|
||||
"""
|
||||
return re2.Set.SearchSet(RE2_OPTIONS)
|
||||
|
||||
@override
|
||||
def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]:
|
||||
"""
|
||||
Check the file against the patterns.
|
||||
|
||||
*file* (:class:`str`) is the normalized file path to check.
|
||||
|
||||
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`).
|
||||
"""
|
||||
# Find best match.
|
||||
# - WARNING: According to the documentation on `RE2::Set::Match()`, there is
|
||||
# no guarantee matches will be produced in order! Later expressions have
|
||||
# higher priority.
|
||||
match_ids: Optional[list[int]] = self._set.Match(file)
|
||||
if not match_ids:
|
||||
return (None, None)
|
||||
|
||||
regex_data = self._regex_data
|
||||
pattern_index = max(regex_data[__id].index for __id in match_ids)
|
||||
pattern = self._patterns[pattern_index]
|
||||
return (pattern.include, pattern_index)
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
"""
|
||||
This module provides the simple backend for :class:`~pathspec.gitignore.GitIgnoreSpec`.
|
||||
|
||||
WARNING: The *pathspec._backends.simple* package is not part of the public API.
|
||||
Its contents and structure are likely to change.
|
||||
"""
|
||||
|
||||
from collections.abc import (
|
||||
Sequence)
|
||||
from typing import (
|
||||
Optional) # Replaced by `X | None` in 3.10.
|
||||
|
||||
from pathspec.pattern import (
|
||||
RegexPattern)
|
||||
from pathspec.patterns.gitignore.spec import (
|
||||
_DIR_MARK)
|
||||
from pathspec._typing import (
|
||||
override) # Added in 3.12.
|
||||
|
||||
from .pathspec import (
|
||||
SimplePsBackend)
|
||||
|
||||
|
||||
class SimpleGiBackend(SimplePsBackend):
|
||||
"""
|
||||
The :class:`SimpleGiBackend` class is the default (or simple) implementation
|
||||
used by :class:`~pathspec.gitignore.GitIgnoreSpec` for matching files.
|
||||
"""
|
||||
|
||||
# Change type hint.
|
||||
_patterns: list[tuple[int, RegexPattern]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
patterns: Sequence[RegexPattern],
|
||||
*,
|
||||
no_filter: Optional[bool] = None,
|
||||
no_reverse: Optional[bool] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the :class:`SimpleGiBackend` instance.
|
||||
|
||||
*patterns* (:class:`Sequence` of :class:`.RegexPattern`) contains the
|
||||
compiled patterns.
|
||||
|
||||
*no_filter* (:class:`bool`) is whether to keep no-op patterns (:data:`True`),
|
||||
or remove them (:data:`False`).
|
||||
|
||||
*no_reverse* (:class:`bool`) is whether to keep the pattern order
|
||||
(:data:`True`), or reverse the order (:data:`True`).
|
||||
"""
|
||||
super().__init__(patterns, no_filter=no_filter, no_reverse=no_reverse)
|
||||
|
||||
@override
|
||||
def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]:
|
||||
"""
|
||||
Check the file against the patterns.
|
||||
|
||||
*file* (:class:`str`) is the normalized file path to check.
|
||||
|
||||
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`).
|
||||
"""
|
||||
is_reversed = self._is_reversed
|
||||
|
||||
out_include: Optional[bool] = None
|
||||
out_index: Optional[int] = None
|
||||
out_priority = 0
|
||||
for index, pattern in self._patterns:
|
||||
if (
|
||||
(include := pattern.include) is not None
|
||||
and (match := pattern.match_file(file)) 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 is_reversed:
|
||||
if priority > out_priority:
|
||||
out_include = include
|
||||
out_index = index
|
||||
out_priority = priority
|
||||
else:
|
||||
# Forward.
|
||||
if (include and dir_mark) or priority >= out_priority:
|
||||
out_include = include
|
||||
out_index = index
|
||||
out_priority = priority
|
||||
|
||||
if is_reversed and priority == 2:
|
||||
# Patterns are being checked in reverse order. The first pattern that
|
||||
# matches with priority 2 takes precedence.
|
||||
break
|
||||
|
||||
return (out_include, out_index)
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
"""
|
||||
This module provides the simple backend for :class:`~pathspec.pathspec.PathSpec`.
|
||||
|
||||
WARNING: The *pathspec._backends.simple* package is not part of the public API.
|
||||
Its contents and structure are likely to change.
|
||||
"""
|
||||
|
||||
from collections.abc import (
|
||||
Sequence)
|
||||
from typing import (
|
||||
Optional) # Replaced by `X | None` in 3.10.
|
||||
|
||||
from pathspec.backend import (
|
||||
_Backend)
|
||||
from pathspec.pattern import (
|
||||
Pattern)
|
||||
from pathspec._typing import (
|
||||
override) # Added in 3.12.
|
||||
from pathspec.util import (
|
||||
check_match_file)
|
||||
|
||||
from .._utils import (
|
||||
enumerate_patterns)
|
||||
|
||||
|
||||
class SimplePsBackend(_Backend):
|
||||
"""
|
||||
The :class:`SimplePsBackend` class is the default (or simple) implementation
|
||||
used by :class:`~pathspec.pathspec.PathSpec` for matching files.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
patterns: Sequence[Pattern],
|
||||
*,
|
||||
no_filter: Optional[bool] = None,
|
||||
no_reverse: Optional[bool] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the :class:`SimplePsBackend` instance.
|
||||
|
||||
*patterns* (:class:`Sequence` of :class:`.Pattern`) contains the compiled
|
||||
patterns.
|
||||
|
||||
*no_filter* (:class:`bool`) is whether to keep no-op patterns (:data:`True`),
|
||||
or remove them (:data:`False`).
|
||||
|
||||
*no_reverse* (:class:`bool`) is whether to keep the pattern order
|
||||
(:data:`True`), or reverse the order (:data:`True`).
|
||||
"""
|
||||
|
||||
self._is_reversed: bool = not no_reverse
|
||||
"""
|
||||
*_is_reversed* (:class:`bool`) is whether to the pattern order was reversed.
|
||||
"""
|
||||
|
||||
self._patterns: list[tuple[int, Pattern]] = enumerate_patterns(
|
||||
patterns, filter=not no_filter, reverse=not no_reverse,
|
||||
)
|
||||
"""
|
||||
*_patterns* (:class:`list` of :class:`tuple`) contains the enumerated
|
||||
patterns.
|
||||
"""
|
||||
|
||||
@override
|
||||
def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]:
|
||||
"""
|
||||
Check the file against the patterns.
|
||||
|
||||
*file* (:class:`str`) is the normalized file path to check.
|
||||
|
||||
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`).
|
||||
"""
|
||||
return check_match_file(self._patterns, file, self._is_reversed)
|
||||
Loading…
Add table
Add a link
Reference in a new issue